diff --git a/.github/workflows/call-core-tests.yml b/.github/workflows/call-core-tests.yml
index e6f1d5c2bd5..bb38f9e9a07 100644
--- a/.github/workflows/call-core-tests.yml
+++ b/.github/workflows/call-core-tests.yml
@@ -30,6 +30,7 @@ jobs:
strategy:
matrix:
node-version: [18.18.0]
+ override-react-version: ['', 'beta']
env:
CI: true
steps:
@@ -41,22 +42,10 @@ jobs:
cache: npm
- name: Install dependencies
run: npm ci
+ - name: Install React ${{ matrix.override-react-version }}
+ if: matrix.override-react-version != ''
+ # This should be safe since we are caching ~/.npm and not node_modules
+ run: |
+ node ./scripts/override-react.js --version=${{ matrix.override-react-version }}
+ grep version node_modules/{react,react-dom}/package.json
- run: npm run test-unit
-
- integration:
- runs-on: ubuntu-latest
- strategy:
- matrix:
- node-version: [18.18.0]
- env:
- CI: true
- steps:
- - uses: actions/checkout@v4
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v4
- with:
- node-version: ${{ matrix.node-version }}
- cache: npm
- - name: Install dependencies
- run: npm ci
- - run: npm run test-integration
diff --git a/.github/workflows/call-e2e-all-tests.yml b/.github/workflows/call-e2e-all-tests.yml
index d1e15f11aef..8c91b5e5230 100644
--- a/.github/workflows/call-e2e-all-tests.yml
+++ b/.github/workflows/call-e2e-all-tests.yml
@@ -124,3 +124,20 @@ jobs:
browser: ${{ matrix.browser }}
editor-mode: ${{ matrix.editor-mode }}
events-mode: ${{ matrix.events-mode }}
+
+ react-beta:
+ strategy:
+ matrix:
+ # Currently using a single combination for every-patch e2e tests of
+ # react beta to reduce cost impact
+ editor-mode: ['rich-text']
+ prod: [false]
+ uses: ./.github/workflows/call-e2e-test.yml
+ with:
+ os: 'macos-latest'
+ browser: 'chromium'
+ node-version: 18.18.0
+ events-mode: 'modern-events'
+ editor-mode: ${{ matrix.editor-mode }}
+ prod: ${{ matrix.prod }}
+ override-react-version: beta
diff --git a/.github/workflows/call-e2e-test.yml b/.github/workflows/call-e2e-test.yml
index 48e14617237..a7ab4e4d4c7 100644
--- a/.github/workflows/call-e2e-test.yml
+++ b/.github/workflows/call-e2e-test.yml
@@ -9,6 +9,7 @@ on:
editor-mode: {required: true, type: string}
events-mode: {required: true, type: string}
prod: {required: false, type: boolean}
+ override-react-version: {required: false, type: string}
jobs:
e2e-test:
@@ -18,6 +19,7 @@ jobs:
CI: true
E2E_EDITOR_MODE: ${{ inputs.editor-mode }}
E2E_EVENTS_MODE: ${{ inputs.events-mode }}
+ OVERRIDE_REACT_VERSION: ${{ inputs.override-react-version }}
cache_playwright_path: ${{ inputs.os == 'macos-latest' && '~/Library/Caches/ms-playwright' || inputs.os == 'windows-latest' && 'C:\Users\runneradmin\AppData\Local\ms-playwright' || '~/.cache/ms-playwright' }}
test_results_path: ${{ inputs.os == 'windows-latest' && '~/.npm/_logs/' || 'test-results/' }}
test_script: test-e2e-${{ inputs.editor-mode == 'rich-text-with-collab' && 'collab-' || '' }}${{ inputs.prod && 'prod-' || '' }}ci-${{ inputs.browser }}
@@ -35,6 +37,12 @@ jobs:
sudo apt-get install xvfb
- name: Install dependencies
run: npm ci
+ - name: Install React ${{ inputs.override-react-version }}
+ if: inputs.override-react-version != ''
+ # This should be safe since we are caching ~/.npm and not node_modules
+ run: |
+ node ./scripts/override-react.js --version=${{ inputs.override-react-version }}
+ grep version node_modules/{react,react-dom}/package.json
- name: Restore playwright from cache
uses: actions/cache/restore@v4
id: playwright-cache
@@ -55,6 +63,6 @@ jobs:
if: failure()
uses: actions/upload-artifact@v4
with:
- name: Test Results ${{ inputs.os }}-${{ inputs.browser }}-${{ inputs.editor-mode }}-${{ inputs.events-mode }}-${{ inputs.prod && 'prod' || 'dev' }}-${{ inputs.node-version }}
+ name: Test Results ${{ inputs.os }}-${{ inputs.browser }}-${{ inputs.editor-mode }}-${{ inputs.events-mode }}-${{ inputs.prod && 'prod' || 'dev' }}-${{ inputs.node-version }}-${{ inputs.override-react-version }}
path: ${{ env.test_results_path }}
retention-days: 7
diff --git a/.github/workflows/call-integration-tests.yml b/.github/workflows/call-integration-tests.yml
new file mode 100644
index 00000000000..9cc38f3d41a
--- /dev/null
+++ b/.github/workflows/call-integration-tests.yml
@@ -0,0 +1,23 @@
+name: Lexical Integration Tests
+
+on:
+ workflow_call:
+
+jobs:
+ integration:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ node-version: [18.18.0]
+ env:
+ CI: true
+ steps:
+ - uses: actions/checkout@v4
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: npm
+ - name: Install dependencies
+ run: npm ci
+ - run: npm run test-integration
diff --git a/.github/workflows/tests-extended.yml b/.github/workflows/tests-extended.yml
index 9c9ff869f4c..fbb522084a8 100644
--- a/.github/workflows/tests-extended.yml
+++ b/.github/workflows/tests-extended.yml
@@ -2,9 +2,10 @@ name: Lexical Tests (Extended)
on:
pull_request:
- types: [labeled, synchronize]
+ types: [labeled, synchronize, reopened]
paths-ignore:
- 'packages/lexical-website/**'
+ - 'packages/*/README.md'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -14,3 +15,7 @@ jobs:
e2e-tests:
if: github.repository_owner == 'facebook' && contains(github.event.pull_request.labels.*.name, 'extended-tests')
uses: ./.github/workflows/call-e2e-all-tests.yml
+
+ integration-tests:
+ if: github.repository_owner == 'facebook' && contains(github.event.pull_request.labels.*.name, 'extended-tests')
+ uses: ./.github/workflows/call-integration-tests.yml
diff --git a/examples/react-rich-collab/package-lock.json b/examples/react-rich-collab/package-lock.json
index 77c166f1d23..18f7a887890 100644
--- a/examples/react-rich-collab/package-lock.json
+++ b/examples/react-rich-collab/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "@lexical/react-rich-example",
+ "name": "@lexical/react-rich-collab-example",
"version": "0.14.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
- "name": "@lexical/react-rich-example",
+ "name": "@lexical/react-rich-collab-example",
"version": "0.14.5",
"dependencies": {
"@lexical/react": "0.14.5",
@@ -13,6 +13,7 @@
"lexical": "0.14.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "y-webrtc": "^10.3.0",
"y-websocket": "^2.0.2",
"yjs": "^13.6.15"
},
@@ -23,8 +24,7 @@
"concurrently": "^8.2.2",
"cross-env": "^7.0.3",
"typescript": "^5.2.2",
- "vite": "^5.1.4",
- "y-webrtc": "^10.3.0"
+ "vite": "^5.1.4"
}
},
"node_modules/@ampproject/remapping": {
@@ -1341,7 +1341,6 @@
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
- "devOptional": true,
"funding": [
{
"type": "github",
@@ -1652,7 +1651,6 @@
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
- "dev": true,
"dependencies": {
"ms": "2.1.2"
},
@@ -1708,8 +1706,7 @@
"node_modules/err-code": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/err-code/-/err-code-3.0.1.tgz",
- "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==",
- "dev": true
+ "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA=="
},
"node_modules/errno": {
"version": "0.1.8",
@@ -1805,8 +1802,7 @@
"node_modules/get-browser-rtc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/get-browser-rtc/-/get-browser-rtc-1.1.0.tgz",
- "integrity": "sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ==",
- "dev": true
+ "integrity": "sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ=="
},
"node_modules/get-caller-file": {
"version": "2.0.5",
@@ -1839,7 +1835,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
- "devOptional": true,
"funding": [
{
"type": "github",
@@ -1864,8 +1859,7 @@
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "devOptional": true
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
@@ -2118,8 +2112,7 @@
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/nanoid": {
"version": "3.3.7",
@@ -2223,7 +2216,6 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -2243,7 +2235,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
- "dev": true,
"dependencies": {
"safe-buffer": "^5.1.0"
}
@@ -2299,7 +2290,6 @@
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
- "devOptional": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -2368,7 +2358,6 @@
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
- "devOptional": true,
"funding": [
{
"type": "github",
@@ -2435,7 +2424,6 @@
"version": "9.11.1",
"resolved": "https://registry.npmjs.org/simple-peer/-/simple-peer-9.11.1.tgz",
"integrity": "sha512-D1SaWpOW8afq1CZGWB8xTfrT3FekjQmPValrqncJMX7QFl8YwhrPTZvMCANLtgBwwdS+7zURyqxDDEmY558tTw==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -2464,7 +2452,6 @@
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -2503,7 +2490,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
- "devOptional": true,
"dependencies": {
"safe-buffer": "~5.2.0"
}
@@ -2616,8 +2602,7 @@
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "devOptional": true
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/vite": {
"version": "5.1.4",
@@ -2797,7 +2782,6 @@
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/y-webrtc/-/y-webrtc-10.3.0.tgz",
"integrity": "sha512-KalJr7dCgUgyVFxoG3CQYbpS0O2qybegD0vI4bYnYHI0MOwoVbucED3RZ5f2o1a5HZb1qEssUKS0H/Upc6p1lA==",
- "dev": true,
"dependencies": {
"lib0": "^0.2.42",
"simple-peer": "^9.11.0",
@@ -2824,7 +2808,6 @@
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
- "dev": true,
"optional": true,
"engines": {
"node": ">=10.0.0"
@@ -3830,8 +3813,7 @@
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
- "devOptional": true
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"browserslist": {
"version": "4.23.0",
@@ -4022,7 +4004,6 @@
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
- "dev": true,
"requires": {
"ms": "2.1.2"
}
@@ -4064,8 +4045,7 @@
"err-code": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/err-code/-/err-code-3.0.1.tgz",
- "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==",
- "dev": true
+ "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA=="
},
"errno": {
"version": "0.1.8",
@@ -4135,8 +4115,7 @@
"get-browser-rtc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/get-browser-rtc/-/get-browser-rtc-1.1.0.tgz",
- "integrity": "sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ==",
- "dev": true
+ "integrity": "sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ=="
},
"get-caller-file": {
"version": "2.0.5",
@@ -4159,8 +4138,7 @@
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
- "devOptional": true
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
},
"immediate": {
"version": "3.3.0",
@@ -4171,8 +4149,7 @@
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "devOptional": true
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"is-fullwidth-code-point": {
"version": "3.0.0",
@@ -4359,8 +4336,7 @@
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"nanoid": {
"version": "3.3.7",
@@ -4423,14 +4399,12 @@
"queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
- "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
- "dev": true
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
- "dev": true,
"requires": {
"safe-buffer": "^5.1.0"
}
@@ -4470,7 +4444,6 @@
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
- "devOptional": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -4523,8 +4496,7 @@
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
- "devOptional": true
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"scheduler": {
"version": "0.23.0",
@@ -4565,7 +4537,6 @@
"version": "9.11.1",
"resolved": "https://registry.npmjs.org/simple-peer/-/simple-peer-9.11.1.tgz",
"integrity": "sha512-D1SaWpOW8afq1CZGWB8xTfrT3FekjQmPValrqncJMX7QFl8YwhrPTZvMCANLtgBwwdS+7zURyqxDDEmY558tTw==",
- "dev": true,
"requires": {
"buffer": "^6.0.3",
"debug": "^4.3.2",
@@ -4580,7 +4551,6 @@
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
- "dev": true,
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
@@ -4604,7 +4574,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
- "devOptional": true,
"requires": {
"safe-buffer": "~5.2.0"
}
@@ -4675,8 +4644,7 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "devOptional": true
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"vite": {
"version": "5.1.4",
@@ -4773,7 +4741,6 @@
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/y-webrtc/-/y-webrtc-10.3.0.tgz",
"integrity": "sha512-KalJr7dCgUgyVFxoG3CQYbpS0O2qybegD0vI4bYnYHI0MOwoVbucED3RZ5f2o1a5HZb1qEssUKS0H/Upc6p1lA==",
- "dev": true,
"requires": {
"lib0": "^0.2.42",
"simple-peer": "^9.11.0",
@@ -4785,7 +4752,6 @@
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
- "dev": true,
"optional": true,
"requires": {}
}
diff --git a/examples/react-rich-collab/package.json b/examples/react-rich-collab/package.json
index 57c28b58a0f..96bd2a75917 100644
--- a/examples/react-rich-collab/package.json
+++ b/examples/react-rich-collab/package.json
@@ -1,5 +1,5 @@
{
- "name": "@lexical/react-rich-example",
+ "name": "@lexical/react-rich-collab-example",
"private": true,
"version": "0.15.0",
"type": "module",
diff --git a/jest.config.js b/jest.config.js
index b5502828557..630ceba07aa 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -33,6 +33,7 @@ module.exports = {
...common,
displayName: 'unit',
globals: {
+ // https://react.dev/blog/2022/03/08/react-18-upgrade-guide#configuring-your-testing-environment
IS_REACT_ACT_ENVIRONMENT: true,
__DEV__: true,
},
diff --git a/package-lock.json b/package-lock.json
index 260d3fb0377..2e9ce84f1b4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -89,7 +89,7 @@
"ts-jest": "^29.1.2",
"ts-node": "^10.9.1",
"typedoc": "^0.25.12",
- "typescript": "5.1.6",
+ "typescript": "^5.4.5",
"vite": "^5.2.11"
},
"engines": {
@@ -29739,6 +29739,28 @@
"typedoc": ">=0.24.0"
}
},
+ "node_modules/typedoc-plugin-rename-defaults": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/typedoc-plugin-rename-defaults/-/typedoc-plugin-rename-defaults-0.7.0.tgz",
+ "integrity": "sha512-NudSQ1o/XLHNF9c4y7LzIZxfE9ltz09yCDklBPJpP5VMRvuBpYGIbQ0ZgmPz+EIV8vPx9Z/OyKwzp4HT2vDtfg==",
+ "dependencies": {
+ "camelcase": "^8.0.0"
+ },
+ "peerDependencies": {
+ "typedoc": "0.22.x || 0.23.x || 0.24.x || 0.25.x"
+ }
+ },
+ "node_modules/typedoc-plugin-rename-defaults/node_modules/camelcase": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz",
+ "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/typedoc/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@@ -29764,9 +29786,9 @@
}
},
"node_modules/typescript": {
- "version": "5.1.6",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
- "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
+ "version": "5.4.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
+ "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@@ -32659,7 +32681,7 @@
"@types/react-dom": "^18.2.18",
"@vitejs/plugin-react": "^4.2.1",
"lexical": "0.15.0",
- "typescript": "^5.3.3",
+ "typescript": "^5.4.5",
"vite": "^5.2.2",
"wxt": "^0.17.0"
}
@@ -32681,19 +32703,6 @@
"react-dom": ">=17.x"
}
},
- "packages/lexical-devtools/node_modules/typescript": {
- "version": "5.4.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz",
- "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==",
- "dev": true,
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
"packages/lexical-dragon": {
"name": "@lexical/dragon",
"version": "0.15.0",
@@ -32956,6 +32965,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typedoc-plugin-markdown": "^3.17.1",
+ "typedoc-plugin-rename-defaults": "^0.7.0",
"unist-util-visit": "^5.0.0"
},
"devDependencies": {
@@ -37393,18 +37403,10 @@
"lexical": "0.15.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "typescript": "^5.3.3",
+ "typescript": "^5.4.5",
"vite": "^5.2.2",
"wxt": "^0.17.0",
"zustand": "^4.5.1"
- },
- "dependencies": {
- "typescript": {
- "version": "5.4.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz",
- "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==",
- "dev": true
- }
}
},
"@lexical/devtools-core": {
@@ -37598,6 +37600,7 @@
"react-dom": "^18.2.0",
"tailwindcss": "^3.3.3",
"typedoc-plugin-markdown": "^3.17.1",
+ "typedoc-plugin-rename-defaults": "^0.7.0",
"unist-util-visit": "^5.0.0",
"webpack": "^5.76.0"
}
@@ -54281,10 +54284,25 @@
"handlebars": "^4.7.7"
}
},
+ "typedoc-plugin-rename-defaults": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/typedoc-plugin-rename-defaults/-/typedoc-plugin-rename-defaults-0.7.0.tgz",
+ "integrity": "sha512-NudSQ1o/XLHNF9c4y7LzIZxfE9ltz09yCDklBPJpP5VMRvuBpYGIbQ0ZgmPz+EIV8vPx9Z/OyKwzp4HT2vDtfg==",
+ "requires": {
+ "camelcase": "^8.0.0"
+ },
+ "dependencies": {
+ "camelcase": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz",
+ "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="
+ }
+ }
+ },
"typescript": {
- "version": "5.1.6",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
- "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
+ "version": "5.4.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
+ "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true
},
"ufo": {
diff --git a/package.json b/package.json
index 075e6c3445d..be73460e4c8 100644
--- a/package.json
+++ b/package.json
@@ -184,7 +184,7 @@
"ts-jest": "^29.1.2",
"ts-node": "^10.9.1",
"typedoc": "^0.25.12",
- "typescript": "5.1.6",
+ "typescript": "^5.4.5",
"vite": "^5.2.11"
},
"dependencies": {
diff --git a/packages/lexical-devtools/package.json b/packages/lexical-devtools/package.json
index 7f40106fb86..876ef7083bd 100644
--- a/packages/lexical-devtools/package.json
+++ b/packages/lexical-devtools/package.json
@@ -42,7 +42,7 @@
"@types/react-dom": "^18.2.18",
"@vitejs/plugin-react": "^4.2.1",
"lexical": "0.15.0",
- "typescript": "^5.3.3",
+ "typescript": "^5.4.5",
"vite": "^5.2.2",
"wxt": "^0.17.0"
},
diff --git a/packages/lexical-devtools/tsconfig.json b/packages/lexical-devtools/tsconfig.json
index 5174eae633b..cd83eb1f445 100644
--- a/packages/lexical-devtools/tsconfig.json
+++ b/packages/lexical-devtools/tsconfig.json
@@ -161,6 +161,7 @@
"shared/environment": ["../shared/src/environment.ts"],
"shared/invariant": ["../shared/src/invariant.ts"],
"shared/normalizeClassNames": ["../shared/src/normalizeClassNames.ts"],
+ "shared/react-test-utils": ["../shared/src/react-test-utils.ts"],
"shared/simpleDiffWithCursor": ["../shared/src/simpleDiffWithCursor.ts"],
"shared/useLayoutEffect": ["../shared/src/useLayoutEffect.ts"],
"shared/warnOnlyOnce": ["../shared/src/warnOnlyOnce.ts"]
diff --git a/packages/lexical-eslint-plugin/src/rules/rules-of-lexical.js b/packages/lexical-eslint-plugin/src/rules/rules-of-lexical.js
index 494adecc0d8..b24e2dc3e6e 100644
--- a/packages/lexical-eslint-plugin/src/rules/rules-of-lexical.js
+++ b/packages/lexical-eslint-plugin/src/rules/rules-of-lexical.js
@@ -285,7 +285,9 @@ module.exports.rulesOfLexical = {
const pushIgnoredNode = (/** @type {Node} */ node) => ignoreSet.add(node);
const popIgnoredNode = (/** @type {Node} */ node) => ignoreSet.delete(node);
const pushFunction = (/** @type {Node} */ node) => {
- const name = getFunctionNameIdentifier(getLexicalFunctionName(node));
+ const name = getFunctionNameIdentifier(
+ /** @type {Node | undefined} */ (getLexicalFunctionName(node)),
+ );
funStack.push({name, node});
if (
matchers.isDollarFunction(name) ||
diff --git a/packages/lexical-history/src/__tests__/unit/LexicalHistory.test.tsx b/packages/lexical-history/src/__tests__/unit/LexicalHistory.test.tsx
index a27095d3201..3a495a42e48 100644
--- a/packages/lexical-history/src/__tests__/unit/LexicalHistory.test.tsx
+++ b/packages/lexical-history/src/__tests__/unit/LexicalHistory.test.tsx
@@ -35,7 +35,7 @@ import {
import {createTestEditor, TestComposer} from 'lexical/src/__tests__/utils';
import React from 'react';
import {createRoot, Root} from 'react-dom/client';
-import * as ReactTestUtils from 'react-dom/test-utils';
+import * as ReactTestUtils from 'shared/react-test-utils';
describe('LexicalHistory tests', () => {
let container: HTMLDivElement | null = null;
diff --git a/packages/lexical-playground/vite.config.ts b/packages/lexical-playground/vite.config.ts
index bbfcd2cc45a..d7d0d3b5c48 100644
--- a/packages/lexical-playground/vite.config.ts
+++ b/packages/lexical-playground/vite.config.ts
@@ -73,7 +73,11 @@ export default defineConfig(({command}) => {
}),
react(),
viteCopyEsm(),
- commonjs(),
+ commonjs({
+ // This is required for React 19 (at least 19.0.0-beta-26f2496093-20240514)
+ // because @rollup/plugin-commonjs does not analyze it correctly
+ strictRequires: [/\/node_modules\/(react-dom|react)\/[^/]\.js$/],
+ }),
],
resolve: {
alias: moduleResolution(command === 'serve' ? 'source' : 'development'),
diff --git a/packages/lexical-playground/vite.prod.config.ts b/packages/lexical-playground/vite.prod.config.ts
index ad4b3499cec..083f4697032 100644
--- a/packages/lexical-playground/vite.prod.config.ts
+++ b/packages/lexical-playground/vite.prod.config.ts
@@ -66,7 +66,11 @@ export default defineConfig({
}),
react(),
viteCopyEsm(),
- commonjs(),
+ commonjs({
+ // This is required for React 19 (at least 19.0.0-beta-26f2496093-20240514)
+ // because @rollup/plugin-commonjs does not analyze it correctly
+ strictRequires: [/\/node_modules\/(react-dom|react)\/[^/]\.js$/],
+ }),
],
resolve: {
alias: moduleResolution('production'),
diff --git a/packages/lexical-react/src/__tests__/unit/LexicalComposer.test.tsx b/packages/lexical-react/src/__tests__/unit/LexicalComposer.test.tsx
index 2a79c9bc636..0b8d3d2b38c 100644
--- a/packages/lexical-react/src/__tests__/unit/LexicalComposer.test.tsx
+++ b/packages/lexical-react/src/__tests__/unit/LexicalComposer.test.tsx
@@ -7,13 +7,19 @@
*/
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
+import {
+ $createParagraphNode,
+ $createTextNode,
+ $getRoot,
+ LexicalEditor,
+} from 'lexical';
import * as React from 'react';
import {createRoot, Root} from 'react-dom/client';
-import * as ReactTestUtils from 'react-dom/test-utils';
+import * as ReactTestUtils from 'shared/react-test-utils';
import {LexicalComposer} from '../../LexicalComposer';
-describe('LexicalNodeHelpers tests', () => {
+describe('LexicalComposer tests', () => {
let container: HTMLDivElement | null = null;
let reactRoot: Root;
@@ -59,4 +65,65 @@ describe('LexicalNodeHelpers tests', () => {
reactRoot.render();
});
});
+
+ describe('LexicalComposerContext editor identity', () => {
+ (
+ [
+ {name: 'StrictMode', size: 2},
+ {name: 'Fragment', size: 1},
+ ] as const
+ ).forEach(({name, size}) => {
+ const Wrapper = React[name];
+ const editors = new Set();
+ const pluginEditors = new Set();
+ function Plugin() {
+ pluginEditors.add(useLexicalComposerContext()[0]);
+ return null;
+ }
+ function App() {
+ return (
+ {
+ const p = $createParagraphNode();
+ p.append($createTextNode('initial state'));
+ $getRoot().append(p);
+ });
+ },
+ namespace: '',
+ nodes: [],
+ onError: () => {
+ throw Error();
+ },
+ }}>
+
+
+ );
+ }
+ it(`renders ${size} editors under ${name}`, async () => {
+ await ReactTestUtils.act(async () => {
+ reactRoot.render(
+
+
+ ,
+ );
+ });
+ // 2 editors may be created since useMemo is still called twice,
+ // but only one result is used!
+ expect(editors.size).toBe(size);
+ [...editors].forEach((editor, i) => {
+ // This confirms that editorState() was only called once per editor,
+ // otherwise you could see 'initial stateinitial state'.
+ expect([
+ i,
+ editor.getEditorState().read(() => $getRoot().getTextContent()),
+ ]).toEqual([i, 'initial state']);
+ });
+ // Only one context is created in both cases though!
+ expect(pluginEditors.size).toBe(1);
+ });
+ });
+ });
});
diff --git a/packages/lexical-react/src/__tests__/unit/PlainRichTextPlugin.test.tsx b/packages/lexical-react/src/__tests__/unit/PlainRichTextPlugin.test.tsx
index 9a36cc4fdbe..62cc7bd850a 100644
--- a/packages/lexical-react/src/__tests__/unit/PlainRichTextPlugin.test.tsx
+++ b/packages/lexical-react/src/__tests__/unit/PlainRichTextPlugin.test.tsx
@@ -26,7 +26,7 @@ import {
} from 'lexical';
import * as React from 'react';
import {createRoot, Root} from 'react-dom/client';
-import * as ReactTestUtils from 'react-dom/test-utils';
+import * as ReactTestUtils from 'shared/react-test-utils';
import {LexicalComposer} from '../../LexicalComposer';
import {ContentEditable} from '../../LexicalContentEditable';
diff --git a/packages/lexical-react/src/__tests__/unit/React19.test.tsx b/packages/lexical-react/src/__tests__/unit/React19.test.tsx
new file mode 100644
index 00000000000..a59eaeb181b
--- /dev/null
+++ b/packages/lexical-react/src/__tests__/unit/React19.test.tsx
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import * as React from 'react';
+import {createRoot, Root} from 'react-dom/client';
+import * as ReactTestUtils from 'shared/react-test-utils';
+
+const IS_REACT_19 = parseInt(React.version.split('.')[0], 10) >= 19;
+const OVERRIDE_REACT_VERSION = process.env.OVERRIDE_REACT_VERSION ?? '';
+
+describe(`React expectations (${React.version}) OVERRIDE_REACT_VERSION=${OVERRIDE_REACT_VERSION}`, () => {
+ let container: HTMLDivElement;
+ let reactRoot: Root;
+ beforeEach(() => {
+ container = document.createElement('div');
+ reactRoot = createRoot(container);
+ document.body.appendChild(container);
+ });
+ // This checks our assumption that we are testing against the correct version of React
+ // The inverse is not checked so the test doesn't fail when our dependencies
+ // are upgraded.
+ if (OVERRIDE_REACT_VERSION) {
+ test(`Expecting React >= 19`, () => {
+ expect(IS_REACT_19).toBe(true);
+ });
+ }
+ const cacheExpect = IS_REACT_19 ? 'cached' : 'not cached';
+ test(`StrictMode useMemo is ${cacheExpect}`, () => {
+ const memoFun = jest
+ .fn()
+ .mockReturnValueOnce('cached')
+ .mockReturnValue('not cached');
+ function MemoComponent() {
+ return React.useMemo(memoFun, []);
+ }
+ ReactTestUtils.act(() => {
+ reactRoot.render(
+
+
+ ,
+ );
+ });
+ expect(container.textContent).toBe(cacheExpect);
+ expect(memoFun).toBeCalledTimes(2);
+ });
+});
diff --git a/packages/lexical-react/src/__tests__/unit/useLexicalIsTextContentEmpty.test.tsx b/packages/lexical-react/src/__tests__/unit/useLexicalIsTextContentEmpty.test.tsx
index 652946f168b..9d1c30f7bdf 100644
--- a/packages/lexical-react/src/__tests__/unit/useLexicalIsTextContentEmpty.test.tsx
+++ b/packages/lexical-react/src/__tests__/unit/useLexicalIsTextContentEmpty.test.tsx
@@ -17,7 +17,7 @@ import {
import * as React from 'react';
import {createRef} from 'react';
import {createRoot, Root} from 'react-dom/client';
-import * as ReactTestUtils from 'react-dom/test-utils';
+import * as ReactTestUtils from 'shared/react-test-utils';
import {useLexicalIsTextContentEmpty} from '../../useLexicalIsTextContentEmpty';
diff --git a/packages/lexical-react/src/__tests__/unit/utils.tsx b/packages/lexical-react/src/__tests__/unit/utils.tsx
index 46824a9a6ad..3f1433f3534 100644
--- a/packages/lexical-react/src/__tests__/unit/utils.tsx
+++ b/packages/lexical-react/src/__tests__/unit/utils.tsx
@@ -11,7 +11,7 @@ import {LexicalEditor} from 'lexical';
import * as React from 'react';
import {Container} from 'react-dom';
import {createRoot, Root} from 'react-dom/client';
-import * as ReactTestUtils from 'react-dom/test-utils';
+import * as ReactTestUtils from 'shared/react-test-utils';
import * as Y from 'yjs';
import {useCollaborationContext} from '../../LexicalCollaborationContext';
diff --git a/packages/lexical-react/src/shared/useCharacterLimit.ts b/packages/lexical-react/src/shared/useCharacterLimit.ts
index 6d0c0782da8..a365bea22b9 100644
--- a/packages/lexical-react/src/shared/useCharacterLimit.ts
+++ b/packages/lexical-react/src/shared/useCharacterLimit.ts
@@ -101,7 +101,6 @@ function findOffset(
maxCharacters: number,
strlen: (input: string) => number,
): number {
- // @ts-ignore This is due to be added in a later version of TS
const Segmenter = Intl.Segmenter;
let offsetUtf16 = 0;
let offset = 0;
diff --git a/packages/lexical-react/src/useLexicalEditable.ts b/packages/lexical-react/src/useLexicalEditable.ts
index f57b67ccc5e..a561f156d46 100644
--- a/packages/lexical-react/src/useLexicalEditable.ts
+++ b/packages/lexical-react/src/useLexicalEditable.ts
@@ -20,6 +20,14 @@ function subscription(editor: LexicalEditor): LexicalSubscription {
};
}
+/**
+ * Get the current value for {@link LexicalEditor.isEditable}
+ * using {@link useLexicalSubscription}.
+ * You should prefer this over manually observing the value with
+ * {@link LexicalEditor.registerEditableListener},
+ * which is a bit tricky to do correctly, particularly when using
+ * React StrictMode (the default for development) or concurrency.
+ */
export function useLexicalEditable(): boolean {
return useLexicalSubscription(subscription);
}
diff --git a/packages/lexical-react/src/useLexicalSubscription.tsx b/packages/lexical-react/src/useLexicalSubscription.tsx
index bb59b4768bf..c8230c40033 100644
--- a/packages/lexical-react/src/useLexicalSubscription.tsx
+++ b/packages/lexical-react/src/useLexicalSubscription.tsx
@@ -19,6 +19,7 @@ export type LexicalSubscription = {
/**
* Shortcut to Lexical subscriptions when values are used for render.
+ * @param subscription - The function to create the {@link LexicalSubscription}. This function's identity must be stable (e.g. defined at module scope or with useCallback).
*/
export function useLexicalSubscription(
subscription: (editor: LexicalEditor) => LexicalSubscription,
diff --git a/packages/lexical-rich-text/src/__tests__/unit/LexicalHeadingNode.test.ts b/packages/lexical-rich-text/src/__tests__/unit/LexicalHeadingNode.test.ts
index a58f759e0ea..39b10047c4a 100644
--- a/packages/lexical-rich-text/src/__tests__/unit/LexicalHeadingNode.test.ts
+++ b/packages/lexical-rich-text/src/__tests__/unit/LexicalHeadingNode.test.ts
@@ -44,8 +44,7 @@ describe('LexicalHeadingNode tests', () => {
expect(headingNode.getTag()).toBe('h1');
expect(headingNode.getTextContent()).toBe('');
});
- // @ts-ignore
- expect(() => new HeadingNode()).toThrow();
+ expect(() => new HeadingNode('h1')).toThrow();
});
test('HeadingNode.createDOM()', async () => {
diff --git a/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx b/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx
index 9c6d435e587..2a9930a4276 100644
--- a/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx
+++ b/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx
@@ -36,6 +36,7 @@ import {
LexicalNode,
ParagraphNode,
PointType,
+ type RangeSelection,
TextNode,
} from 'lexical';
import {
@@ -47,8 +48,8 @@ import {
invariant,
TestComposer,
} from 'lexical/src/__tests__/utils';
-import {createRoot} from 'react-dom/client';
-import * as ReactTestUtils from 'react-dom/test-utils';
+import {createRoot, Root} from 'react-dom/client';
+import * as ReactTestUtils from 'shared/react-test-utils';
import {
$setAnchorPoint,
@@ -113,24 +114,27 @@ Range.prototype.getBoundingClientRect = function (): DOMRect {
};
describe('LexicalSelection tests', () => {
- let container: HTMLElement | null = null;
+ let container: HTMLElement;
+ let reactRoot: Root;
+ let editor: LexicalEditor | null = null;
beforeEach(async () => {
container = document.createElement('div');
document.body.appendChild(container);
-
+ reactRoot = createRoot(container);
await init();
});
- afterEach(() => {
- if (container) {
- document.body.removeChild(container);
- }
- container = null;
+ afterEach(async () => {
+ // Ensure we are clearing out any React state and running effects with
+ // act
+ await ReactTestUtils.act(async () => {
+ reactRoot.unmount();
+ await Promise.resolve().then();
+ });
+ document.body.removeChild(container);
});
- let editor: LexicalEditor | null = null;
-
async function init() {
function TestBase() {
function TestPlugin() {
@@ -187,10 +191,10 @@ describe('LexicalSelection tests', () => {
);
}
- ReactTestUtils.act(() => {
- createRoot(container!).render();
+ await ReactTestUtils.act(async () => {
+ reactRoot.render();
+ await Promise.resolve().then();
});
-
editor!.getRootElement()!.focus();
await Promise.resolve().then();
@@ -208,8 +212,6 @@ describe('LexicalSelection tests', () => {
await ReactTestUtils.act(async () => {
await editor!.update(fn);
});
-
- return Promise.resolve().then();
}
test('Expect initial output to be a block with no text.', () => {
@@ -1132,7 +1134,7 @@ describe('LexicalSelection tests', () => {
await applySelectionInputs(testUnit.inputs, update, editor!);
// Validate HTML matches
- expect(container!.innerHTML).toBe(testUnit.expectedHTML);
+ expect(container.innerHTML).toBe(testUnit.expectedHTML);
// Validate selection matches
const rootElement = editor!.getRootElement()!;
@@ -2653,7 +2655,6 @@ describe('LexicalSelection tests', () => {
offset: text.__text.length,
type: 'text',
});
- // @ts-ignore
const selection = $getSelection() as RangeSelection;
const columnChildrenPrev = column.getChildren();
@@ -2721,7 +2722,6 @@ describe('LexicalSelection tests', () => {
offset: 0,
type: 'element',
});
- // @ts-ignore
const selection = $getSelection() as RangeSelection;
$setBlocksType(selection, () => {
diff --git a/packages/lexical-selection/src/__tests__/utils/index.ts b/packages/lexical-selection/src/__tests__/utils/index.ts
index b8317d56b3e..84c82edecfa 100644
--- a/packages/lexical-selection/src/__tests__/utils/index.ts
+++ b/packages/lexical-selection/src/__tests__/utils/index.ts
@@ -32,7 +32,6 @@ type Segment = {
segment: string;
};
-// @ts-ignore
if (!Selection.prototype.modify) {
const wordBreakPolyfillRegex =
/[\s.,\\/#!$%^&*;:{}=\-`~()\uD800-\uDBFF\uDC00-\uDFFF\u3000-\u303F]/u;
@@ -87,7 +86,6 @@ if (!Selection.prototype.modify) {
return segments;
};
- // @ts-ignore
Selection.prototype.modify = function (alter, direction, granularity) {
// This is not a thorough implementation, it was more to get tests working
// given the refactor to use this selection method.
diff --git a/packages/lexical-table/src/__tests__/unit/LexicalTableSelection.test.tsx b/packages/lexical-table/src/__tests__/unit/LexicalTableSelection.test.tsx
index 7112c101222..a3dab04aef0 100644
--- a/packages/lexical-table/src/__tests__/unit/LexicalTableSelection.test.tsx
+++ b/packages/lexical-table/src/__tests__/unit/LexicalTableSelection.test.tsx
@@ -21,7 +21,7 @@ import {
import {createTestEditor} from 'lexical/src/__tests__/utils';
import {createRef, useEffect, useMemo} from 'react';
import {createRoot, Root} from 'react-dom/client';
-import * as ReactTestUtils from 'react-dom/test-utils';
+import * as ReactTestUtils from 'shared/react-test-utils';
describe('table selection', () => {
let originalText: TextNode;
diff --git a/packages/lexical-utils/src/__tests__/unit/LexicalEventHelpers.test.tsx b/packages/lexical-utils/src/__tests__/unit/LexicalEventHelpers.test.tsx
index de2bfcb7950..be8e06d3b2c 100644
--- a/packages/lexical-utils/src/__tests__/unit/LexicalEventHelpers.test.tsx
+++ b/packages/lexical-utils/src/__tests__/unit/LexicalEventHelpers.test.tsx
@@ -25,7 +25,7 @@ import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
import {LexicalEditor} from 'lexical';
import {initializeClipboard, TestComposer} from 'lexical/src/__tests__/utils';
import {createRoot} from 'react-dom/client';
-import * as ReactTestUtils from 'react-dom/test-utils';
+import * as ReactTestUtils from 'shared/react-test-utils';
jest.mock('shared/environment', () => {
const originalModule = jest.requireActual('shared/environment');
diff --git a/packages/lexical-website/docs/concepts/editor-state.md b/packages/lexical-website/docs/concepts/editor-state.md
index 915eaf9783e..8ddd92bf098 100644
--- a/packages/lexical-website/docs/concepts/editor-state.md
+++ b/packages/lexical-website/docs/concepts/editor-state.md
@@ -87,17 +87,19 @@ const onSubmit = () => {
}
```
-For React it could be something following:
+For React it could be something like the following:
```jsx
const initialEditorState = await loadContent();
-const editorStateRef = useRef();
+const editorStateRef = useRef(undefined);
- editorStateRef.current = editorState} />
+ {
+ editorStateRef.current = editorState;
+ }} />