Skip to content

Commit 7eaa34d

Browse files
committed
Remove tippy.js and replace with Floating UI (#5398)
* start experimenting with floating-ui * add options to floating-ui bubble menu plugin & fix smaller issues * add vue support for new floating-ui * start experimenting with floating-ui * adjust floating-menu plugin for floating-ui & update react component * add vue support for floating-menu with floating-ui * update tests for new floating-ui integration * added changeset file * move floating-ui dependency to peerDeps * add install notice to changelog * remove unnecessary code for destroying and removing component element in Vue suggestion.js * remove unnecessary code for destroying and removing component element in React suggestion.js * sync package-lock * widen range for peerDeps again
1 parent 0da4390 commit 7eaa34d

File tree

34 files changed

+2677
-4659
lines changed

34 files changed

+2677
-4659
lines changed

.changeset/dirty-bats-look.md

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
"@tiptap/extension-floating-menu": major
3+
"@tiptap/extension-bubble-menu": major
4+
"@tiptap/extension-mention": major
5+
"@tiptap/suggestion": major
6+
"@tiptap/react": major
7+
"@tiptap/vue-2": major
8+
"@tiptap/vue-3": major
9+
---
10+
11+
Removed tippy.js and replaced it with [Floating UI](https://floating-ui.com/) - a newer, more lightweight and customizable floating element library.
12+
13+
This change is breaking existing menu implementations and will require a manual migration.
14+
15+
**Affected packages:**
16+
17+
- `@tiptap/extension-floating-menu`
18+
- `@tiptap/extension-bubble-menu`
19+
- `@tiptap/extension-mention`
20+
- `@tiptap/suggestion`
21+
- `@tiptap/react`
22+
- `@tiptap/vue-2`
23+
- `@tiptap/vue-3`
24+
25+
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.
26+
27+
- [FloatingMenu](https://tiptap.dev/docs/editor/extensions/functionality/floatingmenu)
28+
- [BubbleMenu](https://tiptap.dev/docs/editor/extensions/functionality/bubble-menu)
29+
30+
You'll also need to install `@floating-ui/dom` as a peer dependency to your project like this:
31+
32+
```bash
33+
npm install @floating-ui/dom@^1.6.0
34+
```
35+
36+
The new `options` object is compatible with all components that use these extensions.

demos/includeDependencies.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ react-dom
2323
react-dom/client
2424
shiki
2525
simplify-js
26-
tippy.js
26+
@floating-ui/dom
2727
uuid
2828
y-webrtc
2929
yjs

demos/src/Examples/Community/React/index.spec.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ context('/src/Examples/Community/React/', () => {
2020
cy.get('.tiptap').type('{selectall}{backspace}@')
2121

2222
// check if the mention autocomplete is visible
23-
cy.get('.tippy-content .dropdown-menu').should('be.visible')
23+
cy.get('.dropdown-menu').should('be.visible')
2424

2525
// select the first user
26-
cy.get('.tippy-content .dropdown-menu button').first().then($el => {
26+
cy.get('.dropdown-menu button').first().then($el => {
2727
const name = $el.text()
2828

2929
$el.click()

demos/src/Examples/Community/React/suggestion.js

+32-18
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,29 @@
1-
import { ReactRenderer } from '@tiptap/react'
2-
import tippy from 'tippy.js'
1+
import {
2+
computePosition,
3+
flip,
4+
shift,
5+
} from '@floating-ui/dom'
6+
import { posToDOMRect, ReactRenderer } from '@tiptap/react'
37

48
import { MentionList } from './MentionList.jsx'
59

10+
const updatePosition = (editor, element) => {
11+
const virtualElement = {
12+
getBoundingClientRect: () => posToDOMRect(editor.view, editor.state.selection.from, editor.state.selection.to),
13+
}
14+
15+
computePosition(virtualElement, element, {
16+
placement: 'bottom-start',
17+
strategy: 'absolute',
18+
middleware: [shift(), flip()],
19+
}).then(({ x, y, strategy }) => {
20+
element.style.width = 'max-content'
21+
element.style.position = strategy
22+
element.style.left = `${x}px`
23+
element.style.top = `${y}px`
24+
})
25+
}
26+
627
export default {
728
items: ({ query }) => {
829
return [
@@ -12,7 +33,6 @@ export default {
1233

1334
render: () => {
1435
let reactRenderer
15-
let popup
1636

1737
return {
1838
onStart: props => {
@@ -26,15 +46,11 @@ export default {
2646
editor: props.editor,
2747
})
2848

29-
popup = tippy('body', {
30-
getReferenceClientRect: props.clientRect,
31-
appendTo: () => document.body,
32-
content: reactRenderer.element,
33-
showOnCreate: true,
34-
interactive: true,
35-
trigger: 'manual',
36-
placement: 'bottom-start',
37-
})
49+
reactRenderer.element.style.position = 'absolute'
50+
51+
document.body.appendChild(reactRenderer.element)
52+
53+
updatePosition(props.editor, reactRenderer.element)
3854
},
3955

4056
onUpdate(props) {
@@ -43,15 +59,13 @@ export default {
4359
if (!props.clientRect) {
4460
return
4561
}
46-
47-
popup[0].setProps({
48-
getReferenceClientRect: props.clientRect,
49-
})
62+
updatePosition(props.editor, reactRenderer.element)
5063
},
5164

5265
onKeyDown(props) {
5366
if (props.event.key === 'Escape') {
54-
popup[0].hide()
67+
reactRenderer.destroy()
68+
reactRenderer.element.remove()
5569

5670
return true
5771
}
@@ -60,8 +74,8 @@ export default {
6074
},
6175

6276
onExit() {
63-
popup[0].destroy()
6477
reactRenderer.destroy()
78+
reactRenderer.element.remove()
6579
},
6680
}
6781
},

demos/src/Examples/Community/Vue/index.spec.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ context('/src/Examples/Community/Vue/', () => {
2020
cy.get('.tiptap').type('{selectall}{backspace}@')
2121

2222
// check if the mention autocomplete is visible
23-
cy.get('.tippy-content .dropdown-menu').should('be.visible')
23+
cy.get('.dropdown-menu').should('be.visible')
2424

2525
// select the first user
26-
cy.get('.tippy-content .dropdown-menu button').first().then($el => {
26+
cy.get('.dropdown-menu button').first().then($el => {
2727
const name = $el.text()
2828

2929
$el.click()

demos/src/Examples/Community/Vue/suggestion.js

+32-17
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,29 @@
1-
import { VueRenderer } from '@tiptap/vue-3'
2-
import tippy from 'tippy.js'
1+
import {
2+
computePosition,
3+
flip,
4+
shift,
5+
} from '@floating-ui/dom'
6+
import { posToDOMRect, VueRenderer } from '@tiptap/vue-3'
37

48
import MentionList from './MentionList.vue'
59

10+
const updatePosition = (editor, element) => {
11+
const virtualElement = {
12+
getBoundingClientRect: () => posToDOMRect(editor.view, editor.state.selection.from, editor.state.selection.to),
13+
}
14+
15+
computePosition(virtualElement, element, {
16+
placement: 'bottom-start',
17+
strategy: 'absolute',
18+
middleware: [shift(), flip()],
19+
}).then(({ x, y, strategy }) => {
20+
element.style.width = 'max-content'
21+
element.style.position = strategy
22+
element.style.left = `${x}px`
23+
element.style.top = `${y}px`
24+
})
25+
}
26+
627
export default {
728
items: ({ query }) => {
829
return [
@@ -12,7 +33,6 @@ export default {
1233

1334
render: () => {
1435
let component
15-
let popup
1636

1737
return {
1838
onStart: props => {
@@ -28,15 +48,11 @@ export default {
2848
return
2949
}
3050

31-
popup = tippy('body', {
32-
getReferenceClientRect: props.clientRect,
33-
appendTo: () => document.body,
34-
content: component.element,
35-
showOnCreate: true,
36-
interactive: true,
37-
trigger: 'manual',
38-
placement: 'bottom-start',
39-
})
51+
component.element.style.position = 'absolute'
52+
53+
document.body.appendChild(component.element)
54+
55+
updatePosition(props.editor, component.element)
4056
},
4157

4258
onUpdate(props) {
@@ -46,14 +62,13 @@ export default {
4662
return
4763
}
4864

49-
popup[0].setProps({
50-
getReferenceClientRect: props.clientRect,
51-
})
65+
updatePosition(props.editor, component.element)
5266
},
5367

5468
onKeyDown(props) {
5569
if (props.event.key === 'Escape') {
56-
popup[0].hide()
70+
component.destroy()
71+
component.element.remove()
5772

5873
return true
5974
}
@@ -62,8 +77,8 @@ export default {
6277
},
6378

6479
onExit() {
65-
popup[0].destroy()
6680
component.destroy()
81+
component.element.remove()
6782
},
6883
}
6984
},

demos/src/Examples/Menus/React/index.jsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default () => {
2626

2727
return (
2828
<>
29-
{editor && <BubbleMenu className="bubble-menu" tippyOptions={{ duration: 100 }} editor={editor}>
29+
{editor && <BubbleMenu className="bubble-menu" editor={editor}>
3030
<button
3131
onClick={() => editor.chain().focus().toggleBold().run()}
3232
className={editor.isActive('bold') ? 'is-active' : ''}
@@ -47,7 +47,7 @@ export default () => {
4747
</button>
4848
</BubbleMenu>}
4949

50-
{editor && <FloatingMenu className="floating-menu" tippyOptions={{ duration: 100 }} editor={editor}>
50+
{editor && <FloatingMenu className="floating-menu" editor={editor}>
5151
<button
5252
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
5353
className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}

demos/src/Examples/Menus/React/index.spec.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@ context('/src/Examples/Menus/React/', () => {
1010
})
1111

1212
it('should show menu when the editor is empty', () => {
13-
cy.get('#app')
14-
.find('[data-tippy-root]')
13+
cy.get('body')
14+
.find('.floating-menu')
1515
})
1616

1717
it('should show menu when text is selected', () => {
1818
cy.get('.tiptap')
1919
.type('Test')
2020
.type('{selectall}')
2121

22-
cy.get('#app')
23-
.find('[data-tippy-root]')
22+
cy.get('body')
23+
.find('.bubble-menu')
2424
})
2525

2626
const marks = [
@@ -44,8 +44,8 @@ context('/src/Examples/Menus/React/', () => {
4444
.type('Test')
4545
.type('{selectall}')
4646

47-
cy.get('#app')
48-
.find('[data-tippy-root]')
47+
cy.get('body')
48+
.find('.bubble-menu')
4949
.contains(mark.button)
5050
.click()
5151

demos/src/Examples/Menus/Vue/index.spec.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@ context('/src/Examples/Menus/Vue/', () => {
1010
})
1111

1212
it('should show menu when the editor is empty', () => {
13-
cy.get('#app')
14-
.find('[data-tippy-root]')
13+
cy.get('body')
14+
.find('.floating-menu')
1515
})
1616

1717
it('should show menu when text is selected', () => {
1818
cy.get('.tiptap')
1919
.type('Test')
2020
.type('{selectall}')
2121

22-
cy.get('#app')
23-
.find('[data-tippy-root]')
22+
cy.get('body')
23+
.find('.bubble-menu')
2424
})
2525

2626
const marks = [
@@ -44,8 +44,8 @@ context('/src/Examples/Menus/Vue/', () => {
4444
.type('Test')
4545
.type('{selectall}')
4646

47-
cy.get('#app')
48-
.find('[data-tippy-root]')
47+
cy.get('body')
48+
.find('.bubble-menu')
4949
.contains(mark.button)
5050
.click()
5151

demos/src/Examples/Menus/Vue/index.vue

+22-22
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,35 @@
11
<template>
22
<div v-if="editor">
33
<bubble-menu
4-
class="bubble-menu"
5-
:tippy-options="{ duration: 100 }"
64
:editor="editor"
75
>
8-
<button @click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }">
9-
Bold
10-
</button>
11-
<button @click="editor.chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }">
12-
Italic
13-
</button>
14-
<button @click="editor.chain().focus().toggleStrike().run()" :class="{ 'is-active': editor.isActive('strike') }">
15-
Strike
16-
</button>
6+
<div class="bubble-menu">
7+
<button @click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }">
8+
Bold
9+
</button>
10+
<button @click="editor.chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }">
11+
Italic
12+
</button>
13+
<button @click="editor.chain().focus().toggleStrike().run()" :class="{ 'is-active': editor.isActive('strike') }">
14+
Strike
15+
</button>
16+
</div>
1717
</bubble-menu>
1818

1919
<floating-menu
20-
class="floating-menu"
21-
:tippy-options="{ duration: 100 }"
2220
:editor="editor"
2321
>
24-
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }">
25-
H1
26-
</button>
27-
<button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }">
28-
H2
29-
</button>
30-
<button @click="editor.chain().focus().toggleBulletList().run()" :class="{ 'is-active': editor.isActive('bulletList') }">
31-
Bullet list
32-
</button>
22+
<div class="floating-menu">
23+
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }">
24+
H1
25+
</button>
26+
<button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }">
27+
H2
28+
</button>
29+
<button @click="editor.chain().focus().toggleBulletList().run()" :class="{ 'is-active': editor.isActive('bulletList') }">
30+
Bullet list
31+
</button>
32+
</div>
3333
</floating-menu>
3434
</div>
3535

0 commit comments

Comments
 (0)