@tiptap/react 3.0.0-beta.1 → 3.0.0-beta.10

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,12 +1,13 @@
1
- import type { DecorationWithType, Editor, NodeViewProps, NodeViewRenderer, NodeViewRendererOptions } from '@tiptap/core'
1
+ import type { DecorationWithType, Editor, NodeViewRenderer, NodeViewRendererOptions } from '@tiptap/core'
2
2
  import { getRenderedAttributes, NodeView } from '@tiptap/core'
3
3
  import type { Node, Node as ProseMirrorNode } from '@tiptap/pm/model'
4
4
  import type { Decoration, DecorationSource, NodeView as ProseMirrorNodeView } from '@tiptap/pm/view'
5
- import type { ComponentType } from 'react'
6
- import React from 'react'
5
+ import type { ComponentType, NamedExoticComponent } from 'react'
6
+ import { createElement, createRef, memo } from 'react'
7
7
 
8
8
  import type { EditorWithContentComponent } from './Editor.js'
9
9
  import { ReactRenderer } from './ReactRenderer.js'
10
+ import type { ReactNodeViewProps } from './types.js'
10
11
  import type { ReactNodeViewContextProps } from './useReactNodeView.js'
11
12
  import { ReactNodeViewContext } from './useReactNodeView.js'
12
13
 
@@ -45,14 +46,15 @@ export interface ReactNodeViewRendererOptions extends NodeViewRendererOptions {
45
46
  }
46
47
 
47
48
  export class ReactNodeView<
48
- Component extends ComponentType<NodeViewProps> = ComponentType<NodeViewProps>,
49
+ T = HTMLElement,
50
+ Component extends ComponentType<ReactNodeViewProps<T>> = ComponentType<ReactNodeViewProps<T>>,
49
51
  NodeEditor extends Editor = Editor,
50
52
  Options extends ReactNodeViewRendererOptions = ReactNodeViewRendererOptions,
51
53
  > extends NodeView<Component, NodeEditor, Options> {
52
54
  /**
53
55
  * The renderer instance.
54
56
  */
55
- renderer!: ReactRenderer<unknown, NodeViewProps>
57
+ renderer!: ReactRenderer<unknown, ReactNodeViewProps<T>>
56
58
 
57
59
  /**
58
60
  * The element that holds the rich-text content of the node.
@@ -76,7 +78,8 @@ export class ReactNodeView<
76
78
  getPos: () => this.getPos(),
77
79
  updateAttributes: (attributes = {}) => this.updateAttributes(attributes),
78
80
  deleteNode: () => this.deleteNode(),
79
- } satisfies NodeViewProps
81
+ ref: createRef<T>(),
82
+ } satisfies ReactNodeViewProps<T>
80
83
 
81
84
  if (!(this.component as any).displayName) {
82
85
  const capitalizeFirstChar = (string: string): string => {
@@ -96,10 +99,10 @@ export class ReactNodeView<
96
99
  const Component = this.component
97
100
  // For performance reasons, we memoize the provider component
98
101
  // And all of the things it requires are declared outside of the component, so it doesn't need to re-render
99
- const ReactNodeViewProvider: React.FunctionComponent<NodeViewProps> = React.memo(componentProps => {
102
+ const ReactNodeViewProvider: NamedExoticComponent<ReactNodeViewProps<T>> = memo(componentProps => {
100
103
  return (
101
104
  <ReactNodeViewContext.Provider value={context}>
102
- {React.createElement(Component, componentProps)}
105
+ {createElement(Component, componentProps)}
103
106
  </ReactNodeViewContext.Provider>
104
107
  )
105
108
  })
@@ -302,8 +305,8 @@ export class ReactNodeView<
302
305
  /**
303
306
  * Create a React node view renderer.
304
307
  */
305
- export function ReactNodeViewRenderer(
306
- component: ComponentType<NodeViewProps>,
308
+ export function ReactNodeViewRenderer<T = HTMLElement>(
309
+ component: ComponentType<ReactNodeViewProps<T>>,
307
310
  options?: Partial<ReactNodeViewRendererOptions>,
308
311
  ): NodeViewRenderer {
309
312
  return props => {
@@ -314,6 +317,6 @@ export function ReactNodeViewRenderer(
314
317
  return {} as unknown as ProseMirrorNodeView
315
318
  }
316
319
 
317
- return new ReactNodeView(component, props, options)
320
+ return new ReactNodeView<T>(component, props, options)
318
321
  }
319
322
  }
@@ -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
 
@@ -120,14 +196,26 @@ export class ReactRenderer<R = unknown, P extends Record<string, any> = object>
120
196
  const props = this.props
121
197
  const editor = this.editor as EditorWithContentComponent
122
198
 
123
- if (isClassComponent(Component) || isForwardRefComponent(Component)) {
124
- // @ts-ignore This is a hack to make the ref work
125
- props.ref = (ref: R) => {
199
+ // Handle ref forwarding with React 18/19 compatibility
200
+ const isReact19 = isReact19Plus()
201
+ const componentCanReceiveRef = canReceiveRef(Component)
202
+
203
+ const elementProps = { ...props }
204
+
205
+ // Always remove ref if the component cannot receive it (unless React 19+)
206
+ if (elementProps.ref && !(isReact19 || componentCanReceiveRef)) {
207
+ delete elementProps.ref
208
+ }
209
+
210
+ // Only assign our own ref if allowed
211
+ if (!elementProps.ref && (isReact19 || componentCanReceiveRef)) {
212
+ // @ts-ignore - Setting ref prop for compatible components
213
+ elementProps.ref = (ref: R) => {
126
214
  this.ref = ref
127
215
  }
128
216
  }
129
217
 
130
- this.reactElement = <Component {...props} />
218
+ this.reactElement = <Component {...elementProps} />
131
219
 
132
220
  editor?.contentComponent?.setRenderer(this.id, this)
133
221
  }
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