@blockslides/react 0.1.0

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.
@@ -0,0 +1,344 @@
1
+ import type {
2
+ DecorationWithType,
3
+ Editor,
4
+ NodeViewRenderer,
5
+ NodeViewRendererOptions,
6
+ NodeViewRendererProps,
7
+ } from '@blockslides/core'
8
+ import { getRenderedAttributes, NodeView } from '@blockslides/core'
9
+ import type { Node, Node as ProseMirrorNode } from '@blockslides/pm/model'
10
+ import type { Decoration, DecorationSource, NodeView as ProseMirrorNodeView } from '@blockslides/pm/view'
11
+ import type { ComponentType, NamedExoticComponent } from 'react'
12
+ import { createElement, createRef, memo } from 'react'
13
+
14
+ import type { EditorWithContentComponent } from './Editor.js'
15
+ import { ReactRenderer } from './ReactRenderer.js'
16
+ import type { ReactNodeViewProps } from './types.js'
17
+ import type { ReactNodeViewContextProps } from './useReactNodeView.js'
18
+ import { ReactNodeViewContext } from './useReactNodeView.js'
19
+
20
+ export interface ReactNodeViewRendererOptions extends NodeViewRendererOptions {
21
+ /**
22
+ * This function is called when the node view is updated.
23
+ * It allows you to compare the old node with the new node and decide if the component should update.
24
+ */
25
+ update:
26
+ | ((props: {
27
+ oldNode: ProseMirrorNode
28
+ oldDecorations: readonly Decoration[]
29
+ oldInnerDecorations: DecorationSource
30
+ newNode: ProseMirrorNode
31
+ newDecorations: readonly Decoration[]
32
+ innerDecorations: DecorationSource
33
+ updateProps: () => void
34
+ }) => boolean)
35
+ | null
36
+ /**
37
+ * The tag name of the element wrapping the React component.
38
+ */
39
+ as?: string
40
+ /**
41
+ * The class name of the element wrapping the React component.
42
+ */
43
+ className?: string
44
+ /**
45
+ * Attributes that should be applied to the element wrapping the React component.
46
+ * If this is a function, it will be called each time the node view is updated.
47
+ * If this is an object, it will be applied once when the node view is mounted.
48
+ */
49
+ attrs?:
50
+ | Record<string, string>
51
+ | ((props: { node: ProseMirrorNode; HTMLAttributes: Record<string, any> }) => Record<string, string>)
52
+ }
53
+
54
+ export class ReactNodeView<
55
+ T = HTMLElement,
56
+ Component extends ComponentType<ReactNodeViewProps<T>> = ComponentType<ReactNodeViewProps<T>>,
57
+ NodeEditor extends Editor = Editor,
58
+ Options extends ReactNodeViewRendererOptions = ReactNodeViewRendererOptions,
59
+ > extends NodeView<Component, NodeEditor, Options> {
60
+ /**
61
+ * The renderer instance.
62
+ */
63
+ renderer!: ReactRenderer<unknown, ReactNodeViewProps<T>>
64
+
65
+ /**
66
+ * The element that holds the rich-text content of the node.
67
+ */
68
+ contentDOMElement!: HTMLElement | null
69
+
70
+ constructor(component: Component, props: NodeViewRendererProps, options?: Partial<Options>) {
71
+ super(component, props, options)
72
+
73
+ if (!this.node.isLeaf) {
74
+ if (this.options.contentDOMElementTag) {
75
+ this.contentDOMElement = document.createElement(this.options.contentDOMElementTag)
76
+ } else {
77
+ this.contentDOMElement = document.createElement(this.node.isInline ? 'span' : 'div')
78
+ }
79
+
80
+ this.contentDOMElement.dataset.nodeViewContentReact = ''
81
+ this.contentDOMElement.dataset.nodeViewWrapper = ''
82
+
83
+ // For some reason the whiteSpace prop is not inherited properly in Chrome and Safari
84
+ // With this fix it seems to work fine
85
+ // See: https://github.com/ueberdosis/tiptap/issues/1197
86
+ this.contentDOMElement.style.whiteSpace = 'inherit'
87
+
88
+ const contentTarget = this.dom.querySelector('[data-node-view-content]')
89
+
90
+ if (!contentTarget) {
91
+ return
92
+ }
93
+
94
+ contentTarget.appendChild(this.contentDOMElement)
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Setup the React component.
100
+ * Called on initialization.
101
+ */
102
+ mount() {
103
+ const props = {
104
+ editor: this.editor,
105
+ node: this.node,
106
+ decorations: this.decorations as DecorationWithType[],
107
+ innerDecorations: this.innerDecorations,
108
+ view: this.view,
109
+ selected: false,
110
+ extension: this.extension,
111
+ HTMLAttributes: this.HTMLAttributes,
112
+ getPos: () => this.getPos(),
113
+ updateAttributes: (attributes = {}) => this.updateAttributes(attributes),
114
+ deleteNode: () => this.deleteNode(),
115
+ ref: createRef<T>(),
116
+ } satisfies ReactNodeViewProps<T>
117
+
118
+ if (!(this.component as any).displayName) {
119
+ const capitalizeFirstChar = (string: string): string => {
120
+ return string.charAt(0).toUpperCase() + string.substring(1)
121
+ }
122
+
123
+ this.component.displayName = capitalizeFirstChar(this.extension.name)
124
+ }
125
+
126
+ const onDragStart = this.onDragStart.bind(this)
127
+ const nodeViewContentRef: ReactNodeViewContextProps['nodeViewContentRef'] = element => {
128
+ if (element && this.contentDOMElement && element.firstChild !== this.contentDOMElement) {
129
+ // remove the nodeViewWrapper attribute from the element
130
+ if (element.hasAttribute('data-node-view-wrapper')) {
131
+ element.removeAttribute('data-node-view-wrapper')
132
+ }
133
+ element.appendChild(this.contentDOMElement)
134
+ }
135
+ }
136
+ const context = { onDragStart, nodeViewContentRef }
137
+ const Component = this.component
138
+ // For performance reasons, we memoize the provider component
139
+ // And all of the things it requires are declared outside of the component, so it doesn't need to re-render
140
+ const ReactNodeViewProvider: NamedExoticComponent<ReactNodeViewProps<T>> = memo(componentProps => {
141
+ return (
142
+ <ReactNodeViewContext.Provider value={context}>
143
+ {createElement(Component, componentProps)}
144
+ </ReactNodeViewContext.Provider>
145
+ )
146
+ })
147
+
148
+ ReactNodeViewProvider.displayName = 'ReactNodeView'
149
+
150
+ let as = this.node.isInline ? 'span' : 'div'
151
+
152
+ if (this.options.as) {
153
+ as = this.options.as
154
+ }
155
+
156
+ const { className = '' } = this.options
157
+
158
+ this.handleSelectionUpdate = this.handleSelectionUpdate.bind(this)
159
+
160
+ this.renderer = new ReactRenderer(ReactNodeViewProvider, {
161
+ editor: this.editor,
162
+ props,
163
+ as,
164
+ className: `node-${this.node.type.name} ${className}`.trim(),
165
+ })
166
+
167
+ this.editor.on('selectionUpdate', this.handleSelectionUpdate)
168
+ this.updateElementAttributes()
169
+ }
170
+
171
+ /**
172
+ * Return the DOM element.
173
+ * This is the element that will be used to display the node view.
174
+ */
175
+ get dom() {
176
+ if (
177
+ this.renderer.element.firstElementChild &&
178
+ !this.renderer.element.firstElementChild?.hasAttribute('data-node-view-wrapper')
179
+ ) {
180
+ throw Error('Please use the NodeViewWrapper component for your node view.')
181
+ }
182
+
183
+ return this.renderer.element
184
+ }
185
+
186
+ /**
187
+ * Return the content DOM element.
188
+ * This is the element that will be used to display the rich-text content of the node.
189
+ */
190
+ get contentDOM() {
191
+ if (this.node.isLeaf) {
192
+ return null
193
+ }
194
+
195
+ return this.contentDOMElement
196
+ }
197
+
198
+ /**
199
+ * On editor selection update, check if the node is selected.
200
+ * If it is, call `selectNode`, otherwise call `deselectNode`.
201
+ */
202
+ handleSelectionUpdate() {
203
+ const { from, to } = this.editor.state.selection
204
+ const pos = this.getPos()
205
+
206
+ if (typeof pos !== 'number') {
207
+ return
208
+ }
209
+
210
+ if (from <= pos && to >= pos + this.node.nodeSize) {
211
+ if (this.renderer.props.selected) {
212
+ return
213
+ }
214
+
215
+ this.selectNode()
216
+ } else {
217
+ if (!this.renderer.props.selected) {
218
+ return
219
+ }
220
+
221
+ this.deselectNode()
222
+ }
223
+ }
224
+
225
+ /**
226
+ * On update, update the React component.
227
+ * To prevent unnecessary updates, the `update` option can be used.
228
+ */
229
+ update(node: Node, decorations: readonly Decoration[], innerDecorations: DecorationSource): boolean {
230
+ const rerenderComponent = (props?: Record<string, any>) => {
231
+ this.renderer.updateProps(props)
232
+ if (typeof this.options.attrs === 'function') {
233
+ this.updateElementAttributes()
234
+ }
235
+ }
236
+
237
+ if (node.type !== this.node.type) {
238
+ return false
239
+ }
240
+
241
+ if (typeof this.options.update === 'function') {
242
+ const oldNode = this.node
243
+ const oldDecorations = this.decorations
244
+ const oldInnerDecorations = this.innerDecorations
245
+
246
+ this.node = node
247
+ this.decorations = decorations
248
+ this.innerDecorations = innerDecorations
249
+
250
+ return this.options.update({
251
+ oldNode,
252
+ oldDecorations,
253
+ newNode: node,
254
+ newDecorations: decorations,
255
+ oldInnerDecorations,
256
+ innerDecorations,
257
+ updateProps: () => rerenderComponent({ node, decorations, innerDecorations }),
258
+ })
259
+ }
260
+
261
+ if (node === this.node && this.decorations === decorations && this.innerDecorations === innerDecorations) {
262
+ return true
263
+ }
264
+
265
+ this.node = node
266
+ this.decorations = decorations
267
+ this.innerDecorations = innerDecorations
268
+
269
+ rerenderComponent({ node, decorations, innerDecorations })
270
+
271
+ return true
272
+ }
273
+
274
+ /**
275
+ * Select the node.
276
+ * Add the `selected` prop and the `ProseMirror-selectednode` class.
277
+ */
278
+ selectNode() {
279
+ this.renderer.updateProps({
280
+ selected: true,
281
+ })
282
+ this.renderer.element.classList.add('ProseMirror-selectednode')
283
+ }
284
+
285
+ /**
286
+ * Deselect the node.
287
+ * Remove the `selected` prop and the `ProseMirror-selectednode` class.
288
+ */
289
+ deselectNode() {
290
+ this.renderer.updateProps({
291
+ selected: false,
292
+ })
293
+ this.renderer.element.classList.remove('ProseMirror-selectednode')
294
+ }
295
+
296
+ /**
297
+ * Destroy the React component instance.
298
+ */
299
+ destroy() {
300
+ this.renderer.destroy()
301
+ this.editor.off('selectionUpdate', this.handleSelectionUpdate)
302
+ this.contentDOMElement = null
303
+ }
304
+
305
+ /**
306
+ * Update the attributes of the top-level element that holds the React component.
307
+ * Applying the attributes defined in the `attrs` option.
308
+ */
309
+ updateElementAttributes() {
310
+ if (this.options.attrs) {
311
+ let attrsObj: Record<string, string> = {}
312
+
313
+ if (typeof this.options.attrs === 'function') {
314
+ const extensionAttributes = this.editor.extensionManager.attributes
315
+ const HTMLAttributes = getRenderedAttributes(this.node, extensionAttributes)
316
+
317
+ attrsObj = this.options.attrs({ node: this.node, HTMLAttributes })
318
+ } else {
319
+ attrsObj = this.options.attrs
320
+ }
321
+
322
+ this.renderer.updateAttributes(attrsObj)
323
+ }
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Create a React node view renderer.
329
+ */
330
+ export function ReactNodeViewRenderer<T = HTMLElement>(
331
+ component: ComponentType<ReactNodeViewProps<T>>,
332
+ options?: Partial<ReactNodeViewRendererOptions>,
333
+ ): NodeViewRenderer {
334
+ return props => {
335
+ // try to get the parent component
336
+ // this is important for vue devtools to show the component hierarchy correctly
337
+ // maybe it’s `undefined` because <editor-content> isn’t rendered yet
338
+ if (!(props.editor as EditorWithContentComponent).contentComponent) {
339
+ return {} as unknown as ProseMirrorNodeView
340
+ }
341
+
342
+ return new ReactNodeView<T>(component, props, options)
343
+ }
344
+ }
@@ -0,0 +1,265 @@
1
+ import type { Editor } from '@blockslides/core'
2
+ import type {
3
+ ComponentClass,
4
+ ForwardRefExoticComponent,
5
+ FunctionComponent,
6
+ PropsWithoutRef,
7
+ ReactNode,
8
+ RefAttributes,
9
+ } from 'react'
10
+ import { version as reactVersion } from 'react'
11
+ import { flushSync } from 'react-dom'
12
+
13
+ import type { EditorWithContentComponent } from './Editor.js'
14
+
15
+ /**
16
+ * Check if a component is a class component.
17
+ * @param Component
18
+ * @returns {boolean}
19
+ */
20
+ function isClassComponent(Component: any) {
21
+ return !!(typeof Component === 'function' && Component.prototype && Component.prototype.isReactComponent)
22
+ }
23
+
24
+ /**
25
+ * Check if a component is a forward ref component.
26
+ * @param Component
27
+ * @returns {boolean}
28
+ */
29
+ function isForwardRefComponent(Component: any) {
30
+ return !!(
31
+ typeof Component === 'object' &&
32
+ Component.$$typeof &&
33
+ (Component.$$typeof.toString() === 'Symbol(react.forward_ref)' ||
34
+ Component.$$typeof.description === 'react.forward_ref')
35
+ )
36
+ }
37
+
38
+ /**
39
+ * Check if a component is a memoized component.
40
+ * @param Component
41
+ * @returns {boolean}
42
+ */
43
+ function isMemoComponent(Component: any) {
44
+ return !!(
45
+ typeof Component === 'object' &&
46
+ Component.$$typeof &&
47
+ (Component.$$typeof.toString() === 'Symbol(react.memo)' || Component.$$typeof.description === 'react.memo')
48
+ )
49
+ }
50
+
51
+ /**
52
+ * Check if a component can safely receive a ref prop.
53
+ * This includes class components, forwardRef components, and memoized components
54
+ * that wrap forwardRef or class components.
55
+ * @param Component
56
+ * @returns {boolean}
57
+ */
58
+ function canReceiveRef(Component: any) {
59
+ // Check if it's a class component
60
+ if (isClassComponent(Component)) {
61
+ return true
62
+ }
63
+
64
+ // Check if it's a forwardRef component
65
+ if (isForwardRefComponent(Component)) {
66
+ return true
67
+ }
68
+
69
+ // Check if it's a memoized component
70
+ if (isMemoComponent(Component)) {
71
+ // For memoized components, check the wrapped component
72
+ const wrappedComponent = Component.type
73
+ if (wrappedComponent) {
74
+ return isClassComponent(wrappedComponent) || isForwardRefComponent(wrappedComponent)
75
+ }
76
+ }
77
+
78
+ return false
79
+ }
80
+
81
+ /**
82
+ * Check if we're running React 19+ by detecting if function components support ref props
83
+ * @returns {boolean}
84
+ */
85
+ function isReact19Plus(): boolean {
86
+ // React 19 is detected by checking React version if available
87
+ // In practice, we'll use a more conservative approach and assume React 18 behavior
88
+ // unless we can definitively detect React 19
89
+ try {
90
+ // @ts-ignore
91
+ if (reactVersion) {
92
+ const majorVersion = parseInt(reactVersion.split('.')[0], 10)
93
+ return majorVersion >= 19
94
+ }
95
+ } catch {
96
+ // Fallback to React 18 behavior if we can't determine version
97
+ }
98
+ return false
99
+ }
100
+
101
+ export interface ReactRendererOptions {
102
+ /**
103
+ * The editor instance.
104
+ * @type {Editor}
105
+ */
106
+ editor: Editor
107
+
108
+ /**
109
+ * The props for the component.
110
+ * @type {Record<string, any>}
111
+ * @default {}
112
+ */
113
+ props?: Record<string, any>
114
+
115
+ /**
116
+ * The tag name of the element.
117
+ * @type {string}
118
+ * @default 'div'
119
+ */
120
+ as?: string
121
+
122
+ /**
123
+ * The class name of the element.
124
+ * @type {string}
125
+ * @default ''
126
+ * @example 'foo bar'
127
+ */
128
+ className?: string
129
+ }
130
+
131
+ type ComponentType<R, P> =
132
+ | ComponentClass<P>
133
+ | FunctionComponent<P>
134
+ | ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<R>>
135
+
136
+ /**
137
+ * The ReactRenderer class. It's responsible for rendering React components inside the editor.
138
+ * @example
139
+ * new ReactRenderer(MyComponent, {
140
+ * editor,
141
+ * props: {
142
+ * foo: 'bar',
143
+ * },
144
+ * as: 'span',
145
+ * })
146
+ */
147
+ export class ReactRenderer<R = unknown, P extends Record<string, any> = object> {
148
+ id: string
149
+
150
+ editor: Editor
151
+
152
+ component: any
153
+
154
+ element: HTMLElement
155
+
156
+ props: P
157
+
158
+ reactElement: ReactNode
159
+
160
+ ref: R | null = null
161
+
162
+ /**
163
+ * Immediately creates element and renders the provided React component.
164
+ */
165
+ constructor(
166
+ component: ComponentType<R, P>,
167
+ { editor, props = {}, as = 'div', className = '' }: ReactRendererOptions,
168
+ ) {
169
+ this.id = Math.floor(Math.random() * 0xffffffff).toString()
170
+ this.component = component
171
+ this.editor = editor as EditorWithContentComponent
172
+ this.props = props as P
173
+ this.element = document.createElement(as)
174
+ this.element.classList.add('react-renderer')
175
+
176
+ if (className) {
177
+ this.element.classList.add(...className.split(' '))
178
+ }
179
+
180
+ // If the editor is already initialized, we will need to
181
+ // synchronously render the component to ensure it renders
182
+ // together with Prosemirror's rendering.
183
+ if (this.editor.isInitialized) {
184
+ flushSync(() => {
185
+ this.render()
186
+ })
187
+ } else {
188
+ queueMicrotask(() => {
189
+ this.render()
190
+ })
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Render the React component.
196
+ */
197
+ render(): void {
198
+ const Component = this.component
199
+ const props = this.props
200
+ const editor = this.editor as EditorWithContentComponent
201
+
202
+ // Handle ref forwarding with React 18/19 compatibility
203
+ const isReact19 = isReact19Plus()
204
+ const componentCanReceiveRef = canReceiveRef(Component)
205
+
206
+ const elementProps = { ...props }
207
+
208
+ // Always remove ref if the component cannot receive it (unless React 19+)
209
+ if (elementProps.ref && !(isReact19 || componentCanReceiveRef)) {
210
+ delete elementProps.ref
211
+ }
212
+
213
+ // Only assign our own ref if allowed
214
+ if (!elementProps.ref && (isReact19 || componentCanReceiveRef)) {
215
+ // @ts-ignore - Setting ref prop for compatible components
216
+ elementProps.ref = (ref: R) => {
217
+ this.ref = ref
218
+ }
219
+ }
220
+
221
+ this.reactElement = <Component {...elementProps} />
222
+
223
+ editor?.contentComponent?.setRenderer(this.id, this)
224
+ }
225
+
226
+ /**
227
+ * Re-renders the React component with new props.
228
+ */
229
+ updateProps(props: Record<string, any> = {}): void {
230
+ this.props = {
231
+ ...this.props,
232
+ ...props,
233
+ }
234
+
235
+ this.render()
236
+ }
237
+
238
+ /**
239
+ * Destroy the React component.
240
+ */
241
+ destroy(): void {
242
+ const editor = this.editor as EditorWithContentComponent
243
+
244
+ editor?.contentComponent?.removeRenderer(this.id)
245
+ // If the consumer appended the element to the document (for example
246
+ // many demos append the renderer element to document.body), make sure
247
+ // we remove it here to avoid leaking DOM nodes / React roots.
248
+ try {
249
+ if (this.element && this.element.parentNode) {
250
+ this.element.parentNode.removeChild(this.element)
251
+ }
252
+ } catch {
253
+ // ignore DOM removal errors
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Update the attributes of the element that holds the React component.
259
+ */
260
+ updateAttributes(attributes: Record<string, string>): void {
261
+ Object.keys(attributes).forEach(key => {
262
+ this.element.setAttribute(key, attributes[key])
263
+ })
264
+ }
265
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export * from "./Context.js";
2
+ export * from "./EditorContent.js";
3
+ export * from "./NodeViewContent.js";
4
+ export * from "./NodeViewWrapper.js";
5
+ export * from "./ReactMarkViewRenderer.js";
6
+ export * from "./ReactNodeViewRenderer.js";
7
+ export * from "./ReactRenderer.js";
8
+ export * from "./types.js";
9
+ export * from "./useEditor.js";
10
+ export * from "./useEditorState.js";
11
+ export * from "./useReactNodeView.js";
12
+ export * from "@blockslides/core";
@@ -0,0 +1,89 @@
1
+ import { type BubbleMenuPluginProps, BubbleMenuPlugin } from '@blockslides/extension-bubble-menu'
2
+ import { useCurrentEditor } from '@blockslides/react'
3
+ import React, { useEffect, useRef } from 'react'
4
+ import { createPortal } from 'react-dom'
5
+
6
+ type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
7
+
8
+ export type BubbleMenuProps = Optional<Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>, 'element'>, 'editor'> &
9
+ React.HTMLAttributes<HTMLDivElement>
10
+
11
+ export const BubbleMenu = React.forwardRef<HTMLDivElement, BubbleMenuProps>(
12
+ (
13
+ {
14
+ pluginKey = 'bubbleMenu',
15
+ editor,
16
+ updateDelay,
17
+ resizeDelay,
18
+ appendTo,
19
+ shouldShow = null,
20
+ getReferencedVirtualElement,
21
+ options,
22
+ children,
23
+ ...restProps
24
+ },
25
+ ref,
26
+ ) => {
27
+ const menuEl = useRef(document.createElement('div'))
28
+
29
+ if (typeof ref === 'function') {
30
+ ref(menuEl.current)
31
+ } else if (ref) {
32
+ ref.current = menuEl.current
33
+ }
34
+
35
+ const { editor: currentEditor } = useCurrentEditor()
36
+
37
+ useEffect(() => {
38
+ const bubbleMenuElement = menuEl.current
39
+ bubbleMenuElement.style.visibility = 'hidden'
40
+ bubbleMenuElement.style.position = 'absolute'
41
+
42
+ if (editor?.isDestroyed || (currentEditor as any)?.isDestroyed) {
43
+ return
44
+ }
45
+
46
+ const attachToEditor = editor || currentEditor
47
+
48
+ if (!attachToEditor) {
49
+ console.warn('BubbleMenu component is not rendered inside of an editor component or does not have editor prop.')
50
+ return
51
+ }
52
+
53
+ const plugin = BubbleMenuPlugin({
54
+ updateDelay,
55
+ resizeDelay,
56
+ editor: attachToEditor,
57
+ element: bubbleMenuElement,
58
+ appendTo,
59
+ pluginKey,
60
+ shouldShow,
61
+ getReferencedVirtualElement,
62
+ options,
63
+ })
64
+
65
+ attachToEditor.registerPlugin(plugin)
66
+
67
+ return () => {
68
+ attachToEditor.unregisterPlugin(pluginKey)
69
+ window.requestAnimationFrame(() => {
70
+ if (bubbleMenuElement.parentNode) {
71
+ bubbleMenuElement.parentNode.removeChild(bubbleMenuElement)
72
+ }
73
+ })
74
+ }
75
+ }, [
76
+ editor,
77
+ currentEditor,
78
+ pluginKey,
79
+ updateDelay,
80
+ resizeDelay,
81
+ appendTo,
82
+ shouldShow,
83
+ getReferencedVirtualElement,
84
+ options,
85
+ ])
86
+
87
+ return createPortal(<div {...restProps}>{children}</div>, menuEl.current)
88
+ },
89
+ )