Skip to content

Commit 1e92f79

Browse files
authored
Merge pull request #11198 from marmelab/update-tiptap-3
Update tiptap 3
2 parents afd9de5 + 30d0d6a commit 1e92f79

File tree

13 files changed

+690
-775
lines changed

13 files changed

+690
-775
lines changed

examples/ra-offline/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,4 @@
4242
"vite-plugin-pwa": "^1.2.0"
4343
},
4444
"name": "ra-offline"
45-
}
45+
}

packages/ra-input-rich-text/package.json

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,15 @@
2222
"build": "zshy --silent"
2323
},
2424
"dependencies": {
25-
"@tiptap/core": "^2.0.3",
26-
"@tiptap/extension-color": "^2.0.3",
27-
"@tiptap/extension-highlight": "^2.0.3",
28-
"@tiptap/extension-image": "^2.0.3",
29-
"@tiptap/extension-link": "^2.0.3",
30-
"@tiptap/extension-placeholder": "^2.0.3",
31-
"@tiptap/extension-text-align": "^2.0.3",
32-
"@tiptap/extension-text-style": "^2.0.3",
33-
"@tiptap/extension-underline": "^2.0.3",
34-
"@tiptap/pm": "^2.0.3",
35-
"@tiptap/react": "^2.0.3",
36-
"@tiptap/starter-kit": "^2.0.3",
25+
"@tiptap/core": "^3.20.4",
26+
"@tiptap/extension-color": "^3.20.4",
27+
"@tiptap/extension-highlight": "^3.20.4",
28+
"@tiptap/extension-image": "^3.20.4",
29+
"@tiptap/extension-text-align": "^3.20.4",
30+
"@tiptap/extension-text-style": "^3.20.4",
31+
"@tiptap/pm": "^3.20.4",
32+
"@tiptap/react": "^3.20.4",
33+
"@tiptap/starter-kit": "^3.20.4",
3734
"clsx": "^2.1.1"
3835
},
3936
"peerDependencies": {
@@ -45,19 +42,19 @@
4542
"react-dom": "^18.0.0 || ^19.0.0"
4643
},
4744
"devDependencies": {
45+
"@floating-ui/dom": "^1.0.0",
4846
"@mui/icons-material": "^5.16.12",
4947
"@mui/material": "^5.16.12",
5048
"@testing-library/react": "^15.0.7",
51-
"@tiptap/extension-mention": "^2.0.3",
52-
"@tiptap/suggestion": "^2.0.3",
49+
"@tiptap/extension-mention": "^3.20.4",
50+
"@tiptap/suggestion": "^3.20.4",
5351
"data-generator-retail": "^5.14.4",
5452
"ra-core": "^5.14.4",
5553
"ra-data-fakerest": "^5.14.4",
5654
"ra-ui-materialui": "^5.14.4",
5755
"react": "^18.3.1",
5856
"react-dom": "^18.3.1",
5957
"react-hook-form": "^7.65.0",
60-
"tippy.js": "^6.3.7",
6158
"typescript": "^5.1.3",
6259
"zshy": "^0.5.0"
6360
},

packages/ra-input-rich-text/src/RichTextInput.spec.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import { render, waitFor } from '@testing-library/react';
44
import { Basic } from './RichTextInput.stories';
55

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

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

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

2020
await waitFor(() => {
2121
expect(container.querySelector('.ProseMirror')?.innerHTML).toEqual(
22-
'<h1>Goodbye world!</h1>'
22+
'<h1>Goodbye world!</h1><p><br class="ProseMirror-trailingBreak"></p>'
2323
);
2424
});
2525
});

packages/ra-input-rich-text/src/RichTextInput.stories.tsx

Lines changed: 68 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import fakeRestDataProvider from 'ra-data-fakerest';
2222
import { Routes, Route } from 'react-router-dom';
2323
import Mention from '@tiptap/extension-mention';
2424
import { Editor, ReactRenderer } from '@tiptap/react';
25-
import tippy, { Instance as TippyInstance } from 'tippy.js';
25+
import { computePosition, flip, shift, offset } from '@floating-ui/dom';
2626
import {
2727
DefaultEditorOptions,
2828
RichTextInput,
@@ -375,13 +375,13 @@ export const CustomOptions = () => (
375375
</TestMemoryRouter>
376376
);
377377

378-
const MentionList = React.forwardRef<
379-
MentionListRef,
380-
{
381-
items: string[];
382-
command: (props: { id: string }) => void;
383-
}
384-
>((props, ref) => {
378+
const MentionList = (props: {
379+
items: string[];
380+
command: (props: { id: string }) => void;
381+
onKeyDownRef: React.MutableRefObject<
382+
((props: { event: KeyboardEvent }) => boolean) | null
383+
>;
384+
}) => {
385385
const [selectedIndex, setSelectedIndex] = React.useState(0);
386386

387387
const selectItem = index => {
@@ -392,42 +392,30 @@ const MentionList = React.forwardRef<
392392
}
393393
};
394394

395-
const upHandler = () => {
396-
setSelectedIndex(
397-
(selectedIndex + props.items.length - 1) % props.items.length
398-
);
399-
};
400-
401-
const downHandler = () => {
402-
setSelectedIndex((selectedIndex + 1) % props.items.length);
403-
};
404-
405-
const enterHandler = () => {
406-
selectItem(selectedIndex);
407-
};
408-
409395
React.useEffect(() => setSelectedIndex(0), [props.items]);
410396

411-
React.useImperativeHandle(ref, () => ({
412-
onKeyDown: ({ event }) => {
397+
React.useEffect(() => {
398+
props.onKeyDownRef.current = ({ event }) => {
413399
if (event.key === 'ArrowUp') {
414-
upHandler();
400+
setSelectedIndex(
401+
i => (i + props.items.length - 1) % props.items.length
402+
);
415403
return true;
416404
}
417405

418406
if (event.key === 'ArrowDown') {
419-
downHandler();
407+
setSelectedIndex(i => (i + 1) % props.items.length);
420408
return true;
421409
}
422410

423411
if (event.key === 'Enter') {
424-
enterHandler();
412+
selectItem(selectedIndex);
425413
return true;
426414
}
427415

428416
return false;
429-
},
430-
}));
417+
};
418+
});
431419

432420
return (
433421
<Paper>
@@ -438,7 +426,10 @@ const MentionList = React.forwardRef<
438426
dense
439427
selected={index === selectedIndex}
440428
key={index}
441-
onClick={() => selectItem(index)}
429+
onMouseDown={e => {
430+
e.preventDefault();
431+
selectItem(index);
432+
}}
442433
>
443434
{item}
444435
</ListItemButton>
@@ -451,10 +442,6 @@ const MentionList = React.forwardRef<
451442
</List>
452443
</Paper>
453444
);
454-
});
455-
456-
type MentionListRef = {
457-
onKeyDown: (props: { event: React.KeyboardEvent }) => boolean;
458445
};
459446
const suggestions = tags => {
460447
return {
@@ -467,75 +454,84 @@ const suggestions = tags => {
467454
},
468455

469456
render: () => {
470-
let component: ReactRenderer<MentionListRef>;
471-
let popup: TippyInstance[];
457+
let component: ReactRenderer;
458+
let floatingEl: HTMLElement;
459+
const onKeyDownRef: React.MutableRefObject<
460+
((props: { event: KeyboardEvent }) => boolean) | null
461+
> = { current: null };
462+
463+
const updatePosition = (clientRect: () => DOMRect) => {
464+
if (!floatingEl) return;
465+
const virtualEl = {
466+
getBoundingClientRect: clientRect,
467+
};
468+
computePosition(virtualEl, floatingEl, {
469+
placement: 'bottom-start',
470+
middleware: [offset(8), flip(), shift()],
471+
}).then(({ x, y }) => {
472+
Object.assign(floatingEl.style, {
473+
left: `${x}px`,
474+
top: `${y}px`,
475+
});
476+
});
477+
};
472478

473479
return {
474480
onStart: props => {
475481
component = new ReactRenderer(MentionList, {
476-
props,
482+
props: { ...props, onKeyDownRef },
477483
editor: props.editor,
478484
});
479485

480-
if (!props.clientRect) {
481-
return;
482-
}
486+
floatingEl = document.createElement('div');
487+
floatingEl.style.position = 'absolute';
488+
floatingEl.style.zIndex = '1300';
489+
floatingEl.addEventListener('mousedown', e =>
490+
e.preventDefault()
491+
);
492+
floatingEl.appendChild(component.element);
493+
props.editor.view.dom.parentElement.appendChild(floatingEl);
483494

484-
popup = tippy('body', {
485-
getReferenceClientRect: props.clientRect,
486-
appendTo: () => document.body,
487-
content: component.element,
488-
showOnCreate: true,
489-
interactive: true,
490-
trigger: 'manual',
491-
placement: 'bottom-start',
492-
});
495+
if (props.clientRect) {
496+
updatePosition(props.clientRect);
497+
}
493498
},
494499

495500
onUpdate(props) {
496501
if (component) {
497-
component.updateProps(props);
502+
component.updateProps({ ...props, onKeyDownRef });
498503
}
499504

500-
if (!props.clientRect) {
501-
return;
502-
}
503-
504-
if (popup && popup[0]) {
505-
popup[0].setProps({
506-
getReferenceClientRect: props.clientRect,
507-
});
505+
if (props.clientRect) {
506+
updatePosition(props.clientRect);
508507
}
509508
},
510509

511510
onKeyDown(props) {
512-
if (popup && popup[0] && props.event.key === 'Escape') {
513-
popup[0].hide();
514-
511+
if (props.event.key === 'Escape') {
512+
if (floatingEl) {
513+
floatingEl.style.display = 'none';
514+
}
515515
return true;
516516
}
517517

518-
if (!component.ref) {
518+
if (!onKeyDownRef.current) {
519519
return false;
520520
}
521521

522-
return component.ref.onKeyDown(props);
522+
return onKeyDownRef.current(props);
523523
},
524524

525525
onExit() {
526+
onKeyDownRef.current = null;
526527
queueMicrotask(() => {
527-
if (popup && popup[0] && !popup[0].state.isDestroyed) {
528-
popup[0].destroy();
529-
}
530528
if (component) {
531529
component.destroy();
532530
}
533-
// Remove references to the old popup and component upon destruction/exit.
534-
// (This should prevent redundant calls to `popup.destroy()`, which Tippy
535-
// warns in the console is a sign of a memory leak, as the `suggestion`
536-
// plugin seems to call `onExit` both when a suggestion menu is closed after
537-
// a user chooses an option, *and* when the editor itself is destroyed.)
538-
popup = undefined;
531+
if (floatingEl && floatingEl.parentNode) {
532+
floatingEl.parentNode.removeChild(floatingEl);
533+
}
534+
floatingEl = undefined;
539535
component = undefined;
540536
});
541537
},

0 commit comments

Comments
 (0)