diff --git a/.changeset/eight-peas-tan.md b/.changeset/eight-peas-tan.md new file mode 100644 index 00000000000..f21f7e98f29 --- /dev/null +++ b/.changeset/eight-peas-tan.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Update to React.useId() when using React 18 diff --git a/.eslintrc.json b/.eslintrc.json index afe3f863fbf..3a7a07ad84f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -104,6 +104,19 @@ { "ts-ignore": "allow-with-description" } + ], + "no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "@react-aria/ssr", + "importNames": ["useSSRSafeId"], + "message": "Please use the `useId` hook from `src/hooks/useId.ts` instead" + } + ], + "patterns": [] + } ] } }, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8594283c69b..7c56badfb3a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,11 +46,11 @@ jobs: run: node script/set-react-version.js ${{ matrix.react }} - name: Install dependencies - if: ${{ matrix.react == 18 }} + if: ${{ matrix.react == 17 }} run: npm install --legacy-peer-deps - name: Install dependencies - if: ${{ matrix.react == 17 }} + if: ${{ matrix.react == 18 }} run: npm ci - name: Build @@ -80,11 +80,11 @@ jobs: run: node script/set-react-version.js ${{ matrix.react }} - name: Install dependencies - if: ${{ matrix.react == 18 }} + if: ${{ matrix.react == 17 }} run: npm install --legacy-peer-deps - name: Install dependencies - if: ${{ matrix.react != 18 }} + if: ${{ matrix.react != 17 }} run: npm ci - name: Type check diff --git a/package-lock.json b/package-lock.json index edd43cd82d4..f414c288d28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,7 +69,7 @@ "@storybook/theming": "6.5.14", "@testing-library/dom": "8.13.0", "@testing-library/jest-dom": "5.16.5", - "@testing-library/react": "12.1.5", + "@testing-library/react": "13.4.0", "@testing-library/react-hooks": "7.0.2", "@testing-library/user-event": "^14.3.0", "@types/chroma-js": "2.1.4", @@ -79,6 +79,8 @@ "@types/lodash.isempty": "4.4.7", "@types/lodash.isobject": "3.0.7", "@types/node": "16.11.11", + "@types/react": "18.0.26", + "@types/react-dom": "18.0.9", "@typescript-eslint/eslint-plugin": "4.33.0", "@typescript-eslint/parser": "4.33.0", "axe-core": "4.5.2", @@ -121,11 +123,11 @@ "lodash.isempty": "4.4.0", "lodash.isobject": "3.0.2", "prettier": "2.7.1", - "react": "17.0.2", + "react": "18.2.0", "react-dnd": "14.0.4", "react-dnd-html5-backend": "14.0.2", - "react-dom": "17.0.2", - "react-test-renderer": "17.0.2", + "react-dom": "18.2.0", + "react-test-renderer": "18.2.0", "recast": "0.21.5", "rimraf": "3.0.2", "rollup": "2.79.0", @@ -6643,6 +6645,19 @@ "size-limit": "7.0.3" } }, + "node_modules/@size-limit/time/node_modules/react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "dev": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@size-limit/webpack": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@size-limit/webpack/-/webpack-7.0.3.tgz", @@ -13574,21 +13589,21 @@ } }, "node_modules/@testing-library/react": { - "version": "12.1.5", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.5.tgz", - "integrity": "sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg==", + "version": "13.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.4.0.tgz", + "integrity": "sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==", "dev": true, "dependencies": { "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^8.0.0", - "@types/react-dom": "<18.0.0" + "@testing-library/dom": "^8.5.0", + "@types/react-dom": "^18.0.0" }, "engines": { "node": ">=12" }, "peerDependencies": { - "react": "<18.0.0", - "react-dom": "<18.0.0" + "react": "^18.0.0", + "react-dom": "^18.0.0" } }, "node_modules/@testing-library/react-hooks": { @@ -14003,9 +14018,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "17.0.11", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.11.tgz", - "integrity": "sha512-yFRQbD+whVonItSk7ZzP/L+gPTJVBkL/7shLEF+i9GC/1cV3JmUxEQz6+9ylhUpWSDuqo1N9qEvqS6vTj4USUA==", + "version": "18.0.26", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz", + "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -14013,9 +14028,9 @@ } }, "node_modules/@types/react-dom": { - "version": "17.0.9", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz", - "integrity": "sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==", + "version": "18.0.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.9.tgz", + "integrity": "sha512-qnVvHxASt/H7i+XG1U1xMiY5t+IHcPGUK7TDMDzom08xa7e86eCeKOiLZezwCKVxJn6NEiiy2ekgX8aQssjIKg==", "dev": true, "dependencies": { "@types/react": "*" @@ -35827,12 +35842,11 @@ } }, "node_modules/react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" }, "engines": { "node": ">=0.10.0" @@ -35917,17 +35931,16 @@ "dev": true }, "node_modules/react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", "dev": true, "dependencies": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" + "scheduler": "^0.23.0" }, "peerDependencies": { - "react": "17.0.2" + "react": "^18.2.0" } }, "node_modules/react-element-to-jsx-string": { @@ -36023,16 +36036,16 @@ } }, "node_modules/react-shallow-renderer": { - "version": "16.14.1", - "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz", - "integrity": "sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg==", + "version": "16.15.0", + "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", + "integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==", "dev": true, "dependencies": { "object-assign": "^4.1.1", - "react-is": "^16.12.0 || ^17.0.0" + "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" }, "peerDependencies": { - "react": "^16.0.0 || ^17.0.0" + "react": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/react-sizeme": { @@ -36064,24 +36077,23 @@ } }, "node_modules/react-test-renderer": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz", - "integrity": "sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz", + "integrity": "sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA==", "dev": true, "dependencies": { - "object-assign": "^4.1.1", - "react-is": "^17.0.2", - "react-shallow-renderer": "^16.13.1", - "scheduler": "^0.20.2" + "react-is": "^18.2.0", + "react-shallow-renderer": "^16.15.0", + "scheduler": "^0.23.0" }, "peerDependencies": { - "react": "17.0.2" + "react": "^18.2.0" } }, "node_modules/react-test-renderer/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, "node_modules/read-pkg": { @@ -37266,13 +37278,12 @@ } }, "node_modules/scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", "dev": true, "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "node_modules/schema-utils": { @@ -46269,6 +46280,18 @@ "requires": { "estimo": "^2.3.0", "react": "^17.0.2" + }, + "dependencies": { + "react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + } } }, "@size-limit/webpack": { @@ -51353,14 +51376,14 @@ } }, "@testing-library/react": { - "version": "12.1.5", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.5.tgz", - "integrity": "sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg==", + "version": "13.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.4.0.tgz", + "integrity": "sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==", "dev": true, "requires": { "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^8.0.0", - "@types/react-dom": "<18.0.0" + "@testing-library/dom": "^8.5.0", + "@types/react-dom": "^18.0.0" } }, "@testing-library/react-hooks": { @@ -51744,9 +51767,9 @@ "dev": true }, "@types/react": { - "version": "17.0.11", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.11.tgz", - "integrity": "sha512-yFRQbD+whVonItSk7ZzP/L+gPTJVBkL/7shLEF+i9GC/1cV3JmUxEQz6+9ylhUpWSDuqo1N9qEvqS6vTj4USUA==", + "version": "18.0.26", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz", + "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==", "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -51754,9 +51777,9 @@ } }, "@types/react-dom": { - "version": "17.0.9", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz", - "integrity": "sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==", + "version": "18.0.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.9.tgz", + "integrity": "sha512-qnVvHxASt/H7i+XG1U1xMiY5t+IHcPGUK7TDMDzom08xa7e86eCeKOiLZezwCKVxJn6NEiiy2ekgX8aQssjIKg==", "dev": true, "requires": { "@types/react": "*" @@ -68452,12 +68475,11 @@ } }, "react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "react-dnd": { @@ -68516,14 +68538,13 @@ "requires": {} }, "react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", "dev": true, "requires": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" + "scheduler": "^0.23.0" } }, "react-element-to-jsx-string": { @@ -68595,13 +68616,13 @@ "dev": true }, "react-shallow-renderer": { - "version": "16.14.1", - "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz", - "integrity": "sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg==", + "version": "16.15.0", + "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", + "integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==", "dev": true, "requires": { "object-assign": "^4.1.1", - "react-is": "^16.12.0 || ^17.0.0" + "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" } }, "react-sizeme": { @@ -68630,21 +68651,20 @@ } }, "react-test-renderer": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz", - "integrity": "sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz", + "integrity": "sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA==", "dev": true, "requires": { - "object-assign": "^4.1.1", - "react-is": "^17.0.2", - "react-shallow-renderer": "^16.13.1", - "scheduler": "^0.20.2" + "react-is": "^18.2.0", + "react-shallow-renderer": "^16.15.0", + "scheduler": "^0.23.0" }, "dependencies": { "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true } } @@ -69556,13 +69576,12 @@ } }, "scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", "dev": true, "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "schema-utils": { diff --git a/package.json b/package.json index ea9c1f1608f..41282022f12 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "@storybook/theming": "6.5.14", "@testing-library/dom": "8.13.0", "@testing-library/jest-dom": "5.16.5", - "@testing-library/react": "12.1.5", + "@testing-library/react": "13.4.0", "@testing-library/react-hooks": "7.0.2", "@testing-library/user-event": "^14.3.0", "@types/chroma-js": "2.1.4", @@ -193,11 +193,11 @@ "lodash.isempty": "4.4.0", "lodash.isobject": "3.0.2", "prettier": "2.7.1", - "react": "17.0.2", + "react": "18.2.0", "react-dnd": "14.0.4", "react-dnd-html5-backend": "14.0.2", - "react-dom": "17.0.2", - "react-test-renderer": "17.0.2", + "react-dom": "18.2.0", + "react-test-renderer": "18.2.0", "recast": "0.21.5", "rimraf": "3.0.2", "rollup": "2.79.0", @@ -209,7 +209,9 @@ "styled-components": "4.4.1", "ts-toolbelt": "9.6.0", "typescript": "4.9.3", - "webpack": "5.74.0" + "webpack": "5.74.0", + "@types/react": "18.0.26", + "@types/react-dom": "18.0.9" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0", diff --git a/src/ActionList/Group.tsx b/src/ActionList/Group.tsx index bd2052fb2e8..f6332760c1f 100644 --- a/src/ActionList/Group.tsx +++ b/src/ActionList/Group.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {useSSRSafeId} from '@react-aria/ssr' +import {useId} from '../hooks/useId' import Box from '../Box' import {SxProp} from '../sx' import {ListContext, ActionListProps} from './List' @@ -44,7 +44,7 @@ export const Group: React.FC> = ({ sx = {}, ...props }) => { - const labelId = useSSRSafeId() + const labelId = useId() const {role: listRole} = React.useContext(ListContext) return ( diff --git a/src/ActionList/Item.tsx b/src/ActionList/Item.tsx index 02ebfa6a87d..1555b5db24c 100644 --- a/src/ActionList/Item.tsx +++ b/src/ActionList/Item.tsx @@ -1,10 +1,10 @@ import {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' -import {useSSRSafeId} from '@react-aria/ssr' import React from 'react' import styled from 'styled-components' import Box, {BoxProps} from '../Box' import sx, {BetterSystemStyleObject, merge, SxProp} from '../sx' import {useTheme} from '../ThemeProvider' +import {useId} from '../hooks/useId' import {ActionListContainerContext} from './ActionListContainerContext' import {ActionListGroupProps, GroupContext} from './Group' import {ActionListProps, ListContext} from './List' @@ -168,9 +168,9 @@ export const Item = React.forwardRef( ) // use props.id if provided, otherwise generate one. - const labelId = useSSRSafeId(id) - const inlineDescriptionId = useSSRSafeId(id && `${id}--inline-description`) - const blockDescriptionId = useSSRSafeId(id && `${id}--block-description`) + const labelId = useId(id) + const inlineDescriptionId = useId(id && `${id}--inline-description`) + const blockDescriptionId = useId(id && `${id}--block-description`) const ItemWrapper = _PrivateItemWrapper || React.Fragment diff --git a/src/ActionMenu.tsx b/src/ActionMenu.tsx index 558ddf02abb..97778d4e9a3 100644 --- a/src/ActionMenu.tsx +++ b/src/ActionMenu.tsx @@ -1,5 +1,4 @@ import React from 'react' -import {useSSRSafeId} from '@react-aria/ssr' import {TriangleDownIcon} from '@primer/octicons-react' import {AnchoredOverlay, AnchoredOverlayProps} from './AnchoredOverlay' import {OverlayProps} from './Overlay' @@ -7,6 +6,7 @@ import {useProvidedRefOrCreate, useProvidedStateOrCreate, useMenuKeyboardNavigat import {Divider} from './ActionList/Divider' import {ActionListContainerContext} from './ActionList/ActionListContainerContext' import {Button, ButtonProps} from './Button' +import {useId} from './hooks/useId' import {MandateProps} from './utils/types' import {merge, BetterSystemStyleObject} from './sx' @@ -46,7 +46,7 @@ const Menu: React.FC> = ({ const onClose = React.useCallback(() => setCombinedOpenState(false), [setCombinedOpenState]) const anchorRef = useProvidedRefOrCreate(externalAnchorRef) - const anchorId = useSSRSafeId() + const anchorId = useId() let renderAnchor: AnchoredOverlayProps['renderAnchor'] = null // 🚨 Hack for good API! diff --git a/src/AnchoredOverlay/AnchoredOverlay.tsx b/src/AnchoredOverlay/AnchoredOverlay.tsx index 77b4806ad70..1cdb74d9dca 100644 --- a/src/AnchoredOverlay/AnchoredOverlay.tsx +++ b/src/AnchoredOverlay/AnchoredOverlay.tsx @@ -3,7 +3,7 @@ import Overlay, {OverlayProps} from '../Overlay' import {FocusTrapHookSettings, useFocusTrap} from '../hooks/useFocusTrap' import {FocusZoneHookSettings, useFocusZone} from '../hooks/useFocusZone' import {useAnchoredPosition, useProvidedRefOrCreate, useRenderForcingRef} from '../hooks' -import {useSSRSafeId} from '@react-aria/ssr' +import {useId} from '../hooks/useId' import type {PositionSettings} from '@primer/behaviors' interface AnchoredOverlayPropsWithAnchor { @@ -104,7 +104,7 @@ export const AnchoredOverlay: React.FC { const anchorRef = useProvidedRefOrCreate(externalAnchorRef) const [overlayRef, updateOverlayRef] = useRenderForcingRef() - const anchorId = useSSRSafeId(externalAnchorId) + const anchorId = useId(externalAnchorId) const onClickOutside = useCallback(() => onClose?.('click-outside'), [onClose]) const onEscape = useCallback(() => onClose?.('escape'), [onClose]) diff --git a/src/Autocomplete/Autocomplete.tsx b/src/Autocomplete/Autocomplete.tsx index b6493429c56..4a3fd185b4c 100644 --- a/src/Autocomplete/Autocomplete.tsx +++ b/src/Autocomplete/Autocomplete.tsx @@ -1,10 +1,10 @@ import React, {useCallback, useReducer, useRef} from 'react' -import {useSSRSafeId} from '@react-aria/ssr' import {ComponentProps} from '../utils/types' import {AutocompleteContext} from './AutocompleteContext' import AutocompleteInput from './AutocompleteInput' import AutocompleteMenu from './AutocompleteMenu' import AutocompleteOverlay from './AutocompleteOverlay' +import {useId} from '../hooks/useId' type Action = | {type: 'showMenu' | 'isMenuDirectlyActivated'; payload: boolean} @@ -66,7 +66,7 @@ const Autocomplete: React.FC> = ({childre const setSelectedItemLength = useCallback((value: State['selectedItemLength']) => { dispatch({type: 'selectedItemLength', payload: value}) }, []) - const id = useSSRSafeId(idProp) + const id = useId(idProp) return ( (null) for (const footerButton of footerButtons) { if (footerButton.autoFocus) { diff --git a/src/FilteredActionList/FilteredActionList.tsx b/src/FilteredActionList/FilteredActionList.tsx index 352cc01982b..7a6cf8ef007 100644 --- a/src/FilteredActionList/FilteredActionList.tsx +++ b/src/FilteredActionList/FilteredActionList.tsx @@ -1,5 +1,4 @@ import React, {KeyboardEventHandler, useCallback, useEffect, useRef} from 'react' -import {useSSRSafeId} from '@react-aria/ssr' import {GroupedListProps, ListPropsBase} from '../deprecated/ActionList/List' import TextInput, {TextInputProps} from '../TextInput' import Box from '../Box' @@ -14,6 +13,7 @@ import useScrollFlash from '../hooks/useScrollFlash' import {scrollIntoView} from '@primer/behaviors' import type {ScrollIntoViewOptions} from '@primer/behaviors' import {SxProp} from '../sx' +import {useId} from '../hooks/useId' const menuScrollMargins: ScrollIntoViewOptions = {startMargin: 0, endMargin: 8} @@ -59,7 +59,7 @@ export function FilteredActionList({ const listContainerRef = useRef(null) const inputRef = useProvidedRefOrCreate(providedInputRef) const activeDescendantRef = useRef() - const listId = useSSRSafeId() + const listId = useId() const onInputKeyPress: KeyboardEventHandler = useCallback( event => { if (event.key === 'Enter' && activeDescendantRef.current) { diff --git a/src/NavList/NavList.tsx b/src/NavList/NavList.tsx index 034c8756ad7..11a500e8004 100644 --- a/src/NavList/NavList.tsx +++ b/src/NavList/NavList.tsx @@ -1,6 +1,5 @@ import {ChevronDownIcon} from '@primer/octicons-react' import {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' -import {useSSRSafeId} from '@react-aria/ssr' import React, {isValidElement} from 'react' import styled from 'styled-components' import { @@ -12,6 +11,7 @@ import { import Box from '../Box' import StyledOcticon from '../StyledOcticon' import sx, {merge, SxProp} from '../sx' +import {useId} from '../hooks/useId' // ---------------------------------------------------------------------------- // NavList @@ -103,8 +103,8 @@ const ItemWithSubNavContext = React.createContext<{buttonId: string; subNavId: s // TODO: ref prop // TODO: Animate open/close transition function ItemWithSubNav({children, subNav, sx: sxProp = {}}: ItemWithSubNavProps) { - const buttonId = useSSRSafeId() - const subNavId = useSSRSafeId() + const buttonId = useId() + const subNavId = useId() const [isOpen, setIsOpen] = React.useState(false) const subNavRef = React.useRef(null) const [containsCurrentItem, setContainsCurrentItem] = React.useState(false) diff --git a/src/TreeView/TreeView.tsx b/src/TreeView/TreeView.tsx index 4f0a10c8d87..b7fb6b83ce4 100644 --- a/src/TreeView/TreeView.tsx +++ b/src/TreeView/TreeView.tsx @@ -4,7 +4,6 @@ import { FileDirectoryFillIcon, FileDirectoryOpenFillIcon, } from '@primer/octicons-react' -import {useSSRSafeId} from '@react-aria/ssr' import classnames from 'classnames' import React from 'react' import styled, {keyframes} from 'styled-components' @@ -12,6 +11,7 @@ import {get} from '../constants' import {ConfirmationDialog} from '../Dialog/ConfirmationDialog' import {useControllableState} from '../hooks/useControllableState' import useSafeTimeout from '../hooks/useSafeTimeout' +import {useId} from '../hooks/useId' import Spinner from '../Spinner' import sx, {SxProp} from '../sx' import Text from '../Text' @@ -308,9 +308,9 @@ const Item = React.forwardRef( ref, ) => { const {expandedStateCache} = React.useContext(RootContext) - const labelId = useSSRSafeId() - const leadingVisualId = useSSRSafeId() - const trailingVisualId = useSSRSafeId() + const labelId = useId() + const leadingVisualId = useId() + const trailingVisualId = useId() const [isExpanded, setIsExpanded] = useControllableState({ name: itemId, // If the item was previously mounted, it's expanded state might be cached. @@ -654,7 +654,7 @@ type LoadingItemProps = { } const LoadingItem = React.forwardRef(({count}, ref) => { - const itemId = useSSRSafeId() + const itemId = useId() if (count) { return ( diff --git a/src/UnderlineNav2/UnderlineNav.tsx b/src/UnderlineNav2/UnderlineNav.tsx index 4bc837d9ca1..6bffd1d08b7 100644 --- a/src/UnderlineNav2/UnderlineNav.tsx +++ b/src/UnderlineNav2/UnderlineNav.tsx @@ -14,8 +14,8 @@ import {Button} from '../Button' import {TriangleDownIcon} from '@primer/octicons-react' import {useOnEscapePress} from '../hooks/useOnEscapePress' import {useOnOutsideClick} from '../hooks/useOnOutsideClick' +import {useId} from '../hooks/useId' import {ActionList} from '../ActionList' -import {useSSRSafeId} from '@react-aria/ssr' export type UnderlineNavProps = { 'aria-label'?: React.AriaAttributes['aria-label'] @@ -148,7 +148,7 @@ export const UnderlineNav = forwardRef( const moreMenuRef = useRef(null) const moreMenuBtnRef = useRef(null) const containerRef = React.useRef(null) - const disclosureWidgetId = useSSRSafeId() + const disclosureWidgetId = useId() const {theme} = useTheme() diff --git a/src/UnderlineNav2/features.stories.tsx b/src/UnderlineNav2/features.stories.tsx index c195e1842df..f0627fa160a 100644 --- a/src/UnderlineNav2/features.stories.tsx +++ b/src/UnderlineNav2/features.stories.tsx @@ -82,6 +82,10 @@ export const OverflowTemplate = ({initialSelectedIndex = 1}: {initialSelectedInd key={item.navigation} icon={item.icon} aria-current={index === selectedIndex ? 'page' : undefined} + // Set so that navigation in interaction tests does not cause the + // page to load the storybook iframe URL and instead keeps the test in + // the local preview + target="_self" onSelect={event => { event.preventDefault() setSelectedIndex(index) diff --git a/src/UnderlineNav2/interactions.stories.tsx b/src/UnderlineNav2/interactions.stories.tsx index b6174d71c84..528fbf822db 100644 --- a/src/UnderlineNav2/interactions.stories.tsx +++ b/src/UnderlineNav2/interactions.stories.tsx @@ -16,82 +16,117 @@ const KeyboardNavigation = () => { return } +const delay = (ms: number = 500) => new Promise(resolve => setTimeout(resolve, ms)) + KeyboardNavigation.storyName = 'Keyboard navigation' KeyboardNavigation.play = async ({canvasElement}: {canvasElement: HTMLElement}) => { - const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) - await delay(2000) - canvasElement.style.width = '800px' const canvas = within(canvasElement) + canvasElement.style.width = '800px' + + await delay(1000) + + // Code const firstItem = canvas.getByRole('link', {name: 'Code'}) firstItem.focus() - await userEvent.tab() - await delay(500) - await userEvent.tab() - await delay(500) - await userEvent.tab() - await delay(500) - await userEvent.tab() - await delay(500) - await userEvent.tab() - await delay(500) - await userEvent.tab() - await delay(500) - // current focus element - let activeElement = document.activeElement - // check if the active element is the more button - expect(activeElement).toHaveTextContent('More') - // Open the more Menu - activeElement && userEvent.click(activeElement) - await userEvent.tab() - await delay(500) - await userEvent.tab() - await delay(500) + // Issues + await delay() + userEvent.tab() + expect(document.activeElement).toHaveTextContent('Issues') + + // Pull Requests + await delay() + userEvent.tab() + expect(document.activeElement).toHaveTextContent('Pull Requests') + + // Discussions + await delay() + userEvent.tab() + expect(document.activeElement).toHaveTextContent('Discussions') + + // Actions + await delay() + userEvent.tab() + expect(document.activeElement).toHaveTextContent('Actions') + + // Projects + await delay() + userEvent.tab() + expect(document.activeElement).toHaveTextContent('Projects') + + // More + await delay() + userEvent.tab() + expect(document.activeElement).toHaveTextContent('More') + + // Click to open menu + await delay() + userEvent.click(document.activeElement as Element) + + // Insights + await delay() + userEvent.tab() + expect(document.activeElement).toHaveTextContent('Insights') + + // Settings + await delay() + userEvent.tab() + expect(document.activeElement).toHaveTextContent('Settings') + + // Click to navigate + await delay() let menuItem = canvas.getByRole('link', {name: 'Settings (10)'}) userEvent.click(menuItem) - expect(activeElement).toHaveFocus() + await delay() menuItem = canvas.getByRole('link', {name: 'Settings (10)'}) - expect(menuItem).toHaveAttribute('aria-current', 'page') const lastListItem = canvas.getByRole('list').children[5] const menuListItem = canvas.getByText('Settings').closest('li') as HTMLLIElement + // expect Settings be the last element on the list. expect(lastListItem).toEqual(menuListItem) + + // Settings userEvent.tab({shift: true}) - await delay(500) + + // Actions + await delay() userEvent.tab({shift: true}) - await delay(500) + + // Discussions + await delay() userEvent.tab({shift: true}) - await delay(500) + + // Pull Requests + await delay() userEvent.tab({shift: true}) - await delay(500) - // current focus element - activeElement = document.activeElement - userEvent.keyboard('{Enter}') - expect(activeElement).toHaveTextContent('Pull Requests') + expect(document.activeElement).toHaveTextContent('Pull Requests') } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore SelectAMenuItem.play = async ({canvasElement}: {canvasElement: HTMLElement}) => { - const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) - canvasElement.style.width = '800px' - await delay(2000) const canvas = within(canvasElement) + canvasElement.style.width = '800px' + + await delay(1000) + const moreBtn = canvas.getByRole('button', {name: 'More Repository items'}) userEvent.hover(moreBtn) - await delay(1000) + + await delay() userEvent.click(moreBtn) - await delay(1000) + await delay() let menuItem = canvas.getByRole('link', {name: 'Settings (10)'}) userEvent.click(menuItem) - expect(moreBtn).toHaveFocus() - menuItem = canvas.getByRole('link', {name: 'Settings (10)'}) + await delay() + menuItem = canvas.getByRole('link', {name: 'Settings (10)'}) expect(menuItem).toHaveAttribute('aria-current', 'page') + const lastListItem = canvas.getByRole('list').children[5] const menuListItem = canvas.getByText('Settings').closest('li') as HTMLLIElement // expect Settings be the last element on the list. @@ -104,7 +139,6 @@ const KeepSelectedItemVisible = () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore KeepSelectedItemVisible.play = async ({canvasElement}: {canvasElement: HTMLElement}) => { - const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) const canvas = within(canvasElement) // await delay(2000) const selectedItem = canvas.getByRole('link', {name: 'Settings (10)'}) diff --git a/src/deprecated/ActionList/Item.tsx b/src/deprecated/ActionList/Item.tsx index 95a7672ecd6..755a78be028 100644 --- a/src/deprecated/ActionList/Item.tsx +++ b/src/deprecated/ActionList/Item.tsx @@ -13,7 +13,7 @@ import { activeDescendantActivatedIndirectly, isActiveDescendantAttribute, } from '@primer/behaviors' -import {useSSRSafeId} from '@react-aria/ssr' +import {useId} from '../../hooks/useId' import {ForwardRefComponent as PolymorphicForwardRefComponent} from '../../utils/polymorphic' import {AriaRole} from '../../utils/types' @@ -352,8 +352,8 @@ export const Item = React.forwardRef((itemProps, ref) => { ...props } = itemProps - const labelId = useSSRSafeId() - const descriptionId = useSSRSafeId() + const labelId = useId() + const descriptionId = useId() const keyPressHandler = useCallback( (event: React.KeyboardEvent) => { diff --git a/src/drafts/MarkdownEditor/MarkdownEditor.tsx b/src/drafts/MarkdownEditor/MarkdownEditor.tsx index 41d3cbb9eba..f077d1a1cb3 100644 --- a/src/drafts/MarkdownEditor/MarkdownEditor.tsx +++ b/src/drafts/MarkdownEditor/MarkdownEditor.tsx @@ -1,4 +1,3 @@ -import {useSSRSafeId} from '@react-aria/ssr' import React, { forwardRef, useCallback, @@ -11,6 +10,7 @@ import React, { } from 'react' import Box from '../../Box' import {FileType} from '../hooks/useUnifiedFileSelect' +import {useId} from '../../hooks/useId' import {useIgnoreKeyboardActionsWhileComposing} from '../hooks/useIgnoreKeyboardActionsWhileComposing' import {useResizeObserver} from '../../hooks/useResizeObserver' import {useSyntheticChange} from '../hooks/useSyntheticChange' @@ -274,7 +274,7 @@ const MarkdownEditor = forwardRef( }, [condensed]) // the ID must be unique for each instance while remaining constant across renders - const id = useSSRSafeId() + const id = useId() const descriptionId = `${id}-description` const savedRepliesRef = useRef(null) diff --git a/src/drafts/hooks/useCombobox.ts b/src/drafts/hooks/useCombobox.ts index 249f85dc3ea..bc7ae732348 100644 --- a/src/drafts/hooks/useCombobox.ts +++ b/src/drafts/hooks/useCombobox.ts @@ -1,6 +1,6 @@ import Combobox from '@github/combobox-nav' -import {useSSRSafeId} from '@react-aria/ssr' import {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react' +import {useId} from '../../hooks/useId' export type ComboboxCommitEvent = { /** The underlying `combobox-commit` event. */ @@ -64,7 +64,7 @@ export const useCombobox = ({ tabInsertsSuggestions = false, defaultFirstOption = false, }: UseComboboxSettings) => { - const id = useSSRSafeId() + const id = useId() const optionIdPrefix = `combobox-${id}__option` const isOpenRef = useRef(isOpen) diff --git a/src/hooks/useId.ts b/src/hooks/useId.ts new file mode 100644 index 00000000000..06e5fcf8d24 --- /dev/null +++ b/src/hooks/useId.ts @@ -0,0 +1,34 @@ +// eslint-disable-next-line import/no-namespace +import * as React from 'react' +// eslint-disable-next-line no-restricted-imports +import {useSSRSafeId} from '@react-aria/ssr' + +/** + * Detect if `React.useId()` is present. This strategy is a workaround for: + * https://github.com/webpack/webpack/issues/14814 + * + * This technique is inspired by Material UI: + * https://github.com/mui/material-ui/blob/7bc478ec00a3b5625427f36c827e00b0a17be3d0/packages/mui-utils/src/useId.ts#L21 + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any, no-useless-concat +const useReactId: undefined | (() => string) = (React as any)['useId' + ''] + +export function useId(id?: string) { + // Force useSSRSafeId in test environments to maintain snapshot parity between + // major versions of React + if (process.env.NODE_ENV === 'test') { + // eslint-disable-next-line react-hooks/rules-of-hooks + return useSSRSafeId(id) + } + + if (useReactId !== undefined) { + if (id) { + return id + } + // eslint-disable-next-line react-hooks/rules-of-hooks + return useReactId() + } + + // eslint-disable-next-line react-hooks/rules-of-hooks + return useSSRSafeId(id) +} diff --git a/src/utils/ssr.tsx b/src/utils/ssr.tsx index 8980f196e49..a74b004a343 100644 --- a/src/utils/ssr.tsx +++ b/src/utils/ssr.tsx @@ -1 +1,2 @@ +// eslint-disable-next-line no-restricted-imports export {SSRProvider, useSSRSafeId} from '@react-aria/ssr'