@tiptap/react 3.20.1 → 3.20.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -7
- package/src/menus/BubbleMenu.spec.ts +174 -0
- package/src/menus/BubbleMenu.tsx +12 -4
- package/src/menus/FloatingMenu.spec.ts +164 -0
- package/src/menus/FloatingMenu.tsx +12 -15
- package/src/menus/getAutoPluginKey.ts +5 -0
- package/src/menus/useMenuElementProps.ts +354 -0
- package/dist/index.cjs +0 -1195
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -534
- package/dist/index.d.ts +0 -534
- package/dist/index.js +0 -1131
- package/dist/index.js.map +0 -1
- package/dist/menus/index.cjs +0 -229
- package/dist/menus/index.cjs.map +0 -1
- package/dist/menus/index.d.cts +0 -19
- package/dist/menus/index.d.ts +0 -19
- package/dist/menus/index.js +0 -191
- package/dist/menus/index.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiptap/react",
|
|
3
3
|
"description": "React components for tiptap",
|
|
4
|
-
"version": "3.20.
|
|
4
|
+
"version": "3.20.3",
|
|
5
5
|
"homepage": "https://tiptap.dev",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"tiptap",
|
|
@@ -48,20 +48,20 @@
|
|
|
48
48
|
"@types/react-dom": "^19.0.0",
|
|
49
49
|
"react": "^19.0.0",
|
|
50
50
|
"react-dom": "^19.0.0",
|
|
51
|
-
"@tiptap/core": "^3.20.
|
|
52
|
-
"@tiptap/pm": "^3.20.
|
|
51
|
+
"@tiptap/core": "^3.20.3",
|
|
52
|
+
"@tiptap/pm": "^3.20.3"
|
|
53
53
|
},
|
|
54
54
|
"optionalDependencies": {
|
|
55
|
-
"@tiptap/extension-bubble-menu": "^3.20.
|
|
56
|
-
"@tiptap/extension-floating-menu": "^3.20.
|
|
55
|
+
"@tiptap/extension-bubble-menu": "^3.20.3",
|
|
56
|
+
"@tiptap/extension-floating-menu": "^3.20.3"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
60
60
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
61
61
|
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
62
62
|
"@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
63
|
-
"@tiptap/core": "^3.20.
|
|
64
|
-
"@tiptap/pm": "^3.20.
|
|
63
|
+
"@tiptap/core": "^3.20.3",
|
|
64
|
+
"@tiptap/pm": "^3.20.3"
|
|
65
65
|
},
|
|
66
66
|
"repository": {
|
|
67
67
|
"type": "git",
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { render } from '@testing-library/react'
|
|
2
|
+
import React, { createRef } from 'react'
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { BubbleMenu } from './BubbleMenu.js'
|
|
6
|
+
|
|
7
|
+
const { bubbleMenuPluginMock } = vi.hoisted(() => ({
|
|
8
|
+
bubbleMenuPluginMock: vi.fn(() => ({ key: 'bubble-menu-plugin' })),
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
vi.mock('@tiptap/extension-bubble-menu', () => ({
|
|
12
|
+
BubbleMenuPlugin: bubbleMenuPluginMock,
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
function createEditor() {
|
|
16
|
+
const tr = {
|
|
17
|
+
setMeta: vi.fn(() => tr),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
isDestroyed: false,
|
|
22
|
+
registerPlugin: vi.fn(),
|
|
23
|
+
unregisterPlugin: vi.fn(),
|
|
24
|
+
view: {
|
|
25
|
+
dispatch: vi.fn(),
|
|
26
|
+
},
|
|
27
|
+
state: {
|
|
28
|
+
tr,
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('BubbleMenu', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
bubbleMenuPluginMock.mockClear()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
document.body.innerHTML = ''
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('applies html props to the actual menu element', () => {
|
|
43
|
+
const editor = createEditor()
|
|
44
|
+
const ref = createRef<HTMLDivElement>()
|
|
45
|
+
const handleClick = vi.fn()
|
|
46
|
+
let lastEvent: any
|
|
47
|
+
const handleClickCapture = vi.fn()
|
|
48
|
+
const handleDoubleClick = vi.fn()
|
|
49
|
+
const initialProps = {
|
|
50
|
+
editor: editor as never,
|
|
51
|
+
className: 'bubble-menu',
|
|
52
|
+
'data-testid': 'menu-element',
|
|
53
|
+
'aria-label': 'Bubble menu',
|
|
54
|
+
style: { zIndex: 9999, marginTop: 8, position: 'relative' },
|
|
55
|
+
onClick: (event: unknown) => {
|
|
56
|
+
lastEvent = event
|
|
57
|
+
handleClick()
|
|
58
|
+
},
|
|
59
|
+
onClickCapture: handleClickCapture,
|
|
60
|
+
onDoubleClick: handleDoubleClick,
|
|
61
|
+
dangerouslySetInnerHTML: { __html: 'ignored' },
|
|
62
|
+
pluginKey: 'bubbleMenu',
|
|
63
|
+
tabIndex: 3,
|
|
64
|
+
ref,
|
|
65
|
+
children: React.createElement('button', { type: 'button' }, 'Menu action'),
|
|
66
|
+
} as any
|
|
67
|
+
const updatedProps = {
|
|
68
|
+
editor: editor as never,
|
|
69
|
+
className: 'bubble-menu-updated',
|
|
70
|
+
style: { zIndex: 1000, marginTop: 16, position: 'static' },
|
|
71
|
+
onClick: undefined,
|
|
72
|
+
onClickCapture: undefined,
|
|
73
|
+
onDoubleClick: undefined,
|
|
74
|
+
pluginKey: 'bubbleMenu',
|
|
75
|
+
tabIndex: undefined,
|
|
76
|
+
ref,
|
|
77
|
+
children: React.createElement('button', { type: 'button' }, 'Updated action'),
|
|
78
|
+
} as any
|
|
79
|
+
|
|
80
|
+
const { rerender, unmount } = render(React.createElement(BubbleMenu, initialProps))
|
|
81
|
+
|
|
82
|
+
expect(editor.registerPlugin).toHaveBeenCalledTimes(1)
|
|
83
|
+
expect(bubbleMenuPluginMock).toHaveBeenCalledTimes(1)
|
|
84
|
+
|
|
85
|
+
const [{ element }] = bubbleMenuPluginMock.mock.calls[0] as unknown as [{ element: HTMLDivElement }]
|
|
86
|
+
|
|
87
|
+
expect(element).toBeInstanceOf(HTMLDivElement)
|
|
88
|
+
expect(element).toBe(ref.current)
|
|
89
|
+
expect(element.className).toBe('bubble-menu')
|
|
90
|
+
expect(element.getAttribute('data-testid')).toBe('menu-element')
|
|
91
|
+
expect(element.getAttribute('aria-label')).toBe('Bubble menu')
|
|
92
|
+
expect(element.tabIndex).toBe(3)
|
|
93
|
+
expect(element.style.zIndex).toBe('9999')
|
|
94
|
+
expect(element.style.marginTop).toBe('8px')
|
|
95
|
+
expect(element.style.position).toBe('absolute')
|
|
96
|
+
expect(element.getAttribute('dangerouslySetInnerHTML')).toBeNull()
|
|
97
|
+
|
|
98
|
+
element.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
99
|
+
element.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))
|
|
100
|
+
element.querySelector('button')?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
101
|
+
|
|
102
|
+
expect(handleClick).toHaveBeenCalledTimes(2)
|
|
103
|
+
expect(handleDoubleClick).toHaveBeenCalledTimes(1)
|
|
104
|
+
expect(handleClickCapture).toHaveBeenCalledTimes(2)
|
|
105
|
+
expect(lastEvent.nativeEvent).toBeInstanceOf(MouseEvent)
|
|
106
|
+
expect(lastEvent.currentTarget).toBe(element)
|
|
107
|
+
expect(lastEvent.target).toBeInstanceOf(Element)
|
|
108
|
+
expect(typeof lastEvent.persist).toBe('function')
|
|
109
|
+
expect(element.textContent).toContain('Menu action')
|
|
110
|
+
|
|
111
|
+
rerender(React.createElement(BubbleMenu, updatedProps))
|
|
112
|
+
|
|
113
|
+
expect(element.className).toBe('bubble-menu-updated')
|
|
114
|
+
expect(element.getAttribute('data-testid')).toBeNull()
|
|
115
|
+
expect(element.getAttribute('aria-label')).toBeNull()
|
|
116
|
+
expect(element.tabIndex).toBe(-1)
|
|
117
|
+
expect(element.style.zIndex).toBe('1000')
|
|
118
|
+
expect(element.style.marginTop).toBe('16px')
|
|
119
|
+
expect(element.style.position).toBe('absolute')
|
|
120
|
+
|
|
121
|
+
element.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
122
|
+
element.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))
|
|
123
|
+
element.querySelector('button')?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
124
|
+
|
|
125
|
+
expect(handleClick).toHaveBeenCalledTimes(2)
|
|
126
|
+
expect(handleDoubleClick).toHaveBeenCalledTimes(1)
|
|
127
|
+
expect(handleClickCapture).toHaveBeenCalledTimes(2)
|
|
128
|
+
expect(element.textContent).toContain('Updated action')
|
|
129
|
+
|
|
130
|
+
unmount()
|
|
131
|
+
|
|
132
|
+
expect(editor.unregisterPlugin).toHaveBeenCalledWith('bubbleMenu')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('creates unique plugin keys when none are provided', () => {
|
|
136
|
+
const editor = createEditor()
|
|
137
|
+
const shouldShowA = vi.fn(() => true)
|
|
138
|
+
const shouldShowB = vi.fn(() => false)
|
|
139
|
+
|
|
140
|
+
const { unmount } = render(
|
|
141
|
+
React.createElement(
|
|
142
|
+
React.Fragment,
|
|
143
|
+
null,
|
|
144
|
+
React.createElement(BubbleMenu, {
|
|
145
|
+
editor: editor as never,
|
|
146
|
+
shouldShow: shouldShowA,
|
|
147
|
+
children: React.createElement('button', { type: 'button' }, 'First'),
|
|
148
|
+
} as any),
|
|
149
|
+
React.createElement(BubbleMenu, {
|
|
150
|
+
editor: editor as never,
|
|
151
|
+
shouldShow: shouldShowB,
|
|
152
|
+
children: React.createElement('button', { type: 'button' }, 'Second'),
|
|
153
|
+
} as any),
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
expect(bubbleMenuPluginMock).toHaveBeenCalledTimes(2)
|
|
158
|
+
|
|
159
|
+
const pluginCalls = bubbleMenuPluginMock.mock.calls as unknown as Array<[{ pluginKey: unknown }]>
|
|
160
|
+
const firstPluginKey = pluginCalls[0][0].pluginKey
|
|
161
|
+
const secondPluginKey = pluginCalls[1][0].pluginKey
|
|
162
|
+
|
|
163
|
+
expect(firstPluginKey).toBeDefined()
|
|
164
|
+
expect(secondPluginKey).toBeDefined()
|
|
165
|
+
expect(firstPluginKey).not.toBe(secondPluginKey)
|
|
166
|
+
|
|
167
|
+
unmount()
|
|
168
|
+
|
|
169
|
+
const unregisterCalls = editor.unregisterPlugin.mock.calls as unknown as Array<[unknown]>
|
|
170
|
+
|
|
171
|
+
expect(unregisterCalls.some(([key]) => key === firstPluginKey)).toBe(true)
|
|
172
|
+
expect(unregisterCalls.some(([key]) => key === secondPluginKey)).toBe(true)
|
|
173
|
+
})
|
|
174
|
+
})
|
package/src/menus/BubbleMenu.tsx
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { type BubbleMenuPluginProps, BubbleMenuPlugin } from '@tiptap/extension-bubble-menu'
|
|
2
|
+
import type { PluginKey } from '@tiptap/pm/state'
|
|
2
3
|
import { useCurrentEditor } from '@tiptap/react'
|
|
3
4
|
import React, { useEffect, useRef, useState } from 'react'
|
|
4
5
|
import { createPortal } from 'react-dom'
|
|
5
6
|
|
|
7
|
+
import { getAutoPluginKey } from './getAutoPluginKey.js'
|
|
8
|
+
import { useMenuElementProps } from './useMenuElementProps.js'
|
|
9
|
+
|
|
6
10
|
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
|
|
7
11
|
|
|
8
12
|
export type BubbleMenuProps = Optional<Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>, 'element'>, 'editor'> &
|
|
@@ -11,7 +15,7 @@ export type BubbleMenuProps = Optional<Omit<Optional<BubbleMenuPluginProps, 'plu
|
|
|
11
15
|
export const BubbleMenu = React.forwardRef<HTMLDivElement, BubbleMenuProps>(
|
|
12
16
|
(
|
|
13
17
|
{
|
|
14
|
-
pluginKey
|
|
18
|
+
pluginKey,
|
|
15
19
|
editor,
|
|
16
20
|
updateDelay,
|
|
17
21
|
resizeDelay,
|
|
@@ -25,6 +29,9 @@ export const BubbleMenu = React.forwardRef<HTMLDivElement, BubbleMenuProps>(
|
|
|
25
29
|
ref,
|
|
26
30
|
) => {
|
|
27
31
|
const menuEl = useRef(document.createElement('div'))
|
|
32
|
+
const resolvedPluginKey = useRef<PluginKey | string>(getAutoPluginKey(pluginKey, 'bubbleMenu')).current
|
|
33
|
+
|
|
34
|
+
useMenuElementProps(menuEl.current, restProps)
|
|
28
35
|
|
|
29
36
|
if (typeof ref === 'function') {
|
|
30
37
|
ref(menuEl.current)
|
|
@@ -45,7 +52,7 @@ export const BubbleMenu = React.forwardRef<HTMLDivElement, BubbleMenuProps>(
|
|
|
45
52
|
updateDelay,
|
|
46
53
|
resizeDelay,
|
|
47
54
|
appendTo,
|
|
48
|
-
pluginKey,
|
|
55
|
+
pluginKey: resolvedPluginKey,
|
|
49
56
|
shouldShow,
|
|
50
57
|
getReferencedVirtualElement,
|
|
51
58
|
options,
|
|
@@ -124,7 +131,7 @@ export const BubbleMenu = React.forwardRef<HTMLDivElement, BubbleMenuProps>(
|
|
|
124
131
|
}
|
|
125
132
|
|
|
126
133
|
pluginEditor.view.dispatch(
|
|
127
|
-
pluginEditor.state.tr.setMeta(
|
|
134
|
+
pluginEditor.state.tr.setMeta(resolvedPluginKey, {
|
|
128
135
|
type: 'updateOptions',
|
|
129
136
|
options: bubbleMenuPluginPropsRef.current,
|
|
130
137
|
}),
|
|
@@ -138,8 +145,9 @@ export const BubbleMenu = React.forwardRef<HTMLDivElement, BubbleMenuProps>(
|
|
|
138
145
|
options,
|
|
139
146
|
appendTo,
|
|
140
147
|
getReferencedVirtualElement,
|
|
148
|
+
resolvedPluginKey,
|
|
141
149
|
])
|
|
142
150
|
|
|
143
|
-
return createPortal(
|
|
151
|
+
return createPortal(children, menuEl.current)
|
|
144
152
|
},
|
|
145
153
|
)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { render } from '@testing-library/react'
|
|
2
|
+
import React, { createRef } from 'react'
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { FloatingMenu } from './FloatingMenu.js'
|
|
6
|
+
|
|
7
|
+
const { floatingMenuPluginMock } = vi.hoisted(() => ({
|
|
8
|
+
floatingMenuPluginMock: vi.fn(() => ({ key: 'floating-menu-plugin' })),
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
vi.mock('@tiptap/extension-floating-menu', () => ({
|
|
12
|
+
FloatingMenuPlugin: floatingMenuPluginMock,
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
function createEditor() {
|
|
16
|
+
const tr = {
|
|
17
|
+
setMeta: vi.fn(() => tr),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
isDestroyed: false,
|
|
22
|
+
registerPlugin: vi.fn(),
|
|
23
|
+
unregisterPlugin: vi.fn(),
|
|
24
|
+
view: {
|
|
25
|
+
dispatch: vi.fn(),
|
|
26
|
+
},
|
|
27
|
+
state: {
|
|
28
|
+
tr,
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('FloatingMenu', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
floatingMenuPluginMock.mockClear()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
document.body.innerHTML = ''
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('applies html props to the actual menu element', () => {
|
|
43
|
+
const editor = createEditor()
|
|
44
|
+
const ref = createRef<HTMLDivElement>()
|
|
45
|
+
const handleClick = vi.fn()
|
|
46
|
+
const handleClickCapture = vi.fn()
|
|
47
|
+
const handleDoubleClick = vi.fn()
|
|
48
|
+
const initialProps = {
|
|
49
|
+
editor: editor as never,
|
|
50
|
+
className: 'floating-menu',
|
|
51
|
+
'data-testid': 'floating-element',
|
|
52
|
+
'aria-label': 'Floating menu',
|
|
53
|
+
style: { zIndex: 8888, marginTop: 12, position: 'relative' },
|
|
54
|
+
onClick: handleClick,
|
|
55
|
+
onClickCapture: handleClickCapture,
|
|
56
|
+
onDoubleClick: handleDoubleClick,
|
|
57
|
+
pluginKey: 'floatingMenu',
|
|
58
|
+
tabIndex: 5,
|
|
59
|
+
ref,
|
|
60
|
+
children: React.createElement('button', { type: 'button' }, 'Floating action'),
|
|
61
|
+
} as any
|
|
62
|
+
const updatedProps = {
|
|
63
|
+
editor: editor as never,
|
|
64
|
+
className: 'floating-menu-updated',
|
|
65
|
+
style: { zIndex: 7777, marginTop: 20, position: 'static' },
|
|
66
|
+
onClick: undefined,
|
|
67
|
+
onClickCapture: undefined,
|
|
68
|
+
onDoubleClick: undefined,
|
|
69
|
+
pluginKey: 'floatingMenu',
|
|
70
|
+
tabIndex: undefined,
|
|
71
|
+
ref,
|
|
72
|
+
children: React.createElement('button', { type: 'button' }, 'Updated floating action'),
|
|
73
|
+
} as any
|
|
74
|
+
|
|
75
|
+
const { rerender, unmount } = render(React.createElement(FloatingMenu, initialProps))
|
|
76
|
+
|
|
77
|
+
expect(editor.registerPlugin).toHaveBeenCalledTimes(1)
|
|
78
|
+
expect(floatingMenuPluginMock).toHaveBeenCalledTimes(1)
|
|
79
|
+
|
|
80
|
+
const [{ element }] = floatingMenuPluginMock.mock.calls[0] as unknown as [{ element: HTMLDivElement }]
|
|
81
|
+
|
|
82
|
+
expect(element).toBeInstanceOf(HTMLDivElement)
|
|
83
|
+
expect(element).toBe(ref.current)
|
|
84
|
+
expect(element.className).toBe('floating-menu')
|
|
85
|
+
expect(element.getAttribute('data-testid')).toBe('floating-element')
|
|
86
|
+
expect(element.getAttribute('aria-label')).toBe('Floating menu')
|
|
87
|
+
expect(element.tabIndex).toBe(5)
|
|
88
|
+
expect(element.style.zIndex).toBe('8888')
|
|
89
|
+
expect(element.style.marginTop).toBe('12px')
|
|
90
|
+
expect(element.style.position).toBe('absolute')
|
|
91
|
+
|
|
92
|
+
element.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
93
|
+
element.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))
|
|
94
|
+
element.querySelector('button')?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
95
|
+
|
|
96
|
+
expect(handleClick).toHaveBeenCalledTimes(2)
|
|
97
|
+
expect(handleDoubleClick).toHaveBeenCalledTimes(1)
|
|
98
|
+
expect(handleClickCapture).toHaveBeenCalledTimes(2)
|
|
99
|
+
expect(element.textContent).toContain('Floating action')
|
|
100
|
+
|
|
101
|
+
rerender(React.createElement(FloatingMenu, updatedProps))
|
|
102
|
+
|
|
103
|
+
expect(element.className).toBe('floating-menu-updated')
|
|
104
|
+
expect(element.getAttribute('data-testid')).toBeNull()
|
|
105
|
+
expect(element.getAttribute('aria-label')).toBeNull()
|
|
106
|
+
expect(element.tabIndex).toBe(-1)
|
|
107
|
+
expect(element.style.zIndex).toBe('7777')
|
|
108
|
+
expect(element.style.marginTop).toBe('20px')
|
|
109
|
+
expect(element.style.position).toBe('absolute')
|
|
110
|
+
|
|
111
|
+
element.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
112
|
+
element.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))
|
|
113
|
+
element.querySelector('button')?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
114
|
+
|
|
115
|
+
expect(handleClick).toHaveBeenCalledTimes(2)
|
|
116
|
+
expect(handleDoubleClick).toHaveBeenCalledTimes(1)
|
|
117
|
+
expect(handleClickCapture).toHaveBeenCalledTimes(2)
|
|
118
|
+
expect(element.textContent).toContain('Updated floating action')
|
|
119
|
+
|
|
120
|
+
unmount()
|
|
121
|
+
|
|
122
|
+
expect(editor.unregisterPlugin).toHaveBeenCalledWith('floatingMenu')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('creates unique plugin keys when none are provided', () => {
|
|
126
|
+
const editor = createEditor()
|
|
127
|
+
const shouldShowA = vi.fn(() => true)
|
|
128
|
+
const shouldShowB = vi.fn(() => false)
|
|
129
|
+
|
|
130
|
+
const { unmount } = render(
|
|
131
|
+
React.createElement(
|
|
132
|
+
React.Fragment,
|
|
133
|
+
null,
|
|
134
|
+
React.createElement(FloatingMenu, {
|
|
135
|
+
editor: editor as never,
|
|
136
|
+
shouldShow: shouldShowA,
|
|
137
|
+
children: React.createElement('button', { type: 'button' }, 'First'),
|
|
138
|
+
} as any),
|
|
139
|
+
React.createElement(FloatingMenu, {
|
|
140
|
+
editor: editor as never,
|
|
141
|
+
shouldShow: shouldShowB,
|
|
142
|
+
children: React.createElement('button', { type: 'button' }, 'Second'),
|
|
143
|
+
} as any),
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
expect(floatingMenuPluginMock).toHaveBeenCalledTimes(2)
|
|
148
|
+
|
|
149
|
+
const pluginCalls = floatingMenuPluginMock.mock.calls as unknown as Array<[{ pluginKey: unknown }]>
|
|
150
|
+
const firstPluginKey = pluginCalls[0][0].pluginKey
|
|
151
|
+
const secondPluginKey = pluginCalls[1][0].pluginKey
|
|
152
|
+
|
|
153
|
+
expect(firstPluginKey).toBeDefined()
|
|
154
|
+
expect(secondPluginKey).toBeDefined()
|
|
155
|
+
expect(firstPluginKey).not.toBe(secondPluginKey)
|
|
156
|
+
|
|
157
|
+
unmount()
|
|
158
|
+
|
|
159
|
+
const unregisterCalls = editor.unregisterPlugin.mock.calls as unknown as Array<[unknown]>
|
|
160
|
+
|
|
161
|
+
expect(unregisterCalls.some(([key]) => key === firstPluginKey)).toBe(true)
|
|
162
|
+
expect(unregisterCalls.some(([key]) => key === secondPluginKey)).toBe(true)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { FloatingMenuPluginProps } from '@tiptap/extension-floating-menu'
|
|
2
2
|
import { FloatingMenuPlugin } from '@tiptap/extension-floating-menu'
|
|
3
|
+
import type { PluginKey } from '@tiptap/pm/state'
|
|
3
4
|
import { useCurrentEditor } from '@tiptap/react'
|
|
4
5
|
import React, { useEffect, useRef, useState } from 'react'
|
|
5
6
|
import { createPortal } from 'react-dom'
|
|
6
7
|
|
|
8
|
+
import { getAutoPluginKey } from './getAutoPluginKey.js'
|
|
9
|
+
import { useMenuElementProps } from './useMenuElementProps.js'
|
|
10
|
+
|
|
7
11
|
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
|
|
8
12
|
|
|
9
13
|
export type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKey'>, 'element' | 'editor'> & {
|
|
@@ -13,20 +17,13 @@ export type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKe
|
|
|
13
17
|
|
|
14
18
|
export const FloatingMenu = React.forwardRef<HTMLDivElement, FloatingMenuProps>(
|
|
15
19
|
(
|
|
16
|
-
{
|
|
17
|
-
pluginKey = 'floatingMenu',
|
|
18
|
-
editor,
|
|
19
|
-
updateDelay,
|
|
20
|
-
resizeDelay,
|
|
21
|
-
appendTo,
|
|
22
|
-
shouldShow = null,
|
|
23
|
-
options,
|
|
24
|
-
children,
|
|
25
|
-
...restProps
|
|
26
|
-
},
|
|
20
|
+
{ pluginKey, editor, updateDelay, resizeDelay, appendTo, shouldShow = null, options, children, ...restProps },
|
|
27
21
|
ref,
|
|
28
22
|
) => {
|
|
29
23
|
const menuEl = useRef(document.createElement('div'))
|
|
24
|
+
const resolvedPluginKey = useRef<PluginKey | string>(getAutoPluginKey(pluginKey, 'floatingMenu')).current
|
|
25
|
+
|
|
26
|
+
useMenuElementProps(menuEl.current, restProps)
|
|
30
27
|
|
|
31
28
|
if (typeof ref === 'function') {
|
|
32
29
|
ref(menuEl.current)
|
|
@@ -47,7 +44,7 @@ export const FloatingMenu = React.forwardRef<HTMLDivElement, FloatingMenuProps>(
|
|
|
47
44
|
updateDelay,
|
|
48
45
|
resizeDelay,
|
|
49
46
|
appendTo,
|
|
50
|
-
pluginKey,
|
|
47
|
+
pluginKey: resolvedPluginKey,
|
|
51
48
|
shouldShow,
|
|
52
49
|
options,
|
|
53
50
|
}
|
|
@@ -128,13 +125,13 @@ export const FloatingMenu = React.forwardRef<HTMLDivElement, FloatingMenuProps>(
|
|
|
128
125
|
}
|
|
129
126
|
|
|
130
127
|
pluginEditor.view.dispatch(
|
|
131
|
-
pluginEditor.state.tr.setMeta(
|
|
128
|
+
pluginEditor.state.tr.setMeta(resolvedPluginKey, {
|
|
132
129
|
type: 'updateOptions',
|
|
133
130
|
options: floatingMenuPluginPropsRef.current,
|
|
134
131
|
}),
|
|
135
132
|
)
|
|
136
|
-
}, [pluginInitialized, pluginEditor, updateDelay, resizeDelay, shouldShow, options, appendTo])
|
|
133
|
+
}, [pluginInitialized, pluginEditor, updateDelay, resizeDelay, shouldShow, options, appendTo, resolvedPluginKey])
|
|
137
134
|
|
|
138
|
-
return createPortal(
|
|
135
|
+
return createPortal(children, menuEl.current)
|
|
139
136
|
},
|
|
140
137
|
)
|