Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/ra-offline/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@
"vite-plugin-pwa": "^1.2.0"
},
"name": "ra-offline"
}
}
27 changes: 12 additions & 15 deletions packages/ra-input-rich-text/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,15 @@
"build": "zshy --silent"
},
"dependencies": {
"@tiptap/core": "^2.0.3",
"@tiptap/extension-color": "^2.0.3",
"@tiptap/extension-highlight": "^2.0.3",
"@tiptap/extension-image": "^2.0.3",
"@tiptap/extension-link": "^2.0.3",
"@tiptap/extension-placeholder": "^2.0.3",
"@tiptap/extension-text-align": "^2.0.3",
"@tiptap/extension-text-style": "^2.0.3",
"@tiptap/extension-underline": "^2.0.3",
"@tiptap/pm": "^2.0.3",
"@tiptap/react": "^2.0.3",
"@tiptap/starter-kit": "^2.0.3",
"@tiptap/core": "^3.20.4",
"@tiptap/extension-color": "^3.20.4",
"@tiptap/extension-highlight": "^3.20.4",
"@tiptap/extension-image": "^3.20.4",
"@tiptap/extension-text-align": "^3.20.4",
"@tiptap/extension-text-style": "^3.20.4",
"@tiptap/pm": "^3.20.4",
"@tiptap/react": "^3.20.4",
"@tiptap/starter-kit": "^3.20.4",
"clsx": "^2.1.1"
},
"peerDependencies": {
Expand All @@ -45,19 +42,19 @@
"react-dom": "^18.0.0 || ^19.0.0"
},
"devDependencies": {
"@floating-ui/dom": "^1.0.0",
"@mui/icons-material": "^5.16.12",
"@mui/material": "^5.16.12",
"@testing-library/react": "^15.0.7",
"@tiptap/extension-mention": "^2.0.3",
"@tiptap/suggestion": "^2.0.3",
"@tiptap/extension-mention": "^3.20.4",
"@tiptap/suggestion": "^3.20.4",
"data-generator-retail": "^5.14.4",
"ra-core": "^5.14.4",
"ra-data-fakerest": "^5.14.4",
"ra-ui-materialui": "^5.14.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.65.0",
"tippy.js": "^6.3.7",
"typescript": "^5.1.3",
"zshy": "^0.5.0"
},
Expand Down
6 changes: 3 additions & 3 deletions packages/ra-input-rich-text/src/RichTextInput.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { render, waitFor } from '@testing-library/react';
import { Basic } from './RichTextInput.stories';

describe('<RichTextInput />', () => {
it('should update its content when fields value changes', async () => {
it('should update its content when fields value changes and add a trailing break to it', async () => {
const record = { id: 123, body: '<h1>Hello world!</h1>' };
const { container, rerender } = render(<Basic record={record} />);

await waitFor(() => {
expect(container.querySelector('.ProseMirror')?.innerHTML).toEqual(
'<h1>Hello world!</h1>'
'<h1>Hello world!</h1><p><br class="ProseMirror-trailingBreak"></p>'
);
});

Expand All @@ -19,7 +19,7 @@ describe('<RichTextInput />', () => {

await waitFor(() => {
expect(container.querySelector('.ProseMirror')?.innerHTML).toEqual(
'<h1>Goodbye world!</h1>'
'<h1>Goodbye world!</h1><p><br class="ProseMirror-trailingBreak"></p>'
);
});
});
Expand Down
140 changes: 68 additions & 72 deletions packages/ra-input-rich-text/src/RichTextInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import fakeRestDataProvider from 'ra-data-fakerest';
import { Routes, Route } from 'react-router-dom';
import Mention from '@tiptap/extension-mention';
import { Editor, ReactRenderer } from '@tiptap/react';
import tippy, { Instance as TippyInstance } from 'tippy.js';
import { computePosition, flip, shift, offset } from '@floating-ui/dom';
import {
DefaultEditorOptions,
RichTextInput,
Expand Down Expand Up @@ -375,13 +375,13 @@ export const CustomOptions = () => (
</TestMemoryRouter>
);

const MentionList = React.forwardRef<
MentionListRef,
{
items: string[];
command: (props: { id: string }) => void;
}
>((props, ref) => {
const MentionList = (props: {
items: string[];
command: (props: { id: string }) => void;
onKeyDownRef: React.MutableRefObject<
((props: { event: KeyboardEvent }) => boolean) | null
>;
}) => {
const [selectedIndex, setSelectedIndex] = React.useState(0);

const selectItem = index => {
Expand All @@ -392,42 +392,30 @@ const MentionList = React.forwardRef<
}
};

const upHandler = () => {
setSelectedIndex(
(selectedIndex + props.items.length - 1) % props.items.length
);
};

const downHandler = () => {
setSelectedIndex((selectedIndex + 1) % props.items.length);
};

const enterHandler = () => {
selectItem(selectedIndex);
};

React.useEffect(() => setSelectedIndex(0), [props.items]);

React.useImperativeHandle(ref, () => ({
onKeyDown: ({ event }) => {
React.useEffect(() => {
props.onKeyDownRef.current = ({ event }) => {
if (event.key === 'ArrowUp') {
upHandler();
setSelectedIndex(
i => (i + props.items.length - 1) % props.items.length
);
return true;
}

if (event.key === 'ArrowDown') {
downHandler();
setSelectedIndex(i => (i + 1) % props.items.length);
return true;
}

if (event.key === 'Enter') {
enterHandler();
selectItem(selectedIndex);
return true;
}

return false;
},
}));
};
});

return (
<Paper>
Expand All @@ -438,7 +426,10 @@ const MentionList = React.forwardRef<
dense
selected={index === selectedIndex}
key={index}
onClick={() => selectItem(index)}
onMouseDown={e => {
e.preventDefault();
selectItem(index);
}}
>
{item}
</ListItemButton>
Expand All @@ -451,10 +442,6 @@ const MentionList = React.forwardRef<
</List>
</Paper>
);
});

type MentionListRef = {
onKeyDown: (props: { event: React.KeyboardEvent }) => boolean;
};
const suggestions = tags => {
return {
Expand All @@ -467,75 +454,84 @@ const suggestions = tags => {
},

render: () => {
let component: ReactRenderer<MentionListRef>;
let popup: TippyInstance[];
let component: ReactRenderer;
let floatingEl: HTMLElement;
const onKeyDownRef: React.MutableRefObject<
((props: { event: KeyboardEvent }) => boolean) | null
> = { current: null };

const updatePosition = (clientRect: () => DOMRect) => {
if (!floatingEl) return;
const virtualEl = {
getBoundingClientRect: clientRect,
};
computePosition(virtualEl, floatingEl, {
placement: 'bottom-start',
middleware: [offset(8), flip(), shift()],
}).then(({ x, y }) => {
Object.assign(floatingEl.style, {
left: `${x}px`,
top: `${y}px`,
});
});
};

return {
onStart: props => {
component = new ReactRenderer(MentionList, {
props,
props: { ...props, onKeyDownRef },
editor: props.editor,
});

if (!props.clientRect) {
return;
}
floatingEl = document.createElement('div');
floatingEl.style.position = 'absolute';
floatingEl.style.zIndex = '1300';
floatingEl.addEventListener('mousedown', e =>
e.preventDefault()
);
floatingEl.appendChild(component.element);
props.editor.view.dom.parentElement.appendChild(floatingEl);

popup = tippy('body', {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: 'manual',
placement: 'bottom-start',
});
if (props.clientRect) {
updatePosition(props.clientRect);
}
},

onUpdate(props) {
if (component) {
component.updateProps(props);
component.updateProps({ ...props, onKeyDownRef });
}

if (!props.clientRect) {
return;
}

if (popup && popup[0]) {
popup[0].setProps({
getReferenceClientRect: props.clientRect,
});
if (props.clientRect) {
updatePosition(props.clientRect);
}
},

onKeyDown(props) {
if (popup && popup[0] && props.event.key === 'Escape') {
popup[0].hide();

if (props.event.key === 'Escape') {
if (floatingEl) {
floatingEl.style.display = 'none';
}
return true;
}

if (!component.ref) {
if (!onKeyDownRef.current) {
return false;
}

return component.ref.onKeyDown(props);
return onKeyDownRef.current(props);
},

onExit() {
onKeyDownRef.current = null;
queueMicrotask(() => {
if (popup && popup[0] && !popup[0].state.isDestroyed) {
popup[0].destroy();
}
if (component) {
component.destroy();
}
// Remove references to the old popup and component upon destruction/exit.
// (This should prevent redundant calls to `popup.destroy()`, which Tippy
// warns in the console is a sign of a memory leak, as the `suggestion`
// plugin seems to call `onExit` both when a suggestion menu is closed after
// a user chooses an option, *and* when the editor itself is destroyed.)
popup = undefined;
if (floatingEl && floatingEl.parentNode) {
floatingEl.parentNode.removeChild(floatingEl);
}
floatingEl = undefined;
component = undefined;
});
},
Expand Down
Loading
Loading