@tiptap/react 3.0.0-next.8 → 3.0.1

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.
@@ -1,5 +1,13 @@
1
1
  import type { Editor } from '@tiptap/core'
2
- import React from 'react'
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'
3
11
  import { flushSync } from 'react-dom'
4
12
 
5
13
  import type { EditorWithContentComponent } from './Editor.js'
@@ -19,7 +27,75 @@ function isClassComponent(Component: any) {
19
27
  * @returns {boolean}
20
28
  */
21
29
  function isForwardRefComponent(Component: any) {
22
- return !!(typeof Component === 'object' && Component.$$typeof?.toString() === 'Symbol(react.forward_ref)')
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
23
99
  }
24
100
 
25
101
  export interface ReactRendererOptions {
@@ -53,9 +129,9 @@ export interface ReactRendererOptions {
53
129
  }
54
130
 
55
131
  type ComponentType<R, P> =
56
- | React.ComponentClass<P>
57
- | React.FunctionComponent<P>
58
- | React.ForwardRefExoticComponent<React.PropsWithoutRef<P> & React.RefAttributes<R>>
132
+ | ComponentClass<P>
133
+ | FunctionComponent<P>
134
+ | ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<R>>
59
135
 
60
136
  /**
61
137
  * The ReactRenderer class. It's responsible for rendering React components inside the editor.
@@ -79,7 +155,7 @@ export class ReactRenderer<R = unknown, P extends Record<string, any> = object>
79
155
 
80
156
  props: P
81
157
 
82
- reactElement: React.ReactNode
158
+ reactElement: ReactNode
83
159
 
84
160
  ref: R | null = null
85
161
 
@@ -101,14 +177,17 @@ export class ReactRenderer<R = unknown, P extends Record<string, any> = object>
101
177
  this.element.classList.add(...className.split(' '))
102
178
  }
103
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.
104
183
  if (this.editor.isInitialized) {
105
- // On first render, we need to flush the render synchronously
106
- // Renders afterwards can be async, but this fixes a cursor positioning issue
107
184
  flushSync(() => {
108
185
  this.render()
109
186
  })
110
187
  } else {
111
- this.render()
188
+ queueMicrotask(() => {
189
+ this.render()
190
+ })
112
191
  }
113
192
  }
114
193
 
@@ -120,14 +199,26 @@ export class ReactRenderer<R = unknown, P extends Record<string, any> = object>
120
199
  const props = this.props
121
200
  const editor = this.editor as EditorWithContentComponent
122
201
 
123
- if (isClassComponent(Component) || isForwardRefComponent(Component)) {
124
- // @ts-ignore This is a hack to make the ref work
125
- props.ref = (ref: R) => {
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) => {
126
217
  this.ref = ref
127
218
  }
128
219
  }
129
220
 
130
- this.reactElement = <Component {...props} />
221
+ this.reactElement = <Component {...elementProps} />
131
222
 
132
223
  editor?.contentComponent?.setRenderer(this.id, this)
133
224
  }
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ export * from './NodeViewWrapper.js'
5
5
  export * from './ReactMarkViewRenderer.js'
6
6
  export * from './ReactNodeViewRenderer.js'
7
7
  export * from './ReactRenderer.js'
8
+ export * from './types.js'
8
9
  export * from './useEditor.js'
9
10
  export * from './useEditorState.js'
10
11
  export * from './useReactNodeView.js'
package/src/types.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { NodeViewProps as CoreNodeViewProps } from '@tiptap/core'
2
+ import type React from 'react'
3
+
4
+ export type ReactNodeViewProps<T = HTMLElement> = CoreNodeViewProps & {
5
+ ref: React.RefObject<T | null>
6
+ }
package/src/useEditor.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type EditorOptions, Editor } from '@tiptap/core'
2
2
  import type { DependencyList, MutableRefObject } from 'react'
3
3
  import { useDebugValue, useEffect, useRef, useState } from 'react'
4
- import { useSyncExternalStore } from 'use-sync-external-store/shim'
4
+ import { useSyncExternalStore } from 'use-sync-external-store/shim/index.js'
5
5
 
6
6
  import { useEditorState } from './useEditorState.js'
7
7
 
@@ -1,7 +1,7 @@
1
1
  import type { Editor } from '@tiptap/core'
2
- import deepEqual from 'fast-deep-equal/es6/react'
2
+ import deepEqual from 'fast-deep-equal/es6/react.js'
3
3
  import { useDebugValue, useEffect, useLayoutEffect, useState } from 'react'
4
- import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'
4
+ import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector.js'
5
5
 
6
6
  const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
7
7