@tiptap/react 3.0.0-beta.2 → 3.0.0-beta.22

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,6 +1,13 @@
1
1
  import type { Editor } from '@tiptap/core'
2
- import React from 'react'
3
- import { flushSync } from 'react-dom'
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'
4
11
 
5
12
  import type { EditorWithContentComponent } from './Editor.js'
6
13
 
@@ -19,7 +26,75 @@ function isClassComponent(Component: any) {
19
26
  * @returns {boolean}
20
27
  */
21
28
  function isForwardRefComponent(Component: any) {
22
- return !!(typeof Component === 'object' && Component.$$typeof?.toString() === 'Symbol(react.forward_ref)')
29
+ return !!(
30
+ typeof Component === 'object' &&
31
+ Component.$$typeof &&
32
+ (Component.$$typeof.toString() === 'Symbol(react.forward_ref)' ||
33
+ Component.$$typeof.description === 'react.forward_ref')
34
+ )
35
+ }
36
+
37
+ /**
38
+ * Check if a component is a memoized component.
39
+ * @param Component
40
+ * @returns {boolean}
41
+ */
42
+ function isMemoComponent(Component: any) {
43
+ return !!(
44
+ typeof Component === 'object' &&
45
+ Component.$$typeof &&
46
+ (Component.$$typeof.toString() === 'Symbol(react.memo)' || Component.$$typeof.description === 'react.memo')
47
+ )
48
+ }
49
+
50
+ /**
51
+ * Check if a component can safely receive a ref prop.
52
+ * This includes class components, forwardRef components, and memoized components
53
+ * that wrap forwardRef or class components.
54
+ * @param Component
55
+ * @returns {boolean}
56
+ */
57
+ function canReceiveRef(Component: any) {
58
+ // Check if it's a class component
59
+ if (isClassComponent(Component)) {
60
+ return true
61
+ }
62
+
63
+ // Check if it's a forwardRef component
64
+ if (isForwardRefComponent(Component)) {
65
+ return true
66
+ }
67
+
68
+ // Check if it's a memoized component
69
+ if (isMemoComponent(Component)) {
70
+ // For memoized components, check the wrapped component
71
+ const wrappedComponent = Component.type
72
+ if (wrappedComponent) {
73
+ return isClassComponent(wrappedComponent) || isForwardRefComponent(wrappedComponent)
74
+ }
75
+ }
76
+
77
+ return false
78
+ }
79
+
80
+ /**
81
+ * Check if we're running React 19+ by detecting if function components support ref props
82
+ * @returns {boolean}
83
+ */
84
+ function isReact19Plus(): boolean {
85
+ // React 19 is detected by checking React version if available
86
+ // In practice, we'll use a more conservative approach and assume React 18 behavior
87
+ // unless we can definitively detect React 19
88
+ try {
89
+ // @ts-ignore
90
+ if (reactVersion) {
91
+ const majorVersion = parseInt(reactVersion.split('.')[0], 10)
92
+ return majorVersion >= 19
93
+ }
94
+ } catch {
95
+ // Fallback to React 18 behavior if we can't determine version
96
+ }
97
+ return false
23
98
  }
24
99
 
25
100
  export interface ReactRendererOptions {
@@ -53,9 +128,9 @@ export interface ReactRendererOptions {
53
128
  }
54
129
 
55
130
  type ComponentType<R, P> =
56
- | React.ComponentClass<P>
57
- | React.FunctionComponent<P>
58
- | React.ForwardRefExoticComponent<React.PropsWithoutRef<P> & React.RefAttributes<R>>
131
+ | ComponentClass<P>
132
+ | FunctionComponent<P>
133
+ | ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<R>>
59
134
 
60
135
  /**
61
136
  * The ReactRenderer class. It's responsible for rendering React components inside the editor.
@@ -79,7 +154,7 @@ export class ReactRenderer<R = unknown, P extends Record<string, any> = object>
79
154
 
80
155
  props: P
81
156
 
82
- reactElement: React.ReactNode
157
+ reactElement: ReactNode
83
158
 
84
159
  ref: R | null = null
85
160
 
@@ -101,15 +176,9 @@ export class ReactRenderer<R = unknown, P extends Record<string, any> = object>
101
176
  this.element.classList.add(...className.split(' '))
102
177
  }
103
178
 
104
- 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
- flushSync(() => {
108
- this.render()
109
- })
110
- } else {
179
+ queueMicrotask(() => {
111
180
  this.render()
112
- }
181
+ })
113
182
  }
114
183
 
115
184
  /**
@@ -120,14 +189,26 @@ export class ReactRenderer<R = unknown, P extends Record<string, any> = object>
120
189
  const props = this.props
121
190
  const editor = this.editor as EditorWithContentComponent
122
191
 
123
- if (isClassComponent(Component) || isForwardRefComponent(Component)) {
124
- // @ts-ignore This is a hack to make the ref work
125
- props.ref = (ref: R) => {
192
+ // Handle ref forwarding with React 18/19 compatibility
193
+ const isReact19 = isReact19Plus()
194
+ const componentCanReceiveRef = canReceiveRef(Component)
195
+
196
+ const elementProps = { ...props }
197
+
198
+ // Always remove ref if the component cannot receive it (unless React 19+)
199
+ if (elementProps.ref && !(isReact19 || componentCanReceiveRef)) {
200
+ delete elementProps.ref
201
+ }
202
+
203
+ // Only assign our own ref if allowed
204
+ if (!elementProps.ref && (isReact19 || componentCanReceiveRef)) {
205
+ // @ts-ignore - Setting ref prop for compatible components
206
+ elementProps.ref = (ref: R) => {
126
207
  this.ref = ref
127
208
  }
128
209
  }
129
210
 
130
- this.reactElement = <Component {...props} />
211
+ this.reactElement = <Component {...elementProps} />
131
212
 
132
213
  editor?.contentComponent?.setRenderer(this.id, this)
133
214
  }
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
+ }
@@ -1,5 +1,5 @@
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
4
  import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'
5
5