diff --git a/.changeset/dirty-bats-look.md b/.changeset/dirty-bats-look.md new file mode 100644 index 000000000..23e690073 --- /dev/null +++ b/.changeset/dirty-bats-look.md @@ -0,0 +1,36 @@ +--- +"@tiptap/extension-floating-menu": major +"@tiptap/extension-bubble-menu": major +"@tiptap/extension-mention": major +"@tiptap/suggestion": major +"@tiptap/react": major +"@tiptap/vue-2": major +"@tiptap/vue-3": major +--- + +Removed tippy.js and replaced it with [Floating UI](https://floating-ui.com/) - a newer, more lightweight and customizable floating element library. + +This change is breaking existing menu implementations and will require a manual migration. + +**Affected packages:** + +- `@tiptap/extension-floating-menu` +- `@tiptap/extension-bubble-menu` +- `@tiptap/extension-mention` +- `@tiptap/suggestion` +- `@tiptap/react` +- `@tiptap/vue-2` +- `@tiptap/vue-3` + +Make sure to remove `tippyOptions` from the `FloatingMenu` and `BubbleMenu` components, and replace them with the new `options` object. Check our documentation to see how to migrate your existing menu implementations. + +- [FloatingMenu](https://tiptap.dev/docs/editor/extensions/functionality/floatingmenu) +- [BubbleMenu](https://tiptap.dev/docs/editor/extensions/functionality/bubble-menu) + +You'll also need to install `@floating-ui/dom` as a peer dependency to your project like this: + +```bash +npm install @floating-ui/dom@^1.6.0 +``` + +The new `options` object is compatible with all components that use these extensions. diff --git a/demos/includeDependencies.txt b/demos/includeDependencies.txt index 54929e3c2..ac1b107e9 100644 --- a/demos/includeDependencies.txt +++ b/demos/includeDependencies.txt @@ -23,7 +23,7 @@ react-dom react-dom/client shiki simplify-js -tippy.js +@floating-ui/dom uuid y-webrtc yjs diff --git a/demos/src/Examples/Community/React/index.spec.js b/demos/src/Examples/Community/React/index.spec.js index 721876e8f..b4888f73a 100644 --- a/demos/src/Examples/Community/React/index.spec.js +++ b/demos/src/Examples/Community/React/index.spec.js @@ -20,10 +20,10 @@ context('/src/Examples/Community/React/', () => { cy.get('.tiptap').type('{selectall}{backspace}@') // check if the mention autocomplete is visible - cy.get('.tippy-content .dropdown-menu').should('be.visible') + cy.get('.dropdown-menu').should('be.visible') // select the first user - cy.get('.tippy-content .dropdown-menu button').first().then($el => { + cy.get('.dropdown-menu button').first().then($el => { const name = $el.text() $el.click() diff --git a/demos/src/Examples/Community/React/suggestion.js b/demos/src/Examples/Community/React/suggestion.js index f573d04c7..05b9bd90d 100644 --- a/demos/src/Examples/Community/React/suggestion.js +++ b/demos/src/Examples/Community/React/suggestion.js @@ -1,8 +1,29 @@ -import { ReactRenderer } from '@tiptap/react' -import tippy from 'tippy.js' +import { + computePosition, + flip, + shift, +} from '@floating-ui/dom' +import { posToDOMRect, ReactRenderer } from '@tiptap/react' import { MentionList } from './MentionList.jsx' +const updatePosition = (editor, element) => { + const virtualElement = { + getBoundingClientRect: () => posToDOMRect(editor.view, editor.state.selection.from, editor.state.selection.to), + } + + computePosition(virtualElement, element, { + placement: 'bottom-start', + strategy: 'absolute', + middleware: [shift(), flip()], + }).then(({ x, y, strategy }) => { + element.style.width = 'max-content' + element.style.position = strategy + element.style.left = `${x}px` + element.style.top = `${y}px` + }) +} + export default { items: ({ query }) => { return [ @@ -12,7 +33,6 @@ export default { render: () => { let reactRenderer - let popup return { onStart: props => { @@ -26,15 +46,11 @@ export default { editor: props.editor, }) - popup = tippy('body', { - getReferenceClientRect: props.clientRect, - appendTo: () => document.body, - content: reactRenderer.element, - showOnCreate: true, - interactive: true, - trigger: 'manual', - placement: 'bottom-start', - }) + reactRenderer.element.style.position = 'absolute' + + document.body.appendChild(reactRenderer.element) + + updatePosition(props.editor, reactRenderer.element) }, onUpdate(props) { @@ -43,15 +59,13 @@ export default { if (!props.clientRect) { return } - - popup[0].setProps({ - getReferenceClientRect: props.clientRect, - }) + updatePosition(props.editor, reactRenderer.element) }, onKeyDown(props) { if (props.event.key === 'Escape') { - popup[0].hide() + reactRenderer.destroy() + reactRenderer.element.remove() return true } @@ -60,8 +74,8 @@ export default { }, onExit() { - popup[0].destroy() reactRenderer.destroy() + reactRenderer.element.remove() }, } }, diff --git a/demos/src/Examples/Community/Vue/index.spec.js b/demos/src/Examples/Community/Vue/index.spec.js index e403e10b0..3eca14f78 100644 --- a/demos/src/Examples/Community/Vue/index.spec.js +++ b/demos/src/Examples/Community/Vue/index.spec.js @@ -20,10 +20,10 @@ context('/src/Examples/Community/Vue/', () => { cy.get('.tiptap').type('{selectall}{backspace}@') // check if the mention autocomplete is visible - cy.get('.tippy-content .dropdown-menu').should('be.visible') + cy.get('.dropdown-menu').should('be.visible') // select the first user - cy.get('.tippy-content .dropdown-menu button').first().then($el => { + cy.get('.dropdown-menu button').first().then($el => { const name = $el.text() $el.click() diff --git a/demos/src/Examples/Community/Vue/suggestion.js b/demos/src/Examples/Community/Vue/suggestion.js index d4ea73322..422bb94ef 100644 --- a/demos/src/Examples/Community/Vue/suggestion.js +++ b/demos/src/Examples/Community/Vue/suggestion.js @@ -1,8 +1,29 @@ -import { VueRenderer } from '@tiptap/vue-3' -import tippy from 'tippy.js' +import { + computePosition, + flip, + shift, +} from '@floating-ui/dom' +import { posToDOMRect, VueRenderer } from '@tiptap/vue-3' import MentionList from './MentionList.vue' +const updatePosition = (editor, element) => { + const virtualElement = { + getBoundingClientRect: () => posToDOMRect(editor.view, editor.state.selection.from, editor.state.selection.to), + } + + computePosition(virtualElement, element, { + placement: 'bottom-start', + strategy: 'absolute', + middleware: [shift(), flip()], + }).then(({ x, y, strategy }) => { + element.style.width = 'max-content' + element.style.position = strategy + element.style.left = `${x}px` + element.style.top = `${y}px` + }) +} + export default { items: ({ query }) => { return [ @@ -12,7 +33,6 @@ export default { render: () => { let component - let popup return { onStart: props => { @@ -28,15 +48,11 @@ export default { return } - popup = tippy('body', { - getReferenceClientRect: props.clientRect, - appendTo: () => document.body, - content: component.element, - showOnCreate: true, - interactive: true, - trigger: 'manual', - placement: 'bottom-start', - }) + component.element.style.position = 'absolute' + + document.body.appendChild(component.element) + + updatePosition(props.editor, component.element) }, onUpdate(props) { @@ -46,14 +62,13 @@ export default { return } - popup[0].setProps({ - getReferenceClientRect: props.clientRect, - }) + updatePosition(props.editor, component.element) }, onKeyDown(props) { if (props.event.key === 'Escape') { - popup[0].hide() + component.destroy() + component.element.remove() return true } @@ -62,8 +77,8 @@ export default { }, onExit() { - popup[0].destroy() component.destroy() + component.element.remove() }, } }, diff --git a/demos/src/Examples/Menus/React/index.jsx b/demos/src/Examples/Menus/React/index.jsx index b05c7e1f7..65848b05c 100644 --- a/demos/src/Examples/Menus/React/index.jsx +++ b/demos/src/Examples/Menus/React/index.jsx @@ -26,7 +26,7 @@ export default () => { return ( <> - {editor && + {editor && } - {editor && + {editor &&