Skip to content

Commit 35b4c64

Browse files
authored
INT-3336: Update and provide support for React and ReactDOM 19 (#578)
* INT-3336: Bump `storybook` minor * INT-3336: Bump `react` and `react-dom` major to v19 * INT-3336: Replace deprecated APIs and manually fix syntax errors * INT-3336: Wait for TinyMCE to finish updating the content before rerendering * INT-3336: Wait for editor content to have been updated * INT-3336: Fix tests failing for TinyMCE 4 * INT-3336: Avoid casting the editor `targetElm` * INT-3336: Update peer dependencies for React and ReactDOM to support version 19 * INT-3336: Bump integration version in package.json to 7 and update changelog * INT-3336: Add unused `event` parameter to `once` call to be more explicit. * INT-3336: Bump `storybook` again * INT-3336: Remove redundant if condition * INT-3336: Add comments and remove redundant `filter` * INT-3336: Resolve conflicts * INT-3336: Bump `prop-types` patch
1 parent 807c993 commit 35b4c64

File tree

6 files changed

+420
-1467
lines changed

6 files changed

+420
-1467
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## Unreleased
88

9+
### Added
10+
- Support for `react` version `^19.0.0` by updating the `react` and `react-dom` in the `peerDependencies` to `^19.0.0`
11+
912
## 6.0.0 - 2025-02-21
1013

1114
### Fixed

package.json

+17-17
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
"prop-types": "^15.6.2"
3030
},
3131
"peerDependencies": {
32-
"react": "^18.0.0 || ^17.0.1 || ^16.7.0",
33-
"react-dom": "^18.0.0 || ^17.0.1 || ^16.7.0",
32+
"react": "^19.0.0 || ^18.0.0 || ^17.0.1 || ^16.7.0",
33+
"react-dom": "^19.0.0 || ^18.0.0 || ^17.0.1 || ^16.7.0",
3434
"tinymce": "^7.0.0 || ^6.0.0 || ^5.5.1"
3535
},
3636
"peerDependenciesMeta": {
@@ -50,32 +50,32 @@
5050
"@ephox/mcagar": "^9.0.0-alpha.0",
5151
"@ephox/sand": "^6.0.9",
5252
"@ephox/sugar": "^9.2.1",
53-
"@storybook/addon-essentials": "^8.2.4",
54-
"@storybook/addon-interactions": "^8.2.4",
55-
"@storybook/addon-links": "^8.2.4",
56-
"@storybook/blocks": "^8.2.4",
57-
"@storybook/react": "^8.2.4",
58-
"@storybook/react-vite": "^8.2.4",
53+
"@storybook/addon-essentials": "^8.6.4",
54+
"@storybook/addon-interactions": "^8.6.4",
55+
"@storybook/addon-links": "^8.6.4",
56+
"@storybook/blocks": "^8.6.4",
57+
"@storybook/react": "^8.6.4",
58+
"@storybook/react-vite": "^8.6.4",
5959
"@tinymce/beehive-flow": "^0.19.0",
6060
"@tinymce/eslint-plugin": "^2.4.0",
6161
"@tinymce/miniature": "^6.0.0",
6262
"@types/node": "^22.13.10",
63-
"@types/prop-types": "^15.7.12",
64-
"@types/react": "^18.3.3",
65-
"@types/react-dom": "^18.3.0",
66-
"gh-pages": "^6.3.0",
67-
"react": "^18.3.1",
68-
"react-dom": "^18.3.1",
63+
"@types/prop-types": "^15.7.14",
64+
"@types/react": "^19.0.0",
65+
"@types/react-dom": "^19.0.0",
66+
"gh-pages": "^6.1.0",
67+
"react": "^19.0.0",
68+
"react-dom": "^19.0.0",
6969
"rimraf": "^6.0.1",
70-
"storybook": "^8.2.4",
71-
"tinymce": "^7.7.1",
70+
"storybook": "^8.6.4",
71+
"tinymce": "^7.2.1",
7272
"tinymce-4": "npm:tinymce@^4",
7373
"tinymce-5": "npm:tinymce@^5",
7474
"tinymce-6": "npm:tinymce@^6",
7575
"tinymce-7": "npm:tinymce@^7",
7676
"typescript": "~5.8.2",
7777
"vite": "^6.2.1"
7878
},
79-
"version": "6.0.1-rc",
79+
"version": "7.0.0-rc",
8080
"name": "@tinymce/tinymce-react"
8181
}

src/main/ts/components/Editor.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export class Editor extends React.Component<IAllProps> {
145145
public editor?: TinyMCEEditor;
146146

147147
private id: string;
148-
private elementRef: React.RefObject<HTMLElement>;
148+
private elementRef: React.RefObject<HTMLElement | null>;
149149
private inline: boolean;
150150
private currentContent?: string;
151151
private boundHandlers: Record<string, (event: EditorEvent<unknown>) => unknown>;
@@ -155,7 +155,7 @@ export class Editor extends React.Component<IAllProps> {
155155
public constructor(props: Partial<IAllProps>) {
156156
super(props);
157157
this.id = this.props.id || uuid('tiny-react');
158-
this.elementRef = React.createRef<HTMLElement>();
158+
this.elementRef = React.createRef<HTMLElement | null>();
159159
this.inline = this.props.inline ?? this.props.init?.inline ?? false;
160160
this.boundHandlers = {};
161161
}

src/test/ts/alien/Loader.tsx

+20-10
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { Fun, Optional } from '@ephox/katamari';
22
import { Remove, SugarElement, SugarNode } from '@ephox/sugar';
33
import * as React from 'react';
4-
import * as ReactDOM from 'react-dom';
4+
import * as ReactDOMClient from 'react-dom/client';
55
import { Editor, IAllProps, IProps, Version } from '../../../main/ts/components/Editor';
66
import { Editor as TinyMCEEditor } from 'tinymce';
77
import { before, context } from '@ephox/bedrock-client';
88
import { VersionLoader } from '@tinymce/miniature';
9+
import { setMode } from 'src/main/ts/Utils';
910

1011
// @ts-expect-error Remove when dispose polyfill is not needed
1112
Symbol.dispose ??= Symbol('Symbol.dispose');
@@ -33,6 +34,7 @@ export const render = async (props: Partial<IAllProps> = {}, container: HTMLElem
3334
const originalInit = props.init || {};
3435
const originalSetup = originalInit.setup || Fun.noop;
3536
const ref = React.createRef<Editor>();
37+
const root = ReactDOMClient.createRoot(container);
3638

3739
const ctx = await new Promise<Context>((resolve, reject) => {
3840
const init: IProps['init'] = {
@@ -43,18 +45,18 @@ export const render = async (props: Partial<IAllProps> = {}, container: HTMLElem
4345
editor.on('SkinLoaded', () => {
4446
setTimeout(() => {
4547
Optional.from(ref.current)
46-
.map(ReactDOM.findDOMNode)
47-
.bind(Optional.from)
48+
.bind((editorInstance) => Optional.from(editorInstance.editor?.targetElm))
4849
.map(SugarElement.fromDom)
4950
.filter(SugarNode.isHTMLElement)
5051
.map((val) => val.dom)
5152
.fold(() => reject('Could not find DOMNode'), (DOMNode) => {
5253
resolve({
53-
ref,
54+
ref: ref as React.RefObject<Editor>,
5455
editor,
5556
DOMNode,
5657
});
57-
});
58+
}
59+
);
5860
}, 0);
5961
});
6062
}
@@ -67,20 +69,28 @@ export const render = async (props: Partial<IAllProps> = {}, container: HTMLElem
6769
* touch the nodes created by TinyMCE. Since this only seems to be an issue when rendering TinyMCE 4 directly
6870
* into a root and a fix would be a breaking change, let's just wrap the editor in a <div> here for now.
6971
*/
70-
ReactDOM.render(<div><Editor ref={ref} apiKey='no-api-key' {...props} init={init} /></div>, container);
72+
root.render(<div><Editor ref={ref} apiKey='no-api-key' {...props} init={init} /></div>);
7173
});
7274

7375
const remove = () => {
74-
ReactDOM.unmountComponentAtNode(container);
76+
root.unmount();
7577
Remove.remove(SugarElement.fromDom(container));
7678
};
7779

7880
return {
7981
...ctx,
8082
/** By rendering the Editor into the same root, React will perform a diff and update. */
81-
reRender: (newProps: IAllProps) => new Promise<void>((resolve) =>
82-
ReactDOM.render(<div><Editor apiKey='no-api-key' ref={ctx.ref} {...newProps} /></div>, container, resolve)
83-
),
83+
reRender: (newProps: IAllProps) => new Promise<void>((resolve) => {
84+
root.render(<div><Editor apiKey='no-api-key' ref={ctx.ref} {...newProps} /></div>);
85+
86+
if (newProps.disabled) {
87+
setMode(ctx.editor, 'readonly');
88+
}
89+
90+
newProps.value
91+
? ctx.editor.once('change', (_event) => resolve())
92+
: resolve();
93+
}),
8494
remove,
8595
[Symbol.dispose]: remove
8696
};

src/test/ts/browser/EditorBehaviorTest.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { describe, it } from '@ephox/bedrock-client';
66
import { getTinymce } from '../../../main/ts/TinyMCE';
77
import { EventStore, VERSIONS } from '../alien/TestHelpers';
88
import { Editor as TinyMCEEditor, EditorEvent, Events } from 'tinymce';
9-
import { Assertions } from '@ephox/agar';
9+
import { Assertions, Waiter } from '@ephox/agar';
1010
import { TinyAssertions } from '@ephox/mcagar';
1111

1212
type SetContentEvent = EditorEvent<Events.EditorEventMap['SetContent']>;
@@ -89,6 +89,7 @@ describe('EditorBehaviourTest', () => {
8989
await ctx.reRender({ onSetContent: eventStore.createHandler('onSetContent') });
9090

9191
TinyAssertions.assertContent(ctx.editor, '<p>Initial Content</p>');
92+
await Waiter.pWait(0); // Wait for React's state updates to complete before setting new content
9293
ctx.editor.setContent('<p>New Content</p>');
9394

9495
eventStore.each<SetContentEvent>('onSetContent', (events) => {
@@ -98,7 +99,6 @@ describe('EditorBehaviourTest', () => {
9899
events[0].editorEvent.content
99100
);
100101
});
101-
102102
eventStore.clearState();
103103
});
104104

@@ -114,8 +114,8 @@ describe('EditorBehaviourTest', () => {
114114
});
115115
eventStore.clearState();
116116
ctx.editor.setContent('<p>Initial Content</p>');
117-
118117
await ctx.reRender({ onSetContent: eventStore.createHandler('NewHandler') });
118+
await Waiter.pWait(0); // Wait for React's state updates to complete before setting new content
119119
ctx.editor.setContent('<p>New Content</p>');
120120

121121
eventStore.each<SetContentEvent>('InitialHandler', (events) => {

0 commit comments

Comments
 (0)