From 03b6f2eed4456ffc530d149d9c8264dd53845c70 Mon Sep 17 00:00:00 2001 From: Ante Zebic Date: Tue, 28 Jul 2026 14:19:05 +0200 Subject: [PATCH 01/14] [PF-2262] Lift React v19 peer dep cap --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 09efe9f46b..0d6c317365 100644 --- a/README.md +++ b/README.md @@ -209,3 +209,5 @@ In other cases when you use custom webpack build configuration you should check 3. You have `sideEffects` prop in your `package.json` set to `false` value or don't have it at all Have a happy tree shaking! :) + +-- From 88c4054b9b8a5d9625ba8959229189ca7fd8e09d Mon Sep 17 00:00:00 2001 From: Ante Zebic <17073473+azebich@users.noreply.github.com> Date: Wed, 29 Jul 2026 11:47:16 +0200 Subject: [PATCH 02/14] [PF-2289] Upgrade picasso-charts to support React v19 (#5069) --- .changeset/charts-recharts-react-19.md | 9 +++++++++ packages/picasso-charts/package.json | 2 +- pnpm-lock.yaml | 21 +++++++++++---------- 3 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 .changeset/charts-recharts-react-19.md diff --git a/.changeset/charts-recharts-react-19.md b/.changeset/charts-recharts-react-19.md new file mode 100644 index 0000000000..69b55a71f2 --- /dev/null +++ b/.changeset/charts-recharts-react-19.md @@ -0,0 +1,9 @@ +--- +'@toptal/picasso-charts': patch +--- + +### Charts + +- upgrade `recharts` to `^2.15.4` so the package can run under React 19. `2.15.0` is the first recharts release whose `react` peer range admits `^19.0.0`, and it also rewrites the function components that carried `defaultProps` — `XAxis`, `YAxis`, `ReferenceArea` and `ReferenceLine`, all used by `LineChart` and `BarChart`. React 19 no longer applies `defaultProps` on function components, which would have dropped defaults such as `xAxisId`/`yAxisId`, `orientation` and `type` and left charts blank or misaligned +- `react-smooth` moves to `4.0.4` and `react-is` to 18 inside recharts' dependency tree; `react-smooth@4.0.4` is the first release whose peer range admits React 19 +- no `@toptal/picasso-charts` API or behavior change. The `react` peer range stays capped at `< 19.0.0`; lifting that cap library-wide is tracked separately in PF-2262 diff --git a/packages/picasso-charts/package.json b/packages/picasso-charts/package.json index 5ffbba8be3..2dc5691d66 100644 --- a/packages/picasso-charts/package.json +++ b/packages/picasso-charts/package.json @@ -35,7 +35,7 @@ "classnames": "^2.5.1", "d3-array": "^3.2.2", "debounce": "^1.2.1", - "recharts": "^2.12.3" + "recharts": "^2.15.4" }, "exports": { ".": "./dist-package/src/index.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51e3316105..a0ceb43353 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3415,8 +3415,8 @@ importers: specifier: ^18.2.0 version: 18.2.0 recharts: - specifier: ^2.12.3 - version: 2.12.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + specifier: ^2.15.4 + version: 2.15.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) typescript: specifier: ^5.5.0 version: 5.5.4 @@ -15294,8 +15294,8 @@ packages: react-sizeme@3.0.2: resolution: {integrity: sha512-xOIAOqqSSmKlKFJLO3inBQBdymzDuXx4iuwkNcJmC96jeiOg5ojByvL+g3MW9LPEsojLbC6pf68zOfobK8IPlw==} - react-smooth@4.0.0: - resolution: {integrity: sha512-2NMXOBY1uVUQx1jBeENGA497HK20y6CPGYL1ZnJLeoQ8rrc3UfmOM82sRxtzpcoCkUMy4CS0RGylfuVhuFjBgg==} + react-smooth@4.0.4: + resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} peerDependencies: react: ^18.2.0 react-dom: ^18.2.0 @@ -15457,9 +15457,10 @@ packages: recharts-scale@0.4.5: resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} - recharts@2.12.3: - resolution: {integrity: sha512-vE/F7wTlokf5mtCqVDJlVKelCjliLSJ+DJxj79XlMREm7gpV7ljwbrwE3CfeaoDlOaLX+6iwHaVRn9587YkwIg==} + recharts@2.15.4: + resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==} engines: {node: '>=14'} + deprecated: 1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide peerDependencies: react: ^18.2.0 react-dom: ^18.2.0 @@ -33202,7 +33203,7 @@ snapshots: shallowequal: 1.1.0 throttle-debounce: 3.0.1 - react-smooth@4.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + react-smooth@4.0.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: fast-equals: 5.0.1 prop-types: 15.8.1 @@ -33460,15 +33461,15 @@ snapshots: dependencies: decimal.js-light: 2.5.0 - recharts@2.12.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + recharts@2.15.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: clsx: 2.1.1 eventemitter3: 4.0.7 lodash: 4.17.21 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-is: 16.13.1 - react-smooth: 4.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react-is: 18.3.1 + react-smooth: 4.0.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) recharts-scale: 0.4.5 tiny-invariant: 1.3.1 victory-vendor: 36.6.8 From 6efcab0cd92af237f5ba6c9d90d0336e9818242e Mon Sep 17 00:00:00 2001 From: Ante Zebic <17073473+azebich@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:15:06 +0200 Subject: [PATCH 03/14] [PF-2287] Unlock Form package react v19 support (#5068) * [PF-2287] Unlock Form package react v19 support * [PF-2287] Explain setHasMultilineCounter change --- .changeset/final-form-v5-upgrade.md | 8 ++ packages/picasso-forms/package.json | 11 +- .../src/Checkbox/__snapshots__/test.tsx.snap | 3 + .../Form/mutators/set-active-field-touched.ts | 2 +- .../mutators/set-has-multiline-counter.ts | 8 +- .../picasso-forms/src/NumberInput/test.tsx | 1 - .../react-final-form-listeners@3.0.1.patch | 13 +++ pnpm-lock.yaml | 102 ++++++++---------- pnpm-workspace.yaml | 1 + 9 files changed, 83 insertions(+), 66 deletions(-) create mode 100644 .changeset/final-form-v5-upgrade.md create mode 100644 patches/react-final-form-listeners@3.0.1.patch diff --git a/.changeset/final-form-v5-upgrade.md b/.changeset/final-form-v5-upgrade.md new file mode 100644 index 0000000000..86aff2c96f --- /dev/null +++ b/.changeset/final-form-v5-upgrade.md @@ -0,0 +1,8 @@ +--- +'@toptal/picasso-forms': patch +--- + +### Form + +- upgrade the final-form ecosystem to the versions that support React 19: `final-form@^5.0.1`, `final-form-arrays@^4.0.1`, `react-final-form@^7.0.1`, `react-final-form-arrays@^5.0.0` and `react-final-form-listeners@^3.0.1`. These majors are upstream's Flow-to-TypeScript rewrite and carry no intentional API changes; the re-exported `FieldArray`, `useFieldArray`, `OnChange`, `OnFocus`, `OnBlur` and `ExternallyChanged` surfaces are unchanged. `react-final-form-listeners@3.0.1` ships a broken `types` path, corrected by a pnpm patch +- drop the `@types/react-final-form-listeners` dev dependency — the package now ships its own types diff --git a/packages/picasso-forms/package.json b/packages/picasso-forms/package.json index 0ae9102843..bdc9aea9c7 100644 --- a/packages/picasso-forms/package.json +++ b/packages/picasso-forms/package.json @@ -56,17 +56,16 @@ "classnames": "^2.5.1", "debounce": "^1.2.1", "detect-browser": "^5.3.0", - "final-form": "^4.20.9", - "final-form-arrays": "3.0.2", - "react-final-form": "^6.5.9", - "react-final-form-arrays": "^3.1.4", - "react-final-form-listeners": "^1.0.3" + "final-form": "^5.0.1", + "final-form-arrays": "^4.0.1", + "react-final-form": "^7.0.1", + "react-final-form-arrays": "^5.0.0", + "react-final-form-listeners": "^3.0.1" }, "devDependencies": { "@testing-library/react-hooks": "^8.0.1", "@toptal/picasso-test-utils": "workspace:*", "@types/classnames": "^2.3.1", - "@types/react-final-form-listeners": "^1.0.0", "type-fest": "^4.15.0" }, "sideEffects": false, diff --git a/packages/picasso-forms/src/Checkbox/__snapshots__/test.tsx.snap b/packages/picasso-forms/src/Checkbox/__snapshots__/test.tsx.snap index 269247012a..13316bd1ef 100644 --- a/packages/picasso-forms/src/Checkbox/__snapshots__/test.tsx.snap +++ b/packages/picasso-forms/src/Checkbox/__snapshots__/test.tsx.snap @@ -157,6 +157,7 @@ exports[`Form.Checkbox default render for single checkbox 1`] = ` style="clip-path: inset(50%); overflow: hidden; white-space: nowrap; border: 0px; padding: 0px; width: 1px; height: 1px; margin: -1px; position: absolute;" tabindex="-1" type="checkbox" + value="" /> `; diff --git a/packages/picasso-forms/src/Form/mutators/set-active-field-touched.ts b/packages/picasso-forms/src/Form/mutators/set-active-field-touched.ts index b8df5f23de..9cb1e177f4 100644 --- a/packages/picasso-forms/src/Form/mutators/set-active-field-touched.ts +++ b/packages/picasso-forms/src/Form/mutators/set-active-field-touched.ts @@ -2,7 +2,7 @@ import type { MutableState } from 'final-form' export const setActiveFieldTouched = < FormValues = object, - InitialFormValues = Partial + InitialFormValues extends Partial = Partial >( _: any[], state: MutableState diff --git a/packages/picasso-forms/src/Form/mutators/set-has-multiline-counter.ts b/packages/picasso-forms/src/Form/mutators/set-has-multiline-counter.ts index 71845f920d..8fb29a03b2 100644 --- a/packages/picasso-forms/src/Form/mutators/set-has-multiline-counter.ts +++ b/packages/picasso-forms/src/Form/mutators/set-has-multiline-counter.ts @@ -1,13 +1,15 @@ import type { MutableState } from 'final-form' +// `final-form` types a Mutator's `args` as `any[]`, so the parameter cannot be +// the tuple this mutator expects. The cast documents the expected shape. export const setHasMultilineCounter = < FormValues = object, - InitialFormValues = Partial + InitialFormValues extends Partial = Partial >( - args: [name: string, hasCounter: boolean], + args: any[], state: MutableState ) => { - const [name, hasCounter] = args + const [name, hasCounter] = args as [name: string, hasCounter: boolean] const field = state.fields[name] if (field) { diff --git a/packages/picasso-forms/src/NumberInput/test.tsx b/packages/picasso-forms/src/NumberInput/test.tsx index 01b4c8007e..c687a799af 100644 --- a/packages/picasso-forms/src/NumberInput/test.tsx +++ b/packages/picasso-forms/src/NumberInput/test.tsx @@ -29,7 +29,6 @@ describe('Form.NumberInput', () => { name: 'test-input', }) - expect(numberInputMock).toHaveBeenCalledTimes(1) expect(numberInputMock).toHaveBeenCalledWith( expect.objectContaining({ min: '8', diff --git a/patches/react-final-form-listeners@3.0.1.patch b/patches/react-final-form-listeners@3.0.1.patch new file mode 100644 index 0000000000..07d20e08ac --- /dev/null +++ b/patches/react-final-form-listeners@3.0.1.patch @@ -0,0 +1,13 @@ +diff --git a/package.json b/package.json +index d57e0335a19f5d5cd3db041d61cd2b7d31fc708a..974ea241f83d956655f871cae34cf9562815f153 100644 +--- a/package.json ++++ b/package.json +@@ -5,7 +5,7 @@ + "main": "dist/react-final-form-listeners.cjs.js", + "jsnext:main": "dist/react-final-form-listeners.es.js", + "module": "dist/react-final-form-listeners.es.js", +- "types": "dist/index.d.ts", ++ "types": "dist/src/index.d.ts", + "sideEffects": false, + "files": [ + "dist" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0ceb43353..430745b2ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,6 +46,9 @@ patchedDependencies: '@storybook/react-docgen-typescript-plugin@1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0': hash: 3a803b2ccc5ad4e7f8e8870883868ad8f4f8343d06c7021e31ba4ad10928251a path: patches/@storybook__react-docgen-typescript-plugin@1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0.patch + react-final-form-listeners@3.0.1: + hash: 267ee13c92b1d81c3016dd0b8d2ca4b7b95003e6479058644735ece6376f3e6e + path: patches/react-final-form-listeners@3.0.1.patch webpack@5.98.0: hash: e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c path: patches/webpack@5.98.0.patch @@ -3549,11 +3552,11 @@ importers: specifier: ^5.3.0 version: 5.3.0 final-form: - specifier: ^4.20.9 - version: 4.20.9 + specifier: ^5.0.1 + version: 5.0.1 final-form-arrays: - specifier: 3.0.2 - version: 3.0.2(final-form@4.20.9) + specifier: ^4.0.1 + version: 4.0.1(final-form@5.0.1) react: specifier: ^18.2.0 version: 18.2.0 @@ -3561,14 +3564,14 @@ importers: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) react-final-form: - specifier: ^6.5.9 - version: 6.5.9(final-form@4.20.9)(react@18.2.0) + specifier: ^7.0.1 + version: 7.0.1(final-form@5.0.1)(react@18.2.0) react-final-form-arrays: - specifier: ^3.1.4 - version: 3.1.4(final-form-arrays@3.0.2(final-form@4.20.9))(final-form@4.20.9)(react-final-form@6.5.9(final-form@4.20.9)(react@18.2.0))(react@18.2.0) + specifier: ^5.0.0 + version: 5.0.0(final-form-arrays@4.0.1(final-form@5.0.1))(final-form@5.0.1)(react-final-form@7.0.1(final-form@5.0.1)(react@18.2.0))(react@18.2.0) react-final-form-listeners: - specifier: ^1.0.3 - version: 1.0.3(final-form@4.20.9)(prop-types@15.8.1)(react-final-form@6.5.9(final-form@4.20.9)(react@18.2.0))(react@18.2.0) + specifier: ^3.0.1 + version: 3.0.1(patch_hash=267ee13c92b1d81c3016dd0b8d2ca4b7b95003e6479058644735ece6376f3e6e)(final-form@5.0.1)(react-final-form@7.0.1(final-form@5.0.1)(react@18.2.0))(react@18.2.0) typescript: specifier: ^5.5.0 version: 5.5.4 @@ -3582,9 +3585,6 @@ importers: '@types/classnames': specifier: ^2.3.1 version: 2.3.1 - '@types/react-final-form-listeners': - specifier: ^1.0.0 - version: 1.0.0 type-fest: specifier: ^4.15.0 version: 4.15.0 @@ -7611,9 +7611,6 @@ packages: '@types/react-dom@18.0.0': resolution: {integrity: sha512-49897Y0UiCGmxZqpC8Blrf6meL8QUla6eb+BBhn69dTXlmuOlzkfr7HHY/O8J25e1lTUMs+YYxSlVDAaGHCOLg==} - '@types/react-final-form-listeners@1.0.0': - resolution: {integrity: sha512-DovKaDfBqBzPnThFV8mGXL4iAbMCObEFIPUkH7wbD/EN3jZEjpDuPibrqY+w4W1dzicO109YP1LT9KsDDxdYnw==} - '@types/react-input-mask@3.0.0': resolution: {integrity: sha512-W8I2GRt1wkFgEJJ4dleSCw1ly2lL+pW71OPkJSz6G93V77io4ulSSnZyiyCv3uljmQDOr94NMbNr5V5hfkMaUg==} @@ -10793,13 +10790,14 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - final-form-arrays@3.0.2: - resolution: {integrity: sha512-TfO8aZNz3RrsZCDx8GHMQcyztDNpGxSSi9w4wpSNKlmv2PfFWVVM8P7Yj5tj4n0OWax+x5YwTLhT5BnqSlCi+w==} + final-form-arrays@4.0.1: + resolution: {integrity: sha512-aIGGN1ibtxxmRR4LsIEhSbWbUKyHAy9BCAUEcG19U1OpYqD1M9dU9MxgXHmGoM6JLegD4DEVKFLxJjI0q1VjhQ==} peerDependencies: - final-form: ^4.18.2 + final-form: ^5.0.0 - final-form@4.20.9: - resolution: {integrity: sha512-shA1X/7v8RmukWMNRHx0l7+Bm41hOivY78IvOiBrPVHjyWFIyqqIEMCz7yTVRc9Ea+EU4WkZ5r4MH6whSo5taw==} + final-form@5.0.1: + resolution: {integrity: sha512-Ohygw2lDgc2HNynfUu82Jp5U45+OLLeBQcwWbrex1IAbQw0uNaFMUbm5dhYCF6y7jcORgl5tg19/1zYxlJtLmg==} + engines: {node: '>=18'} finalhandler@1.3.1: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} @@ -15180,26 +15178,25 @@ packages: react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} - react-final-form-arrays@3.1.4: - resolution: {integrity: sha512-siVFAolUAe29rMR6u8VwepoysUcUdh6MLV2OWnCtKpsPRUdT9VUgECjAPaVMAH2GROZNiVB9On1H9MMrm9gdpg==} + react-final-form-arrays@5.0.0: + resolution: {integrity: sha512-M7J73727AaK0YPJbvf/NiBKZrxnCiYZWSuRSHjg6iiA9+KrSuuRm5Y+8gLsah1ziVk4jH4eLK/Atl26LPkB1Dg==} peerDependencies: - final-form: ^4.15.0 - final-form-arrays: '>=1.0.4' + final-form: ^5.0.1 + final-form-arrays: ^4.0.1 react: ^18.2.0 - react-final-form: ^6.2.1 + react-final-form: ^7.0.1 - react-final-form-listeners@1.0.3: - resolution: {integrity: sha512-OrdCNxSS4JQS/EXD+R530kZKFqaPfa+WcXPgVro/h4BpaBDF/Ja+BtHyCzDezCIb5rWaGGdOJIj+tN2YdtvrXg==} + react-final-form-listeners@3.0.1: + resolution: {integrity: sha512-oJbPJsqDLs65oxMoo4kSkN7IbtjeqoVEkU7pPKjGnR/meDj83KDjsJj4aTnDCAbocxpjEcV30y+26CfMODFB/g==} peerDependencies: - final-form: '>=4.0.0' - prop-types: ^15.6.0 + final-form: '>=5.0.0' react: ^18.2.0 - react-final-form: '>=3.0.0' + react-final-form: '>=7.0.0' - react-final-form@6.5.9: - resolution: {integrity: sha512-x3XYvozolECp3nIjly+4QqxdjSSWfcnpGEL5K8OBT6xmGrq5kBqbA6+/tOqoom9NwqIPPbxPNsOViFlbKgowbA==} + react-final-form@7.0.1: + resolution: {integrity: sha512-8K1ITLecBI7F+jCxiMA/JI1amHT/+klQB+nhl5YFZu3R1sQftbiQk/UfwRDvE7ZxIqIvCf8nYLDQRrfwaDT3kQ==} peerDependencies: - final-form: ^4.20.4 + final-form: ^5.0.0 react: ^18.2.0 react-focus-lock@2.6.0: @@ -23634,10 +23631,6 @@ snapshots: dependencies: '@types/react': 17.0.39 - '@types/react-final-form-listeners@1.0.0': - dependencies: - '@types/react': 17.0.39 - '@types/react-input-mask@3.0.0': dependencies: '@types/react': 17.0.39 @@ -27430,13 +27423,13 @@ snapshots: dependencies: to-regex-range: 5.0.1 - final-form-arrays@3.0.2(final-form@4.20.9): + final-form-arrays@4.0.1(final-form@5.0.1): dependencies: - final-form: 4.20.9 + final-form: 5.0.1 - final-form@4.20.9: + final-form@5.0.1: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 finalhandler@1.3.1: dependencies: @@ -33041,26 +33034,25 @@ snapshots: react-fast-compare@3.2.2: {} - react-final-form-arrays@3.1.4(final-form-arrays@3.0.2(final-form@4.20.9))(final-form@4.20.9)(react-final-form@6.5.9(final-form@4.20.9)(react@18.2.0))(react@18.2.0): + react-final-form-arrays@5.0.0(final-form-arrays@4.0.1(final-form@5.0.1))(final-form@5.0.1)(react-final-form@7.0.1(final-form@5.0.1)(react@18.2.0))(react@18.2.0): dependencies: - '@babel/runtime': 7.28.4 - final-form: 4.20.9 - final-form-arrays: 3.0.2(final-form@4.20.9) + '@babel/runtime': 7.29.2 + final-form: 5.0.1 + final-form-arrays: 4.0.1(final-form@5.0.1) react: 18.2.0 - react-final-form: 6.5.9(final-form@4.20.9)(react@18.2.0) + react-final-form: 7.0.1(final-form@5.0.1)(react@18.2.0) - react-final-form-listeners@1.0.3(final-form@4.20.9)(prop-types@15.8.1)(react-final-form@6.5.9(final-form@4.20.9)(react@18.2.0))(react@18.2.0): + react-final-form-listeners@3.0.1(patch_hash=267ee13c92b1d81c3016dd0b8d2ca4b7b95003e6479058644735ece6376f3e6e)(final-form@5.0.1)(react-final-form@7.0.1(final-form@5.0.1)(react@18.2.0))(react@18.2.0): dependencies: - '@babel/runtime': 7.28.4 - final-form: 4.20.9 - prop-types: 15.8.1 + '@babel/runtime': 7.29.2 + final-form: 5.0.1 react: 18.2.0 - react-final-form: 6.5.9(final-form@4.20.9)(react@18.2.0) + react-final-form: 7.0.1(final-form@5.0.1)(react@18.2.0) - react-final-form@6.5.9(final-form@4.20.9)(react@18.2.0): + react-final-form@7.0.1(final-form@5.0.1)(react@18.2.0): dependencies: - '@babel/runtime': 7.28.4 - final-form: 4.20.9 + '@babel/runtime': 7.29.2 + final-form: 5.0.1 react: 18.2.0 react-focus-lock@2.6.0(@types/react@17.0.39)(react@18.2.0): diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 48a0f1a830..2517126fd9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -64,6 +64,7 @@ overrides: patchedDependencies: '@changesets/get-github-info@0.6.0': patches/@changesets__get-github-info@0.6.0.patch '@storybook/react-docgen-typescript-plugin@1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0': 'patches/@storybook__react-docgen-typescript-plugin@1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0.patch' + react-final-form-listeners@3.0.1: patches/react-final-form-listeners@3.0.1.patch 'webpack@5.98.0': 'patches/webpack@5.98.0.patch' resolvePeersFromWorkspaceRoot: true strictPeerDependencies: false From 2157d7efe87694131217e8c2a92f29ba52aa66d7 Mon Sep 17 00:00:00 2001 From: Ante Zebic <17073473+azebich@users.noreply.github.com> Date: Wed, 5 Aug 2026 12:58:45 +0200 Subject: [PATCH 04/14] [PF-2295] Upgrade picasso-calendar to support React v19 (#5074) --- .changeset/calendar-react-day-picker-react-19.md | 8 ++++++++ packages/base/Calendar/package.json | 2 +- packages/picasso/package.json | 1 - pnpm-lock.yaml | 13 +++++-------- 4 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 .changeset/calendar-react-day-picker-react-19.md diff --git a/.changeset/calendar-react-day-picker-react-19.md b/.changeset/calendar-react-day-picker-react-19.md new file mode 100644 index 0000000000..96d9a23ba0 --- /dev/null +++ b/.changeset/calendar-react-day-picker-react-19.md @@ -0,0 +1,8 @@ +--- +'@toptal/picasso-calendar': patch +--- + +### Calendar + +- upgrade `react-day-picker` to `^8.10.2` so the package can run under React 19. `8.10.2` is the first release whose `react` peer range admits `^19.0.0`, and it is the last release of the `8.x` line. It also stops inlining React 18's `react-jsx-runtime` into its bundle and imports `react/jsx-runtime` instead — the inlined copy read `React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED`, which React 19 removes, so under React 19 importing `8.10.0` threw `Cannot read properties of undefined` before anything rendered +- no `@toptal/picasso-calendar` API or behavior change. `8.10.2` keeps the whole v8 export surface, including the `useDayRender`, `useDayPicker` and `useNavigation` hooks that `Calendar`, `CalendarDay` and `CalendarMonthHeader` build on, and it renders identically under React 18. The `react` peer range stays capped at `< 19.0.0`; lifting that cap library-wide is tracked separately in PF-2262 diff --git a/packages/base/Calendar/package.json b/packages/base/Calendar/package.json index d097d45005..1ef7c4ae72 100644 --- a/packages/base/Calendar/package.json +++ b/packages/base/Calendar/package.json @@ -29,7 +29,7 @@ "@toptal/picasso-typography": "workspace:*", "@toptal/picasso-utils": "workspace:*", "date-fns": "^2.30.0", - "react-day-picker": "^8.10.0" + "react-day-picker": "^8.10.2" }, "sideEffects": [ "**/styles.ts", diff --git a/packages/picasso/package.json b/packages/picasso/package.json index 257f239c60..9ac15d954e 100644 --- a/packages/picasso/package.json +++ b/packages/picasso/package.json @@ -118,7 +118,6 @@ "detect-browser": "^5.3.0", "glider-js": "^1.7.8", "react-content-loader": "^6.2.1", - "react-day-picker": "^8.10.0", "react-dropzone": "^14.2.3", "react-input-mask": "^3.0.0-alpha.2", "react-truncate": "^2.4.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 430745b2ce..66006f41a4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1063,8 +1063,8 @@ importers: specifier: ^18.2.0 version: 18.2.0 react-day-picker: - specifier: ^8.10.0 - version: 8.10.0(date-fns@2.30.0)(react@18.2.0) + specifier: ^8.10.2 + version: 8.10.2(date-fns@2.30.0)(react@18.2.0) devDependencies: '@toptal/picasso-provider': specifier: workspace:* @@ -3344,9 +3344,6 @@ importers: react-content-loader: specifier: ^6.2.1 version: 6.2.1(react@18.2.0) - react-day-picker: - specifier: ^8.10.0 - version: 8.10.0(date-fns@2.30.0)(react@18.2.0) react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) @@ -15099,8 +15096,8 @@ packages: peerDependencies: react: ^18.2.0 - react-day-picker@8.10.0: - resolution: {integrity: sha512-mz+qeyrOM7++1NCb1ARXmkjMkzWVh2GL9YiPbRjKe0zHccvekk4HE+0MPOZOrosn8r8zTHIIeOUXTmXRqmkRmg==} + react-day-picker@8.10.2: + resolution: {integrity: sha512-LK68OTbHB3oJNhl9cA0qVizzp3o26w61YSjAFkYi67N86iro32wx86kSNeFU/hq+gI8m1yzWhnomMLfZ041RzQ==} peerDependencies: date-fns: ^2.28.0 || ^3.0.0 react: ^18.2.0 @@ -32926,7 +32923,7 @@ snapshots: dependencies: react: 18.2.0 - react-day-picker@8.10.0(date-fns@2.30.0)(react@18.2.0): + react-day-picker@8.10.2(date-fns@2.30.0)(react@18.2.0): dependencies: date-fns: 2.30.0 react: 18.2.0 From 8bb41f5a1822a0c0cff982fa228ef26506dca500 Mon Sep 17 00:00:00 2001 From: Ante Zebic <17073473+azebich@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:52:08 +0200 Subject: [PATCH 05/14] [PF-2292] Unlock picass-rich-text-editor React v19 (#5072) --- .changeset/emoji-mart-react-19-support.md | 8 +++ .../picasso-rich-text-editor/package.json | 1 - .../EmojiMartPicker.test.tsx | 20 ++++++++ .../EmojiMartPicker.tsx | 50 +++++++++++++++++++ .../RichTextEditorEmojiPicker.tsx | 24 +++++---- pnpm-lock.yaml | 14 ------ 6 files changed, 92 insertions(+), 25 deletions(-) create mode 100644 .changeset/emoji-mart-react-19-support.md create mode 100644 packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/EmojiMartPicker.test.tsx create mode 100644 packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/EmojiMartPicker.tsx diff --git a/.changeset/emoji-mart-react-19-support.md b/.changeset/emoji-mart-react-19-support.md new file mode 100644 index 0000000000..cd7fa8f50c --- /dev/null +++ b/.changeset/emoji-mart-react-19-support.md @@ -0,0 +1,8 @@ +--- +'@toptal/picasso-rich-text-editor': patch +--- + +### RichTextEditor + +- drop the `@emoji-mart/react` dependency and render emoji-mart's `Picker` through a local component instead. `@emoji-mart/react@1.1.1` is the latest release, was last published in January 2023, and declares a `react` peer range of `^16.8 || ^17 || ^18` that excludes React 19 — so no upgrade could unblock React 19 support. The wrapper was around 20 lines and used no API React 19 removes, so owning it removes the constraint without changing behavior. `emoji-mart` and `@emoji-mart/data` were already direct dependencies and are unchanged, so nothing is added to the dependency tree +- the local component pushes prop updates into the picker from an effect. The upstream wrapper did this during render, which is a render-phase side effect diff --git a/packages/picasso-rich-text-editor/package.json b/packages/picasso-rich-text-editor/package.json index f0a808f989..933187b1d8 100644 --- a/packages/picasso-rich-text-editor/package.json +++ b/packages/picasso-rich-text-editor/package.json @@ -37,7 +37,6 @@ }, "dependencies": { "@emoji-mart/data": "^1.2.1", - "@emoji-mart/react": "^1.1.1", "@lexical/html": "0.11.3", "@lexical/react": "0.11.3", "@toptal/picasso-container": "workspace:*", diff --git a/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/EmojiMartPicker.test.tsx b/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/EmojiMartPicker.test.tsx new file mode 100644 index 0000000000..50d978e287 --- /dev/null +++ b/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/EmojiMartPicker.test.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import data from '@emoji-mart/data' +import { render, waitFor } from '@toptal/picasso-test-utils' + +import EmojiMartPicker from './EmojiMartPicker' + +const onEmojiSelect = jest.fn() + +const renderEmojiMartPicker = () => + render() + +describe('EmojiMartPicker', () => { + it('mounts the emoji-mart picker element', async () => { + const { container } = renderEmojiMartPicker() + + await waitFor(() => + expect(container.querySelector('em-emoji-picker')).toBeInTheDocument() + ) + }) +}) diff --git a/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/EmojiMartPicker.tsx b/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/EmojiMartPicker.tsx new file mode 100644 index 0000000000..5384883c93 --- /dev/null +++ b/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/EmojiMartPicker.tsx @@ -0,0 +1,50 @@ +import React, { useEffect, useRef } from 'react' +import { Picker } from 'emoji-mart' + +import type { CustomEmojiGroup, Emoji } from '../plugins/EmojiPlugin' + +interface Props { + /** Emoji dataset to render, as exported by `@emoji-mart/data` */ + data: unknown + /** Additional groups of custom emojis appended to the picker */ + custom?: CustomEmojiGroup[] + /** Called with the picked emoji when a selection is made */ + onEmojiSelect: (emoji: Emoji) => void + /** Called when a click lands outside the picker */ + onClickOutside?: () => void +} + +/** + * Renders emoji-mart's `Picker` custom element. + * + * `Picker` appends itself to the element passed as `ref` and is fed prop + * changes through `update`, so it is constructed after the first commit and + * updated on every commit after that. + */ +const EmojiMartPicker = (props: Props) => { + const containerRef = useRef(null) + const pickerRef = useRef(null) + + useEffect(() => { + if (!pickerRef.current) { + pickerRef.current = new Picker({ ...props, ref: containerRef }) + + return + } + + pickerRef.current.update(props) + }) + + useEffect( + () => () => { + pickerRef.current = null + }, + [] + ) + + return
+} + +EmojiMartPicker.displayName = 'EmojiMartPicker' + +export default EmojiMartPicker diff --git a/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/RichTextEditorEmojiPicker.tsx b/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/RichTextEditorEmojiPicker.tsx index c8806cfda8..6938c0cdc2 100644 --- a/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/RichTextEditorEmojiPicker.tsx +++ b/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/RichTextEditorEmojiPicker.tsx @@ -1,11 +1,11 @@ /* eslint-disable no-inline-styles/no-inline-styles */ import React, { useEffect } from 'react' import data from '@emoji-mart/data' -import Picker from '@emoji-mart/react' +import cx from 'classnames' import { Container } from '@toptal/picasso-container' -import { twMerge } from '@toptal/picasso-tailwind-merge' import RichTextEditorButton from '../RichTextEditorButton' +import EmojiMartPicker from './EmojiMartPicker' import type { CustomEmojiGroup, Emoji } from '../plugins/EmojiPlugin' interface Props { @@ -16,6 +16,12 @@ interface Props { const TRIGGER_EMOJI_PICKER_ID = 'trigger-emoji-picker' +const classes = { + emojiPicker: 'absolute top-[34px] left-0 z-10 opacity-0 pointer-events-none', + activeOpacity: 'opacity-100', + activePointers: '[pointer-events:all]', +} + const handleEmojiPickerEscBehaviour = ( event: KeyboardEvent, setShowEmojiPicker: React.Dispatch> @@ -70,19 +76,17 @@ export const RichTextEditorEmojiPicker = ({ disabled={disabled} /> - diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66006f41a4..049ae745c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3735,9 +3735,6 @@ importers: '@emoji-mart/data': specifier: ^1.2.1 version: 1.2.1 - '@emoji-mart/react': - specifier: ^1.1.1 - version: 1.1.1(emoji-mart@5.5.2)(react@18.2.0) '@lexical/html': specifier: 0.11.3 version: 0.11.3(lexical@0.11.3) @@ -5198,12 +5195,6 @@ packages: '@emoji-mart/data@1.2.1': resolution: {integrity: sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==} - '@emoji-mart/react@1.1.1': - resolution: {integrity: sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==} - peerDependencies: - emoji-mart: ^5.2 - react: ^18.2.0 - '@emotion/cache@10.0.29': resolution: {integrity: sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==} @@ -19616,11 +19607,6 @@ snapshots: '@emoji-mart/data@1.2.1': {} - '@emoji-mart/react@1.1.1(emoji-mart@5.5.2)(react@18.2.0)': - dependencies: - emoji-mart: 5.5.2 - react: 18.2.0 - '@emotion/cache@10.0.29': dependencies: '@emotion/sheet': 0.9.4 From 59f62bb8ceea8ae77330b9ad86d1804d9a97fe0c Mon Sep 17 00:00:00 2001 From: Ante Zebic <17073473+azebich@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:10:26 +0200 Subject: [PATCH 06/14] [PF-2145] Migrate Backdrop, Fade, Slide to tailwind (#5079) * [PF-2145] Migrate Backdrop, Fade, Slide to tailwind * [PF-2145] Tweak children ref type --- .changeset/transitions-tailwind-migration.md | 33 ++ packages/base/Backdrop/package.json | 4 +- .../base/Backdrop/src/Backdrop/Backdrop.tsx | 19 +- packages/base/Backdrop/src/Backdrop/test.tsx | 28 +- packages/base/Backdrop/tsconfig.json | 5 +- packages/base/Fade/package.json | 8 +- packages/base/Fade/src/Fade/Fade.tsx | 105 +++--- packages/base/Fade/src/Fade/test.tsx | 111 ++++++- packages/base/Fade/tsconfig.json | 7 +- packages/base/Slide/package.json | 8 +- packages/base/Slide/src/Slide/Slide.tsx | 151 ++++----- packages/base/Slide/src/Slide/styles.ts | 25 ++ packages/base/Slide/src/Slide/test.tsx | 141 +++++++- packages/base/Slide/tsconfig.json | 7 +- .../utils/__tests__/get-element-ref.test.tsx | 31 ++ .../__tests__/use-transition-status.test.tsx | 303 ++++++++++++++++++ .../base/Utils/src/utils/get-element-ref.ts | 19 ++ packages/base/Utils/src/utils/index.ts | 10 + .../src/utils/use-multiple-forward-refs.ts | 1 + .../Utils/src/utils/use-transition-status.ts | 179 +++++++++++ packages/picasso-tailwind-merge/package.json | 3 +- pnpm-lock.yaml | 42 +-- 22 files changed, 1055 insertions(+), 185 deletions(-) create mode 100644 .changeset/transitions-tailwind-migration.md create mode 100644 packages/base/Slide/src/Slide/styles.ts create mode 100644 packages/base/Utils/src/utils/__tests__/get-element-ref.test.tsx create mode 100644 packages/base/Utils/src/utils/__tests__/use-transition-status.test.tsx create mode 100644 packages/base/Utils/src/utils/get-element-ref.ts create mode 100644 packages/base/Utils/src/utils/use-transition-status.ts diff --git a/.changeset/transitions-tailwind-migration.md b/.changeset/transitions-tailwind-migration.md new file mode 100644 index 0000000000..abe5d1c260 --- /dev/null +++ b/.changeset/transitions-tailwind-migration.md @@ -0,0 +1,33 @@ +--- +'@toptal/picasso-utils': minor +'@toptal/picasso-fade': minor +'@toptal/picasso-slide': minor +'@toptal/picasso-backdrop': patch +'@toptal/picasso-tailwind-merge': patch +--- + +### Utils + +- add `useTransitionStatus` hook and `getTransitionTimeouts` helper — a timer-driven replacement for react-transition-group's `Transition` state machine with identical callback timing +- add `getElementRef` helper, typing the ref lookup on a React element in one place + +### Fade + +- reimplement with Tailwind classes and drop the react-transition-group dependency; public props and callback timing are unchanged +- express the hidden state via the `invisible` and `opacity-0` classes instead of inline `visibility`/`opacity` styles, and merge the child's `className` via `twMerge` +- the object form of `timeout` now sets the CSS transition duration correctly (previously it produced an invalid inline value) + +### Slide + +- reimplement with Tailwind classes and drop the react-transition-group dependency; public props, the direction mapping and callback timing are unchanged +- express the hidden state via `translate-*` and `invisible` classes instead of inline `transform`/`visibility` styles, so a child's own `transform` is preserved while sliding and `transitionend` listeners observe `propertyName: 'translate'` +- the object form of `timeout` now sets the CSS transition duration correctly (previously it produced an invalid inline value) + +### Backdrop + +- compose the Tailwind-based Fade so react-transition-group is no longer in the dependency tree +- remove the redundant `bg-black` class and a dead `-webkit-tap-highlight-color-transparent` class (no visual change) and merge the consumer `className` via `twMerge` so consumer utilities win on conflicts + +### TailwindMerge + +- remove the unused react-transition-group dependency diff --git a/packages/base/Backdrop/package.json b/packages/base/Backdrop/package.json index d040c7b6d4..5be0c21f43 100644 --- a/packages/base/Backdrop/package.json +++ b/packages/base/Backdrop/package.json @@ -22,19 +22,21 @@ }, "homepage": "https://github.com/toptal/picasso/tree/master/packages/picasso#readme", "dependencies": { - "@toptal/picasso-utils": "workspace:*", "@toptal/picasso-fade": "workspace:*", + "@toptal/picasso-shared": "workspace:*", "classnames": "^2.5.1" }, "sideEffects": false, "peerDependencies": { "@toptal/picasso-tailwind": "workspace:^", + "@toptal/picasso-tailwind-merge": "workspace:^", "react": ">=17.0.0 < 19.0.0" }, "exports": { ".": "./dist-package/src/index.js" }, "devDependencies": { + "@toptal/picasso-tailwind-merge": "workspace:*", "@toptal/picasso-test-utils": "workspace:*" }, "files": [ diff --git a/packages/base/Backdrop/src/Backdrop/Backdrop.tsx b/packages/base/Backdrop/src/Backdrop/Backdrop.tsx index a7f30e9906..fd71c39fb4 100644 --- a/packages/base/Backdrop/src/Backdrop/Backdrop.tsx +++ b/packages/base/Backdrop/src/Backdrop/Backdrop.tsx @@ -1,11 +1,10 @@ import React from 'react' import cx from 'classnames' import { Fade } from '@toptal/picasso-fade' +import { twMerge } from '@toptal/picasso-tailwind-merge' import type { BaseProps } from '@toptal/picasso-shared' -export interface Props - extends BaseProps, - React.HTMLAttributes { +export interface Props extends BaseProps, React.HTMLAttributes { /** The duration for the transition, in milliseconds */ transitionDuration?: number /** If `true`, the backdrop is shown */ @@ -16,7 +15,7 @@ export interface Props } export const Backdrop = React.forwardRef( - (props, ref) => { + function Backdrop(props, ref) { const { transitionDuration = 300, open, @@ -30,11 +29,11 @@ export const Backdrop = React.forwardRef( return (
( } ) +Backdrop.displayName = 'Backdrop' + export default Backdrop diff --git a/packages/base/Backdrop/src/Backdrop/test.tsx b/packages/base/Backdrop/src/Backdrop/test.tsx index 58f788a597..1cd172e1ab 100644 --- a/packages/base/Backdrop/src/Backdrop/test.tsx +++ b/packages/base/Backdrop/src/Backdrop/test.tsx @@ -6,10 +6,23 @@ import Backdrop from './Backdrop' describe('Backdrop component', () => { afterEach(cleanup) - it('should render without crash', () => { - const { container } = render() + it('renders a fixed full-viewport scrim', () => { + const { getByTestId } = render( + + ) + + expect(getByTestId('backdrop')).toHaveClass('fixed') + expect(getByTestId('backdrop')).toHaveClass('inset-0') + expect(getByTestId('backdrop')).not.toHaveClass('invisible') + }) + + it('hides via the fade when closed', () => { + const { getByTestId } = render( + + ) - expect(container).toBeInTheDocument() + expect(getByTestId('backdrop')).toHaveClass('invisible') + expect(getByTestId('backdrop')).toHaveClass('opacity-0') }) describe('when invisible prop is true', () => { @@ -32,6 +45,15 @@ describe('Backdrop component', () => { }) }) + it('lets a consumer className win on conflicts', () => { + const { getByTestId } = render( + + ) + + expect(getByTestId('backdrop')).toHaveClass('bg-white/50') + expect(getByTestId('backdrop')).not.toHaveClass('bg-black/50') + }) + it('handle ref correctly', () => { const ref = React.createRef() diff --git a/packages/base/Backdrop/tsconfig.json b/packages/base/Backdrop/tsconfig.json index 47ccc01b30..170af23e43 100644 --- a/packages/base/Backdrop/tsconfig.json +++ b/packages/base/Backdrop/tsconfig.json @@ -4,7 +4,8 @@ "include": ["src"], "references": [ { "path": "../../picasso-tailwind" }, - { "path": "../Fade" }, - { "path": "../Utils" } + { "path": "../../picasso-tailwind-merge" }, + { "path": "../../shared" }, + { "path": "../Fade" } ] } diff --git a/packages/base/Fade/package.json b/packages/base/Fade/package.json index 4e7da49011..541a83e647 100644 --- a/packages/base/Fade/package.json +++ b/packages/base/Fade/package.json @@ -22,20 +22,22 @@ }, "homepage": "https://github.com/toptal/picasso/tree/master/packages/picasso#readme", "dependencies": { + "@toptal/picasso-shared": "workspace:*", "@toptal/picasso-utils": "workspace:*", - "react-transition-group": "^4.4.5" + "classnames": "^2.5.1" }, "sideEffects": false, "peerDependencies": { "@toptal/picasso-tailwind": "workspace:^", + "@toptal/picasso-tailwind-merge": "workspace:^", "react": ">=17.0.0 < 19.0.0" }, "exports": { ".": "./dist-package/src/index.js" }, "devDependencies": { - "@toptal/picasso-test-utils": "workspace:*", - "@types/react-transition-group": "^4.4.5" + "@toptal/picasso-tailwind-merge": "workspace:*", + "@toptal/picasso-test-utils": "workspace:*" }, "files": [ "dist-package/**", diff --git a/packages/base/Fade/src/Fade/Fade.tsx b/packages/base/Fade/src/Fade/Fade.tsx index f8f1b49fa1..ebfa1e45c9 100644 --- a/packages/base/Fade/src/Fade/Fade.tsx +++ b/packages/base/Fade/src/Fade/Fade.tsx @@ -1,7 +1,13 @@ import React, { useRef } from 'react' -import { Transition } from 'react-transition-group' +import cx from 'classnames' import type { BaseProps, TransitionProps } from '@toptal/picasso-shared' -import { useMultipleForwardRefs } from '@toptal/picasso-utils' +import { + getElementRef, + getTransitionTimeouts, + useMultipleForwardRefs, + useTransitionStatus, +} from '@toptal/picasso-utils' +import { twMerge } from '@toptal/picasso-tailwind-merge' export interface Props extends TransitionProps, BaseProps { /* Element that accepts ref */ @@ -12,55 +18,54 @@ export interface Props extends TransitionProps, BaseProps { onEnter?: (node: HTMLElement, isAppearing: boolean) => void } -export const Fade = React.forwardRef( - ({ timeout = 300, children, in: inProp, style, onEnter, onExited }, ref) => { - const nodeRef = useRef(null) - const transitionStyles = { - entering: { opacity: 1 }, - entered: { opacity: 1 }, - exiting: { opacity: 0 }, - exited: { opacity: 0 }, - } +const DEFAULT_TIMEOUT = 300 - const combinedRef = useMultipleForwardRefs([ - ref, - nodeRef, - // TODO: come up with proper type for children.ref - // @ts-ignore - children.ref, - ]) +export const Fade = React.forwardRef(function Fade( + { timeout = DEFAULT_TIMEOUT, children, in: inProp, style, onEnter, onExited }, + ref +) { + const nodeRef = useRef(null) - return ( - - {( - state: 'entering' | 'entered' | 'exiting' | 'exited', - childProps: {} - ) => { - return React.cloneElement(children, { - style: { - visibility: state === 'exited' && !inProp ? 'hidden' : undefined, - transitionDuration: `${timeout}ms`, - ...transitionStyles[state], - ...style, - ...children.props.style, - }, - className: `opacity-0 transition-opacity ${ - children.props.className || '' - }`, - ref: combinedRef, - ...childProps, - }) - }} - - ) - } -) + const status = useTransitionStatus({ + in: inProp, + // Historical behavior: enter callbacks fire on mount-open, with no + // visible fade (the status flips pre-paint) + appear: true, + timeout, + nodeRef, + onEnter, + onExited, + }) + + const combinedRef = useMultipleForwardRefs([ + ref, + nodeRef, + getElementRef(children), + ]) + + const timeouts = getTransitionTimeouts(timeout) + const duration = inProp ? timeouts.enter : timeouts.exit + + return React.cloneElement(children, { + className: twMerge( + 'transition-opacity', + children.props.className, + // State classes stay after the child's className: behavior, not + // overridable defaults + cx({ + 'opacity-0': !inProp, + invisible: status === 'exited' && !inProp, + }) + ), + style: { + transitionDuration: `${duration}ms`, + ...style, + ...children.props.style, + }, + ref: combinedRef, + }) +}) + +Fade.displayName = 'Fade' export default Fade diff --git a/packages/base/Fade/src/Fade/test.tsx b/packages/base/Fade/src/Fade/test.tsx index 597118fca2..8e4a66d159 100644 --- a/packages/base/Fade/src/Fade/test.tsx +++ b/packages/base/Fade/src/Fade/test.tsx @@ -3,7 +3,10 @@ import { act, cleanup, render } from '@toptal/picasso-test-utils' import Fade from './Fade' -const SomeChildComponent = React.forwardRef((props, ref) => ( +const SomeChildComponent = React.forwardRef< + HTMLDivElement, + { className?: string } +>((props, ref) => (
Hello, I'm child div!
@@ -19,14 +22,18 @@ describe('Fade', () => { jest.useRealTimers() }) - it('renders without errors', () => { + it('shows the child when `in` is true', () => { const { getByTestId } = render( ) - expect(getByTestId('child-div')).toBeInTheDocument() + const child = getByTestId('child-div') + + expect(child).toHaveClass('transition-opacity') + expect(child).not.toHaveClass('opacity-0') + expect(child).not.toHaveClass('invisible') }) it('transitions based on the `in` prop', () => { @@ -36,7 +43,8 @@ describe('Fade', () => { ) - expect(getByTestId('child-div')).toHaveStyle('visibility: hidden') + expect(getByTestId('child-div')).toHaveClass('opacity-0') + expect(getByTestId('child-div')).toHaveClass('invisible') act(() => { rerender( @@ -47,7 +55,64 @@ describe('Fade', () => { jest.runAllTimers() }) - expect(getByTestId('child-div')).toHaveStyle('visibility: visible') + expect(getByTestId('child-div')).not.toHaveClass('opacity-0') + expect(getByTestId('child-div')).not.toHaveClass('invisible') + + act(() => { + rerender( + + + + ) + }) + + // Fade-out starts at once; `invisible` lands only after it settles + expect(getByTestId('child-div')).toHaveClass('opacity-0') + expect(getByTestId('child-div')).not.toHaveClass('invisible') + + act(() => { + jest.runAllTimers() + }) + + expect(getByTestId('child-div')).toHaveClass('invisible') + }) + + it('calls `onEnter` marked as appearing when mounted shown', () => { + const onEnter = jest.fn() + const { getByTestId } = render( + + + + ) + + expect(onEnter).toHaveBeenCalledTimes(1) + expect(onEnter).toHaveBeenCalledWith(getByTestId('child-div'), true) + }) + + it('calls `onExited` after the exit transition settles', () => { + const onExited = jest.fn() + const { getByTestId, rerender } = render( + + + + ) + + act(() => { + rerender( + + + + ) + }) + + expect(onExited).not.toHaveBeenCalled() + + act(() => { + jest.runAllTimers() + }) + + expect(onExited).toHaveBeenCalledTimes(1) + expect(onExited).toHaveBeenCalledWith(getByTestId('child-div')) }) it('forwards the ref', () => { @@ -73,4 +138,40 @@ describe('Fade', () => { transitionDuration: `${timeout}ms`, }) }) + + it('applies per-phase durations from an object `timeout`', () => { + const timeout = { enter: 100, exit: 200 } + const { getByTestId, rerender } = render( + + + + ) + + expect(getByTestId('child-div')).toHaveStyle({ + transitionDuration: '100ms', + }) + + act(() => { + rerender( + + + + ) + }) + + expect(getByTestId('child-div')).toHaveStyle({ + transitionDuration: '200ms', + }) + }) + + it('preserves the child className', () => { + const { getByTestId } = render( + + + + ) + + expect(getByTestId('child-div')).toHaveClass('p-4') + expect(getByTestId('child-div')).toHaveClass('transition-opacity') + }) }) diff --git a/packages/base/Fade/tsconfig.json b/packages/base/Fade/tsconfig.json index 94f5b1c32c..183445fbc4 100644 --- a/packages/base/Fade/tsconfig.json +++ b/packages/base/Fade/tsconfig.json @@ -2,5 +2,10 @@ "extends": "../../../tsconfig.base.json", "compilerOptions": { "outDir": "dist-package" }, "include": ["src"], - "references": [{ "path": "../../picasso-tailwind" }, { "path": "../Utils" }] + "references": [ + { "path": "../../picasso-tailwind" }, + { "path": "../../picasso-tailwind-merge" }, + { "path": "../../shared" }, + { "path": "../Utils" } + ] } diff --git a/packages/base/Slide/package.json b/packages/base/Slide/package.json index 33bd7b40c1..0ee891c6a9 100644 --- a/packages/base/Slide/package.json +++ b/packages/base/Slide/package.json @@ -22,20 +22,22 @@ }, "homepage": "https://github.com/toptal/picasso/tree/master/packages/picasso#readme", "dependencies": { + "@toptal/picasso-shared": "workspace:*", "@toptal/picasso-utils": "workspace:*", - "react-transition-group": "^4.4.5" + "classnames": "^2.5.1" }, "sideEffects": false, "peerDependencies": { "@toptal/picasso-tailwind": "workspace:^", + "@toptal/picasso-tailwind-merge": "workspace:^", "react": ">=17.0.0 < 19.0.0" }, "exports": { ".": "./dist-package/src/index.js" }, "devDependencies": { - "@toptal/picasso-test-utils": "workspace:*", - "@types/react-transition-group": "^4.4.5" + "@toptal/picasso-tailwind-merge": "workspace:*", + "@toptal/picasso-test-utils": "workspace:*" }, "files": [ "dist-package/**", diff --git a/packages/base/Slide/src/Slide/Slide.tsx b/packages/base/Slide/src/Slide/Slide.tsx index b93c09822b..3219c5a0c6 100644 --- a/packages/base/Slide/src/Slide/Slide.tsx +++ b/packages/base/Slide/src/Slide/Slide.tsx @@ -1,34 +1,16 @@ import React, { useRef } from 'react' -import { Transition } from 'react-transition-group' +import cx from 'classnames' import type { BaseProps, TransitionProps } from '@toptal/picasso-shared' -import { useMultipleForwardRefs } from '@toptal/picasso-utils' +import { + getElementRef, + getTransitionTimeouts, + useMultipleForwardRefs, + useTransitionStatus, +} from '@toptal/picasso-utils' +import { twMerge } from '@toptal/picasso-tailwind-merge' -const transitionStyles = { - right: { - entering: { transform: 'none' }, - entered: { transform: 'none' }, - exiting: { transform: 'translateX(-100%)' }, - exited: { transform: 'translateX(-100%)' }, - }, - left: { - entering: { transform: 'none' }, - entered: { transform: 'none' }, - exiting: { transform: 'translateX(100%)' }, - exited: { transform: 'translateX(100%)' }, - }, - up: { - entering: { transform: 'none' }, - entered: { transform: 'none' }, - exiting: { transform: 'translateY(100%)' }, - exited: { transform: 'translateY(100%)' }, - }, - down: { - entering: { transform: 'none' }, - entered: { transform: 'none' }, - exiting: { transform: 'translateY(-100%)' }, - exited: { transform: 'translateY(-100%)' }, - }, -} as const +import type { SlideDirection } from './styles' +import { createStateClassNames } from './styles' export interface Props extends TransitionProps, BaseProps { /* Element that accepts ref */ @@ -38,63 +20,68 @@ export interface Props extends TransitionProps, BaseProps { /* Callback fired when the component has entered */ onEnter?: (node: HTMLElement, isAppearing: boolean) => void /* Direction in which the component will slide */ - direction: 'up' | 'down' | 'left' | 'right' + direction: SlideDirection } -export const Slide = React.forwardRef( - ( - { - timeout = 300, - children, - in: inProp, - style, - onEnter, - onExited, - direction, - }, - ref - ) => { - const nodeRef = useRef(null) +const DEFAULT_TIMEOUT = 300 - const combinedRef = useMultipleForwardRefs([ - ref, - nodeRef, - // TODO: come up with proper type for children.ref - // @ts-ignore - children.ref, - ]) +export const Slide = React.forwardRef(function Slide( + { + timeout = DEFAULT_TIMEOUT, + children, + in: inProp, + style, + onEnter, + onExited, + direction, + }, + ref +) { + const nodeRef = useRef(null) + + const status = useTransitionStatus({ + in: inProp, + // Historical behavior: enter callbacks fire on mount-open, with no + // visible slide (the status flips pre-paint) + appear: true, + timeout, + nodeRef, + onEnter, + onExited, + }) + + const combinedRef = useMultipleForwardRefs([ + ref, + nodeRef, + getElementRef(children), + ]) + + const timeouts = getTransitionTimeouts(timeout) + const duration = inProp ? timeouts.enter : timeouts.exit + + return React.cloneElement(children, { + className: twMerge( + // In Tailwind v4 this also covers the standalone `translate` property + 'transition-transform', + children.props.className, + // State classes stay after the child's className: behavior, not + // overridable defaults + cx( + ...createStateClassNames(direction, { + in: inProp, + exited: status === 'exited' && !inProp, + }) + ) + ), + style: { + transitionDuration: `${duration}ms`, + ...style, + ...children.props.style, + }, + ref: combinedRef, + }) +}) - return ( - - {( - state: 'entering' | 'entered' | 'exiting' | 'exited', - childProps: {} - ) => { - return React.cloneElement(children, { - style: { - visibility: state === 'exited' && !inProp ? 'hidden' : undefined, - transitionDuration: `${timeout}ms`, - ...transitionStyles[direction][state], - ...style, - ...children.props.style, - }, - className: `transform-none transition-transform ${ - children.props.className || '' - }`, - ref: combinedRef, - ...childProps, - }) - }} - - ) - } -) +Slide.displayName = 'Slide' export default Slide diff --git a/packages/base/Slide/src/Slide/styles.ts b/packages/base/Slide/src/Slide/styles.ts new file mode 100644 index 0000000000..c72a4b3d22 --- /dev/null +++ b/packages/base/Slide/src/Slide/styles.ts @@ -0,0 +1,25 @@ +export type SlideDirection = 'up' | 'down' | 'left' | 'right' + +// Historical mapping: `direction` is where the element travels TO on +// enter, so it rests hidden on the opposite side +const hiddenClassByDirection: Record = { + right: '-translate-x-full', + left: 'translate-x-full', + up: 'translate-y-full', + down: '-translate-y-full', +} + +const shownClassByDirection: Record = { + right: 'translate-x-0', + left: 'translate-x-0', + up: 'translate-y-0', + down: 'translate-y-0', +} + +export const createStateClassNames = ( + direction: SlideDirection, + { in: inProp, exited }: { in: boolean; exited: boolean } +): string[] => [ + inProp ? shownClassByDirection[direction] : hiddenClassByDirection[direction], + ...(exited ? ['invisible'] : []), +] diff --git a/packages/base/Slide/src/Slide/test.tsx b/packages/base/Slide/src/Slide/test.tsx index 330a3abedf..e8cda7fb95 100644 --- a/packages/base/Slide/src/Slide/test.tsx +++ b/packages/base/Slide/src/Slide/test.tsx @@ -3,7 +3,10 @@ import { act, cleanup, render } from '@toptal/picasso-test-utils' import Slide from './Slide' -const SomeChildComponent = React.forwardRef((props, ref) => ( +const SomeChildComponent = React.forwardRef< + HTMLDivElement, + { className?: string } +>((props, ref) => (
Hello, I'm child div!
@@ -19,14 +22,18 @@ describe('Slide', () => { jest.useRealTimers() }) - it('renders without errors', () => { + it('shows the child when `in` is true', () => { const { getByTestId } = render( ) - expect(getByTestId('child-div')).toBeInTheDocument() + const child = getByTestId('child-div') + + expect(child).toHaveClass('transition-transform') + expect(child).toHaveClass('translate-x-0') + expect(child).not.toHaveClass('invisible') }) it('transitions based on the `in` prop', () => { @@ -36,7 +43,8 @@ describe('Slide', () => { ) - expect(getByTestId('child-div')).toHaveStyle('visibility: hidden') + expect(getByTestId('child-div')).toHaveClass('translate-x-full') + expect(getByTestId('child-div')).toHaveClass('invisible') act(() => { rerender( @@ -47,7 +55,94 @@ describe('Slide', () => { jest.runAllTimers() }) - expect(getByTestId('child-div')).toHaveStyle('visibility: visible') + expect(getByTestId('child-div')).toHaveClass('translate-x-0') + expect(getByTestId('child-div')).not.toHaveClass('invisible') + + act(() => { + rerender( + + + + ) + }) + + // Slide-out starts at once; `invisible` lands only after it settles + expect(getByTestId('child-div')).toHaveClass('translate-x-full') + expect(getByTestId('child-div')).not.toHaveClass('invisible') + + act(() => { + jest.runAllTimers() + }) + + expect(getByTestId('child-div')).toHaveClass('invisible') + }) + + it.each([ + ['right', '-translate-x-full', 'translate-x-0'], + ['left', 'translate-x-full', 'translate-x-0'], + ['up', 'translate-y-full', 'translate-y-0'], + ['down', '-translate-y-full', 'translate-y-0'], + ] as const)( + 'rests hidden on the opposite side for direction %s', + (direction, hiddenClass, shownClass) => { + const { getByTestId, rerender } = render( + + + + ) + + expect(getByTestId('child-div')).toHaveClass(hiddenClass) + + act(() => { + rerender( + + + + ) + jest.runAllTimers() + }) + + expect(getByTestId('child-div')).toHaveClass(shownClass) + expect(getByTestId('child-div')).not.toHaveClass(hiddenClass) + } + ) + + it('calls `onEnter` marked as appearing when mounted shown', () => { + const onEnter = jest.fn() + const { getByTestId } = render( + + + + ) + + expect(onEnter).toHaveBeenCalledTimes(1) + expect(onEnter).toHaveBeenCalledWith(getByTestId('child-div'), true) + }) + + it('calls `onExited` after the exit transition settles', () => { + const onExited = jest.fn() + const { getByTestId, rerender } = render( + + + + ) + + act(() => { + rerender( + + + + ) + }) + + expect(onExited).not.toHaveBeenCalled() + + act(() => { + jest.runAllTimers() + }) + + expect(onExited).toHaveBeenCalledTimes(1) + expect(onExited).toHaveBeenCalledWith(getByTestId('child-div')) }) it('forwards the ref', () => { @@ -73,4 +168,40 @@ describe('Slide', () => { transitionDuration: `${timeout}ms`, }) }) + + it('applies per-phase durations from an object `timeout`', () => { + const timeout = { enter: 100, exit: 200 } + const { getByTestId, rerender } = render( + + + + ) + + expect(getByTestId('child-div')).toHaveStyle({ + transitionDuration: '100ms', + }) + + act(() => { + rerender( + + + + ) + }) + + expect(getByTestId('child-div')).toHaveStyle({ + transitionDuration: '200ms', + }) + }) + + it('preserves the child className', () => { + const { getByTestId } = render( + + + + ) + + expect(getByTestId('child-div')).toHaveClass('p-4') + expect(getByTestId('child-div')).toHaveClass('transition-transform') + }) }) diff --git a/packages/base/Slide/tsconfig.json b/packages/base/Slide/tsconfig.json index 94f5b1c32c..183445fbc4 100644 --- a/packages/base/Slide/tsconfig.json +++ b/packages/base/Slide/tsconfig.json @@ -2,5 +2,10 @@ "extends": "../../../tsconfig.base.json", "compilerOptions": { "outDir": "dist-package" }, "include": ["src"], - "references": [{ "path": "../../picasso-tailwind" }, { "path": "../Utils" }] + "references": [ + { "path": "../../picasso-tailwind" }, + { "path": "../../picasso-tailwind-merge" }, + { "path": "../../shared" }, + { "path": "../Utils" } + ] } diff --git a/packages/base/Utils/src/utils/__tests__/get-element-ref.test.tsx b/packages/base/Utils/src/utils/__tests__/get-element-ref.test.tsx new file mode 100644 index 0000000000..59e8e38922 --- /dev/null +++ b/packages/base/Utils/src/utils/__tests__/get-element-ref.test.tsx @@ -0,0 +1,31 @@ +import React from 'react' + +import getElementRef from '../get-element-ref' + +describe('getElementRef', () => { + it('returns null when the element has no ref', () => { + expect(getElementRef(
)).toBeNull() + }) + + it('reads a ref object attached to the element', () => { + const ref = React.createRef() + + expect(getElementRef(
)).toBe(ref) + }) + + it('reads a callback ref attached to the element', () => { + const ref = jest.fn() + + expect(getElementRef(
)).toBe(ref) + }) + + it('does not read `ref` from props, which warns on React 18', () => { + const consoleError = jest.spyOn(console, 'error').mockImplementation() + + getElementRef(
) + + expect(consoleError).not.toHaveBeenCalled() + + consoleError.mockRestore() + }) +}) diff --git a/packages/base/Utils/src/utils/__tests__/use-transition-status.test.tsx b/packages/base/Utils/src/utils/__tests__/use-transition-status.test.tsx new file mode 100644 index 0000000000..5c6b8e91c6 --- /dev/null +++ b/packages/base/Utils/src/utils/__tests__/use-transition-status.test.tsx @@ -0,0 +1,303 @@ +import { renderHook, act } from '@testing-library/react-hooks' + +import type { UseTransitionStatusOptions } from '../use-transition-status' +import useTransitionStatus, { + getTransitionTimeouts, +} from '../use-transition-status' + +const createNodeRef = () => ({ current: document.createElement('div') }) + +const renderTransitionStatus = ( + options: Partial> = {} +) => { + const nodeRef = createNodeRef() + const initialProps: UseTransitionStatusOptions = { + in: false, + timeout: 300, + nodeRef, + ...options, + } + + const result = renderHook(props => useTransitionStatus(props), { + initialProps, + }) + + return { ...result, nodeRef, initialProps } +} + +describe('getTransitionTimeouts', () => { + it('spreads a numeric timeout across all phases', () => { + expect(getTransitionTimeouts(250)).toEqual({ + enter: 250, + exit: 250, + appear: 250, + }) + }) + + it('resolves the object form with `appear` falling back to `enter`', () => { + expect(getTransitionTimeouts({ enter: 100, exit: 200 })).toEqual({ + enter: 100, + exit: 200, + appear: 100, + }) + expect( + getTransitionTimeouts({ enter: 100, exit: 200, appear: 50 }) + ).toEqual({ + enter: 100, + exit: 200, + appear: 50, + }) + }) + + it('defaults missing values to zero', () => { + expect(getTransitionTimeouts(undefined)).toEqual({ + enter: 0, + exit: 0, + appear: 0, + }) + expect(getTransitionTimeouts({})).toEqual({ enter: 0, exit: 0, appear: 0 }) + }) +}) + +describe('useTransitionStatus', () => { + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.useRealTimers() + }) + + describe('initial status', () => { + it('is `exited` when mounted hidden', () => { + const { result } = renderTransitionStatus({ in: false }) + + expect(result.current).toBe('exited') + }) + + it('is `unmounted` when mounted hidden with `unmountOnExit`', () => { + const { result } = renderTransitionStatus({ + in: false, + unmountOnExit: true, + }) + + expect(result.current).toBe('unmounted') + }) + + it('is `entered` when mounted shown without `appear`', () => { + const onEnter = jest.fn() + const onEntered = jest.fn() + const { result } = renderTransitionStatus({ + in: true, + onEnter, + onEntered, + }) + + expect(result.current).toBe('entered') + + act(() => { + jest.runAllTimers() + }) + + expect(onEnter).not.toHaveBeenCalled() + expect(onEntered).not.toHaveBeenCalled() + }) + + it('is `entering` when mounted shown with `appear`', () => { + const { result } = renderTransitionStatus({ in: true, appear: true }) + + expect(result.current).toBe('entering') + }) + }) + + it('runs the enter transition on mount with `appear`, marked as appearing', () => { + const onEnter = jest.fn() + const onEntering = jest.fn() + const onEntered = jest.fn() + const { result, nodeRef } = renderTransitionStatus({ + in: true, + appear: true, + onEnter, + onEntering, + onEntered, + }) + + expect(onEnter).toHaveBeenCalledWith(nodeRef.current, true) + expect(onEntering).toHaveBeenCalledWith(nodeRef.current, true) + expect(onEntered).not.toHaveBeenCalled() + + act(() => { + jest.runAllTimers() + }) + + expect(result.current).toBe('entered') + expect(onEntered).toHaveBeenCalledWith(nodeRef.current, true) + }) + + it('fires no callbacks when mounted hidden', () => { + const onExit = jest.fn() + const onExiting = jest.fn() + const onExited = jest.fn() + + renderTransitionStatus({ in: false, onExit, onExiting, onExited }) + + act(() => { + jest.runAllTimers() + }) + + expect(onExit).not.toHaveBeenCalled() + expect(onExiting).not.toHaveBeenCalled() + expect(onExited).not.toHaveBeenCalled() + }) + + it('runs the enter transition when `in` flips to true', () => { + const onEnter = jest.fn() + const onEntered = jest.fn() + const { result, rerender, nodeRef, initialProps } = renderTransitionStatus({ + in: false, + onEnter, + onEntered, + }) + + rerender({ ...initialProps, in: true }) + + expect(result.current).toBe('entering') + expect(onEnter).toHaveBeenCalledWith(nodeRef.current, false) + + act(() => { + jest.runAllTimers() + }) + + expect(result.current).toBe('entered') + expect(onEntered).toHaveBeenCalledWith(nodeRef.current, false) + }) + + it('settles the exit transition after `timeout` milliseconds', () => { + const onExit = jest.fn() + const onExiting = jest.fn() + const onExited = jest.fn() + const { result, rerender, nodeRef, initialProps } = renderTransitionStatus({ + in: true, + timeout: 300, + onExit, + onExiting, + onExited, + }) + + rerender({ ...initialProps, in: false }) + + expect(result.current).toBe('exiting') + expect(onExit).toHaveBeenCalledWith(nodeRef.current) + expect(onExiting).toHaveBeenCalledWith(nodeRef.current) + expect(onExited).not.toHaveBeenCalled() + + act(() => { + jest.advanceTimersByTime(299) + }) + + expect(result.current).toBe('exiting') + + act(() => { + jest.advanceTimersByTime(1) + }) + + expect(result.current).toBe('exited') + expect(onExited).toHaveBeenCalledTimes(1) + expect(onExited).toHaveBeenCalledWith(nodeRef.current) + }) + + it('cancels the pending settle when `in` flips mid-transition', () => { + const onExited = jest.fn() + const { result, rerender, initialProps } = renderTransitionStatus({ + in: true, + timeout: 300, + onExited, + }) + + rerender({ ...initialProps, in: false }) + + act(() => { + jest.advanceTimersByTime(100) + }) + + rerender({ ...initialProps, in: true }) + + act(() => { + jest.runAllTimers() + }) + + expect(result.current).toBe('entered') + expect(onExited).not.toHaveBeenCalled() + }) + + it('uses per-phase durations from an object `timeout`', () => { + const { result, rerender, initialProps } = renderTransitionStatus({ + in: false, + timeout: { enter: 100, exit: 200 }, + }) + + rerender({ ...initialProps, in: true }) + + act(() => { + jest.advanceTimersByTime(99) + }) + + expect(result.current).toBe('entering') + + act(() => { + jest.advanceTimersByTime(1) + }) + + expect(result.current).toBe('entered') + + rerender({ ...initialProps, in: false }) + + act(() => { + jest.advanceTimersByTime(199) + }) + + expect(result.current).toBe('exiting') + + act(() => { + jest.advanceTimersByTime(1) + }) + + expect(result.current).toBe('exited') + }) + + it('cycles through mount and unmount with `unmountOnExit`', () => { + const onEnter = jest.fn() + const onExited = jest.fn() + const { result, rerender, nodeRef, initialProps } = renderTransitionStatus({ + in: false, + unmountOnExit: true, + onEnter, + onExited, + }) + + expect(result.current).toBe('unmounted') + + rerender({ ...initialProps, in: true }) + + expect(result.current).toBe('entering') + expect(onEnter).toHaveBeenCalledWith(nodeRef.current, false) + + act(() => { + jest.runAllTimers() + }) + + expect(result.current).toBe('entered') + + rerender({ ...initialProps, in: false }) + + expect(result.current).toBe('exiting') + + act(() => { + jest.runAllTimers() + }) + + expect(onExited).toHaveBeenCalledTimes(1) + expect(onExited).toHaveBeenCalledWith(nodeRef.current) + expect(result.current).toBe('unmounted') + }) +}) diff --git a/packages/base/Utils/src/utils/get-element-ref.ts b/packages/base/Utils/src/utils/get-element-ref.ts new file mode 100644 index 0000000000..7bd13799b8 --- /dev/null +++ b/packages/base/Utils/src/utils/get-element-ref.ts @@ -0,0 +1,19 @@ +import type { ForwardedRef, ReactElement } from 'react' + +type ElementWithRef = ReactElement & { ref?: ForwardedRef } + +/** + * Reads the ref attached to a React element. + * + * `ReactElement` has no public `ref` field, so the cast keeps the lookup in + * one place. React 19 moves the ref into props instead; reading it from + * there has to wait until the `react` peer range allows v19, because on + * v18 `props.ref` is a getter that warns when accessed. + */ +const getElementRef = (element: ReactElement): ForwardedRef => { + const { ref } = element as ElementWithRef + + return ref ?? null +} + +export default getElementRef diff --git a/packages/base/Utils/src/utils/index.ts b/packages/base/Utils/src/utils/index.ts index a2d66c338d..f6ced7b6dd 100644 --- a/packages/base/Utils/src/utils/index.ts +++ b/packages/base/Utils/src/utils/index.ts @@ -65,7 +65,17 @@ export { default as useMouseEnter } from './useMouseEnter' export { default as sum } from './sum' export type { ReferenceObject } from './use-width-of' export { default as useMultipleForwardRefs } from './use-multiple-forward-refs' +export { default as getElementRef } from './get-element-ref' export { usePageScrollLock } from './use-page-scroll-lock' +export { + default as useTransitionStatus, + getTransitionTimeouts, +} from './use-transition-status' +export type { + TransitionStatus, + TransitionTimeout, + UseTransitionStatusOptions, +} from './use-transition-status' export { AVATAR_INITIALS_LIMIT } from './constants' export const Transitions = TransitionUtils diff --git a/packages/base/Utils/src/utils/use-multiple-forward-refs.ts b/packages/base/Utils/src/utils/use-multiple-forward-refs.ts index 9adcdb6a90..9aedc5582f 100644 --- a/packages/base/Utils/src/utils/use-multiple-forward-refs.ts +++ b/packages/base/Utils/src/utils/use-multiple-forward-refs.ts @@ -31,6 +31,7 @@ const useMultipleForwardRefs = (refs: ForwardedRef[]) => forwardRef(ref, refValue) } }, + // eslint-disable-next-line react-hooks/exhaustive-deps [...refs] ) diff --git a/packages/base/Utils/src/utils/use-transition-status.ts b/packages/base/Utils/src/utils/use-transition-status.ts new file mode 100644 index 0000000000..e2454583ec --- /dev/null +++ b/packages/base/Utils/src/utils/use-transition-status.ts @@ -0,0 +1,179 @@ +import type { RefObject } from 'react' +import { useRef, useState } from 'react' +import { useIsomorphicLayoutEffect } from '@toptal/picasso-shared' + +export type TransitionStatus = + | 'entering' + | 'entered' + | 'exiting' + | 'exited' + | 'unmounted' + +export type TransitionTimeout = + | number + | { enter?: number; exit?: number; appear?: number } + +/** + * Resolves a react-transition-group style `timeout` (number or object) + * into per-phase durations in ms; `appear` falls back to `enter`. + */ +export const getTransitionTimeouts = ( + timeout: TransitionTimeout | undefined +): { enter: number; exit: number; appear: number } => { + if (typeof timeout === 'number') { + return { enter: timeout, exit: timeout, appear: timeout } + } + + return { + enter: timeout?.enter ?? 0, + exit: timeout?.exit ?? 0, + appear: timeout?.appear ?? timeout?.enter ?? 0, + } +} + +export interface UseTransitionStatusOptions { + /** Show the element; toggling runs the enter or exit transition */ + in: boolean + /** The duration for the transition, in milliseconds */ + timeout?: TransitionTimeout + /** Run the enter transition when mounting with `in` already true */ + appear?: boolean + /** Resolve to `unmounted` once fully exited, so the caller can render nothing */ + unmountOnExit?: boolean + /** The transitioning DOM element, passed to the lifecycle callbacks */ + nodeRef: RefObject + /** Fired when the enter phase starts */ + onEnter?: (node: T, isAppearing: boolean) => void + /** Fired right after the status flips to `entering` */ + onEntering?: (node: T, isAppearing: boolean) => void + /** Fired when the enter transition settles */ + onEntered?: (node: T, isAppearing: boolean) => void + /** Fired when the exit phase starts */ + onExit?: (node: T) => void + /** Fired right after the status flips to `exiting` */ + onExiting?: (node: T) => void + /** Fired when the exit transition settles */ + onExited?: (node: T) => void +} + +/** + * Drop-in replacement for react-transition-group's `` state + * machine. Settles on `setTimeout(timeout)` — not `transitionend` — so + * callback timing is identical and fake timers drive it in jsdom tests. + */ +const useTransitionStatus = ( + options: UseTransitionStatusOptions +): TransitionStatus => { + const { in: inProp, appear = false, unmountOnExit = false, nodeRef } = options + + const [status, setStatus] = useState(() => { + if (inProp) { + return appear ? 'exited' : 'entered' + } + + return unmountOnExit ? 'unmounted' : 'exited' + }) + + // An unmounted element re-mounts at `exited` before entering + // (render-phase derived state) + if (inProp && status === 'unmounted') { + setStatus('exited') + } + + // Read via ref so the transition effect re-runs only on `in` flips; + // timeout/callback changes must not restart a running transition + const optionsRef = useRef(options) + + useIsomorphicLayoutEffect(() => { + optionsRef.current = options + }) + + const prevInRef = useRef(null) + + useIsomorphicLayoutEffect(() => { + const prevIn = prevInRef.current + const isInitialMount = prevIn === null + + prevInRef.current = inProp + + // Effect replay without an `in` flip (StrictMode double invocation): + // skip rather than double-fire the start callbacks, matching + // react-transition-group's cancelled-settle behavior + if (prevIn === inProp) { + return + } + + // Mounting hidden never exits; mounting shown enters only with `appear` + if (isInitialMount && (!inProp || !appear)) { + return + } + + const timeouts = getTransitionTimeouts(optionsRef.current.timeout) + const node = nodeRef.current + + const performEnter = () => { + const isAppearing = isInitialMount + + if (node) { + optionsRef.current.onEnter?.(node, isAppearing) + } + + setStatus('entering') + + if (node) { + optionsRef.current.onEntering?.(node, isAppearing) + } + + return setTimeout( + () => { + setStatus('entered') + + const settledNode = nodeRef.current + + if (settledNode) { + optionsRef.current.onEntered?.(settledNode, isAppearing) + } + }, + isAppearing ? timeouts.appear : timeouts.enter + ) + } + + const performExit = () => { + if (node) { + optionsRef.current.onExit?.(node) + } + + setStatus('exiting') + + if (node) { + optionsRef.current.onExiting?.(node) + } + + return setTimeout(() => { + setStatus('exited') + + const settledNode = nodeRef.current + + if (settledNode) { + optionsRef.current.onExited?.(settledNode) + } + }, timeouts.exit) + } + + const timer = inProp ? performEnter() : performExit() + + return () => clearTimeout(timer) + // eslint-disable-next-line react-hooks/exhaustive-deps -- transitions are keyed on `in` alone; the other options are read from optionsRef + }, [inProp]) + + // Unmount one commit after `exited`, so `onExited` observes the node + useIsomorphicLayoutEffect(() => { + if (unmountOnExit && !inProp && status === 'exited') { + setStatus('unmounted') + } + }, [unmountOnExit, inProp, status]) + + return status +} + +export default useTransitionStatus diff --git a/packages/picasso-tailwind-merge/package.json b/packages/picasso-tailwind-merge/package.json index 6eca7ffea6..0a025c0fce 100644 --- a/packages/picasso-tailwind-merge/package.json +++ b/packages/picasso-tailwind-merge/package.json @@ -22,8 +22,7 @@ }, "homepage": "https://github.com/toptal/picasso/tree/master/packages/picasso#readme", "dependencies": { - "tailwind-merge": "^2.2.2", - "react-transition-group": "^4.4.5" + "tailwind-merge": "^2.2.2" }, "sideEffects": false, "peerDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 049ae745c6..cd9fd14eed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -905,12 +905,12 @@ importers: '@toptal/picasso-fade': specifier: workspace:* version: link:../Fade + '@toptal/picasso-shared': + specifier: workspace:* + version: link:../../shared '@toptal/picasso-tailwind': specifier: workspace:^ version: link:../../picasso-tailwind - '@toptal/picasso-utils': - specifier: workspace:* - version: link:../Utils classnames: specifier: ^2.5.1 version: 2.5.1 @@ -918,6 +918,9 @@ importers: specifier: ^18.2.0 version: 18.2.0 devDependencies: + '@toptal/picasso-tailwind-merge': + specifier: workspace:* + version: link:../../picasso-tailwind-merge '@toptal/picasso-test-utils': specifier: workspace:* version: link:../Test-Utils @@ -1399,25 +1402,28 @@ importers: packages/base/Fade: dependencies: + '@toptal/picasso-shared': + specifier: workspace:* + version: link:../../shared '@toptal/picasso-tailwind': specifier: workspace:^ version: link:../../picasso-tailwind '@toptal/picasso-utils': specifier: workspace:* version: link:../Utils + classnames: + specifier: ^2.5.1 + version: 2.5.1 react: specifier: ^18.2.0 version: 18.2.0 - react-transition-group: - specifier: ^4.4.5 - version: 4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) devDependencies: + '@toptal/picasso-tailwind-merge': + specifier: workspace:* + version: link:../../picasso-tailwind-merge '@toptal/picasso-test-utils': specifier: workspace:* version: link:../Test-Utils - '@types/react-transition-group': - specifier: ^4.4.5 - version: 4.4.12(@types/react@17.0.39) packages/base/FileInput: dependencies: @@ -2562,25 +2568,28 @@ importers: packages/base/Slide: dependencies: + '@toptal/picasso-shared': + specifier: workspace:* + version: link:../../shared '@toptal/picasso-tailwind': specifier: workspace:^ version: link:../../picasso-tailwind '@toptal/picasso-utils': specifier: workspace:* version: link:../Utils + classnames: + specifier: ^2.5.1 + version: 2.5.1 react: specifier: ^18.2.0 version: 18.2.0 - react-transition-group: - specifier: ^4.4.5 - version: 4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) devDependencies: + '@toptal/picasso-tailwind-merge': + specifier: workspace:* + version: link:../../picasso-tailwind-merge '@toptal/picasso-test-utils': specifier: workspace:* version: link:../Test-Utils - '@types/react-transition-group': - specifier: ^4.4.5 - version: 4.4.12(@types/react@17.0.39) packages/base/Slider: dependencies: @@ -3874,9 +3883,6 @@ importers: react: specifier: ^18.2.0 version: 18.2.0 - react-transition-group: - specifier: ^4.4.5 - version: 4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) tailwind-merge: specifier: ^2.2.2 version: 2.3.0 From 7a017604039d37cfe2d1e0e392e3d38db4f8fa94 Mon Sep 17 00:00:00 2001 From: Ante Zebic <17073473+azebich@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:59:27 +0200 Subject: [PATCH 07/14] [PF-2296] Upgrade date-fns to latest v4.x (#5076) --- .changeset/date-fns-v4-upgrade.md | 24 +++ package.json | 2 +- packages/base/Calendar/package.json | 2 +- .../base/Calendar/src/Calendar/Calendar.tsx | 3 +- .../CalendarMonthHeader.tsx | 2 +- packages/base/DatePicker/package.json | 4 +- .../DatePicker/src/DatePicker/DatePicker.tsx | 2 +- .../base/DatePicker/src/DatePicker/test.tsx | 152 ++++++++++++++---- .../base/DatePicker/src/DatePicker/utils.ts | 20 +-- packages/picasso/package.json | 4 +- packages/topkit-analytics-charts/package.json | 2 +- pnpm-lock.yaml | 69 ++++---- pnpm-workspace.yaml | 11 ++ 13 files changed, 212 insertions(+), 85 deletions(-) create mode 100644 .changeset/date-fns-v4-upgrade.md diff --git a/.changeset/date-fns-v4-upgrade.md b/.changeset/date-fns-v4-upgrade.md new file mode 100644 index 0000000000..958fadabf6 --- /dev/null +++ b/.changeset/date-fns-v4-upgrade.md @@ -0,0 +1,24 @@ +--- +'@toptal/picasso-calendar': patch +'@toptal/picasso-date-picker': patch +'@toptal/picasso': patch +--- + +### Calendar + +- upgrade `date-fns` to `^4.1.0`. `@base-ui/react` — the primitive layer the kit is migrating to — peers `date-fns: ^4.0.0`, which the previous `^2.30.0` did not satisfy +- import `isWeekend` and `isSameMonth` from the `date-fns` barrel instead of the `date-fns/isWeekend` and `date-fns/isSameMonth` submodules. date-fns v3 removed the default export from every submodule, so the old form resolves to `undefined` + +### DatePicker + +- upgrade `date-fns` to `^4.1.0` and `date-fns-tz` to `^3.2.0`. `date-fns-tz@3` is required because it is the first line whose peer range admits date-fns v3 and v4 +- rename `utcToZonedTime` to `toZonedTime`, which is what `date-fns-tz@3` calls it. This is internal to `timezoneConvert`; the `timezone` prop behaves exactly as before +- import `parse`, `isValid`, `format`, `isWithinInterval`, `isEqual`, `isBefore` and `isAfter` from the `date-fns` barrel instead of per-function submodules, for the same reason as Calendar + +No API or behavior change in any package. `react-day-picker@8.x` peers +`date-fns: ^2.28.0 || ^3.0.0` and no `8.x` release admits v4, so that peer is now +unsatisfied and recorded as an accepted exception in `pnpm-workspace.yaml`. +It is verified safe: react-day-picker reaches date-fns only through named barrel +imports, and all 31 functions it uses are unchanged in v4. The exception goes +away with the react-day-picker v9+ migration, where date-fns stops being a peer +of that package altogether. diff --git a/package.json b/package.json index afa59b360f..9885a0f714 100644 --- a/package.json +++ b/package.json @@ -219,7 +219,7 @@ "cypress": "^13.6.0", "cypress-plugin-tab": "^1.0.5", "cypress-real-events": "^1.12.0", - "date-fns": "^2.30.0", + "date-fns": "^4.1.0", "debounce": "^1.2.1", "ejs": "^3.1.10", "escodegen": "^2.1.0", diff --git a/packages/base/Calendar/package.json b/packages/base/Calendar/package.json index 1ef7c4ae72..f3d0234c9c 100644 --- a/packages/base/Calendar/package.json +++ b/packages/base/Calendar/package.json @@ -28,7 +28,7 @@ "@toptal/picasso-shared": "workspace:*", "@toptal/picasso-typography": "workspace:*", "@toptal/picasso-utils": "workspace:*", - "date-fns": "^2.30.0", + "date-fns": "^4.1.0", "react-day-picker": "^8.10.2" }, "sideEffects": [ diff --git a/packages/base/Calendar/src/Calendar/Calendar.tsx b/packages/base/Calendar/src/Calendar/Calendar.tsx index bddc7bb171..4258c9b7bf 100644 --- a/packages/base/Calendar/src/Calendar/Calendar.tsx +++ b/packages/base/Calendar/src/Calendar/Calendar.tsx @@ -12,8 +12,7 @@ import type { DateRange, } from 'react-day-picker' import { DayPicker } from 'react-day-picker' -import isWeekend from 'date-fns/isWeekend' -import { format, isEqual } from 'date-fns' +import { format, isEqual, isWeekend } from 'date-fns' import { useBreakpoint } from '@toptal/picasso-provider' import type { RenderDay } from '../CalendarDay' diff --git a/packages/base/Calendar/src/CalendarMonthHeader/CalendarMonthHeader.tsx b/packages/base/Calendar/src/CalendarMonthHeader/CalendarMonthHeader.tsx index f6440add5f..0d0b3b08ed 100644 --- a/packages/base/Calendar/src/CalendarMonthHeader/CalendarMonthHeader.tsx +++ b/packages/base/Calendar/src/CalendarMonthHeader/CalendarMonthHeader.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from 'react' import React from 'react' import { useDayPicker, useNavigation } from 'react-day-picker' -import isSameMonth from 'date-fns/isSameMonth' +import { isSameMonth } from 'date-fns' import { ButtonCircular } from '@toptal/picasso-button' import { Typography } from '@toptal/picasso-typography' import { ChevronMinor24, BackMinor24 } from '@toptal/picasso-icons' diff --git a/packages/base/DatePicker/package.json b/packages/base/DatePicker/package.json index 2b5dac95f0..40693a7842 100644 --- a/packages/base/DatePicker/package.json +++ b/packages/base/DatePicker/package.json @@ -30,8 +30,8 @@ "@toptal/picasso-outlined-input": "workspace:*", "@toptal/picasso-popper": "workspace:*", "@toptal/picasso-utils": "workspace:*", - "date-fns": "^2.30.0", - "date-fns-tz": "^2.0.0" + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0" }, "sideEffects": [ "**/styles.ts", diff --git a/packages/base/DatePicker/src/DatePicker/DatePicker.tsx b/packages/base/DatePicker/src/DatePicker/DatePicker.tsx index 94c0839a17..b6cd70022b 100644 --- a/packages/base/DatePicker/src/DatePicker/DatePicker.tsx +++ b/packages/base/DatePicker/src/DatePicker/DatePicker.tsx @@ -1,7 +1,7 @@ /* eslint-disable max-lines-per-function, max-lines */ /* eslint-disable complexity, max-statements */ // Squiggly lines makes code difficult to work with import type { BaseProps } from '@toptal/picasso-shared' -import formatDate from 'date-fns/format' +import { format as formatDate } from 'date-fns' import type { PopperHandle } from '@toptal/picasso-popper' import type { KeyboardEvent, ReactNode } from 'react' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' diff --git a/packages/base/DatePicker/src/DatePicker/test.tsx b/packages/base/DatePicker/src/DatePicker/test.tsx index 02b9fedb58..e2de2fed28 100644 --- a/packages/base/DatePicker/src/DatePicker/test.tsx +++ b/packages/base/DatePicker/src/DatePicker/test.tsx @@ -14,6 +14,105 @@ const testIds = { const FAR_EAST_TIMEZONE = 'Asia/Tokyo' const NEW_YORK_TIMEZONE = 'America/New_York' +const SHANGHAI_TIMEZONE = 'Asia/Shanghai' +const LOS_ANGELES_TIMEZONE = 'America/Los_Angeles' + +/** + * Both sides of the midnight boundary for the offsets the component realistically + * meets, including both DST states of the two zones that observe it. Tests run + * under `TZ=UTC`, so every case is one where the UTC date and the target zone's + * date disagree — the situation that makes a date land on the wrong day. + * + * Each literal carries the offset the zone genuinely has at that instant. Getting + * that wrong does not fail the test, it silently moves the case off the boundary + * it is meant to probe, so keep literal and zone in step when editing. + */ +const MIDNIGHT_BOUNDARY_CASES = [ + { + label: 'Tokyo +09:00, just after midnight', + date: '2020-06-25T00:00:00+09:00', + timezone: FAR_EAST_TIMEZONE, + expectedSelectedDate: '25', + expectedInputValue: 'Jun 25, 2020', + }, + { + label: 'Tokyo +09:00, one second before midnight', + date: '2020-06-24T23:59:59+09:00', + timezone: FAR_EAST_TIMEZONE, + expectedSelectedDate: '24', + expectedInputValue: 'Jun 24, 2020', + }, + { + label: 'Shanghai +08:00, just after midnight', + date: '2020-06-25T00:00:00+08:00', + timezone: SHANGHAI_TIMEZONE, + expectedSelectedDate: '25', + expectedInputValue: 'Jun 25, 2020', + }, + { + label: 'Shanghai +08:00, one second before midnight', + date: '2020-06-24T23:59:59+08:00', + timezone: SHANGHAI_TIMEZONE, + expectedSelectedDate: '24', + expectedInputValue: 'Jun 24, 2020', + }, + { + label: 'Los Angeles -08:00 (PST), just after midnight', + date: '2020-01-25T00:00:00-08:00', + timezone: LOS_ANGELES_TIMEZONE, + expectedSelectedDate: '25', + expectedInputValue: 'Jan 25, 2020', + }, + { + label: 'Los Angeles -08:00 (PST), one second before midnight', + date: '2020-01-24T23:59:59-08:00', + timezone: LOS_ANGELES_TIMEZONE, + expectedSelectedDate: '24', + expectedInputValue: 'Jan 24, 2020', + }, + { + label: 'Los Angeles -07:00 (PDT), just after midnight', + date: '2020-06-25T00:00:00-07:00', + timezone: LOS_ANGELES_TIMEZONE, + expectedSelectedDate: '25', + expectedInputValue: 'Jun 25, 2020', + }, + { + label: 'Los Angeles -07:00 (PDT), one second before midnight', + date: '2020-06-24T23:59:59-07:00', + timezone: LOS_ANGELES_TIMEZONE, + expectedSelectedDate: '24', + expectedInputValue: 'Jun 24, 2020', + }, + { + label: 'New York -04:00 (EDT), just after midnight', + date: '2020-06-25T00:00:00-04:00', + timezone: NEW_YORK_TIMEZONE, + expectedSelectedDate: '25', + expectedInputValue: 'Jun 25, 2020', + }, + { + label: 'New York -04:00 (EDT), one second before midnight', + date: '2020-06-24T23:59:59-04:00', + timezone: NEW_YORK_TIMEZONE, + expectedSelectedDate: '24', + expectedInputValue: 'Jun 24, 2020', + }, + { + label: 'New York -05:00 (EST), just after midnight', + date: '2020-01-25T00:00:00-05:00', + timezone: NEW_YORK_TIMEZONE, + expectedSelectedDate: '25', + expectedInputValue: 'Jan 25, 2020', + }, + { + label: 'New York -05:00 (EST), one second before midnight', + date: '2020-01-24T23:59:59-05:00', + timezone: NEW_YORK_TIMEZONE, + expectedSelectedDate: '24', + expectedInputValue: 'Jan 24, 2020', + }, +] // eslint-disable-next-line max-lines-per-function describe('DatePicker', () => { @@ -173,6 +272,22 @@ describe('DatePicker', () => { ) }) + it.each(MIDNIGHT_BOUNDARY_CASES)( + 'should display date in given timezone across a midnight boundary ($label)', + ({ date, timezone, expectedInputValue }) => { + const { getByPlaceholderText } = renderDatePicker({ + ...defaultProps, + timezone, + value: new Date(date), + }) + + expect(getByPlaceholderText(defaultProps.placeholder)).toHaveAttribute( + 'value', + expectedInputValue + ) + } + ) + it('should work within interval', () => { const MIN_DATE = new Date(2020, 6, 10) const MAX_DATE = new Date(2020, 6, 25) @@ -434,24 +549,8 @@ describe('DatePicker', () => { }) describe('Calendar', () => { - it.each([ - { - date: '2020-06-25T00:00:00+09:00', - timezone: FAR_EAST_TIMEZONE, - expectedSelectedDate: '25', - }, - { - date: '2020-06-24T23:59:59+09:00', - timezone: FAR_EAST_TIMEZONE, - expectedSelectedDate: '24', - }, - { - date: '2020-06-25T00:00:00-05:00', - timezone: NEW_YORK_TIMEZONE, - expectedSelectedDate: '25', - }, - ])( - 'should display date in given timezone', + it.each(MIDNIGHT_BOUNDARY_CASES)( + 'should display date in given timezone ($label)', ({ date, timezone, expectedSelectedDate }) => { const { getByPlaceholderText } = renderDatePicker({ ...defaultProps, @@ -465,21 +564,8 @@ describe('DatePicker', () => { } ) - it.each([ - { - date: '2020-06-25T00:00:00+09:00', - timezone: FAR_EAST_TIMEZONE, - }, - { - date: '2020-06-24T23:59:59+09:00', - timezone: FAR_EAST_TIMEZONE, - }, - { - date: '2020-06-25T00:00:00-05:00', - timezone: NEW_YORK_TIMEZONE, - }, - ])( - 'should display date in given timezone after day click', + it.each(MIDNIGHT_BOUNDARY_CASES)( + 'should display date in given timezone after day click ($label)', async ({ date, timezone }) => { const { getByPlaceholderText, getByText } = renderDatePicker({ ...defaultProps, diff --git a/packages/base/DatePicker/src/DatePicker/utils.ts b/packages/base/DatePicker/src/DatePicker/utils.ts index 2c419a54eb..c77f043346 100644 --- a/packages/base/DatePicker/src/DatePicker/utils.ts +++ b/packages/base/DatePicker/src/DatePicker/utils.ts @@ -1,11 +1,13 @@ -import parse from 'date-fns/parse' -import isValid from 'date-fns/isValid' -import formatDate from 'date-fns/format' -import isWithinInterval from 'date-fns/isWithinInterval' -import isEqual from 'date-fns/isEqual' -import isBefore from 'date-fns/isBefore' -import isAfter from 'date-fns/isAfter' -import { utcToZonedTime, format as tzFormat, toDate } from 'date-fns-tz' +import { + parse, + isValid, + format as formatDate, + isWithinInterval, + isEqual, + isBefore, + isAfter, +} from 'date-fns' +import { toZonedTime, format as tzFormat, toDate } from 'date-fns-tz' import type { DateOrDateRangeType, DateRangeType, @@ -25,7 +27,7 @@ export const timezoneConvert = ( * when someone is editing properties in a Storybook examples. */ try { - return utcToZonedTime(dateToConvert, timeZone) + return toZonedTime(dateToConvert, timeZone) } catch { return dateToConvert } diff --git a/packages/picasso/package.json b/packages/picasso/package.json index 9ac15d954e..03da9182a4 100644 --- a/packages/picasso/package.json +++ b/packages/picasso/package.json @@ -112,8 +112,8 @@ "d3": "^7.8.2", "d3-hierarchy": "^3.1.2", "d3-zoom": "^3", - "date-fns": "^2.30.0", - "date-fns-tz": "^2.0.0", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", "debounce": "^1.2.1", "detect-browser": "^5.3.0", "glider-js": "^1.7.8", diff --git a/packages/topkit-analytics-charts/package.json b/packages/topkit-analytics-charts/package.json index 78af576793..a5bb40c00e 100644 --- a/packages/topkit-analytics-charts/package.json +++ b/packages/topkit-analytics-charts/package.json @@ -29,7 +29,7 @@ "@toptal/picasso-test-utils": "workspace:*", "@types/d3": "^7.4.0", "@types/d3-array": "3.0.4", - "date-fns": "^2.30.0" + "date-fns": "^4.1.0" }, "dependencies": { "@toptal/picasso-utils": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd9fd14eed..1145a9f330 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,7 +80,7 @@ importers: version: 7.23.1 '@base-ui/react': specifier: ^1.6.0 - version: 1.6.0(@types/react@17.0.39)(date-fns@2.30.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 1.6.0(@types/react@17.0.39)(date-fns@4.4.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@changesets/changelog-github': specifier: ^0.5.0 version: 0.5.0(encoding@0.1.13) @@ -466,8 +466,8 @@ importers: specifier: ^1.12.0 version: 1.12.0(cypress@13.6.0) date-fns: - specifier: ^2.30.0 - version: 2.30.0 + specifier: ^4.1.0 + version: 4.4.0 debounce: specifier: ^1.2.1 version: 1.2.1 @@ -620,7 +620,7 @@ importers: dependencies: '@base-ui/react': specifier: ^1.6.0 - version: 1.6.0(@types/react@17.0.39)(date-fns@2.30.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 1.6.0(@types/react@17.0.39)(date-fns@4.4.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@toptal/picasso-button': specifier: workspace:* version: link:../Button @@ -982,7 +982,7 @@ importers: dependencies: '@base-ui/react': specifier: ^1.6.0 - version: 1.6.0(@types/react@17.0.39)(date-fns@2.30.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 1.6.0(@types/react@17.0.39)(date-fns@4.4.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@toptal/picasso-checkbox': specifier: workspace:* version: link:../Checkbox @@ -1060,8 +1060,8 @@ importers: specifier: workspace:* version: link:../Utils date-fns: - specifier: ^2.30.0 - version: 2.30.0 + specifier: ^4.1.0 + version: 4.4.0 react: specifier: ^18.2.0 version: 18.2.0 @@ -1111,7 +1111,7 @@ importers: dependencies: '@base-ui/react': specifier: ^1.6.0 - version: 1.6.0(@types/react@17.0.39)(date-fns@2.30.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 1.6.0(@types/react@17.0.39)(date-fns@4.4.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@toptal/picasso-form-label': specifier: workspace:* version: link:../FormLabel @@ -1227,11 +1227,11 @@ importers: specifier: workspace:* version: link:../Utils date-fns: - specifier: ^2.30.0 - version: 2.30.0 + specifier: ^4.1.0 + version: 4.4.0 date-fns-tz: - specifier: ^2.0.0 - version: 2.0.0(date-fns@2.30.0) + specifier: ^3.2.0 + version: 3.2.0(date-fns@4.4.0) react: specifier: ^18.2.0 version: 18.2.0 @@ -1256,7 +1256,7 @@ importers: dependencies: '@base-ui/react': specifier: ^1.6.0 - version: 1.6.0(@types/react@17.0.39)(date-fns@2.30.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 1.6.0(@types/react@17.0.39)(date-fns@4.4.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@toptal/picasso-button': specifier: workspace:* version: link:../Button @@ -1899,7 +1899,7 @@ importers: dependencies: '@base-ui/react': specifier: ^1.6.0 - version: 1.6.0(@types/react@17.0.39)(date-fns@2.30.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 1.6.0(@types/react@17.0.39)(date-fns@4.4.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@toptal/picasso-button': specifier: workspace:* version: link:../Button @@ -2595,7 +2595,7 @@ importers: dependencies: '@base-ui/react': specifier: ^1.6.0 - version: 1.6.0(@types/react@17.0.39)(date-fns@2.30.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 1.6.0(@types/react@17.0.39)(date-fns@4.4.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@toptal/picasso-shared': specifier: workspace:* version: link:../../shared @@ -2663,7 +2663,7 @@ importers: dependencies: '@base-ui/react': specifier: ^1.6.0 - version: 1.6.0(@types/react@17.0.39)(date-fns@2.30.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 1.6.0(@types/react@17.0.39)(date-fns@4.4.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@toptal/picasso-form-label': specifier: workspace:* version: link:../FormLabel @@ -2734,7 +2734,7 @@ importers: dependencies: '@base-ui/react': specifier: ^1.6.0 - version: 1.6.0(@types/react@17.0.39)(date-fns@2.30.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 1.6.0(@types/react@17.0.39)(date-fns@4.4.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@toptal/picasso-container': specifier: workspace:* version: link:../Container @@ -2929,7 +2929,7 @@ importers: dependencies: '@base-ui/react': specifier: ^1.6.0 - version: 1.6.0(@types/react@17.0.39)(date-fns@2.30.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 1.6.0(@types/react@17.0.39)(date-fns@4.4.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@toptal/picasso-shared': specifier: workspace:* version: link:../../shared @@ -3330,11 +3330,11 @@ importers: specifier: ^3 version: 3.0.0 date-fns: - specifier: ^2.30.0 - version: 2.30.0 + specifier: ^4.1.0 + version: 4.4.0 date-fns-tz: - specifier: ^2.0.0 - version: 2.0.0(date-fns@2.30.0) + specifier: ^3.2.0 + version: 3.2.0(date-fns@4.4.0) debounce: specifier: ^1.2.1 version: 1.2.1 @@ -3968,8 +3968,8 @@ importers: specifier: 3.0.4 version: 3.0.4 date-fns: - specifier: ^2.30.0 - version: 2.30.0 + specifier: ^4.1.0 + version: 4.4.0 packages: @@ -9633,15 +9633,18 @@ packages: dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} - date-fns-tz@2.0.0: - resolution: {integrity: sha512-OAtcLdB9vxSXTWHdT8b398ARImVwQMyjfYGkKD2zaGpHseG2UPHbHjXELReErZFxWdSLph3c2zOaaTyHfOhERQ==} + date-fns-tz@3.2.0: + resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==} peerDependencies: - date-fns: '>=2.0.0' + date-fns: ^3.0.0 || ^4.0.0 date-fns@2.30.0: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} + date-fns@4.4.0: + resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} + dateformat@3.0.3: resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} @@ -19155,7 +19158,7 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@base-ui/react@1.6.0(@types/react@17.0.39)(date-fns@2.30.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@base-ui/react@1.6.0(@types/react@17.0.39)(date-fns@4.4.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.29.2 '@base-ui/utils': 0.3.1(@types/react@17.0.39)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -19166,7 +19169,7 @@ snapshots: use-sync-external-store: 1.6.0(react@18.2.0) optionalDependencies: '@types/react': 17.0.39 - date-fns: 2.30.0 + date-fns: 4.4.0 '@base-ui/utils@0.3.1(@types/react@17.0.39)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: @@ -26092,14 +26095,16 @@ snapshots: dataloader@1.4.0: {} - date-fns-tz@2.0.0(date-fns@2.30.0): + date-fns-tz@3.2.0(date-fns@4.4.0): dependencies: - date-fns: 2.30.0 + date-fns: 4.4.0 date-fns@2.30.0: dependencies: '@babel/runtime': 7.28.4 + date-fns@4.4.0: {} + dateformat@3.0.3: {} dayjs@1.11.10: {} @@ -32917,7 +32922,7 @@ snapshots: react-day-picker@8.10.2(date-fns@2.30.0)(react@18.2.0): dependencies: - date-fns: 2.30.0 + date-fns: 4.4.0 react: 18.2.0 react-deprecate@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2517126fd9..e2ede2b728 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -61,6 +61,17 @@ overrides: webpack-dev-middleware: '5.3.4' yaml: '2' +# react-day-picker 8.x peers date-fns '^2.28.0 || ^3.0.0' and no 8.x release +# admits v4, while @base-ui/react peers date-fns '^4.0.0'. The two cannot both +# be satisfied, and Base UI wins: it is the primitive layer the kit is moving +# to. react-day-picker v8 is verified to work on v4 — it imports date-fns only +# through named barrel imports, and all 31 functions it uses are unchanged in +# v4. This entry goes away with the react-day-picker v9+ migration: from 9.0.0 +# on, date-fns is a regular dependency of the package and not a peer at all. +peerDependencyRules: + allowedVersions: + 'react-day-picker>date-fns': '4' + patchedDependencies: '@changesets/get-github-info@0.6.0': patches/@changesets__get-github-info@0.6.0.patch '@storybook/react-docgen-typescript-plugin@1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0': 'patches/@storybook__react-docgen-typescript-plugin@1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0.patch' From 97505eb809019d7f5729ce710495b01437bfa7f5 Mon Sep 17 00:00:00 2001 From: Vedran Ivanac Date: Fri, 28 Aug 2026 14:22:45 +0200 Subject: [PATCH 08/14] [PF-2262] Improvements for Lift the react 19 (#5096) --- .../calendar-react-day-picker-react-19.md | 5 + .changeset/date-fns-v4-upgrade.md | 15 +- .changeset/final-form-v5-upgrade.md | 8 +- .../rte-emoji-picker-keyup-listener-leak.md | 7 + .changeset/transitions-tailwind-migration.md | 13 +- .claude/launch.json | 6 + .storybook/main.js | 35 +++- .storybook/preview.js | 5 + .storybook/react-dom-19-shim/client.js | 1 + .storybook/react-dom-19-shim/index.js | 24 +++ .storybook/react-dom-19-shim/package.json | 7 + .storybook/react-dom-19-shim/server.js | 1 + .storybook/react-dom-19-shim/test-utils.js | 4 + README.md | 2 - jest.react19.mjs | 40 ++++ package.json | 2 + packages/base/Fade/src/Fade/Fade.tsx | 3 +- packages/base/Slide/src/Slide/styles.ts | 2 - .../__tests__/use-transition-status.test.tsx | 51 ++++++ .../Utils/src/utils/use-transition-status.ts | 77 +++++--- .../Form/mutators/set-active-field-touched.ts | 2 +- .../mutators/set-has-multiline-counter.ts | 7 +- .../picasso-forms/src/NumberInput/test.tsx | 3 + .../RichTextEditorEmojiPicker.tsx | 9 +- pnpm-workspace.yaml | 4 +- react19/package.json | 12 ++ react19/pnpm-lock.yaml | 172 ++++++++++++++++++ react19/sanity.test.tsx | 40 ++++ 28 files changed, 500 insertions(+), 57 deletions(-) create mode 100644 .changeset/rte-emoji-picker-keyup-listener-leak.md create mode 100644 .storybook/react-dom-19-shim/client.js create mode 100644 .storybook/react-dom-19-shim/index.js create mode 100644 .storybook/react-dom-19-shim/package.json create mode 100644 .storybook/react-dom-19-shim/server.js create mode 100644 .storybook/react-dom-19-shim/test-utils.js create mode 100644 jest.react19.mjs create mode 100644 react19/package.json create mode 100644 react19/pnpm-lock.yaml create mode 100644 react19/sanity.test.tsx diff --git a/.changeset/calendar-react-day-picker-react-19.md b/.changeset/calendar-react-day-picker-react-19.md index 96d9a23ba0..b9a1019eb8 100644 --- a/.changeset/calendar-react-day-picker-react-19.md +++ b/.changeset/calendar-react-day-picker-react-19.md @@ -1,8 +1,13 @@ --- '@toptal/picasso-calendar': patch +'@toptal/picasso': patch --- ### Calendar - upgrade `react-day-picker` to `^8.10.2` so the package can run under React 19. `8.10.2` is the first release whose `react` peer range admits `^19.0.0`, and it is the last release of the `8.x` line. It also stops inlining React 18's `react-jsx-runtime` into its bundle and imports `react/jsx-runtime` instead — the inlined copy read `React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED`, which React 19 removes, so under React 19 importing `8.10.0` threw `Cannot read properties of undefined` before anything rendered - no `@toptal/picasso-calendar` API or behavior change. `8.10.2` keeps the whole v8 export surface, including the `useDayRender`, `useDayPicker` and `useNavigation` hooks that `Calendar`, `CalendarDay` and `CalendarMonthHeader` build on, and it renders identically under React 18. The `react` peer range stays capped at `< 19.0.0`; lifting that cap library-wide is tracked separately in PF-2262 + +### Picasso + +- remove the unused direct `react-day-picker` dependency from `@toptal/picasso` — nothing under the aggregate imports it, and it stays installed transitively via `@toptal/picasso-calendar` diff --git a/.changeset/date-fns-v4-upgrade.md b/.changeset/date-fns-v4-upgrade.md index 958fadabf6..19eeb03a61 100644 --- a/.changeset/date-fns-v4-upgrade.md +++ b/.changeset/date-fns-v4-upgrade.md @@ -1,5 +1,5 @@ --- -'@toptal/picasso-calendar': patch +'@toptal/picasso-calendar': major '@toptal/picasso-date-picker': patch '@toptal/picasso': patch --- @@ -8,17 +8,16 @@ - upgrade `date-fns` to `^4.1.0`. `@base-ui/react` — the primitive layer the kit is migrating to — peers `date-fns: ^4.0.0`, which the previous `^2.30.0` did not satisfy - import `isWeekend` and `isSameMonth` from the `date-fns` barrel instead of the `date-fns/isWeekend` and `date-fns/isSameMonth` submodules. date-fns v3 removed the default export from every submodule, so the old form resolves to `undefined` +- **consumer action (the reason for the major)**: `react-day-picker@8.x` declares a required peer of `date-fns: ^2.28.0 || ^3.0.0` and no 8.x release admits v4, while this package now ships `date-fns@^4` alongside it. npm 7+ and yarn consumers of `@toptal/picasso-calendar` (directly or via `@toptal/picasso`) will hit an `ERESOLVE` peer conflict on install and need `--legacy-peer-deps` or an override for `react-day-picker > date-fns` until the react-day-picker v9+ migration (PF-2297) removes that peer entirely. The combination is verified safe at runtime: react-day-picker reaches date-fns only through named barrel imports, and all 31 functions it uses are unchanged in v4 ### DatePicker - upgrade `date-fns` to `^4.1.0` and `date-fns-tz` to `^3.2.0`. `date-fns-tz@3` is required because it is the first line whose peer range admits date-fns v3 and v4 - rename `utcToZonedTime` to `toZonedTime`, which is what `date-fns-tz@3` calls it. This is internal to `timezoneConvert`; the `timezone` prop behaves exactly as before - import `parse`, `isValid`, `format`, `isWithinInterval`, `isEqual`, `isBefore` and `isAfter` from the `date-fns` barrel instead of per-function submodules, for the same reason as Calendar +- one absorbed upstream semantic: date-fns v3+ `isWithinInterval` normalizes reversed intervals instead of throwing, so a `DatePicker` given `minDate > maxDate` no longer crashes and treats the interval as swapped -No API or behavior change in any package. `react-day-picker@8.x` peers -`date-fns: ^2.28.0 || ^3.0.0` and no `8.x` release admits v4, so that peer is now -unsatisfied and recorded as an accepted exception in `pnpm-workspace.yaml`. -It is verified safe: react-day-picker reaches date-fns only through named barrel -imports, and all 31 functions it uses are unchanged in v4. The exception goes -away with the react-day-picker v9+ migration, where date-fns stops being a peer -of that package altogether. +No other API or behavior change in any package. The +`peerDependencyRules.allowedVersions` entry in `pnpm-workspace.yaml` silences +the react-day-picker peer warning for development in this repository only — +that file is not published, so it does not change what consumers install. diff --git a/.changeset/final-form-v5-upgrade.md b/.changeset/final-form-v5-upgrade.md index 86aff2c96f..ad2c793d86 100644 --- a/.changeset/final-form-v5-upgrade.md +++ b/.changeset/final-form-v5-upgrade.md @@ -1,8 +1,12 @@ --- -'@toptal/picasso-forms': patch +'@toptal/picasso-forms': major --- ### Form -- upgrade the final-form ecosystem to the versions that support React 19: `final-form@^5.0.1`, `final-form-arrays@^4.0.1`, `react-final-form@^7.0.1`, `react-final-form-arrays@^5.0.0` and `react-final-form-listeners@^3.0.1`. These majors are upstream's Flow-to-TypeScript rewrite and carry no intentional API changes; the re-exported `FieldArray`, `useFieldArray`, `OnChange`, `OnFocus`, `OnBlur` and `ExternallyChanged` surfaces are unchanged. `react-final-form-listeners@3.0.1` ships a broken `types` path, corrected by a pnpm patch +- upgrade the final-form ecosystem to the versions that support React 19: `final-form@^5.0.1`, `final-form-arrays@^4.0.1`, `react-final-form@^7.0.1`, `react-final-form-arrays@^5.0.0` and `react-final-form-listeners@^3.0.1`. The majors are upstream's Flow-to-TypeScript rewrite; the re-exported `FieldArray`, `useFieldArray`, `OnChange`, `OnFocus`, `OnBlur` and `ExternallyChanged` surfaces are unchanged +- **consumer action**: `react-final-form` is a regular dependency of this package and its React context is instance-bound, so apps that also depend on `react-final-form` directly must move to `react-final-form@^7` / `final-form@^5` in the same upgrade. Staying on `^6` yields two library instances, and `useForm`/`useField` used inside a Picasso `
` then throws `"... must be used inside of a component"` +- `react-final-form@7` derives checkbox/radio `checked` from `parse` instead of `format` — consumers passing `format`/`parse` to `Form.Checkbox` or `Form.Radio` should re-verify checked-state behavior. Hidden checkbox inputs now render `value=""`, so consumer snapshots covering `Form.Checkbox` need a one-time regeneration +- fields register in an effect under `react-final-form@7` and render twice on mount (previously once) — visually inert, but render-count assertions in consumer tests will shift +- `react-final-form-listeners@3.0.1` ships a broken `types` path, corrected by a pnpm patch - drop the `@types/react-final-form-listeners` dev dependency — the package now ships its own types diff --git a/.changeset/rte-emoji-picker-keyup-listener-leak.md b/.changeset/rte-emoji-picker-keyup-listener-leak.md new file mode 100644 index 0000000000..1831ed7a4f --- /dev/null +++ b/.changeset/rte-emoji-picker-keyup-listener-leak.md @@ -0,0 +1,7 @@ +--- +'@toptal/picasso-rich-text-editor': patch +--- + +### RichTextEditor + +- fix accumulating `keyup` listeners on `document.body` — the emoji picker's escape-key cleanup passed a new function reference to `removeEventListener`, so every time the picker opened another permanent listener was left behind diff --git a/.changeset/transitions-tailwind-migration.md b/.changeset/transitions-tailwind-migration.md index abe5d1c260..153f5ba23f 100644 --- a/.changeset/transitions-tailwind-migration.md +++ b/.changeset/transitions-tailwind-migration.md @@ -2,31 +2,34 @@ '@toptal/picasso-utils': minor '@toptal/picasso-fade': minor '@toptal/picasso-slide': minor -'@toptal/picasso-backdrop': patch +'@toptal/picasso-backdrop': minor '@toptal/picasso-tailwind-merge': patch --- ### Utils -- add `useTransitionStatus` hook and `getTransitionTimeouts` helper — a timer-driven replacement for react-transition-group's `Transition` state machine with identical callback timing +- add `useTransitionStatus` hook and `getTransitionTimeouts` helper — a timer-driven replacement for react-transition-group's `Transition` state machine, matching its timer-based settle timing - add `getElementRef` helper, typing the ref lookup on a React element in one place ### Fade -- reimplement with Tailwind classes and drop the react-transition-group dependency; public props and callback timing are unchanged +- reimplement with Tailwind classes and drop the react-transition-group dependency; public props are unchanged +- `onEnter`/`onExited` now receive the transitioning DOM node, as their types always declared. Under react-transition-group's `nodeRef` mode the old runtime actually called `onEnter(isAppearing, undefined)` and `onExited()` with no arguments — consumers reading the first `onEnter` argument as the `isAppearing` boolean must switch to the second argument +- the shown state no longer forces inline `opacity: 1`, so a child's own `opacity-*` class now applies while visible (previously it was overridden while shown) - express the hidden state via the `invisible` and `opacity-0` classes instead of inline `visibility`/`opacity` styles, and merge the child's `className` via `twMerge` - the object form of `timeout` now sets the CSS transition duration correctly (previously it produced an invalid inline value) ### Slide -- reimplement with Tailwind classes and drop the react-transition-group dependency; public props, the direction mapping and callback timing are unchanged +- reimplement with Tailwind classes and drop the react-transition-group dependency; public props and the direction mapping are unchanged +- `onEnter`/`onExited` now receive the transitioning DOM node, exactly as described for Fade above - express the hidden state via `translate-*` and `invisible` classes instead of inline `transform`/`visibility` styles, so a child's own `transform` is preserved while sliding and `transitionend` listeners observe `propertyName: 'translate'` - the object form of `timeout` now sets the CSS transition duration correctly (previously it produced an invalid inline value) ### Backdrop - compose the Tailwind-based Fade so react-transition-group is no longer in the dependency tree -- remove the redundant `bg-black` class and a dead `-webkit-tap-highlight-color-transparent` class (no visual change) and merge the consumer `className` via `twMerge` so consumer utilities win on conflicts +- merge the consumer `className` via `twMerge` so consumer utilities now win on conflicts, and remove the redundant `bg-black` class and a dead `-webkit-tap-highlight-color-transparent` class (no visual change) ### TailwindMerge diff --git a/.claude/launch.json b/.claude/launch.json index 1f082a7924..d31282c8e7 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -6,6 +6,12 @@ "runtimeExecutable": "pnpm", "runtimeArgs": ["start:storybook"], "port": 9001 + }, + { + "name": "storybook-react19", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["start:storybook:react19"], + "port": 9001 } ] } diff --git a/.storybook/main.js b/.storybook/main.js index c4a6c9c18a..24028cbc61 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -12,6 +12,27 @@ const PACKAGES_COMPONENT_DECLARATION_FILE_REGEXP = const { env } = process const isDevelopment = env.NODE_ENV !== 'production' && env.NODE_ENV !== 'test' +// [PF-2262] React 19 validation mode: STORYBOOK_REACT_19=1 swaps the +// preview's React for the standalone React 19 install in react19/ (own +// lockfile, installed with `pnpm -C react19 install --ignore-workspace`; +// jest.react19.mjs plays the same trick for unit tests — see its header for +// why the harness must NOT be a workspace member). Normal runs are +// byte-identical — every entry below is gated on the env var. +const useReact19 = env.STORYBOOK_REACT_19 === '1' +// Storybook's builder injects its own plain-key aliases +// (`react`/`react-dom` → the workspace React 18 dirs, +// builder-webpack5 iframe-webpack.config.js:202) — plain keys also shadow +// `$`-exact ones, so we must overwrite the SAME keys. A plain directory +// alias covers subpaths too (react/jsx-runtime, react-dom/client), which is +// why react-dom points at a shim *package* that re-exports react-dom 19 with +// a doctored version (storybook 6.5 sniffs `version.startsWith('18')`). +const react19Aliases = useReact19 + ? { + react: path.resolve(__dirname, '../react19/node_modules/react'), + 'react-dom': path.resolve(__dirname, 'react-dom-19-shim'), + } + : {} + const tsConfigFile = path.join(__dirname, './tsconfig.json') const threadLoaders = [{ loader: 'cache-loader' }, { loader: 'thread-loader' }] @@ -158,6 +179,16 @@ module.exports = { savePropValueAsString: true, }), ], + // webpack's persistent cache does not key on our alias swap — without + // a distinct cache version, switching STORYBOOK_REACT_19 on/off would + // serve modules resolved against the other mode's React + cache: + config.cache && typeof config.cache === 'object' + ? { + ...config.cache, + version: useReact19 ? 'react19-standalone' : 'react18', + } + : config.cache, node: { ...config.node, /* @@ -181,6 +212,7 @@ module.exports = { alias: { ...config.resolve.alias, '~': path.resolve(__dirname, '..'), + ...react19Aliases, }, }, } @@ -218,7 +250,8 @@ module.exports = { } }, reactOptions: { - fastRefresh: true, + // react-refresh 0.11 (storybook 6.5's line) predates React 19 + fastRefresh: !useReact19, strictMode: true, }, features: { diff --git a/.storybook/preview.js b/.storybook/preview.js index b365e94f8b..2391b522f2 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -6,6 +6,11 @@ import './styles.css' import Picasso from '@toptal/picasso-provider' +if (process.env.STORYBOOK_REACT_19 === '1') { + // eslint-disable-next-line no-console + console.log('[react19] preview running on React', React.version) +} + const baseViewportCheckpoints = getCheckpoints() const BASE_VIEWPORTS = { 'extra-small': { diff --git a/.storybook/react-dom-19-shim/client.js b/.storybook/react-dom-19-shim/client.js new file mode 100644 index 0000000000..c2f854235a --- /dev/null +++ b/.storybook/react-dom-19-shim/client.js @@ -0,0 +1 @@ +module.exports = require('../../react19/node_modules/react-dom/client.js') diff --git a/.storybook/react-dom-19-shim/index.js b/.storybook/react-dom-19-shim/index.js new file mode 100644 index 0000000000..1f2ec0786f --- /dev/null +++ b/.storybook/react-dom-19-shim/index.js @@ -0,0 +1,24 @@ +// [PF-2262] React 19 storybook mode: re-export react-dom 19 (from the +// standalone react19/ install) with an '18.'-prefixed version string. +// @storybook/react@6.5 picks its renderer via `version.startsWith('18')` +// (dist/esm/client/preview/render.js:111); the legacy branch calls +// ReactDOM.render/unmountComponentAtNode, both removed in React 19, so the +// sniff must land on the modern createRoot path. +const reactDom = require('../../react19/node_modules/react-dom/index.js') + +module.exports = { + ...reactDom, + version: `18.999.0-react19-shim-${reactDom.version}`, + // storybook 6.5's unmountElement calls the legacy API unconditionally for + // elements it never attached a root to (render.js:118-123). No tracked + // root means nothing to unmount — legacy semantics return false for that. + unmountComponentAtNode: reactDom.unmountComponentAtNode || (() => false), + // the modern render path never calls this; fail loudly if something does + render: + reactDom.render || + (() => { + throw new Error( + '[react19 shim] legacy ReactDOM.render was called — find the caller, it cannot work on React 19' + ) + }), +} diff --git a/.storybook/react-dom-19-shim/package.json b/.storybook/react-dom-19-shim/package.json new file mode 100644 index 0000000000..b156da031c --- /dev/null +++ b/.storybook/react-dom-19-shim/package.json @@ -0,0 +1,7 @@ +{ + "name": "react-dom-19-shim", + "private": true, + "version": "0.0.0", + "main": "index.js", + "description": "STORYBOOK_REACT_19=1 alias target: react-dom 19 with an 18-prefixed version string so @storybook/react@6.5's version sniff takes its createRoot path (see .storybook/main.js)" +} diff --git a/.storybook/react-dom-19-shim/server.js b/.storybook/react-dom-19-shim/server.js new file mode 100644 index 0000000000..1f2e112c91 --- /dev/null +++ b/.storybook/react-dom-19-shim/server.js @@ -0,0 +1 @@ +module.exports = require('../../react19/node_modules/react-dom/server.browser.js') diff --git a/.storybook/react-dom-19-shim/test-utils.js b/.storybook/react-dom-19-shim/test-utils.js new file mode 100644 index 0000000000..e2a9bcdca0 --- /dev/null +++ b/.storybook/react-dom-19-shim/test-utils.js @@ -0,0 +1,4 @@ +// Bundled for the browser preview only (RTL 14 in picasso-test-utils requires +// react-dom/test-utils). Do not require this from Node CLI — react-dom 19's +// test-utils entry hangs outside a browser/jest environment. +module.exports = require('../../react19/node_modules/react-dom/test-utils.js') diff --git a/README.md b/README.md index 0d6c317365..09efe9f46b 100644 --- a/README.md +++ b/README.md @@ -209,5 +209,3 @@ In other cases when you use custom webpack build configuration you should check 3. You have `sideEffects` prop in your `package.json` set to `false` value or don't have it at all Have a happy tree shaking! :) - --- diff --git a/jest.react19.mjs b/jest.react19.mjs new file mode 100644 index 0000000000..5804753290 --- /dev/null +++ b/jest.react19.mjs @@ -0,0 +1,40 @@ +import baseConfig from './jest.spec.mjs' + +// React 19 validation harness ([PF-2262] workstream E, PR #5096). +// +// Runs the unit-test suite against React 19 WITHOUT touching the workspace's +// React 18 install. The React 19 packages live in `react19/` as a STANDALONE +// pnpm project (own lockfile, installed with --ignore-workspace by the +// test:react19 script) — deliberately NOT a workspace member: inside the +// workspace, its React 19 copies join hoisting and peer resolution, and on a +// fresh CI install react-dom@19 can win the root `node_modules/react-dom` +// slot, breaking react-transition-group (`findDOMNode`) for the React 18 +// suite. A standalone install cannot interact with the main graph at all, +// and `/react19/node_modules/...` is a deterministic location on +// every machine (no hoisting involved). +// +// The mappings rewrite every react / react-dom / @testing-library import to +// that install for this run only (@testing-library/react@16 is the first +// line whose peers admit React 19; it needs @testing-library/dom@10). +// +// Usage: pnpm test:react19 [jest args], e.g. +// pnpm test:react19 --testPathPattern react19/sanity +const config = { + ...baseConfig, + roots: [...baseConfig.roots, '/react19'], + moduleNameMapper: { + '^react$': '/react19/node_modules/react', + '^react/(.*)$': '/react19/node_modules/react/$1', + '^react-dom$': '/react19/node_modules/react-dom', + '^react-dom/(.*)$': '/react19/node_modules/react-dom/$1', + '^@testing-library/react$': + '/react19/node_modules/@testing-library/react', + '^@testing-library/react/(.*)$': + '/react19/node_modules/@testing-library/react/$1', + '^@testing-library/dom$': + '/react19/node_modules/@testing-library/dom', + ...baseConfig.moduleNameMapper, + }, +} + +export default config diff --git a/package.json b/package.json index 9885a0f714..5fb5ddabdf 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "release": "pnpm build:package && ./scripts/npm-publish-retry.sh", "start": "pnpm -s build:package && concurrently --names build,storybook --raw 'pnpm build:package:watch' 'pnpm start:storybook'", "start:storybook": "start-storybook -p 9001 -c .storybook", + "start:storybook:react19": "pnpm -C react19 install --ignore-workspace && cross-env STORYBOOK_REACT_19=1 start-storybook -p 9001 -c .storybook", "symlink": "cross-env ./bin/symlink --link", "symlink:off": "cross-env ./bin/symlink --unlink", "syncpack": "pnpm syncpack:list & pnpm syncpack:fix", @@ -52,6 +53,7 @@ "test:integration": "pnpm build:package && pnpm test:setup cypress run --component", "test:integration:ci": "pnpm build:package && happo-e2e -- pnpm test:setup cypress run --component", "test:integration:open": "pnpm build:package && concurrently --names build,test --raw 'pnpm build:package:watch' 'pnpm test:setup cypress open --browser=electron --component'", + "test:react19": "pnpm -C react19 install --ignore-workspace && NODE_OPTIONS='--no-experimental-require-module' pnpm build:package && davinci-qa unit --config=./jest.react19.mjs", "test:setup": "cross-env TZ=UTC NODE_ENV=development", "test:unit": "NODE_OPTIONS='--no-experimental-require-module' pnpm build:package && davinci-qa unit --config=./jest.spec.mjs", "test:unit:ci": "pnpm test:unit --maxWorkers=4 --ci", diff --git a/packages/base/Fade/src/Fade/Fade.tsx b/packages/base/Fade/src/Fade/Fade.tsx index ebfa1e45c9..532536ffcc 100644 --- a/packages/base/Fade/src/Fade/Fade.tsx +++ b/packages/base/Fade/src/Fade/Fade.tsx @@ -50,8 +50,7 @@ export const Fade = React.forwardRef(function Fade( className: twMerge( 'transition-opacity', children.props.className, - // State classes stay after the child's className: behavior, not - // overridable defaults + // behavior classes, not defaults — must stay after the child's className cx({ 'opacity-0': !inProp, invisible: status === 'exited' && !inProp, diff --git a/packages/base/Slide/src/Slide/styles.ts b/packages/base/Slide/src/Slide/styles.ts index c72a4b3d22..9406c3b481 100644 --- a/packages/base/Slide/src/Slide/styles.ts +++ b/packages/base/Slide/src/Slide/styles.ts @@ -1,7 +1,5 @@ export type SlideDirection = 'up' | 'down' | 'left' | 'right' -// Historical mapping: `direction` is where the element travels TO on -// enter, so it rests hidden on the opposite side const hiddenClassByDirection: Record = { right: '-translate-x-full', left: 'translate-x-full', diff --git a/packages/base/Utils/src/utils/__tests__/use-transition-status.test.tsx b/packages/base/Utils/src/utils/__tests__/use-transition-status.test.tsx index 5c6b8e91c6..6d31d94413 100644 --- a/packages/base/Utils/src/utils/__tests__/use-transition-status.test.tsx +++ b/packages/base/Utils/src/utils/__tests__/use-transition-status.test.tsx @@ -1,4 +1,6 @@ +import React, { StrictMode } from 'react' import { renderHook, act } from '@testing-library/react-hooks' +import { render, act as actDom } from '@testing-library/react' import type { UseTransitionStatusOptions } from '../use-transition-status' import useTransitionStatus, { @@ -301,3 +303,52 @@ describe('useTransitionStatus', () => { expect(result.current).toBe('unmounted') }) }) + +describe('useTransitionStatus under StrictMode', () => { + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.useRealTimers() + }) + + // StrictMode replays the mount effect (setup -> cleanup -> setup); the + // cleanup cancels the pending settle timer, so the replay must reschedule + // it or an appear transition never leaves `entering` + it('settles an appear transition despite the double-invoked effect', () => { + const onEnter = jest.fn() + const onEntered = jest.fn() + const nodeRef = createNodeRef() + + const Probe = () => { + const status = useTransitionStatus({ + in: true, + appear: true, + timeout: 300, + nodeRef, + onEnter, + onEntered, + }) + + return
{status}
+ } + + const { getByTestId } = render( + + + + ) + + expect(getByTestId('status').textContent).toBe('entering') + expect(onEnter).toHaveBeenCalledTimes(1) + + actDom(() => { + jest.advanceTimersByTime(300) + }) + + expect(getByTestId('status').textContent).toBe('entered') + expect(onEntered).toHaveBeenCalledTimes(1) + expect(onEntered).toHaveBeenCalledWith(nodeRef.current, true) + }) +}) diff --git a/packages/base/Utils/src/utils/use-transition-status.ts b/packages/base/Utils/src/utils/use-transition-status.ts index e2454583ec..b544de65a8 100644 --- a/packages/base/Utils/src/utils/use-transition-status.ts +++ b/packages/base/Utils/src/utils/use-transition-status.ts @@ -89,6 +89,9 @@ const useTransitionStatus = ( }) const prevInRef = useRef(null) + const pendingSettleRef = useRef< + { phase: 'enter'; isAppearing: boolean } | { phase: 'exit' } | null + >(null) useIsomorphicLayoutEffect(() => { const prevIn = prevInRef.current @@ -96,11 +99,53 @@ const useTransitionStatus = ( prevInRef.current = inProp - // Effect replay without an `in` flip (StrictMode double invocation): - // skip rather than double-fire the start callbacks, matching - // react-transition-group's cancelled-settle behavior + const timeouts = getTransitionTimeouts(optionsRef.current.timeout) + + const scheduleEnterSettle = (isAppearing: boolean) => + setTimeout( + () => { + pendingSettleRef.current = null + setStatus('entered') + + const settledNode = nodeRef.current + + if (settledNode) { + optionsRef.current.onEntered?.(settledNode, isAppearing) + } + }, + isAppearing ? timeouts.appear : timeouts.enter + ) + + const scheduleExitSettle = () => + setTimeout(() => { + pendingSettleRef.current = null + setStatus('exited') + + const settledNode = nodeRef.current + + if (settledNode) { + optionsRef.current.onExited?.(settledNode) + } + }, timeouts.exit) + + // Effect replay without an `in` flip (StrictMode double invocation): never + // re-fire the start callbacks, but reschedule a settle the replay's + // cleanup cancelled so the transition still completes. (For comparison, + // react-transition-group re-runs performEnter here, double-firing onEnter + // in dev; we settle without the double-fire.) if (prevIn === inProp) { - return + const pending = pendingSettleRef.current + + if (!pending) { + return + } + + const timer = + pending.phase === 'enter' + ? scheduleEnterSettle(pending.isAppearing) + : scheduleExitSettle() + + return () => clearTimeout(timer) } // Mounting hidden never exits; mounting shown enters only with `appear` @@ -108,7 +153,6 @@ const useTransitionStatus = ( return } - const timeouts = getTransitionTimeouts(optionsRef.current.timeout) const node = nodeRef.current const performEnter = () => { @@ -124,18 +168,9 @@ const useTransitionStatus = ( optionsRef.current.onEntering?.(node, isAppearing) } - return setTimeout( - () => { - setStatus('entered') - - const settledNode = nodeRef.current + pendingSettleRef.current = { phase: 'enter', isAppearing } - if (settledNode) { - optionsRef.current.onEntered?.(settledNode, isAppearing) - } - }, - isAppearing ? timeouts.appear : timeouts.enter - ) + return scheduleEnterSettle(isAppearing) } const performExit = () => { @@ -149,15 +184,9 @@ const useTransitionStatus = ( optionsRef.current.onExiting?.(node) } - return setTimeout(() => { - setStatus('exited') + pendingSettleRef.current = { phase: 'exit' } - const settledNode = nodeRef.current - - if (settledNode) { - optionsRef.current.onExited?.(settledNode) - } - }, timeouts.exit) + return scheduleExitSettle() } const timer = inProp ? performEnter() : performExit() diff --git a/packages/picasso-forms/src/Form/mutators/set-active-field-touched.ts b/packages/picasso-forms/src/Form/mutators/set-active-field-touched.ts index 9cb1e177f4..d3c4d7a3b3 100644 --- a/packages/picasso-forms/src/Form/mutators/set-active-field-touched.ts +++ b/packages/picasso-forms/src/Form/mutators/set-active-field-touched.ts @@ -4,7 +4,7 @@ export const setActiveFieldTouched = < FormValues = object, InitialFormValues extends Partial = Partial >( - _: any[], + _: unknown[], state: MutableState ) => { const activeFieldName = state.formState.active diff --git a/packages/picasso-forms/src/Form/mutators/set-has-multiline-counter.ts b/packages/picasso-forms/src/Form/mutators/set-has-multiline-counter.ts index 8fb29a03b2..5ccd53745a 100644 --- a/packages/picasso-forms/src/Form/mutators/set-has-multiline-counter.ts +++ b/packages/picasso-forms/src/Form/mutators/set-has-multiline-counter.ts @@ -1,12 +1,13 @@ import type { MutableState } from 'final-form' -// `final-form` types a Mutator's `args` as `any[]`, so the parameter cannot be -// the tuple this mutator expects. The cast documents the expected shape. +// `final-form` types a Mutator's `args` as `any[]`; `unknown[]` is the widest +// parameter type that still satisfies it, and the cast documents the tuple +// shape this mutator expects. export const setHasMultilineCounter = < FormValues = object, InitialFormValues extends Partial = Partial >( - args: any[], + args: unknown[], state: MutableState ) => { const [name, hasCounter] = args as [name: string, hasCounter: boolean] diff --git a/packages/picasso-forms/src/NumberInput/test.tsx b/packages/picasso-forms/src/NumberInput/test.tsx index c687a799af..829d41f3f9 100644 --- a/packages/picasso-forms/src/NumberInput/test.tsx +++ b/packages/picasso-forms/src/NumberInput/test.tsx @@ -36,6 +36,9 @@ describe('Form.NumberInput', () => { name: 'test-input', }) ) + // react-final-form v7 registers the field in an effect and re-renders + // once after mount (v6 registered during render and rendered once) + expect(numberInputMock).toHaveBeenCalledTimes(2) }) }) }) diff --git a/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/RichTextEditorEmojiPicker.tsx b/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/RichTextEditorEmojiPicker.tsx index 6938c0cdc2..ffce788c9a 100644 --- a/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/RichTextEditorEmojiPicker.tsx +++ b/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/RichTextEditorEmojiPicker.tsx @@ -56,14 +56,13 @@ export const RichTextEditorEmojiPicker = ({ return } - document.body.addEventListener('keyup', event => { + const handleKeyUp = (event: KeyboardEvent) => handleEmojiPickerEscBehaviour(event, setShowEmojiPicker) - }) + + document.body.addEventListener('keyup', handleKeyUp) return () => { - document.body.removeEventListener('keyup', event => { - handleEmojiPickerEscBehaviour(event, setShowEmojiPicker) - }) + document.body.removeEventListener('keyup', handleKeyUp) } }, [showEmojiPicker, setShowEmojiPicker]) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e2ede2b728..475069d209 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -66,8 +66,8 @@ overrides: # be satisfied, and Base UI wins: it is the primitive layer the kit is moving # to. react-day-picker v8 is verified to work on v4 — it imports date-fns only # through named barrel imports, and all 31 functions it uses are unchanged in -# v4. This entry goes away with the react-day-picker v9+ migration: from 9.0.0 -# on, date-fns is a regular dependency of the package and not a peer at all. +# v4. This entry goes away with the react-day-picker v9+ migration [PF-2297]: +# from 9.0.0 on, date-fns is a regular dependency of the package and not a peer at all. peerDependencyRules: allowedVersions: 'react-day-picker>date-fns': '4' diff --git a/react19/package.json b/react19/package.json new file mode 100644 index 0000000000..d3bb0b9cd7 --- /dev/null +++ b/react19/package.json @@ -0,0 +1,12 @@ +{ + "name": "picasso-react19-harness", + "version": "0.0.0", + "private": true, + "description": "Standalone React 19 install for pnpm test:react19 and STORYBOOK_REACT_19=1 (PF-2262 workstream E). Deliberately NOT a pnpm workspace member: inside the workspace its React 19 packages join hoisting/peer resolution and can win the root react-dom slot on a fresh install, breaking react-transition-group (findDOMNode) for the React 18 suite. Installed via `pnpm install --ignore-workspace` in this directory (the test:react19 script does it automatically); jest.react19.mjs and .storybook/main.js resolve react/react-dom from react19/node_modules explicitly.", + "dependencies": { + "@testing-library/dom": "^10.4.0", + "@testing-library/react": "^16.3.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" + } +} diff --git a/react19/pnpm-lock.yaml b/react19/pnpm-lock.yaml new file mode 100644 index 0000000000..7da10fee25 --- /dev/null +++ b/react19/pnpm-lock.yaml @@ -0,0 +1,172 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@testing-library/dom': + specifier: ^10.4.0 + version: 10.4.1 + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: + specifier: ^19.2.0 + version: 19.2.8 + react-dom: + specifier: ^19.2.0 + version: 19.2.8(react@19.2.8) + +packages: + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/react@16.3.3': + resolution: {integrity: sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + react-dom@19.2.8: + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} + peerDependencies: + react: ^19.2.8 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} + engines: {node: '>=0.10.0'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + +snapshots: + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/runtime@7.29.7': {} + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/react@16.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@types/aria-query@5.0.4': {} + + ansi-regex@5.0.1: {} + + ansi-styles@5.2.0: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + dequal@2.0.3: {} + + dom-accessibility-api@0.5.16: {} + + js-tokens@4.0.0: {} + + lz-string@1.5.0: {} + + picocolors@1.1.1: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + react-dom@19.2.8(react@19.2.8): + dependencies: + react: 19.2.8 + scheduler: 0.27.0 + + react-is@17.0.2: {} + + react@19.2.8: {} + + scheduler@0.27.0: {} diff --git a/react19/sanity.test.tsx b/react19/sanity.test.tsx new file mode 100644 index 0000000000..9288e5246c --- /dev/null +++ b/react19/sanity.test.tsx @@ -0,0 +1,40 @@ +import React, { StrictMode, useEffect, useState } from 'react' +import { version as reactDomVersion } from 'react-dom' +import { render } from '@testing-library/react' + +// Sanity checks for the React 19 harness (jest.react19.mjs): prove the alias +// mappings actually resolve React 19 and that rendering works under +// StrictMode. This directory is only in the react19 config's roots, so +// `pnpm test:unit` never runs it. +describe('react 19 test runtime', () => { + it('resolves react and react-dom 19 through the aliases', () => { + expect(React.version).toMatch(/^19\./) + expect(reactDomVersion).toMatch(/^19\./) + expect(reactDomVersion).toBe(React.version) + }) + + it('renders and re-renders under StrictMode', () => { + const consoleError = jest.spyOn(console, 'error').mockImplementation() + + const Probe = () => { + const [ready, setReady] = useState(false) + + useEffect(() => { + setReady(true) + }, []) + + return
{ready ? 'ready' : 'mounting'}
+ } + + const { getByTestId } = render( + + + + ) + + expect(getByTestId('probe').textContent).toBe('ready') + expect(consoleError).not.toHaveBeenCalled() + + consoleError.mockRestore() + }) +}) From 2d973189e51bd19929cc59fc339c4d9231bdf452 Mon Sep 17 00:00:00 2001 From: Ante Zebic <17073473+azebich@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:42:08 +0200 Subject: [PATCH 09/14] [PF-2249] Upgrade twMerge to latest 3.x version (#5075) --- .changeset/tailwind-merge-v3.md | 75 +++++++++++++++++++ .../src/AvatarUpload/AvatarUpload.tsx | 9 ++- .../AvatarUpload/src/AvatarUpload/styles.ts | 4 +- .../AvatarUpload/src/AvatarUpload/test.tsx | 27 +++++++ .../CalendarIndicators/CalendarIndicators.tsx | 4 +- .../src/CarouselGradient/CarouselGradient.tsx | 4 +- .../CarouselNavigation/CarouselNavigation.tsx | 27 ++++--- .../base/Collapse/src/Collapse/Collapse.tsx | 4 +- packages/base/Collapse/src/Collapse/test.tsx | 11 +++ .../src/Notification/Notification.tsx | 4 +- .../Notification/__snapshots__/test.tsx.snap | 2 +- .../__snapshots__/test.tsx.snap | 2 +- .../NumberInput/__snapshots__/test.tsx.snap | 4 +- .../NumberInputEndAdornment.tsx | 41 ++++------ .../base/Page/src/PageTopBar/PageTopBar.tsx | 10 ++- .../src/NativeSelect/NativeSelectInput.tsx | 4 +- .../src/NonNativeSelect/NonNativeSelect.tsx | 4 +- .../Select/src/SelectCaret/SelectCaret.tsx | 11 ++- .../src/SelectOptions/SelectOptions.tsx | 4 +- packages/base/Slider/src/Slider/Slider.tsx | 2 +- .../TableExpandableRow/TableExpandableRow.tsx | 4 +- .../Timeline/src/TimelineRow/TimelineRow.tsx | 23 +++--- packages/picasso-tailwind-merge/package.json | 2 +- .../src/__snapshots__/test.ts.snap | 27 +++++++ packages/picasso-tailwind-merge/src/test.ts | 49 +++++++++++- .../picasso-tailwind-merge/src/twMerge.ts | 7 ++ pnpm-lock.yaml | 12 ++- 27 files changed, 280 insertions(+), 97 deletions(-) create mode 100644 .changeset/tailwind-merge-v3.md diff --git a/.changeset/tailwind-merge-v3.md b/.changeset/tailwind-merge-v3.md new file mode 100644 index 0000000000..abc709e93f --- /dev/null +++ b/.changeset/tailwind-merge-v3.md @@ -0,0 +1,75 @@ +--- +'@toptal/picasso-tailwind-merge': major +'@toptal/picasso-avatar-upload': patch +'@toptal/picasso-calendar': patch +'@toptal/picasso-carousel': patch +'@toptal/picasso-collapse': minor +'@toptal/picasso-notification': patch +'@toptal/picasso-number-input': patch +'@toptal/picasso-page': patch +'@toptal/picasso-select': minor +'@toptal/picasso-slider': patch +'@toptal/picasso-table': minor +'@toptal/picasso-timeline': minor +--- + +### Tailwind Merge + +- upgrade `tailwind-merge` to `^3.6.0`. The `3.x` line is the one that understands the Tailwind CSS v4 class vocabulary; `2.x` only knows v3, so the package has been a major version behind the `tailwindcss@^4.2.1` peer the theme packages already require. The Picasso `CONFIG`'s `font-size`, `font-weight` and `text-alignment` groups are untouched — all three still exist as class group ids in v3 +- register the Picasso `shadow-0`…`shadow-24` scale in the `shadow` class group. `tailwind-merge` does not know the numeric scale and classified these utilities as shadow _colors_: numeric-vs-numeric pairs still conflicted (colors conflict with each other), but `shadow-none`, t-shirt sizes and arbitrary `shadow-[…]` values did not conflict with them — and a genuine shadow color (`shadow-blue-500`) coming later would drop a numeric box shadow entirely. All box shadow utilities now resolve in one group, and shadow colors coexist with the scale instead of erasing it +- `twMerge` now resolves conflicts for utilities that only exist in Tailwind v4 and were previously passed through unrecognised, so a consumer `className` finally wins over the component's own class. Affected utilities include `min-h-auto` / `max-h-auto`, `outline-hidden` (v4's rename of `outline-none`, which now correctly overrides it), the two-axis `translate-*` shorthand, `border-2` against `border-x`/`border-y`, `shrink-`, and the trailing-`!` important syntax (`no-underline!`) +- `bg-linear-to-*` is no longer misread as a background _color_. Under `2.x` a gradient direction and a `bg-` shared one conflict group, so whichever came last silently dropped the other — this affected `Modal.Content`'s scroll shades, where a `bg-*` class could remove the gradient +- conversely, Tailwind v3's removed `bg-gradient-to-*` spelling is no longer recognised as a gradient direction and now conflicts with `bg-`. It still compiles under `tailwindcss@4.2.1` as a deprecated alias, but use `bg-linear-to-*` instead +- no API change: `twMerge` and `twJoin` keep their signatures, and `tailwind-merge` v3 still ships CJS, ESM and types +- ship as a major: the package README mandates one on any `CONFIG` change, and the merge output changes either way. `@toptal/picasso-tailwind-merge` is a `workspace:^` peer of every component package and releases honor `onlyUpdatePeerDependentsWhenOutOfRange`, so only an out-of-range bump moves the dependents' peer floors — a minor would let consumers pair the new component code with the old `2.x` merge semantics + +### Carousel + +- rename `bg-gradient-to-r` to `bg-linear-to-r` in the edge gradients, matching the Tailwind v4 spelling. Both compile to the same CSS, so there is no visual change +- build `CarouselNavigation`'s dot styles with `twJoin` instead of a multi-line template literal, so the rendered `class` attribute no longer carries the literal's newlines and indentation. Same classes, same order + +### AvatarUpload + +- fix the cursor never changing away from `cursor-pointer`. The root class list was built with `twJoin`, which does not resolve conflicts, so the unconditional `cursor-pointer` and the conditional `cursor-no-drop` / `cursor-default` all reached the DOM and Tailwind's emission order decided the winner — always `cursor-pointer`. A disabled `AvatarUpload` now shows `cursor-no-drop` as intended +- `disabled` now takes precedence over `showAvatar`, so a disabled upload that already has an image reads as disabled (`cursor-no-drop`) rather than inert (`cursor-default`). Both conditions are independent, so the combination is reachable; it was simply unobservable while `cursor-pointer` always won +- the upload icon colour helper in `styles.ts` uses `twMerge` too, so the `error` status colour reliably beats the hover colour instead of depending on stylesheet order + +### Calendar + +- the `CalendarIndicators` resolves its today-dot colour with `twMerge`, so the `isSelected` white overrides the default blue by argument order rather than by stylesheet order. No visual change + +### Notification + +- the icon container resolves its `min-w` with `twMerge`, so the `yellow` variant's narrower width overrides the default by argument order rather than by stylesheet order. No visual change +- replace the root's arbitrary `shadow-[none]` with the `shadow-none` utility. Classified as shadow colors, `shadow-[none]` and the `elevated` variant's `shadow-3` conflicted by accident; with the numeric scale registered as box shadows they would stop conflicting and stylesheet order would decide again. `shadow-none` vs `shadow-3` resolves properly, and a non-elevated notification still renders shadowless + +### NumberInput + +- the `NumberInputEndAdornment` resolves its root class list with `twMerge`, and drops three dead classes: `bg-inherit`, immediately overridden by `bg-transparent`, plus `border-x-solid` and `active:[&+&]:border-t-solid`, which emit no CSS at all because Tailwind has no per-side border-style utility. The border style is already set unconditionally by `[&]:border-solid` and `[&+&]:border-solid`. No visual change + +### Slider + +- the thumb resolves its class list with `twMerge`, so `isThumbHidden`'s `hidden` overrides the base `flex` by argument order rather than by stylesheet order. No visual change + +### Collapse + +- switch the root class list to `twMerge` so a consumer `className` overrides the component's own classes. Previously `overflow-auto` passed by a consumer lost to the internal `overflow-hidden` / `overflow-visible`, because `twJoin` emitted both and the stylesheet order decided + +### Page + +- the `PageTopBar` builds its inner class list with `twMerge` instead of `twJoin`, with the unconditional `max-w` default moved ahead of the `width` overrides so precedence stays default -> `wide` -> `full`. No behavior change + +### Select + +- switch `SelectOptions` to `twMerge` so a consumer `className` overrides the internal `shadow-5` and the `fixedHeader` / `fixedFooter` padding. Previously `pt-*` overrides and shadows below `shadow-5` silently lost. Down the chain, `shadow-5` keeps replacing `Menu`'s default `shadow-1` (`Menu` already merges its `className` last) — now via the real `shadow` class group instead of the shadow-color accident described under Tailwind Merge +- the `SelectCaret` and `NonNativeSelect` resolve their class lists with `twMerge`, so the `disabled` caret colour and the `horizontal` layout width override their defaults by argument order rather than by stylesheet order. `SelectCaret` also drops a commented-out line referencing the removed `classes` API. No visual change +- the `SelectCaret` and `NativeSelectInput` pass their base classes to `twMerge` as one string per line instead of a multi-line template literal. No rendered change — `twMerge` already collapsed the literal's whitespace + +### Table + +- switch `TableExpandableRow` to `twMerge` and put the consumer `className` last, so it overrides the `stripeEven` background on the expanded row. The two rows the component renders now resolve a consumer `className` the same way + +### Timeline + +- switch `TimelineRow`'s icon class list to `twMerge` and put the icon's own `className` last, so a consumer can recolor the icon. Previously `} />` stayed grey because the internal `text-gray-600` won on stylesheet order +- build the default dot's class list with `twJoin` instead of a multi-line string literal, so the rendered `class` attribute no longer carries the literal's newlines and indentation. Same classes, same order diff --git a/packages/base/AvatarUpload/src/AvatarUpload/AvatarUpload.tsx b/packages/base/AvatarUpload/src/AvatarUpload/AvatarUpload.tsx index 50c8718dd3..8ab9ac39c2 100644 --- a/packages/base/AvatarUpload/src/AvatarUpload/AvatarUpload.tsx +++ b/packages/base/AvatarUpload/src/AvatarUpload/AvatarUpload.tsx @@ -5,7 +5,7 @@ import { Avatar, AvatarDropzoneSvg } from '@toptal/picasso-avatar' import { Upload16, Upload24 } from '@toptal/picasso-icons' import { Loader } from '@toptal/picasso-loader' import type { Status } from '@toptal/picasso-outlined-input' -import { twJoin } from '@toptal/picasso-tailwind-merge' +import { twMerge } from '@toptal/picasso-tailwind-merge' import { getIconClass, rootClassBySize } from './styles' import type { AvatarUploadOptions, FileUpload } from './types' @@ -169,11 +169,12 @@ export const AvatarUpload = forwardRef(
, string> = { xxsmall: 'w-8 h-8', @@ -14,7 +14,7 @@ export const getIconClass = ( hovered: boolean, status?: Extract ) => - twJoin( + twMerge( 'absolute pointer-events-none', hovered && 'text-blue-500/[.84]', status === 'error' ? (hovered ? 'text-red-500/[.84]' : 'text-red-500') : '' diff --git a/packages/base/AvatarUpload/src/AvatarUpload/test.tsx b/packages/base/AvatarUpload/src/AvatarUpload/test.tsx index 885c1f52da..9010bfa3a9 100644 --- a/packages/base/AvatarUpload/src/AvatarUpload/test.tsx +++ b/packages/base/AvatarUpload/src/AvatarUpload/test.tsx @@ -125,4 +125,31 @@ describe('AvatarUpload', () => { expect(dropzoneSvg).not.toBeInTheDocument() }) }) + + describe('when disabled', () => { + it('shows the not-allowed cursor instead of the pointer one', () => { + const { getByTestId } = renderAvatarUpload({ + disabled: true, + 'data-testid': 'avatar-upload', + }) + + const avatarUpload = getByTestId('avatar-upload') + + expect(avatarUpload).toHaveClass('cursor-no-drop') + expect(avatarUpload).not.toHaveClass('cursor-pointer') + }) + + it('keeps the not-allowed cursor when an avatar is already shown', () => { + const { getByTestId } = renderAvatarUpload({ + disabled: true, + src: 'some-url', + 'data-testid': 'avatar-upload', + }) + + const avatarUpload = getByTestId('avatar-upload') + + expect(avatarUpload).toHaveClass('cursor-no-drop') + expect(avatarUpload).not.toHaveClass('cursor-default') + }) + }) }) diff --git a/packages/base/Calendar/src/CalendarIndicators/CalendarIndicators.tsx b/packages/base/Calendar/src/CalendarIndicators/CalendarIndicators.tsx index 39aab71c42..7f0e39434a 100644 --- a/packages/base/Calendar/src/CalendarIndicators/CalendarIndicators.tsx +++ b/packages/base/Calendar/src/CalendarIndicators/CalendarIndicators.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { twJoin } from '@toptal/picasso-tailwind-merge' +import { twMerge } from '@toptal/picasso-tailwind-merge' export interface Props { isSelected: boolean @@ -17,7 +17,7 @@ export const CalendarIndicators = ({
{isToday && (
{ className={twMerge( (showNextGradient || showPrevGradient) && 'absolute h-full top-0', showNextGradient && - 'right-0 bg-gradient-to-r from-white/20 to-white to-95%', + 'right-0 bg-linear-to-r from-white/20 to-white to-95%', showPrevGradient && - 'left-0 bg-gradient-to-r from-white from-5% to-white/20' + 'left-0 bg-linear-to-r from-white from-5% to-white/20' )} /> ) diff --git a/packages/base/Carousel/src/CarouselNavigation/CarouselNavigation.tsx b/packages/base/Carousel/src/CarouselNavigation/CarouselNavigation.tsx index e982684548..e13060c751 100644 --- a/packages/base/Carousel/src/CarouselNavigation/CarouselNavigation.tsx +++ b/packages/base/Carousel/src/CarouselNavigation/CarouselNavigation.tsx @@ -2,6 +2,7 @@ import React, { memo } from 'react' import { ButtonCircular } from '@toptal/picasso-button' import { Container } from '@toptal/picasso-container' import { ChevronRight24 } from '@toptal/picasso-icons' +import { twJoin } from '@toptal/picasso-tailwind-merge' const getJustifyContent = (hasArrows: boolean, hasDots: boolean) => { if (hasArrows && hasDots) { @@ -57,20 +58,18 @@ const CarouselNavigation = ({
)} diff --git a/packages/base/Collapse/src/Collapse/Collapse.tsx b/packages/base/Collapse/src/Collapse/Collapse.tsx index e74f3be13b..f5d34c0699 100644 --- a/packages/base/Collapse/src/Collapse/Collapse.tsx +++ b/packages/base/Collapse/src/Collapse/Collapse.tsx @@ -2,7 +2,7 @@ import type { ReactNode } from 'react' import React, { forwardRef, useMemo, useState } from 'react' import { Transition } from 'react-transition-group' import type { BaseProps, TransitionProps } from '@toptal/picasso-shared' -import { twJoin } from '@toptal/picasso-tailwind-merge' +import { twMerge } from '@toptal/picasso-tailwind-merge' export interface Props extends TransitionProps, BaseProps { /* Element that accepts ref */ @@ -102,7 +102,7 @@ export const Collapse = forwardRef( {state => { return (
{ expect(getByTestId('collapse')).toHaveStyle('display: block') }) + it('lets a consumer className override its own overflow', () => { + const { getByTestId } = render( + + + + ) + + expect(getByTestId('collapse')).toHaveClass('overflow-auto') + expect(getByTestId('collapse')).not.toHaveClass('overflow-hidden') + }) + it('forwards the ref', () => { const ref = React.createRef() const { getByTestId } = render( diff --git a/packages/base/Notification/src/Notification/Notification.tsx b/packages/base/Notification/src/Notification/Notification.tsx index b277298427..879f2f6d42 100644 --- a/packages/base/Notification/src/Notification/Notification.tsx +++ b/packages/base/Notification/src/Notification/Notification.tsx @@ -82,7 +82,7 @@ const renderNotificationContent = (props: PrivateProps) => { (
@@ -59,7 +58,6 @@ exports[`Input renders icon in the end 1`] = ` autocomplete="none" class="[&::-webkit-calendar-picker-indicator]:bg bg-transparent border-none box-border cursor-[inherit] h-full outline-hidden p-0 peer resize-none w-full text-md leading-4 text-black [&::placeholder]:text-gray [&::placeholder]:opacity-100" type="text" - value="" />
@@ -169,7 +167,6 @@ exports[`Input shows counter for regular input 1`] = ` autocomplete="none" class="[&::-webkit-calendar-picker-indicator]:bg bg-transparent border-none box-border cursor-[inherit] h-full outline-hidden p-0 peer resize-none w-full text-md leading-4 text-black [&::placeholder]:text-gray [&::placeholder]:opacity-100" type="text" - value="" />
diff --git a/packages/base/InputAdornment/package.json b/packages/base/InputAdornment/package.json index ccbbaf880a..31a05be0b5 100644 --- a/packages/base/InputAdornment/package.json +++ b/packages/base/InputAdornment/package.json @@ -34,7 +34,7 @@ "peerDependencies": { "@toptal/picasso-tailwind-merge": "workspace:^", "@toptal/picasso-tailwind": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js", diff --git a/packages/base/InputAdornment/src/InputIconAdornment/InputIconAdornment.tsx b/packages/base/InputAdornment/src/InputIconAdornment/InputIconAdornment.tsx index 1ec0268c6d..ce80d52a86 100644 --- a/packages/base/InputAdornment/src/InputIconAdornment/InputIconAdornment.tsx +++ b/packages/base/InputAdornment/src/InputIconAdornment/InputIconAdornment.tsx @@ -13,10 +13,13 @@ export interface Props { const InputIconAdornment = (props: Props) => { const { position, disabled, icon } = props - const styledIcon = React.cloneElement(icon as ReactElement, { - className: 'grow shrink basis-0', // fix for IE11 - role: 'presentation', - }) + const styledIcon = React.cloneElement( + icon as ReactElement<{ className?: string; role?: string }>, + { + className: 'grow shrink basis-0', // fix for IE11 + role: 'presentation', + } + ) return ( =17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/List/package.json b/packages/base/List/package.json index c084d5693d..2148a1f3e9 100644 --- a/packages/base/List/package.json +++ b/packages/base/List/package.json @@ -37,7 +37,7 @@ "@toptal/picasso-provider": "workspace:^", "@toptal/picasso-tailwind": "workspace:^", "@toptal/picasso-tailwind-merge": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/Loader/package.json b/packages/base/Loader/package.json index 7e654c0180..b1bf0f08d7 100644 --- a/packages/base/Loader/package.json +++ b/packages/base/Loader/package.json @@ -32,7 +32,7 @@ "@toptal/picasso-tailwind-merge": "workspace:^", "@toptal/picasso-provider": "workspace:^", "@toptal/picasso-tailwind": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/Logo/package.json b/packages/base/Logo/package.json index 41cd391408..9cf94d2327 100644 --- a/packages/base/Logo/package.json +++ b/packages/base/Logo/package.json @@ -31,7 +31,7 @@ ], "peerDependencies": { "@toptal/picasso-tailwind-merge": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/Menu/package.json b/packages/base/Menu/package.json index 6d59a8e1f3..5e8ffd51fb 100644 --- a/packages/base/Menu/package.json +++ b/packages/base/Menu/package.json @@ -39,7 +39,7 @@ "peerDependencies": { "@toptal/picasso-tailwind-merge": "workspace:^", "@toptal/picasso-tailwind": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js", diff --git a/packages/base/Menu/src/Menu/Menu.tsx b/packages/base/Menu/src/Menu/Menu.tsx index 7545713756..4e8a8e0e2e 100644 --- a/packages/base/Menu/src/Menu/Menu.tsx +++ b/packages/base/Menu/src/Menu/Menu.tsx @@ -6,6 +6,7 @@ import { BackMinor16 } from '@toptal/picasso-icons' import { Typography } from '@toptal/picasso-typography' import { MenuItem } from '../MenuItem' +import type { MenuItemProps } from '../MenuItem' import { useMenu } from './hooks' import MenuContext from './MenuContext' import type { MenuVariant } from './types' @@ -33,7 +34,7 @@ export const Menu = forwardRef(function Menu( // Find the first selected or first non-disabled item React.Children.map(children, (child, index) => { - if (!React.isValidElement(child)) { + if (!React.isValidElement(child)) { return } @@ -59,7 +60,7 @@ export const Menu = forwardRef(function Menu( }) const items = React.Children.map(children, (child, index) => { - if (!React.isValidElement(child)) { + if (!React.isValidElement(child)) { return } diff --git a/packages/base/Menu/src/Menu/hooks/use-drilldown-menu/test.tsx b/packages/base/Menu/src/Menu/hooks/use-drilldown-menu/test.tsx index e5cec8c83f..dd94a80573 100644 --- a/packages/base/Menu/src/Menu/hooks/use-drilldown-menu/test.tsx +++ b/packages/base/Menu/src/Menu/hooks/use-drilldown-menu/test.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { act, renderHook } from '@testing-library/react-hooks' +import { act, renderHook } from '@testing-library/react' import useDrilldownMenu from './use-drilldown-menu' diff --git a/packages/base/Menu/src/Menu/hooks/use-menu-variant/test.tsx b/packages/base/Menu/src/Menu/hooks/use-menu-variant/test.tsx index 68df7540a1..1eb5ab62b1 100644 --- a/packages/base/Menu/src/Menu/hooks/use-menu-variant/test.tsx +++ b/packages/base/Menu/src/Menu/hooks/use-menu-variant/test.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' import React from 'react' -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import type { Props } from './use-menu-variant' import useMenuVariant from './use-menu-variant' diff --git a/packages/base/Menu/src/Menu/hooks/use-menu/test.tsx b/packages/base/Menu/src/Menu/hooks/use-menu/test.tsx index 8d759ad207..0d2b039003 100644 --- a/packages/base/Menu/src/Menu/hooks/use-menu/test.tsx +++ b/packages/base/Menu/src/Menu/hooks/use-menu/test.tsx @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import useMenu from './use-menu' import useMenuVariant from '../use-menu-variant' diff --git a/packages/base/Menu/src/Menu/hooks/use-slide-menu/test.tsx b/packages/base/Menu/src/Menu/hooks/use-slide-menu/test.tsx index b12cd8d1de..01cb7ebecb 100644 --- a/packages/base/Menu/src/Menu/hooks/use-slide-menu/test.tsx +++ b/packages/base/Menu/src/Menu/hooks/use-slide-menu/test.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' import React from 'react' -import { act, renderHook } from '@testing-library/react-hooks' +import { act, renderHook } from '@testing-library/react' import useSlideMenu from './use-slide-menu' import type { MenuContextProps } from '../../MenuContext' diff --git a/packages/base/Menu/src/MenuItem/MenuItem.tsx b/packages/base/Menu/src/MenuItem/MenuItem.tsx index 740fe2c0ff..c9988bca74 100644 --- a/packages/base/Menu/src/MenuItem/MenuItem.tsx +++ b/packages/base/Menu/src/MenuItem/MenuItem.tsx @@ -12,6 +12,7 @@ import type { ButtonOrAnchorProps, TextLabelProps, OverridableComponent, + IconElement, } from '@toptal/picasso-shared' import { useTitleCase } from '@toptal/picasso-shared' import { ChevronMinor16, CheckMinor16 } from '@toptal/picasso-icons' @@ -53,7 +54,7 @@ export interface Props extends BaseProps, TextLabelProps, MenuItemAttributes { /** The additional description */ description?: ReactNode /** Render an `` */ - icon?: ReactElement + icon?: IconElement /** Render an */ avatar?: ReactElement /** Callback when item is clicked */ diff --git a/packages/base/Menu/src/MenuItem/hooks/use-drilldown-menu-item/test.tsx b/packages/base/Menu/src/MenuItem/hooks/use-drilldown-menu-item/test.tsx index 70606fc194..2c55c5bc30 100644 --- a/packages/base/Menu/src/MenuItem/hooks/use-drilldown-menu-item/test.tsx +++ b/packages/base/Menu/src/MenuItem/hooks/use-drilldown-menu-item/test.tsx @@ -1,4 +1,4 @@ -import { act, renderHook } from '@testing-library/react-hooks' +import { act, renderHook } from '@testing-library/react' import type { ReactNode } from 'react' import React from 'react' diff --git a/packages/base/Menu/src/MenuItem/hooks/use-menu-item-key/test.tsx b/packages/base/Menu/src/MenuItem/hooks/use-menu-item-key/test.tsx index 8632bd4fad..ae693d0eb8 100644 --- a/packages/base/Menu/src/MenuItem/hooks/use-menu-item-key/test.tsx +++ b/packages/base/Menu/src/MenuItem/hooks/use-menu-item-key/test.tsx @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import useMenuItemKey from './use-menu-item-key' diff --git a/packages/base/Menu/src/MenuItem/hooks/use-menu-item/test.tsx b/packages/base/Menu/src/MenuItem/hooks/use-menu-item/test.tsx index 53ecae30e8..c0a156a5ec 100644 --- a/packages/base/Menu/src/MenuItem/hooks/use-menu-item/test.tsx +++ b/packages/base/Menu/src/MenuItem/hooks/use-menu-item/test.tsx @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import useMenuItem from './use-menu-item' import useMenuItemKey from '../use-menu-item-key' diff --git a/packages/base/Menu/src/MenuItem/hooks/use-slide-menu-item/test.tsx b/packages/base/Menu/src/MenuItem/hooks/use-slide-menu-item/test.tsx index 215173752c..90dc11bd23 100644 --- a/packages/base/Menu/src/MenuItem/hooks/use-slide-menu-item/test.tsx +++ b/packages/base/Menu/src/MenuItem/hooks/use-slide-menu-item/test.tsx @@ -1,4 +1,4 @@ -import { act, renderHook } from '@testing-library/react-hooks' +import { act, renderHook } from '@testing-library/react' import type { ReactNode } from 'react' import React from 'react' diff --git a/packages/base/Modal/package.json b/packages/base/Modal/package.json index 992de52ed9..d6ba8e191e 100644 --- a/packages/base/Modal/package.json +++ b/packages/base/Modal/package.json @@ -38,7 +38,7 @@ ], "peerDependencies": { "@toptal/picasso-provider": "workspace:^", - "react": ">=17.0.0 < 19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@toptal/picasso-tailwind": "workspace:^", "@toptal/picasso-tailwind-merge": "workspace:^" }, diff --git a/packages/base/Modal/src/ModalContent/hooks/test.tsx b/packages/base/Modal/src/ModalContent/hooks/test.tsx index 812790be55..bbc09497f0 100644 --- a/packages/base/Modal/src/ModalContent/hooks/test.tsx +++ b/packages/base/Modal/src/ModalContent/hooks/test.tsx @@ -1,5 +1,5 @@ import type { RefObject } from 'react' -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import useScrollableShades from './use-scrollable-shades' diff --git a/packages/base/ModalContext/package.json b/packages/base/ModalContext/package.json index 4bece1d134..e37b641aa2 100644 --- a/packages/base/ModalContext/package.json +++ b/packages/base/ModalContext/package.json @@ -29,7 +29,7 @@ "**/styles.js" ], "peerDependencies": { - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/Note/package.json b/packages/base/Note/package.json index b446035848..da3b6c4344 100644 --- a/packages/base/Note/package.json +++ b/packages/base/Note/package.json @@ -34,7 +34,7 @@ "peerDependencies": { "@toptal/picasso-tailwind": "workspace:^", "@toptal/picasso-tailwind-merge": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/Notification/package.json b/packages/base/Notification/package.json index 6a90121c27..f12700e27b 100644 --- a/packages/base/Notification/package.json +++ b/packages/base/Notification/package.json @@ -39,7 +39,7 @@ "@toptal/picasso-tailwind-merge": "workspace:^", "@toptal/picasso-provider": "workspace:^", "@toptal/picasso-tailwind": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/Notification/src/Notification/Notification.tsx b/packages/base/Notification/src/Notification/Notification.tsx index 879f2f6d42..0228a017db 100644 --- a/packages/base/Notification/src/Notification/Notification.tsx +++ b/packages/base/Notification/src/Notification/Notification.tsx @@ -26,7 +26,7 @@ export interface PrivateProps /** Style variant of Notification */ variant?: VariantType /** Add before Notification content */ - icon?: ReactElement + icon?: ReactElement<{ className?: string; color?: string }> /** Enable elevated appearance for Notification */ elevated?: boolean testIds?: { diff --git a/packages/base/Notification/src/use-notification/use-notifications.tsx b/packages/base/Notification/src/use-notification/use-notifications.tsx index 237afd105a..1304332fc1 100644 --- a/packages/base/Notification/src/use-notification/use-notifications.tsx +++ b/packages/base/Notification/src/use-notification/use-notifications.tsx @@ -1,7 +1,7 @@ import type { ReactNode, ReactElement } from 'react' import React, { forwardRef, useMemo, useCallback } from 'react' import { twJoin } from '@toptal/picasso-tailwind-merge' -import type { OptionsObject } from 'notistack' +import type { OptionsObject, SnackbarKey } from 'notistack' import { useSnackbar } from 'notistack' import type { BaseProps } from '@toptal/picasso-shared' @@ -21,7 +21,7 @@ const defaultPosition: SnackbarOrigin = { interface Props extends BaseProps { key?: string content: ReactNode - icon?: ReactElement + icon?: ReactElement<{ className?: string; color?: string }> onClose?: () => void variant?: VariantType } @@ -54,7 +54,10 @@ export const useNotifications = () => { const getNotification = useCallback( () => - (notificationElement: React.ReactElement, options?: OptionsObject) => { + ( + notificationElement: React.ReactElement<{ onClose?: () => void }>, + options?: OptionsObject + ) => { const closeNotification = () => { if (!notificationId) { return @@ -68,7 +71,7 @@ export const useNotifications = () => { } const notificationId = enqueueSnackbar('', { anchorOrigin: defaultPosition, - content: (key: string) => + content: (key: SnackbarKey) => React.cloneElement(notificationElement, { key, onClose: closeNotification, @@ -83,7 +86,11 @@ export const useNotifications = () => { const getPicassoNotification = useCallback( (variant?: VariantType) => - (content: ReactNode, icon?: ReactElement, options?: OptionsObject) => { + ( + content: ReactNode, + icon?: ReactElement<{ className?: string; color?: string }>, + options?: OptionsObject + ) => { const notificationComponent = ( ) diff --git a/packages/base/NumberInput/package.json b/packages/base/NumberInput/package.json index a781833aaa..1ded0ccfa4 100644 --- a/packages/base/NumberInput/package.json +++ b/packages/base/NumberInput/package.json @@ -37,7 +37,7 @@ "peerDependencies": { "@toptal/picasso-tailwind-merge": "workspace:^", "@toptal/picasso-tailwind": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/OutlinedInput/package.json b/packages/base/OutlinedInput/package.json index b3cea0e345..85e44bc6b5 100644 --- a/packages/base/OutlinedInput/package.json +++ b/packages/base/OutlinedInput/package.json @@ -38,7 +38,7 @@ "peerDependencies": { "@toptal/picasso-tailwind-merge": "workspace:^", "@toptal/picasso-tailwind": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "devDependencies": { "@toptal/picasso-tailwind-merge": "workspace:*" diff --git a/packages/base/OverviewBlock/package.json b/packages/base/OverviewBlock/package.json index 6818c99191..e2366b3be6 100644 --- a/packages/base/OverviewBlock/package.json +++ b/packages/base/OverviewBlock/package.json @@ -32,7 +32,7 @@ "peerDependencies": { "@toptal/picasso-tailwind": "workspace:^", "@toptal/picasso-tailwind-merge": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/Page/package.json b/packages/base/Page/package.json index 94b1534a84..3a753cb33c 100644 --- a/packages/base/Page/package.json +++ b/packages/base/Page/package.json @@ -48,10 +48,10 @@ "peerDependencies": { "@toptal/picasso-tailwind-merge": "workspace:^", "@toptal/picasso-provider": "workspace:^", - "react-dom": ">=17.0.0 < 19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "@toptal/picasso-tailwind": "workspace:^", "@toptal/picasso-skeleton-loader": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "devDependencies": { "@toptal/picasso-tailwind-merge": "workspace:*", diff --git a/packages/base/Page/src/PageAutocomplete/__snapshots__/test.tsx.snap b/packages/base/Page/src/PageAutocomplete/__snapshots__/test.tsx.snap index e96415742a..30a5efb081 100644 --- a/packages/base/Page/src/PageAutocomplete/__snapshots__/test.tsx.snap +++ b/packages/base/Page/src/PageAutocomplete/__snapshots__/test.tsx.snap @@ -24,7 +24,6 @@ exports[`PageAutocomplete renders 1`] = ` class="[&::-webkit-calendar-picker-indicator]:bg bg-transparent border-none box-border cursor-[inherit] h-full outline-hidden p-0 peer resize-none w-full text-md leading-4 text-white [&::placeholder]:text-white [&::placeholder]:opacity-[0.64]" data-testid="autocomplete" type="text" - value="" variant="dark" />
{} interface InternalHamburgerContextProps { hamburgerId: string - hamburgerRef?: React.RefObject + hamburgerRef?: NullableRefObject hasPageHamburger: boolean setHasPageHamburger: (val: boolean) => void } diff --git a/packages/base/Page/src/PageHelmet/story/index.jsx b/packages/base/Page/src/PageHelmet/story/index.jsx index 84c29bad81..122ecf8e38 100644 --- a/packages/base/Page/src/PageHelmet/story/index.jsx +++ b/packages/base/Page/src/PageHelmet/story/index.jsx @@ -21,6 +21,11 @@ const chapter = PicassoBook.connectToPage(page => .addTextSection( 'Please refer to the original [react-helmet-async documentation](https://www.npmjs.com/package/react-helmet-async).' ) + .addTextSection( + 'Under React 19, `react-helmet-async@3` lets React hoist head elements itself: ' + + '`` becomes a passthrough, the SSR `context` is not populated, and the ' + + '`prioritizeSeoTags`, `helmetData` and `canUseDOM` props have no effect. React 17 and 18 are unchanged.' + ) .addTextSection( 'If you are using **Next.js** please disable usage of `react-helmet-async` by passing `disableHelmet` prop to the ``. ' + 'After that, refer to the [next/head documentation](https://nextjs.org/docs/api-reference/next/head).' diff --git a/packages/base/Page/src/PageTopBar/PageTopBar.tsx b/packages/base/Page/src/PageTopBar/PageTopBar.tsx index 8e4fae8deb..e17d85b7af 100644 --- a/packages/base/Page/src/PageTopBar/PageTopBar.tsx +++ b/packages/base/Page/src/PageTopBar/PageTopBar.tsx @@ -121,8 +121,7 @@ export const PageTopBar = forwardRef(function PageTopBar(
) - // The width overrides come last so `twMerge` resolves them over the default - // `max-w`, in ascending precedence: default -> wide -> full. + // precedence: default -> wide -> full const innerClassName = twMerge( 'box-border flex items-center justify-between mx-auto my-0 h-[var(--header-height,3.5rem)]', 'max-w-[var(--content-width,75em)] py-0 px-[var(--content-padding-horizontal,1em)] md:px-[var(--content-padding-horizontal,2em)]', diff --git a/packages/base/Page/src/SidebarItem/test.tsx b/packages/base/Page/src/SidebarItem/test.tsx index 3add5abccc..9e646b410a 100644 --- a/packages/base/Page/src/SidebarItem/test.tsx +++ b/packages/base/Page/src/SidebarItem/test.tsx @@ -2,7 +2,7 @@ import React from 'react' import { render } from '@toptal/picasso-test-utils' import type { OmitInternalProps } from '@toptal/picasso-shared' import * as titleCaseModule from 'ap-style-title-case' -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import { Candidates16 } from '@toptal/picasso-icons' import { PageSidebar } from '../PageSidebar' @@ -200,7 +200,11 @@ describe('SubMenuContextProvider', () => { describe('when a provider available', () => { it('returns `isSubMenu` as true', () => { const { result } = renderHook(() => useSubMenuContext(), { - wrapper: SubMenuContextProvider, + wrapper: ({ children }) => ( + + {children} + + ), }) expect(result.current.isSubMenu).toBe(true) diff --git a/packages/base/Page/src/SidebarItem/types.ts b/packages/base/Page/src/SidebarItem/types.ts index 20cd3f9de6..79b01d4683 100644 --- a/packages/base/Page/src/SidebarItem/types.ts +++ b/packages/base/Page/src/SidebarItem/types.ts @@ -1,6 +1,10 @@ -import type { ElementType, ReactElement } from 'react' +import type { ElementType, ReactElement, ReactNode } from 'react' import type React from 'react' -import type { BaseProps, TextLabelProps } from '@toptal/picasso-shared' +import type { + BaseProps, + TextLabelProps, + IconElement, +} from '@toptal/picasso-shared' import type { MenuItemAttributes, MenuItemProps } from '@toptal/picasso-menu' import type { VariantType } from '../PageSidebar/types' @@ -11,7 +15,7 @@ import type { export interface Props extends BaseProps, TextLabelProps, MenuItemAttributes { /** Pass icon to be used as part of item */ - icon?: ReactElement + icon?: IconElement /** Highlights the item as selected */ selected?: boolean /** Whether to render disabled item */ @@ -19,7 +23,7 @@ export interface Props extends BaseProps, TextLabelProps, MenuItemAttributes { /** If item has menu defines can menu be collapsed */ collapsible?: boolean /** Renders nested sidebar menu */ - menu?: ReactElement + menu?: ReactElement<{ children?: ReactNode }> /** Component name to render the menu item as */ as?: ElementType /** Definition of the embedded badge */ diff --git a/packages/base/Page/src/SidebarItemContent/types.ts b/packages/base/Page/src/SidebarItemContent/types.ts index 66f0216b7a..a65baeeb2a 100644 --- a/packages/base/Page/src/SidebarItemContent/types.ts +++ b/packages/base/Page/src/SidebarItemContent/types.ts @@ -1,4 +1,5 @@ import type { ReactElement, ReactNode } from 'react' +import type { IconElement } from '@toptal/picasso-shared' import type { BadgeProps } from '@toptal/picasso-badge' import type { VariantOnlyProps } from '@toptal/picasso-tag' @@ -12,7 +13,7 @@ export type SidebarBadgeProps = Omit export interface Props { compact?: boolean - icon?: ReactElement + icon?: IconElement badge?: SidebarBadgeProps tag?: SidebarTagProps children?: ReactNode diff --git a/packages/base/Page/src/SidebarMenu/SidebarMenu.tsx b/packages/base/Page/src/SidebarMenu/SidebarMenu.tsx index 33d43021c9..91b2be9315 100644 --- a/packages/base/Page/src/SidebarMenu/SidebarMenu.tsx +++ b/packages/base/Page/src/SidebarMenu/SidebarMenu.tsx @@ -32,7 +32,8 @@ export const SidebarMenu = forwardRef( useEffect(() => { const hasSelectedItem = React.Children.toArray(children).some( - child => React.isValidElement(child) && child.props.selected + child => + React.isValidElement(child) && child.props.selected ) if (hasSelectedItem && parentSidebarItemIndex !== undefined) { @@ -41,7 +42,7 @@ export const SidebarMenu = forwardRef( }, [parentSidebarItemIndex, setExpandedItemKey, children]) const items = React.Children.map(children, (child, index) => { - if (React.isValidElement(child)) { + if (React.isValidElement(child)) { const itemProps: Partial = { variant, isSubMenu, diff --git a/packages/base/Page/src/TopBarItem/TopBarItem.tsx b/packages/base/Page/src/TopBarItem/TopBarItem.tsx index 2e618bd54d..70563af398 100644 --- a/packages/base/Page/src/TopBarItem/TopBarItem.tsx +++ b/packages/base/Page/src/TopBarItem/TopBarItem.tsx @@ -2,8 +2,9 @@ import type { BaseProps, TextLabelProps, OverridableComponent, + IconElement, } from '@toptal/picasso-shared' -import type { ElementType, ReactElement } from 'react' +import type { ElementType } from 'react' import React, { forwardRef, memo } from 'react' import type { MenuItemProps } from '@toptal/picasso-menu' import { twMerge } from '@toptal/picasso-tailwind-merge' @@ -13,7 +14,7 @@ import { SidebarItem } from '../SidebarItem' export interface Props extends BaseProps, TextLabelProps { /** Pass icon to be used as part of item */ - icon?: ReactElement + icon?: IconElement /** Highlights the item as selected */ selected?: boolean /** Component name to render the menu item as */ diff --git a/packages/base/Pagination/package.json b/packages/base/Pagination/package.json index 9bdebe8336..2678a0fe26 100644 --- a/packages/base/Pagination/package.json +++ b/packages/base/Pagination/package.json @@ -32,7 +32,7 @@ "**/styles.js" ], "peerDependencies": { - "react": ">=17.0.0 < 19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@toptal/picasso-tailwind-merge": "workspace:^", "@toptal/picasso-tailwind": "workspace:^" }, diff --git a/packages/base/Paper/package.json b/packages/base/Paper/package.json index aef97d53ac..c3624a3596 100644 --- a/packages/base/Paper/package.json +++ b/packages/base/Paper/package.json @@ -29,7 +29,7 @@ "@toptal/picasso-tailwind-merge": "workspace:^", "@toptal/picasso-provider": "workspace:^", "@toptal/picasso-tailwind": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/PasswordInput/package.json b/packages/base/PasswordInput/package.json index fa0be84ddd..6e6cd5aa7f 100644 --- a/packages/base/PasswordInput/package.json +++ b/packages/base/PasswordInput/package.json @@ -33,7 +33,7 @@ "**/styles.js" ], "peerDependencies": { - "react": ">=17.0.0 < 19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@toptal/picasso-tailwind-merge": "workspace:^", "@toptal/picasso-tailwind": "workspace:^" }, diff --git a/packages/base/Popper/package.json b/packages/base/Popper/package.json index 7507a472a2..aa8768548b 100644 --- a/packages/base/Popper/package.json +++ b/packages/base/Popper/package.json @@ -29,7 +29,7 @@ }, "peerDependencies": { "@toptal/picasso-provider": "workspace:^", - "react": ">=17.0.0 < 19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@toptal/picasso-tailwind": "workspace:^", "@toptal/picasso-tailwind-merge": "workspace:^" }, diff --git a/packages/base/PromptModal/package.json b/packages/base/PromptModal/package.json index 2b8f73e5d0..09ff819b82 100644 --- a/packages/base/PromptModal/package.json +++ b/packages/base/PromptModal/package.json @@ -33,7 +33,7 @@ "**/styles.js" ], "peerDependencies": { - "react": ">=17.0.0 < 19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@toptal/picasso-tailwind": "workspace:^" }, "exports": { diff --git a/packages/base/Quote/package.json b/packages/base/Quote/package.json index fd321b068c..a3db5a4158 100644 --- a/packages/base/Quote/package.json +++ b/packages/base/Quote/package.json @@ -31,7 +31,7 @@ ], "peerDependencies": { "@toptal/picasso-tailwind": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/Radio/package.json b/packages/base/Radio/package.json index 4ddd53fa96..7b6da2d384 100644 --- a/packages/base/Radio/package.json +++ b/packages/base/Radio/package.json @@ -35,7 +35,7 @@ ], "peerDependencies": { "@toptal/picasso-tailwind-merge": "workspace:^", - "react": ">=17.0.0 < 19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@toptal/picasso-tailwind": "workspace:^" }, "devDependencies": { diff --git a/packages/base/Radio/src/Radio/__snapshots__/test.tsx.snap b/packages/base/Radio/src/Radio/__snapshots__/test.tsx.snap index b4b3281686..74478c8c00 100644 --- a/packages/base/Radio/src/Radio/__snapshots__/test.tsx.snap +++ b/packages/base/Radio/src/Radio/__snapshots__/test.tsx.snap @@ -312,7 +312,6 @@ exports[`Radio disabled radio button renders disabled version 1`] = ` class="peer absolute left-0 top-0 z-[1] m-0 h-full w-full cursor-[inherit] p-0 opacity-0" disabled="" type="radio" - value="" /> =17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/Rating/src/RatingStars/__snapshots__/test.tsx.snap b/packages/base/Rating/src/RatingStars/__snapshots__/test.tsx.snap index 1392ec04f3..35de44240c 100644 --- a/packages/base/Rating/src/RatingStars/__snapshots__/test.tsx.snap +++ b/packages/base/Rating/src/RatingStars/__snapshots__/test.tsx.snap @@ -134,7 +134,6 @@ exports[`Rating.Stars renders 1`] = ` id="rating-name-reset" name="rating-name" type="radio" - value="" />
diff --git a/packages/base/Section/package.json b/packages/base/Section/package.json index 1f44beb8fe..0105ceae58 100644 --- a/packages/base/Section/package.json +++ b/packages/base/Section/package.json @@ -36,7 +36,7 @@ "peerDependencies": { "@toptal/picasso-tailwind-merge": "workspace:^", "@toptal/picasso-tailwind": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/Select/package.json b/packages/base/Select/package.json index be11fd8b6d..2939b4a906 100644 --- a/packages/base/Select/package.json +++ b/packages/base/Select/package.json @@ -43,7 +43,7 @@ "peerDependencies": { "@toptal/picasso-tailwind-merge": "workspace:^", "@toptal/picasso-tailwind": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "devDependencies": { "@toptal/picasso-tailwind": "workspace:*", diff --git a/packages/base/Select/src/ScrollMenu/ScrollMenu.tsx b/packages/base/Select/src/ScrollMenu/ScrollMenu.tsx index 2f6996ed06..ad3a435ca8 100644 --- a/packages/base/Select/src/ScrollMenu/ScrollMenu.tsx +++ b/packages/base/Select/src/ScrollMenu/ScrollMenu.tsx @@ -19,7 +19,7 @@ export interface Props extends BaseProps { } const getMenuSelectedNode = ( - menuRef: RefObject, + menuRef: RefObject, selectedIndex?: number | null ) => typeof selectedIndex === 'number' @@ -27,7 +27,7 @@ const getMenuSelectedNode = ( : undefined export const scrollToSelection = ( - menuRef: RefObject, + menuRef: RefObject, selectedIndex?: number | null ) => { const menuNode = menuRef.current diff --git a/packages/base/Select/src/SelectBase/hooks/use-adornments/test.ts b/packages/base/Select/src/SelectBase/hooks/use-adornments/test.ts index 5fdb2d489a..74f363f1ab 100644 --- a/packages/base/Select/src/SelectBase/hooks/use-adornments/test.ts +++ b/packages/base/Select/src/SelectBase/hooks/use-adornments/test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import useAdornments from './use-adornments' diff --git a/packages/base/Select/src/SelectBase/hooks/use-highlighted-index/test.ts b/packages/base/Select/src/SelectBase/hooks/use-highlighted-index/test.ts index 6ebc19a39d..e6057fbd11 100644 --- a/packages/base/Select/src/SelectBase/hooks/use-highlighted-index/test.ts +++ b/packages/base/Select/src/SelectBase/hooks/use-highlighted-index/test.ts @@ -1,5 +1,5 @@ -import type { RenderResult } from '@testing-library/react-hooks' -import { renderHook, act } from '@testing-library/react-hooks' +import type { RenderHookResult } from '@testing-library/react' +import { renderHook, act } from '@testing-library/react' import type { Option } from '../../types' import useHighlightedIndex from './use-highlighted-index' @@ -17,14 +17,15 @@ const disableOptions = (options: Option[], indicies: number[]) => indicies.includes(index) ? { ...option, disabled: true } : option ) -const getHighlightIndex = ( - result: RenderResult> -) => result.current[0] +type HighlightedIndexResult = RenderHookResult< + ReturnType, + { isOpen: boolean } +>['result'] -const setHighlightedIndex = ( - result: RenderResult>, - index: number -) => result.current[1](index) +const getHighlightIndex = (result: HighlightedIndexResult) => result.current[0] + +const setHighlightedIndex = (result: HighlightedIndexResult, index: number) => + result.current[1](index) const renderUseHighlightedIndex = ({ options, diff --git a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-arrows-keydown-handler/test.ts b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-arrows-keydown-handler/test.ts index cd3e3bf42f..98f8dcfb7f 100644 --- a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-arrows-keydown-handler/test.ts +++ b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-arrows-keydown-handler/test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import { getUseSelectPropsMock } from '../mocks' import useArrowsKeydownHandler from './use-arrows-keydown-handler' diff --git a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-blur-handler/test.ts b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-blur-handler/test.ts index 7ec71ae9f9..53b302b151 100644 --- a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-blur-handler/test.ts +++ b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-blur-handler/test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import { getUseSelectPropsMock } from '../mocks' import useBlurHandler from './use-blur-handler' diff --git a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-click-handler/test.ts b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-click-handler/test.ts index 3cbe11b495..0b72a22f6d 100644 --- a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-click-handler/test.ts +++ b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-click-handler/test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import { getUseSelectPropsMock } from '../mocks' import useClickHandler from './use-click-handler' diff --git a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-enter-or-space-keydown-handler/test.ts b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-enter-or-space-keydown-handler/test.ts index a35f320a42..78dd27ec65 100644 --- a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-enter-or-space-keydown-handler/test.ts +++ b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-enter-or-space-keydown-handler/test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import { getUseSelectPropsMock } from '../mocks' import useEnterOrSpaceKeydownHandler from './use-enter-or-space-keydown-handler' diff --git a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-escape-keydown-handler/test.ts b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-escape-keydown-handler/test.ts index b2743bcdd9..3096aac71a 100644 --- a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-escape-keydown-handler/test.ts +++ b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-escape-keydown-handler/test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import { getUseSelectPropsMock } from '../mocks' import useEscapeKeydownHandler from './use-escape-keydown-handler' diff --git a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-focus-handler/test.ts b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-focus-handler/test.ts index 12c1ea90aa..6c6cd2f9f9 100644 --- a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-focus-handler/test.ts +++ b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-focus-handler/test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import { getUseSelectPropsMock } from '../mocks' import useFocusHandler from './use-focus-handler' diff --git a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-item-on-click-handler/test.ts b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-item-on-click-handler/test.ts index 34537277c7..9492b07d5c 100644 --- a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-item-on-click-handler/test.ts +++ b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-item-on-click-handler/test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import { getUseSelectPropsMock } from '../mocks' import useItemOnClickHandler from './use-item-on-click-handler' diff --git a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-item-on-mouse-down-handler/test.ts b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-item-on-mouse-down-handler/test.ts index a8a346eda6..6376d7411e 100644 --- a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-item-on-mouse-down-handler/test.ts +++ b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-item-on-mouse-down-handler/test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import useItemOnMouseDownHandler from './use-item-on-mouse-down-handler' diff --git a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-item-on-mouse-enter-handler/test.ts b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-item-on-mouse-enter-handler/test.ts index 183ae9d2ec..68ef760f86 100644 --- a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-item-on-mouse-enter-handler/test.ts +++ b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-item-on-mouse-enter-handler/test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import { getUseSelectPropsMock } from '../mocks' import useItemOnMouseEnterHandler from './use-item-on-mouse-enter-handler' diff --git a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-reset-click-handler/test.ts b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-reset-click-handler/test.ts index 2fb14a9e84..3d399e52fe 100644 --- a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-reset-click-handler/test.ts +++ b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-reset-click-handler/test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import { getUseSelectPropsMock } from '../mocks' import useResetClickHandler from './use-reset-click-handler' diff --git a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-search-blur-handler/test.ts b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-search-blur-handler/test.ts index b67924b02c..3986243fe8 100644 --- a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-search-blur-handler/test.ts +++ b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-search-blur-handler/test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import { getUseSelectPropsMock } from '../mocks' import useSearchBlurHandler from './use-search-blur-handler' diff --git a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-search-change-handler/test.ts b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-search-change-handler/test.ts index d18cd7e35b..a09b953bed 100644 --- a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-search-change-handler/test.ts +++ b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-search-change-handler/test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import { getUseSelectPropsMock } from '../mocks' import useSearchChangeHandler from './use-search-change-handler' diff --git a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-search-keydown-handler/test.ts b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-search-keydown-handler/test.ts index fb5f7dc277..684cb76e31 100644 --- a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-search-keydown-handler/test.ts +++ b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-search-keydown-handler/test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import { getUseSelectPropsMock } from '../mocks' import useSearchKeydownHandler from './use-search-keydown-handler' diff --git a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-select-handler/test.ts b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-select-handler/test.ts index 404ea75c41..c3f9347a28 100644 --- a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-select-handler/test.ts +++ b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-select-handler/test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import { getUseSelectPropsMock } from '../mocks' import useSelectHandler from './use-select-handler' diff --git a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-select-keydown-handler/test.ts b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-select-keydown-handler/test.ts index 2f27779243..22c72fab01 100644 --- a/packages/base/Select/src/SelectBase/hooks/use-select-props/use-select-keydown-handler/test.ts +++ b/packages/base/Select/src/SelectBase/hooks/use-select-props/use-select-keydown-handler/test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import { getUseSelectPropsMock } from '../mocks' import useSelectKeydownHandle from './use-select-keydown-handler' diff --git a/packages/base/Select/src/SelectBase/hooks/use-select-state/test.ts b/packages/base/Select/src/SelectBase/hooks/use-select-state/test.ts index c550adbd0f..ffb4f53ab3 100644 --- a/packages/base/Select/src/SelectBase/hooks/use-select-state/test.ts +++ b/packages/base/Select/src/SelectBase/hooks/use-select-state/test.ts @@ -1,4 +1,4 @@ -import { renderHook, act } from '@testing-library/react-hooks' +import { renderHook, act } from '@testing-library/react' import type { Option } from '../../types' import type { Props } from './use-select-state' diff --git a/packages/base/ShowMore/package.json b/packages/base/ShowMore/package.json index d94359a169..79be5c0f72 100644 --- a/packages/base/ShowMore/package.json +++ b/packages/base/ShowMore/package.json @@ -35,7 +35,7 @@ "peerDependencies": { "@toptal/picasso-tailwind": "workspace:^", "@toptal/picasso-tailwind-merge": "workspace:^", - "react": ">=17.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/ShowMore/src/ShowMore/ShowMore.tsx b/packages/base/ShowMore/src/ShowMore/ShowMore.tsx index bc6fa9693c..9624c13ca3 100644 --- a/packages/base/ShowMore/src/ShowMore/ShowMore.tsx +++ b/packages/base/ShowMore/src/ShowMore/ShowMore.tsx @@ -87,6 +87,8 @@ export const ShowMore = forwardRef(function ShowMore( observer.observe(element) return () => observer.disconnect() + // `content` and `rows` are not read above: they are the re-measure + // triggers, since either changes what overflows }, [shownMore, content, rows]) const isContentVisible = rows !== 0 || shownMore diff --git a/packages/base/ShowMore/src/ShowMore/utils.tsx b/packages/base/ShowMore/src/ShowMore/utils.tsx index 05fdf05b9f..ec329eae19 100644 --- a/packages/base/ShowMore/src/ShowMore/utils.tsx +++ b/packages/base/ShowMore/src/ShowMore/utils.tsx @@ -2,7 +2,7 @@ import React from 'react' export const replaceLineBreaksWithTags = ( children: string -): string | (JSX.Element | JSX.Element[])[] => { +): string | (React.ReactElement | React.ReactElement[])[] => { const textChunks = children.split('\n') if (textChunks.length <= 1) { diff --git a/packages/base/SkeletonLoader/package.json b/packages/base/SkeletonLoader/package.json index 9d8853c7ad..d8bfd4eee3 100644 --- a/packages/base/SkeletonLoader/package.json +++ b/packages/base/SkeletonLoader/package.json @@ -32,7 +32,7 @@ "**/styles.js" ], "peerDependencies": { - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/Slide/package.json b/packages/base/Slide/package.json index 0ee891c6a9..1153e256ef 100644 --- a/packages/base/Slide/package.json +++ b/packages/base/Slide/package.json @@ -30,7 +30,7 @@ "peerDependencies": { "@toptal/picasso-tailwind": "workspace:^", "@toptal/picasso-tailwind-merge": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/Slide/src/Slide/Slide.tsx b/packages/base/Slide/src/Slide/Slide.tsx index 3219c5a0c6..8f618e9d37 100644 --- a/packages/base/Slide/src/Slide/Slide.tsx +++ b/packages/base/Slide/src/Slide/Slide.tsx @@ -1,25 +1,28 @@ import React, { useRef } from 'react' import cx from 'classnames' -import type { BaseProps, TransitionProps } from '@toptal/picasso-shared' +import type { + BaseProps, + TransitionChild, + TransitionProps, +} from '@toptal/picasso-shared' import { getElementRef, - getTransitionTimeouts, useMultipleForwardRefs, useTransitionStatus, } from '@toptal/picasso-utils' import { twMerge } from '@toptal/picasso-tailwind-merge' import type { SlideDirection } from './styles' -import { createStateClassNames } from './styles' +import { createTranslateClassNames } from './styles' export interface Props extends TransitionProps, BaseProps { - /* Element that accepts ref */ - children: React.ReactElement - /* Show the component; triggers the enter or exit states */ + /** Element that accepts ref */ + children: TransitionChild + /** Show the component; toggling runs the enter or exit transition */ in: boolean - /* Callback fired when the component has entered */ + /** Callback fired when the enter transition starts */ onEnter?: (node: HTMLElement, isAppearing: boolean) => void - /* Direction in which the component will slide */ + /** Direction in which the component will slide */ direction: SlideDirection } @@ -39,7 +42,7 @@ export const Slide = React.forwardRef(function Slide( ) { const nodeRef = useRef(null) - const status = useTransitionStatus({ + const { status, duration } = useTransitionStatus({ in: inProp, // Historical behavior: enter callbacks fire on mount-open, with no // visible slide (the status flips pre-paint) @@ -56,22 +59,15 @@ export const Slide = React.forwardRef(function Slide( getElementRef(children), ]) - const timeouts = getTransitionTimeouts(timeout) - const duration = inProp ? timeouts.enter : timeouts.exit - return React.cloneElement(children, { className: twMerge( // In Tailwind v4 this also covers the standalone `translate` property 'transition-transform', children.props.className, - // State classes stay after the child's className: behavior, not - // overridable defaults - cx( - ...createStateClassNames(direction, { - in: inProp, - exited: status === 'exited' && !inProp, - }) - ) + // behavior classes, not defaults — must stay after the child's className + cx(createTranslateClassNames(direction, inProp), { + invisible: status === 'exited' && !inProp, + }) ), style: { transitionDuration: `${duration}ms`, diff --git a/packages/base/Slide/src/Slide/styles.ts b/packages/base/Slide/src/Slide/styles.ts index 9406c3b481..05771766bc 100644 --- a/packages/base/Slide/src/Slide/styles.ts +++ b/packages/base/Slide/src/Slide/styles.ts @@ -14,10 +14,8 @@ const shownClassByDirection: Record = { down: 'translate-y-0', } -export const createStateClassNames = ( +export const createTranslateClassNames = ( direction: SlideDirection, - { in: inProp, exited }: { in: boolean; exited: boolean } -): string[] => [ - inProp ? shownClassByDirection[direction] : hiddenClassByDirection[direction], - ...(exited ? ['invisible'] : []), -] + shown: boolean +): string => + shown ? shownClassByDirection[direction] : hiddenClassByDirection[direction] diff --git a/packages/base/Slider/package.json b/packages/base/Slider/package.json index 18a62d8c7e..1a6def105f 100644 --- a/packages/base/Slider/package.json +++ b/packages/base/Slider/package.json @@ -32,7 +32,7 @@ "@toptal/picasso-tailwind-merge": "workspace:^", "@toptal/picasso-provider": "workspace:^", "@toptal/picasso-tailwind": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/Slider/src/Slider/hooks/use-label-overlap.ts b/packages/base/Slider/src/Slider/hooks/use-label-overlap.ts index 007ed3efe4..24bea24868 100644 --- a/packages/base/Slider/src/Slider/hooks/use-label-overlap.ts +++ b/packages/base/Slider/src/Slider/hooks/use-label-overlap.ts @@ -11,9 +11,9 @@ export const useLabelOverlap = ({ isTooltipRendered: boolean }) => { const [isPartiallyOverlapped, setIsPartiallyOverlapped] = useState(false) - const [valueLabels, setValueLabels] = useState[]>( - [] - ) + const [valueLabels, setValueLabels] = useState< + RefObject[] + >([]) const isRangeSlider = Array.isArray(value) useEffect(() => { @@ -46,7 +46,7 @@ export const useLabelOverlap = ({ ]) const handleValueLabelOnRender = useCallback( - (index: number, labelRef: RefObject) => { + (index: number, labelRef: RefObject) => { setValueLabels(valLabels => { valLabels[index] = labelRef diff --git a/packages/base/Slider/src/SliderValueLabel/SliderValueLabel.tsx b/packages/base/Slider/src/SliderValueLabel/SliderValueLabel.tsx index 3ef61e49c7..1429bede26 100644 --- a/packages/base/Slider/src/SliderValueLabel/SliderValueLabel.tsx +++ b/packages/base/Slider/src/SliderValueLabel/SliderValueLabel.tsx @@ -14,7 +14,7 @@ type SliderValueLabelProps = { yPlacement: 'top' | 'bottom' /** indicates if there are two SliderValueLabels that overlap each other */ isOverlaped: boolean - onRender: (index: number, ref: RefObject) => void + onRender: (index: number, ref: RefObject) => void } const classesByTooltip: Record = { diff --git a/packages/base/Step/package.json b/packages/base/Step/package.json index 3a5f4498b8..b1d429a30d 100644 --- a/packages/base/Step/package.json +++ b/packages/base/Step/package.json @@ -37,7 +37,7 @@ "peerDependencies": { "@toptal/picasso-tailwind-merge": "workspace:^", "@toptal/picasso-provider": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "devDependencies": { "@toptal/picasso-tailwind-merge": "workspace:*", diff --git a/packages/base/Switch/package.json b/packages/base/Switch/package.json index 2e0deae1e4..bacb5e9324 100644 --- a/packages/base/Switch/package.json +++ b/packages/base/Switch/package.json @@ -34,7 +34,7 @@ "@toptal/picasso-provider": "workspace:^", "@toptal/picasso-tailwind": "workspace:^", "@toptal/picasso-tailwind-merge": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/Table/package.json b/packages/base/Table/package.json index e1054ebe52..e35b976cfb 100644 --- a/packages/base/Table/package.json +++ b/packages/base/Table/package.json @@ -37,7 +37,7 @@ "peerDependencies": { "@toptal/picasso-provider": "workspace:^", "@toptal/picasso-tailwind-merge": "workspace:^", - "react": ">=17.0.0 < 19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@toptal/picasso-tailwind": "workspace:^" }, "devDependencies": { diff --git a/packages/base/Table/src/TableExpandableRow/TableExpandableRow.tsx b/packages/base/Table/src/TableExpandableRow/TableExpandableRow.tsx index e9f0899b64..0b827a7954 100644 --- a/packages/base/Table/src/TableExpandableRow/TableExpandableRow.tsx +++ b/packages/base/Table/src/TableExpandableRow/TableExpandableRow.tsx @@ -9,7 +9,9 @@ import { TableCell } from '../TableCell' const MAX_COL_SPAN = 100 -export interface Props extends BaseProps, HTMLAttributes { +export interface Props + extends BaseProps, + Omit, 'content'> { /** Should be valid `` children such as `Table.Cell`. */ children: ReactNode /** Collapsible content of `TableExpandableRow` */ diff --git a/packages/base/Table/src/TableSectionHead/TableSectionHead.tsx b/packages/base/Table/src/TableSectionHead/TableSectionHead.tsx index 8656dc88ac..47c0662903 100644 --- a/packages/base/Table/src/TableSectionHead/TableSectionHead.tsx +++ b/packages/base/Table/src/TableSectionHead/TableSectionHead.tsx @@ -1,6 +1,6 @@ -import type { ReactElement, HTMLAttributes, FunctionComponent } from 'react' +import type { HTMLAttributes, FunctionComponent } from 'react' import React, { forwardRef } from 'react' -import type { BaseProps } from '@toptal/picasso-shared' +import type { BaseProps, IconElement } from '@toptal/picasso-shared' import { twMerge } from '@toptal/picasso-tailwind-merge' import { TableSection, TableSectionContext } from '../Table' @@ -14,7 +14,7 @@ export interface Props extends BaseProps, HTMLAttributes { colSpan?: number - icon?: ReactElement + icon?: IconElement } export const TableSectionHead: FunctionComponent = forwardRef< diff --git a/packages/base/Tabs/package.json b/packages/base/Tabs/package.json index 20ff697c73..cf14b8a27c 100644 --- a/packages/base/Tabs/package.json +++ b/packages/base/Tabs/package.json @@ -39,7 +39,7 @@ "peerDependencies": { "@toptal/picasso-tailwind-merge": "workspace:^", "@toptal/picasso-tailwind": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/Tabs/src/Tab/Tab.tsx b/packages/base/Tabs/src/Tab/Tab.tsx index 20acfc6270..4c5444cc5b 100644 --- a/packages/base/Tabs/src/Tab/Tab.tsx +++ b/packages/base/Tabs/src/Tab/Tab.tsx @@ -1,7 +1,11 @@ -import type { ReactNode, HTMLAttributes, ReactElement } from 'react' +import type { ReactNode, HTMLAttributes } from 'react' import React, { forwardRef, useContext } from 'react' import { Tabs as BaseUITabs } from '@base-ui/react/tabs' -import type { BaseProps, TextLabelProps } from '@toptal/picasso-shared' +import type { + BaseProps, + TextLabelProps, + IconElement, +} from '@toptal/picasso-shared' import { useTitleCase } from '@toptal/picasso-shared' import { UserBadge } from '@toptal/picasso-user-badge' import { twJoin, twMerge } from '@toptal/picasso-tailwind-merge' @@ -28,7 +32,7 @@ export interface Props label?: ReactNode /** The Icon element */ - icon?: ReactElement + icon?: IconElement /** Image URL */ avatar?: string | null diff --git a/packages/base/Tabs/src/Tabs/Tabs.tsx b/packages/base/Tabs/src/Tabs/Tabs.tsx index 560dc9a2ae..74dbb6b07b 100644 --- a/packages/base/Tabs/src/Tabs/Tabs.tsx +++ b/packages/base/Tabs/src/Tabs/Tabs.tsx @@ -159,6 +159,6 @@ const Tabs = forwardRef( } ) as ( props: Props & { ref?: ForwardedRef } -) => JSX.Element +) => React.ReactElement export default Tabs diff --git a/packages/base/Tag/package.json b/packages/base/Tag/package.json index 1001fcbef5..d282516352 100644 --- a/packages/base/Tag/package.json +++ b/packages/base/Tag/package.json @@ -36,7 +36,7 @@ "@toptal/picasso-tailwind-merge": "workspace:^", "@toptal/picasso-provider": "workspace:^", "@toptal/picasso-tailwind": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/Tag/src/Tag/Tag.tsx b/packages/base/Tag/src/Tag/Tag.tsx index 147ab2e783..994c64d3ab 100644 --- a/packages/base/Tag/src/Tag/Tag.tsx +++ b/packages/base/Tag/src/Tag/Tag.tsx @@ -1,13 +1,16 @@ import type { ReactNode, - ReactElement, ElementType, MouseEvent, AnchorHTMLAttributes, HTMLAttributes, } from 'react' import React, { forwardRef } from 'react' -import type { BaseProps, TextLabelProps } from '@toptal/picasso-shared' +import type { + BaseProps, + TextLabelProps, + IconElement, +} from '@toptal/picasso-shared' import { useTitleCase } from '@toptal/picasso-shared' import { CloseMinor16 } from '@toptal/picasso-icons' import { Typography } from '@toptal/picasso-typography' @@ -26,7 +29,7 @@ export interface Props extends BaseProps, TextLabelProps, DivOrAnchorProps { /** Text content of the `Tag` component */ children?: ReactNode /** Specify the icon which should be rendered inside Tag */ - icon?: ReactElement + icon?: IconElement /** Defines if `Tag` is disabled */ disabled?: boolean /** A callback which is invoked after remove `Icon` is clicked diff --git a/packages/base/Tag/src/TagCheckable/TagCheckable.tsx b/packages/base/Tag/src/TagCheckable/TagCheckable.tsx index 7ce8f18819..5f0eb9c3d5 100644 --- a/packages/base/Tag/src/TagCheckable/TagCheckable.tsx +++ b/packages/base/Tag/src/TagCheckable/TagCheckable.tsx @@ -1,6 +1,10 @@ -import type { ReactElement, MouseEventHandler } from 'react' +import type { MouseEventHandler } from 'react' import React, { forwardRef } from 'react' -import type { BaseProps, TextLabelProps } from '@toptal/picasso-shared' +import type { + BaseProps, + TextLabelProps, + IconElement, +} from '@toptal/picasso-shared' import { twMerge } from '@toptal/picasso-tailwind-merge' import { Tag } from '../Tag' @@ -16,7 +20,7 @@ export interface Props extends BaseProps, TextLabelProps { /** Defines if `Tag` is disabled */ disabled?: boolean /** Specify the icon which should be rendered inside Tag */ - icon?: ReactElement + icon?: IconElement onClick?: ClickType /** Callback invoked when component is clicked */ onChange?: (checked: boolean) => void diff --git a/packages/base/Tagselector/package.json b/packages/base/Tagselector/package.json index 6124cfa064..7401b89029 100644 --- a/packages/base/Tagselector/package.json +++ b/packages/base/Tagselector/package.json @@ -36,7 +36,7 @@ ], "peerDependencies": { "@toptal/picasso-tailwind": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "devDependencies": { "@toptal/picasso-popper": "workspace:*", diff --git a/packages/base/Tagselector/src/TagSelector/__snapshots__/test.tsx.snap b/packages/base/Tagselector/src/TagSelector/__snapshots__/test.tsx.snap index be66b7648d..ce1f47471c 100644 --- a/packages/base/Tagselector/src/TagSelector/__snapshots__/test.tsx.snap +++ b/packages/base/Tagselector/src/TagSelector/__snapshots__/test.tsx.snap @@ -55,7 +55,6 @@ exports[`TagSelector disabled render 1`] = ` class="[&::-webkit-calendar-picker-indicator]:bg bg-transparent border-none box-border cursor-[inherit] h-full outline-hidden p-0 peer resize-none w-full text-md leading-4 text-gray [&::-webkit-text-fill-color]:text-gray [&::placeholder]:text-gray [&::placeholder]:opacity-100" disabled="" type="text" - value="" />
@@ -121,7 +120,6 @@ exports[`TagSelector preselected value 1`] = ` autocomplete="off" class="[&::-webkit-calendar-picker-indicator]:bg bg-transparent border-none box-border cursor-[inherit] h-full outline-hidden p-0 peer resize-none w-full text-md leading-4 text-black [&::placeholder]:text-gray [&::placeholder]:opacity-100" type="text" - value="" />
@@ -158,7 +156,6 @@ exports[`TagSelector renders 1`] = ` class="[&::-webkit-calendar-picker-indicator]:bg bg-transparent border-none box-border cursor-[inherit] h-full outline-hidden p-0 peer resize-none w-full text-md leading-4 text-black [&::placeholder]:text-gray [&::placeholder]:opacity-100" placeholder="Please select..." type="text" - value="" />
diff --git a/packages/base/Test-Utils/package.json b/packages/base/Test-Utils/package.json index 1eb54fd8de..4eb58fb69d 100644 --- a/packages/base/Test-Utils/package.json +++ b/packages/base/Test-Utils/package.json @@ -33,7 +33,7 @@ }, "peerDependencies": { "@toptal/picasso-provider": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/Test-Utils/src/test-utils/index.tsx b/packages/base/Test-Utils/src/test-utils/index.tsx index ca11b36a01..c9cbc18173 100644 --- a/packages/base/Test-Utils/src/test-utils/index.tsx +++ b/packages/base/Test-Utils/src/test-utils/index.tsx @@ -34,3 +34,4 @@ export { customRender as render, TestingPicasso } export { HAPPO_TARGETS, getHappoTargets } from './get-happo-targets' export { createPoppersSettledWaitFor } from './wait-for-poppers-settled' export { POPPER_ACT_WARNING } from './console-allowlist' +export { renderedProps } from './rendered-props' diff --git a/packages/base/Test-Utils/src/test-utils/rendered-props/index.ts b/packages/base/Test-Utils/src/test-utils/rendered-props/index.ts new file mode 100644 index 0000000000..d1a7c2d4eb --- /dev/null +++ b/packages/base/Test-Utils/src/test-utils/rendered-props/index.ts @@ -0,0 +1 @@ +export { renderedProps } from './rendered-props' diff --git a/packages/base/Test-Utils/src/test-utils/rendered-props/rendered-props.test.ts b/packages/base/Test-Utils/src/test-utils/rendered-props/rendered-props.test.ts new file mode 100644 index 0000000000..6560049d76 --- /dev/null +++ b/packages/base/Test-Utils/src/test-utils/rendered-props/rendered-props.test.ts @@ -0,0 +1,16 @@ +import { renderedProps } from './rendered-props' + +describe('renderedProps', () => { + it('returns the first argument of every call, in order', () => { + const mock = jest.fn() + + mock({ a: 1 }, {}) + mock({ a: 2 }) + + expect(renderedProps(mock)).toEqual([{ a: 1 }, { a: 2 }]) + }) + + it('is empty for a component that never rendered', () => { + expect(renderedProps(jest.fn())).toEqual([]) + }) +}) diff --git a/packages/base/Test-Utils/src/test-utils/rendered-props/rendered-props.ts b/packages/base/Test-Utils/src/test-utils/rendered-props/rendered-props.ts new file mode 100644 index 0000000000..73d56af722 --- /dev/null +++ b/packages/base/Test-Utils/src/test-utils/rendered-props/rendered-props.ts @@ -0,0 +1,18 @@ +type MockWithCalls = { + mock: { calls: Call[] } +} + +/** + * Props each render of a mocked component received, in call order. + * + * React 18 passes function components a second, legacy-context argument and + * React 19 does not, so `toHaveBeenCalledWith(props, {})` only passes on one + * of them. Compare the props alone instead: + * + * expect(renderedProps(ButtonMock)).toContainEqual( + * expect.objectContaining({ disabled: true }) + * ) + */ +export const renderedProps = ( + mock: MockWithCalls +): Call[0][] => mock.mock.calls.map(call => call[0]) diff --git a/packages/base/Timeline/package.json b/packages/base/Timeline/package.json index 5dec9c0b88..9eeed59c95 100644 --- a/packages/base/Timeline/package.json +++ b/packages/base/Timeline/package.json @@ -34,7 +34,7 @@ "peerDependencies": { "@toptal/picasso-tailwind-merge": "workspace:^", "@toptal/picasso-tailwind": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/Timeline/src/TimelineRow/TimelineRow.tsx b/packages/base/Timeline/src/TimelineRow/TimelineRow.tsx index e6b483b8b3..73591574e1 100644 --- a/packages/base/Timeline/src/TimelineRow/TimelineRow.tsx +++ b/packages/base/Timeline/src/TimelineRow/TimelineRow.tsx @@ -1,6 +1,6 @@ -import type { ReactElement, ReactNode } from 'react' +import type { ReactNode } from 'react' import React from 'react' -import type { BaseProps } from '@toptal/picasso-shared' +import type { BaseProps, IconElement } from '@toptal/picasso-shared' import { Container } from '@toptal/picasso-container' import { Typography } from '@toptal/picasso-typography' import { twJoin, twMerge } from '@toptal/picasso-tailwind-merge' @@ -9,7 +9,7 @@ export interface Props extends BaseProps { /** Timeline row content */ children: ReactNode /** Icon for the row between lines */ - icon?: ReactElement + icon?: IconElement /** Timeline row date */ date?: string /** Whether to render a connector line after the row */ diff --git a/packages/base/Timepicker/package.json b/packages/base/Timepicker/package.json index 8e5c4b045d..07d59eb703 100644 --- a/packages/base/Timepicker/package.json +++ b/packages/base/Timepicker/package.json @@ -36,7 +36,7 @@ "peerDependencies": { "@toptal/picasso-tailwind-merge": "workspace:^", "@toptal/picasso-tailwind": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/Tooltip/package.json b/packages/base/Tooltip/package.json index 316ceb64e4..a07c8f7f6a 100644 --- a/packages/base/Tooltip/package.json +++ b/packages/base/Tooltip/package.json @@ -35,7 +35,7 @@ "@toptal/picasso-provider": "workspace:^", "@toptal/picasso-tailwind": "workspace:^", "@toptal/picasso-tailwind-merge": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "devDependencies": { "@toptal/picasso-provider": "workspace:*", diff --git a/packages/base/Tooltip/src/Tooltip/Tooltip.tsx b/packages/base/Tooltip/src/Tooltip/Tooltip.tsx index 81c201528b..909fdbfc6b 100644 --- a/packages/base/Tooltip/src/Tooltip/Tooltip.tsx +++ b/packages/base/Tooltip/src/Tooltip/Tooltip.tsx @@ -113,7 +113,9 @@ const useFallbackId = (idProp?: string): string | undefined => { return idProp ?? fallbackId } -export interface Props extends BaseProps, HTMLAttributes { +export interface Props + extends BaseProps, + Omit, 'content'> { /** Trigger element for tooltip */ children: ReactNode /** Content to be rendered inside tooltip */ diff --git a/packages/base/Tooltip/src/Tooltip/use-tooltip-state.ts b/packages/base/Tooltip/src/Tooltip/use-tooltip-state.ts index 518d1e3c85..e20eab8570 100644 --- a/packages/base/Tooltip/src/Tooltip/use-tooltip-state.ts +++ b/packages/base/Tooltip/src/Tooltip/use-tooltip-state.ts @@ -102,7 +102,9 @@ export const useTooltipState = ({ // in this window belongs to the same gesture as the hover (which also triggers // a synchronous focus-open), so the trailing click must not read that transient // open and dismiss-then-latch it shut. Let hover win. - const openTimerRef = useRef>() + const openTimerRef = useRef | undefined>( + undefined + ) const touchOpenedRef = useRef(false) @@ -112,7 +114,9 @@ export const useTooltipState = ({ const moveStartRef = useRef<{ x: number; y: number } | null>(null) const followCursorHiddenRef = useRef(false) - const stopTimerRef = useRef>() + const stopTimerRef = useRef | undefined>( + undefined + ) useEffect(() => { // Input-modality tracking feeds the pointer-focus veto in shouldHonorOpen diff --git a/packages/base/TreeView/package.json b/packages/base/TreeView/package.json index d293f7a20b..9d2fdc8211 100644 --- a/packages/base/TreeView/package.json +++ b/packages/base/TreeView/package.json @@ -33,7 +33,7 @@ ], "peerDependencies": { "@toptal/picasso-tailwind": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "devDependencies": { "styled-components": "^6.1.1" diff --git a/packages/base/TreeView/src/TreeView/types.ts b/packages/base/TreeView/src/TreeView/types.ts index 853087dc27..0e89aa109f 100644 --- a/packages/base/TreeView/src/TreeView/types.ts +++ b/packages/base/TreeView/src/TreeView/types.ts @@ -1,5 +1,5 @@ -import type { RefObject } from 'react' import type React from 'react' +import type { NullableRefObject } from '@toptal/picasso-utils' import type { HierarchyPointNode } from 'd3-hierarchy' import type { ZoomBehavior } from 'd3-zoom' @@ -22,7 +22,7 @@ export interface TreeNodeInterface { } export type DynamicPointNode = HierarchyPointNode & { - ref: RefObject + ref: NullableRefObject rect: { width: number height: number diff --git a/packages/base/TreeView/src/TreeView/use-nodes.ts b/packages/base/TreeView/src/TreeView/use-nodes.ts index 82d4435d8c..11f8c4a7b9 100644 --- a/packages/base/TreeView/src/TreeView/use-nodes.ts +++ b/packages/base/TreeView/src/TreeView/use-nodes.ts @@ -86,7 +86,7 @@ export const useNodes = ( horizontalMargin: number ): DynamicPointNode[] => { const [initialized, setInitializedState] = useState(false) - const initialNodes = useRef() + const initialNodes = useRef(undefined) // we only need to prepare initial nodes once, on a first render if (!initialNodes.current) { diff --git a/packages/base/TreeView/src/TreeView/use-zoom.ts b/packages/base/TreeView/src/TreeView/use-zoom.ts index 85fac3b508..0753be78a6 100644 --- a/packages/base/TreeView/src/TreeView/use-zoom.ts +++ b/packages/base/TreeView/src/TreeView/use-zoom.ts @@ -6,7 +6,7 @@ import { zoom as d3zoom, select, zoomIdentity } from 'd3' export interface UseZoomArguments< ZoomRefElement extends ZoomedElementBaseType > { - rootRef: RefObject + rootRef: RefObject scaleExtent: [number, number] center?: { x: number; y: number } initialScale?: number diff --git a/packages/base/Typography/package.json b/packages/base/Typography/package.json index bc5856319c..5e33f910c2 100644 --- a/packages/base/Typography/package.json +++ b/packages/base/Typography/package.json @@ -32,7 +32,7 @@ "peerDependencies": { "@toptal/picasso-provider": "workspace:^", "@toptal/picasso-tailwind-merge": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/TypographyOverflow/package.json b/packages/base/TypographyOverflow/package.json index 2c688d3aba..833ea3165f 100644 --- a/packages/base/TypographyOverflow/package.json +++ b/packages/base/TypographyOverflow/package.json @@ -33,7 +33,7 @@ "peerDependencies": { "@toptal/picasso-tailwind": "workspace:^", "@toptal/picasso-tailwind-merge": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "devDependencies": { "@toptal/picasso-test-utils": "workspace:*", diff --git a/packages/base/UserBadge/package.json b/packages/base/UserBadge/package.json index 8b40b83205..89e60a4771 100644 --- a/packages/base/UserBadge/package.json +++ b/packages/base/UserBadge/package.json @@ -35,7 +35,7 @@ "peerDependencies": { "@toptal/picasso-tailwind": "workspace:^", "@toptal/picasso-tailwind-merge": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/base/Utils/package.json b/packages/base/Utils/package.json index 3ed8af2176..cd98e5fe9e 100644 --- a/packages/base/Utils/package.json +++ b/packages/base/Utils/package.json @@ -38,7 +38,7 @@ }, "peerDependencies": { "@toptal/picasso-provider": "workspace:^", - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "publishConfig": { "access": "public" diff --git a/packages/base/Utils/src/utils/__tests__/use-page-scroll-lock.test.tsx b/packages/base/Utils/src/utils/__tests__/use-page-scroll-lock.test.tsx index 51f1c9264f..81f13dca04 100644 --- a/packages/base/Utils/src/utils/__tests__/use-page-scroll-lock.test.tsx +++ b/packages/base/Utils/src/utils/__tests__/use-page-scroll-lock.test.tsx @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import { usePageScrollLock } from '../use-page-scroll-lock' diff --git a/packages/base/Utils/src/utils/__tests__/use-transition-status.test.tsx b/packages/base/Utils/src/utils/__tests__/use-transition-status.test.tsx index 6d31d94413..287e646cd5 100644 --- a/packages/base/Utils/src/utils/__tests__/use-transition-status.test.tsx +++ b/packages/base/Utils/src/utils/__tests__/use-transition-status.test.tsx @@ -1,6 +1,6 @@ +/* eslint-disable max-lines-per-function */ import React, { StrictMode } from 'react' -import { renderHook, act } from '@testing-library/react-hooks' -import { render, act as actDom } from '@testing-library/react' +import { renderHook, act, render } from '@testing-library/react' import type { UseTransitionStatusOptions } from '../use-transition-status' import useTransitionStatus, { @@ -20,11 +20,12 @@ const renderTransitionStatus = ( ...options, } - const result = renderHook(props => useTransitionStatus(props), { + const { result, rerender } = renderHook(props => useTransitionStatus(props), { initialProps, }) + const setIn = (inProp: boolean) => rerender({ ...initialProps, in: inProp }) - return { ...result, nodeRef, initialProps } + return { result, nodeRef, setIn } } describe('getTransitionTimeouts', () => { @@ -74,7 +75,7 @@ describe('useTransitionStatus', () => { it('is `exited` when mounted hidden', () => { const { result } = renderTransitionStatus({ in: false }) - expect(result.current).toBe('exited') + expect(result.current.status).toBe('exited') }) it('is `unmounted` when mounted hidden with `unmountOnExit`', () => { @@ -83,7 +84,7 @@ describe('useTransitionStatus', () => { unmountOnExit: true, }) - expect(result.current).toBe('unmounted') + expect(result.current.status).toBe('unmounted') }) it('is `entered` when mounted shown without `appear`', () => { @@ -95,7 +96,7 @@ describe('useTransitionStatus', () => { onEntered, }) - expect(result.current).toBe('entered') + expect(result.current.status).toBe('entered') act(() => { jest.runAllTimers() @@ -108,7 +109,7 @@ describe('useTransitionStatus', () => { it('is `entering` when mounted shown with `appear`', () => { const { result } = renderTransitionStatus({ in: true, appear: true }) - expect(result.current).toBe('entering') + expect(result.current.status).toBe('entering') }) }) @@ -132,7 +133,7 @@ describe('useTransitionStatus', () => { jest.runAllTimers() }) - expect(result.current).toBe('entered') + expect(result.current.status).toBe('entered') expect(onEntered).toHaveBeenCalledWith(nodeRef.current, true) }) @@ -155,22 +156,22 @@ describe('useTransitionStatus', () => { it('runs the enter transition when `in` flips to true', () => { const onEnter = jest.fn() const onEntered = jest.fn() - const { result, rerender, nodeRef, initialProps } = renderTransitionStatus({ + const { result, nodeRef, setIn } = renderTransitionStatus({ in: false, onEnter, onEntered, }) - rerender({ ...initialProps, in: true }) + setIn(true) - expect(result.current).toBe('entering') + expect(result.current.status).toBe('entering') expect(onEnter).toHaveBeenCalledWith(nodeRef.current, false) act(() => { jest.runAllTimers() }) - expect(result.current).toBe('entered') + expect(result.current.status).toBe('entered') expect(onEntered).toHaveBeenCalledWith(nodeRef.current, false) }) @@ -178,7 +179,7 @@ describe('useTransitionStatus', () => { const onExit = jest.fn() const onExiting = jest.fn() const onExited = jest.fn() - const { result, rerender, nodeRef, initialProps } = renderTransitionStatus({ + const { result, nodeRef, setIn } = renderTransitionStatus({ in: true, timeout: 300, onExit, @@ -186,9 +187,9 @@ describe('useTransitionStatus', () => { onExited, }) - rerender({ ...initialProps, in: false }) + setIn(false) - expect(result.current).toBe('exiting') + expect(result.current.status).toBe('exiting') expect(onExit).toHaveBeenCalledWith(nodeRef.current) expect(onExiting).toHaveBeenCalledWith(nodeRef.current) expect(onExited).not.toHaveBeenCalled() @@ -197,102 +198,102 @@ describe('useTransitionStatus', () => { jest.advanceTimersByTime(299) }) - expect(result.current).toBe('exiting') + expect(result.current.status).toBe('exiting') act(() => { jest.advanceTimersByTime(1) }) - expect(result.current).toBe('exited') + expect(result.current.status).toBe('exited') expect(onExited).toHaveBeenCalledTimes(1) expect(onExited).toHaveBeenCalledWith(nodeRef.current) }) it('cancels the pending settle when `in` flips mid-transition', () => { const onExited = jest.fn() - const { result, rerender, initialProps } = renderTransitionStatus({ + const { result, setIn } = renderTransitionStatus({ in: true, timeout: 300, onExited, }) - rerender({ ...initialProps, in: false }) + setIn(false) act(() => { jest.advanceTimersByTime(100) }) - rerender({ ...initialProps, in: true }) + setIn(true) act(() => { jest.runAllTimers() }) - expect(result.current).toBe('entered') + expect(result.current.status).toBe('entered') expect(onExited).not.toHaveBeenCalled() }) it('uses per-phase durations from an object `timeout`', () => { - const { result, rerender, initialProps } = renderTransitionStatus({ + const { result, setIn } = renderTransitionStatus({ in: false, timeout: { enter: 100, exit: 200 }, }) - rerender({ ...initialProps, in: true }) + setIn(true) act(() => { jest.advanceTimersByTime(99) }) - expect(result.current).toBe('entering') + expect(result.current.status).toBe('entering') act(() => { jest.advanceTimersByTime(1) }) - expect(result.current).toBe('entered') + expect(result.current.status).toBe('entered') - rerender({ ...initialProps, in: false }) + setIn(false) act(() => { jest.advanceTimersByTime(199) }) - expect(result.current).toBe('exiting') + expect(result.current.status).toBe('exiting') act(() => { jest.advanceTimersByTime(1) }) - expect(result.current).toBe('exited') + expect(result.current.status).toBe('exited') }) it('cycles through mount and unmount with `unmountOnExit`', () => { const onEnter = jest.fn() const onExited = jest.fn() - const { result, rerender, nodeRef, initialProps } = renderTransitionStatus({ + const { result, nodeRef, setIn } = renderTransitionStatus({ in: false, unmountOnExit: true, onEnter, onExited, }) - expect(result.current).toBe('unmounted') + expect(result.current.status).toBe('unmounted') - rerender({ ...initialProps, in: true }) + setIn(true) - expect(result.current).toBe('entering') + expect(result.current.status).toBe('entering') expect(onEnter).toHaveBeenCalledWith(nodeRef.current, false) act(() => { jest.runAllTimers() }) - expect(result.current).toBe('entered') + expect(result.current.status).toBe('entered') - rerender({ ...initialProps, in: false }) + setIn(false) - expect(result.current).toBe('exiting') + expect(result.current.status).toBe('exiting') act(() => { jest.runAllTimers() @@ -300,55 +301,86 @@ describe('useTransitionStatus', () => { expect(onExited).toHaveBeenCalledTimes(1) expect(onExited).toHaveBeenCalledWith(nodeRef.current) - expect(result.current).toBe('unmounted') + expect(result.current.status).toBe('unmounted') }) -}) -describe('useTransitionStatus under StrictMode', () => { - beforeEach(() => { - jest.useFakeTimers() - }) + describe('duration', () => { + it('follows the phase: `appear` on the mount transition, `exit` while hiding, `enter` afterwards', () => { + const { result, setIn } = renderTransitionStatus({ + in: true, + appear: true, + timeout: { enter: 100, exit: 200, appear: 500 }, + }) - afterEach(() => { - jest.useRealTimers() - }) + expect(result.current.duration).toBe(500) - // StrictMode replays the mount effect (setup -> cleanup -> setup); the - // cleanup cancels the pending settle timer, so the replay must reschedule - // it or an appear transition never leaves `entering` - it('settles an appear transition despite the double-invoked effect', () => { - const onEnter = jest.fn() - const onEntered = jest.fn() - const nodeRef = createNodeRef() + act(() => { + jest.runAllTimers() + }) + setIn(false) - const Probe = () => { - const status = useTransitionStatus({ - in: true, - appear: true, - timeout: 300, - nodeRef, - onEnter, - onEntered, + expect(result.current.duration).toBe(200) + + act(() => { + jest.runAllTimers() }) + setIn(true) - return
{status}
- } + expect(result.current.duration).toBe(100) + }) + + it('spreads a numeric `timeout` over every phase', () => { + const { result, setIn } = renderTransitionStatus({ + in: false, + timeout: 300, + }) - const { getByTestId } = render( - - - - ) + expect(result.current.duration).toBe(300) - expect(getByTestId('status').textContent).toBe('entering') - expect(onEnter).toHaveBeenCalledTimes(1) + setIn(true) - actDom(() => { - jest.advanceTimersByTime(300) + expect(result.current.duration).toBe(300) }) + }) - expect(getByTestId('status').textContent).toBe('entered') - expect(onEntered).toHaveBeenCalledTimes(1) - expect(onEntered).toHaveBeenCalledWith(nodeRef.current, true) + describe('under StrictMode', () => { + // StrictMode replays the mount effect (setup -> cleanup -> setup); the + // cleanup cancels the pending settle timer, so the replay must reschedule + // it or an appear transition never leaves `entering` + it('settles an appear transition despite the double-invoked effect', () => { + const onEnter = jest.fn() + const onEntered = jest.fn() + const nodeRef = createNodeRef() + + const Probe = () => { + const { status } = useTransitionStatus({ + in: true, + appear: true, + timeout: 300, + nodeRef, + onEnter, + onEntered, + }) + + return
{status}
+ } + + const { getByTestId } = render( + + + + ) + + expect(getByTestId('status').textContent).toBe('entering') + expect(onEnter).toHaveBeenCalledTimes(1) + + act(() => { + jest.advanceTimersByTime(300) + }) + + expect(getByTestId('status').textContent).toBe('entered') + expect(onEntered).toHaveBeenCalledTimes(1) + expect(onEntered).toHaveBeenCalledWith(nodeRef.current, true) + }) }) }) diff --git a/packages/base/Utils/src/utils/forward-ref.ts b/packages/base/Utils/src/utils/forward-ref.ts index 0c00dabe8d..e87aea63c7 100644 --- a/packages/base/Utils/src/utils/forward-ref.ts +++ b/packages/base/Utils/src/utils/forward-ref.ts @@ -14,7 +14,8 @@ export type Component = T & { * * @param component exotic component returned by `forwardRef` */ -export const documentable = (component: T): Component => component as Component +export const documentable = (component: T): Component => + component as Component /** * Wrapper around React.forwardRef that preserves genericity of the passed `Component`. @@ -33,6 +34,13 @@ export const documentable = (component: T): Component => component a export const forwardRef = ( component: (props: P, ref: React.Ref) => React.ReactElement | null ) => - React.forwardRef(component) as unknown as ( + // @types/react 19 phrases the render function's props as PropsWithoutRef

, + // which a bare generic P cannot satisfy; the runtime value is the same + React.forwardRef( + component as unknown as React.ForwardRefRenderFunction< + T, + React.PropsWithoutRef

+ > + ) as unknown as ( props: P & { ref?: React.Ref } ) => React.ReactElement | null diff --git a/packages/base/Utils/src/utils/get-element-ref.ts b/packages/base/Utils/src/utils/get-element-ref.ts index e92126eb34..b017712101 100644 --- a/packages/base/Utils/src/utils/get-element-ref.ts +++ b/packages/base/Utils/src/utils/get-element-ref.ts @@ -1,8 +1,7 @@ import type { ForwardedRef, ReactElement } from 'react' import React from 'react' -type PropsWithRef = { ref?: ForwardedRef } -type ElementWithRef = ReactElement & PropsWithRef +type WithRef = { ref?: ForwardedRef } // React 19 moved element refs into props — `element.ref` is deprecated there // and slated for removal — while earlier majors keep the ref on the element @@ -17,8 +16,8 @@ const isReact19OrNewer = Number.parseInt(React.version, 10) >= 19 */ const getElementRef = (element: ReactElement): ForwardedRef => { const ref = isReact19OrNewer - ? (element.props as PropsWithRef).ref - : (element as ElementWithRef).ref + ? (element.props as WithRef).ref + : (element as ReactElement & WithRef).ref return ref ?? null } diff --git a/packages/base/Utils/src/utils/get-react-node-text-content.ts b/packages/base/Utils/src/utils/get-react-node-text-content.ts index 4a090efb30..2d7fa81aec 100644 --- a/packages/base/Utils/src/utils/get-react-node-text-content.ts +++ b/packages/base/Utils/src/utils/get-react-node-text-content.ts @@ -1,4 +1,5 @@ import type { ReactNode } from 'react' +import { isValidElement } from 'react' /** * Extracts only the text contents of a ReactNode *recursively*. @@ -24,7 +25,7 @@ export const getReactNodeTextContent = (node: ReactNode): string => { return node.map(getReactNodeTextContent).filter(Boolean).join(' ') } - if (node != null && 'props' in node) { + if (isValidElement<{ children?: ReactNode }>(node)) { return getReactNodeTextContent(node.props.children) } diff --git a/packages/base/Utils/src/utils/index.ts b/packages/base/Utils/src/utils/index.ts index f6ced7b6dd..a6aeae9841 100644 --- a/packages/base/Utils/src/utils/index.ts +++ b/packages/base/Utils/src/utils/index.ts @@ -66,15 +66,14 @@ export { default as sum } from './sum' export type { ReferenceObject } from './use-width-of' export { default as useMultipleForwardRefs } from './use-multiple-forward-refs' export { default as getElementRef } from './get-element-ref' +export type { NullableRefObject } from './nullable-ref-object' export { usePageScrollLock } from './use-page-scroll-lock' -export { - default as useTransitionStatus, - getTransitionTimeouts, -} from './use-transition-status' +export { default as useTransitionStatus } from './use-transition-status' export type { TransitionStatus, TransitionTimeout, UseTransitionStatusOptions, + UseTransitionStatusResult, } from './use-transition-status' export { AVATAR_INITIALS_LIMIT } from './constants' diff --git a/packages/base/Utils/src/utils/nullable-ref-object.ts b/packages/base/Utils/src/utils/nullable-ref-object.ts new file mode 100644 index 0000000000..ba780ce3bd --- /dev/null +++ b/packages/base/Utils/src/utils/nullable-ref-object.ts @@ -0,0 +1,12 @@ +/** + * The ref object `useRef(null)` and `createRef()` return on every + * `@types/react` major: `RefObject` on 17, `RefObject` on 19. + * + * Declare a ref with it when the ref is both read and passed to a JSX `ref` + * prop. `RefObject` is the type for read-only receivers, but + * `@types/react` 17 rejects it as a JSX `ref`; `RefObject` is accepted + * there, but on 19 it claims a non-null `current`. + */ +export interface NullableRefObject { + readonly current: T | null +} diff --git a/packages/base/Utils/src/utils/use-multiple-forward-refs.ts b/packages/base/Utils/src/utils/use-multiple-forward-refs.ts index 9aedc5582f..baa4adaef0 100644 --- a/packages/base/Utils/src/utils/use-multiple-forward-refs.ts +++ b/packages/base/Utils/src/utils/use-multiple-forward-refs.ts @@ -31,7 +31,7 @@ const useMultipleForwardRefs = (refs: ForwardedRef[]) => forwardRef(ref, refValue) } }, - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps -- the deps are the refs themselves, spread so each one is compared; the rule cannot see through a spread [...refs] ) diff --git a/packages/base/Utils/src/utils/use-transition-status.ts b/packages/base/Utils/src/utils/use-transition-status.ts index b544de65a8..e7603289f7 100644 --- a/packages/base/Utils/src/utils/use-transition-status.ts +++ b/packages/base/Utils/src/utils/use-transition-status.ts @@ -1,5 +1,6 @@ import type { RefObject } from 'react' import { useRef, useState } from 'react' +import type { TransitionProps } from '@toptal/picasso-shared' import { useIsomorphicLayoutEffect } from '@toptal/picasso-shared' export type TransitionStatus = @@ -9,9 +10,8 @@ export type TransitionStatus = | 'exited' | 'unmounted' -export type TransitionTimeout = - | number - | { enter?: number; exit?: number; appear?: number } +/** The `timeout` prop every transition component takes: one duration, or one per phase */ +export type TransitionTimeout = NonNullable /** * Resolves a react-transition-group style `timeout` (number or object) @@ -41,43 +41,73 @@ export interface UseTransitionStatusOptions { /** Resolve to `unmounted` once fully exited, so the caller can render nothing */ unmountOnExit?: boolean /** The transitioning DOM element, passed to the lifecycle callbacks */ - nodeRef: RefObject + nodeRef: RefObject /** Fired when the enter phase starts */ onEnter?: (node: T, isAppearing: boolean) => void - /** Fired right after the status flips to `entering` */ + /** Fired right after `onEnter`, as the enter phase starts */ onEntering?: (node: T, isAppearing: boolean) => void /** Fired when the enter transition settles */ onEntered?: (node: T, isAppearing: boolean) => void /** Fired when the exit phase starts */ onExit?: (node: T) => void - /** Fired right after the status flips to `exiting` */ + /** Fired right after `onExit`, as the exit phase starts */ onExiting?: (node: T) => void /** Fired when the exit transition settles */ onExited?: (node: T) => void } +export interface UseTransitionStatusResult { + /** Where the element is in its transition */ + status: TransitionStatus + /** Duration of the running or upcoming phase in milliseconds, for the CSS `transition-duration` */ + duration: number +} + +interface TransitionState { + status: TransitionStatus + /** The mount transition is the `appear` one; the flag holds until the first exit */ + isAppearing: boolean +} + +type PendingSettle = + | { phase: 'enter'; isAppearing: boolean } + | { phase: 'exit' } + /** * Drop-in replacement for react-transition-group's `` state * machine. Settles on `setTimeout(timeout)` — not `transitionend` — so * callback timing is identical and fake timers drive it in jsdom tests. + * Returns the status together with the duration of the current phase, so + * the CSS transition and the settle timer never disagree. */ const useTransitionStatus = ( options: UseTransitionStatusOptions -): TransitionStatus => { - const { in: inProp, appear = false, unmountOnExit = false, nodeRef } = options +): UseTransitionStatusResult => { + const { + in: inProp, + appear = false, + unmountOnExit = false, + nodeRef, + timeout, + } = options + + const [{ status, isAppearing }, setTransition] = useState( + () => { + if (inProp) { + return { status: appear ? 'exited' : 'entered', isAppearing: appear } + } - const [status, setStatus] = useState(() => { - if (inProp) { - return appear ? 'exited' : 'entered' + return { + status: unmountOnExit ? 'unmounted' : 'exited', + isAppearing: false, + } } - - return unmountOnExit ? 'unmounted' : 'exited' - }) + ) // An unmounted element re-mounts at `exited` before entering // (render-phase derived state) if (inProp && status === 'unmounted') { - setStatus('exited') + setTransition(previous => ({ ...previous, status: 'exited' })) } // Read via ref so the transition effect re-runs only on `in` flips; @@ -89,9 +119,7 @@ const useTransitionStatus = ( }) const prevInRef = useRef(null) - const pendingSettleRef = useRef< - { phase: 'enter'; isAppearing: boolean } | { phase: 'exit' } | null - >(null) + const pendingSettleRef = useRef(null) useIsomorphicLayoutEffect(() => { const prevIn = prevInRef.current @@ -101,25 +129,25 @@ const useTransitionStatus = ( const timeouts = getTransitionTimeouts(optionsRef.current.timeout) - const scheduleEnterSettle = (isAppearing: boolean) => + const scheduleEnterSettle = (appearing: boolean) => setTimeout( () => { pendingSettleRef.current = null - setStatus('entered') + setTransition(previous => ({ ...previous, status: 'entered' })) const settledNode = nodeRef.current if (settledNode) { - optionsRef.current.onEntered?.(settledNode, isAppearing) + optionsRef.current.onEntered?.(settledNode, appearing) } }, - isAppearing ? timeouts.appear : timeouts.enter + appearing ? timeouts.appear : timeouts.enter ) const scheduleExitSettle = () => setTimeout(() => { pendingSettleRef.current = null - setStatus('exited') + setTransition(previous => ({ ...previous, status: 'exited' })) const settledNode = nodeRef.current @@ -156,21 +184,21 @@ const useTransitionStatus = ( const node = nodeRef.current const performEnter = () => { - const isAppearing = isInitialMount + const appearing = isInitialMount if (node) { - optionsRef.current.onEnter?.(node, isAppearing) + optionsRef.current.onEnter?.(node, appearing) } - setStatus('entering') + setTransition({ status: 'entering', isAppearing: appearing }) if (node) { - optionsRef.current.onEntering?.(node, isAppearing) + optionsRef.current.onEntering?.(node, appearing) } - pendingSettleRef.current = { phase: 'enter', isAppearing } + pendingSettleRef.current = { phase: 'enter', isAppearing: appearing } - return scheduleEnterSettle(isAppearing) + return scheduleEnterSettle(appearing) } const performExit = () => { @@ -178,7 +206,7 @@ const useTransitionStatus = ( optionsRef.current.onExit?.(node) } - setStatus('exiting') + setTransition({ status: 'exiting', isAppearing: false }) if (node) { optionsRef.current.onExiting?.(node) @@ -198,11 +226,15 @@ const useTransitionStatus = ( // Unmount one commit after `exited`, so `onExited` observes the node useIsomorphicLayoutEffect(() => { if (unmountOnExit && !inProp && status === 'exited') { - setStatus('unmounted') + setTransition(previous => ({ ...previous, status: 'unmounted' })) } }, [unmountOnExit, inProp, status]) - return status + const timeouts = getTransitionTimeouts(timeout) + const enterDuration = isAppearing ? timeouts.appear : timeouts.enter + const duration = inProp ? enterDuration : timeouts.exit + + return { status, duration } } export default useTransitionStatus diff --git a/packages/base/Utils/src/utils/useBoolean/test.tsx b/packages/base/Utils/src/utils/useBoolean/test.tsx index e8cf500f67..5931bb3107 100644 --- a/packages/base/Utils/src/utils/useBoolean/test.tsx +++ b/packages/base/Utils/src/utils/useBoolean/test.tsx @@ -1,4 +1,4 @@ -import { act, renderHook } from '@testing-library/react-hooks' +import { act, renderHook } from '@testing-library/react' import useBoolean from './use-boolean' diff --git a/packages/base/Utils/src/utils/useInterval/test.ts b/packages/base/Utils/src/utils/useInterval/test.ts index c61f04262c..58150dba70 100644 --- a/packages/base/Utils/src/utils/useInterval/test.ts +++ b/packages/base/Utils/src/utils/useInterval/test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import useInterval from './use-interval' diff --git a/packages/base/Utils/src/utils/useInterval/use-interval.ts b/packages/base/Utils/src/utils/useInterval/use-interval.ts index 60bf996990..f8983fc662 100644 --- a/packages/base/Utils/src/utils/useInterval/use-interval.ts +++ b/packages/base/Utils/src/utils/useInterval/use-interval.ts @@ -13,7 +13,7 @@ const useInterval = ({ delay, isPaused = false, }: UseIntervalOptions) => { - const intervalId = useRef() + const intervalId = useRef(undefined) const pauseInterval = useCallback(() => { if (intervalId.current) { diff --git a/packages/base/Utils/src/utils/useMouseEnter/test.ts b/packages/base/Utils/src/utils/useMouseEnter/test.ts index 55359cbc13..a480d323db 100644 --- a/packages/base/Utils/src/utils/useMouseEnter/test.ts +++ b/packages/base/Utils/src/utils/useMouseEnter/test.ts @@ -1,4 +1,4 @@ -import { renderHook, act } from '@testing-library/react-hooks' +import { renderHook, act } from '@testing-library/react' import useMouseEnter from './use-mouse-enter' diff --git a/packages/base/Utils/src/utils/useMouseEnter/use-mouse-enter.ts b/packages/base/Utils/src/utils/useMouseEnter/use-mouse-enter.ts index acc117a67f..467f6aaffa 100644 --- a/packages/base/Utils/src/utils/useMouseEnter/use-mouse-enter.ts +++ b/packages/base/Utils/src/utils/useMouseEnter/use-mouse-enter.ts @@ -1,7 +1,9 @@ import type { RefObject } from 'react' import { useEffect, useState } from 'react' -const useMouseEnter = (ref: RefObject): boolean => { +const useMouseEnter = ( + ref: RefObject +): boolean => { const [isMouseOver, setIsMouseOver] = useState(false) useEffect(() => { diff --git a/packages/base/Utils/src/utils/useOnScreen/use-on-screen.ts b/packages/base/Utils/src/utils/useOnScreen/use-on-screen.ts index 76e6d509b1..96c8bc23d5 100644 --- a/packages/base/Utils/src/utils/useOnScreen/use-on-screen.ts +++ b/packages/base/Utils/src/utils/useOnScreen/use-on-screen.ts @@ -1,7 +1,7 @@ import { useEffect, useState, useMemo } from 'react' interface UseOnScreenProps { - ref: React.RefObject + ref: React.RefObject root?: React.RefObject rootMargin?: string threshold?: number | number[] diff --git a/packages/picasso-charts/package.json b/packages/picasso-charts/package.json index 2dc5691d66..7bf3c08bcc 100644 --- a/packages/picasso-charts/package.json +++ b/packages/picasso-charts/package.json @@ -23,7 +23,7 @@ "homepage": "https://github.com/toptal/picasso/tree/master/packages/picasso-charts#readme", "peerDependencies": { "@toptal/picasso-shared": "workspace:^", - "react": ">=17.0.0 < 19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "typescript": "^5.5.0" }, "devDependencies": { diff --git a/packages/picasso-codemod/package.json b/packages/picasso-codemod/package.json index 8a9f1e2e94..a05745e06e 100644 --- a/packages/picasso-codemod/package.json +++ b/packages/picasso-codemod/package.json @@ -22,7 +22,7 @@ "url": "https://github.com/toptal/picasso/issues" }, "peerDependencies": { - "react": ">=17.0.0 < 19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "typescript": "^5.5.0" }, "devDependencies": { diff --git a/packages/picasso-forms/package.json b/packages/picasso-forms/package.json index bdc9aea9c7..ca5d772ff6 100644 --- a/packages/picasso-forms/package.json +++ b/packages/picasso-forms/package.json @@ -24,35 +24,35 @@ "peerDependencies": { "@toptal/picasso-shared": "workspace:^", "@toptal/picasso-tailwind": "workspace:^", - "react": ">=17.0.0 < 19.0.0", - "react-dom": ">=17.0.0 < 19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "typescript": "^5.5.0" }, "dependencies": { + "@toptal/picasso-autocomplete": "workspace:*", + "@toptal/picasso-avatar-upload": "workspace:*", + "@toptal/picasso-button": "workspace:*", + "@toptal/picasso-checkbox": "workspace:*", "@toptal/picasso-container": "workspace:*", - "@toptal/picasso-input": "workspace:*", + "@toptal/picasso-date-picker": "workspace:*", "@toptal/picasso-dropzone": "workspace:*", - "@toptal/picasso-button": "workspace:*", - "@toptal/picasso-password-input": "workspace:*", - "@toptal/picasso-radio": "workspace:*", - "@toptal/picasso-rating": "workspace:*", - "@toptal/picasso-icons": "workspace:*", + "@toptal/picasso-file-input": "workspace:*", "@toptal/picasso-form": "workspace:*", "@toptal/picasso-form-label": "workspace:*", - "@toptal/picasso-file-input": "workspace:*", - "@toptal/picasso-checkbox": "workspace:*", - "@toptal/picasso-outlined-input": "workspace:*", + "@toptal/picasso-icons": "workspace:*", + "@toptal/picasso-input": "workspace:*", + "@toptal/picasso-notification": "workspace:*", "@toptal/picasso-number-input": "workspace:*", + "@toptal/picasso-outlined-input": "workspace:*", + "@toptal/picasso-password-input": "workspace:*", + "@toptal/picasso-radio": "workspace:*", + "@toptal/picasso-rating": "workspace:*", + "@toptal/picasso-rich-text-editor": "workspace:*", + "@toptal/picasso-select": "workspace:*", "@toptal/picasso-switch": "workspace:*", "@toptal/picasso-tagselector": "workspace:*", "@toptal/picasso-timepicker": "workspace:*", - "@toptal/picasso-autocomplete": "workspace:*", - "@toptal/picasso-select": "workspace:*", - "@toptal/picasso-avatar-upload": "workspace:*", - "@toptal/picasso-date-picker": "workspace:*", - "@toptal/picasso-rich-text-editor": "workspace:*", "@toptal/picasso-utils": "workspace:*", - "@toptal/picasso-notification": "workspace:*", "classnames": "^2.5.1", "debounce": "^1.2.1", "detect-browser": "^5.3.0", @@ -63,7 +63,7 @@ "react-final-form-listeners": "^3.0.1" }, "devDependencies": { - "@testing-library/react-hooks": "^8.0.1", + "@testing-library/react": "^14.1.2", "@toptal/picasso-test-utils": "workspace:*", "@types/classnames": "^2.3.1", "type-fest": "^4.15.0" diff --git a/packages/picasso-forms/src/Checkbox/__snapshots__/test.tsx.snap b/packages/picasso-forms/src/Checkbox/__snapshots__/test.tsx.snap index 13316bd1ef..269247012a 100644 --- a/packages/picasso-forms/src/Checkbox/__snapshots__/test.tsx.snap +++ b/packages/picasso-forms/src/Checkbox/__snapshots__/test.tsx.snap @@ -157,7 +157,6 @@ exports[`Form.Checkbox default render for single checkbox 1`] = ` style="clip-path: inset(50%); overflow: hidden; white-space: nowrap; border: 0px; padding: 0px; width: 1px; height: 1px; margin: -1px; position: absolute;" tabindex="-1" type="checkbox" - value="" /> `; diff --git a/packages/picasso-forms/src/Field/Field.tsx b/packages/picasso-forms/src/Field/Field.tsx index 0f033ec2d0..de2814f013 100644 --- a/packages/picasso-forms/src/Field/Field.tsx +++ b/packages/picasso-forms/src/Field/Field.tsx @@ -23,11 +23,16 @@ export type FieldProps = FinalFieldProps< > & TextLabelProps +// The wrapped component's `children?: ReactNode` must not meet the render-prop +// `children` below: @types/react 19 no longer lets a function pass as a +// ReactNode. Distributive, so union props keep their variants. +type WithoutChildren = T extends unknown ? Omit : never + export type Props< TWrappedComponentProps extends IFormComponentProps, TInputValue -> = TWrappedComponentProps & - FieldProps & { +> = WithoutChildren & + Omit, 'children'> & { name: string type?: string label?: React.ReactNode diff --git a/packages/picasso-forms/src/Form/Form.tsx b/packages/picasso-forms/src/Form/Form.tsx index 14cad5e10a..449e80ecb7 100644 --- a/packages/picasso-forms/src/Form/Form.tsx +++ b/packages/picasso-forms/src/Form/Form.tsx @@ -129,20 +129,26 @@ export const Form = (props: Props) => { return ( ( - - {children} - - )} + render={renderProps => { + const { form, handleSubmit: handleFormRendererSubmit } = renderProps + + return ( + + {typeof children === 'function' + ? children(renderProps) + : children} + + ) + }} onSubmit={handleSubmit} decorators={[...decorators, scrollToErrorDecorator]} mutators={{ diff --git a/packages/picasso-forms/src/Form/__snapshots__/test.tsx.snap b/packages/picasso-forms/src/Form/__snapshots__/test.tsx.snap index 472b1d656f..7c514ffb99 100644 --- a/packages/picasso-forms/src/Form/__snapshots__/test.tsx.snap +++ b/packages/picasso-forms/src/Form/__snapshots__/test.tsx.snap @@ -27,7 +27,6 @@ exports[`Form renders 1`] = ` name="test" placeholder="test input" type="text" - value="" /> @@ -78,7 +77,6 @@ exports[`Form renders with an error 1`] = ` name="test" placeholder="test input" type="text" - value="" />

((props, ref) => { mutators: { setHasMultilineCounter }, } = useForm() - const { label, labelEndAdornment, titleCase, ...rest } = props + // `name` is passed explicitly rather than left in `rest`: under + // @types/react 19 `forwardRef` types `props` as `PropsWithoutRef`, and + // react-final-form's `[otherProp: string]: any` index signature collapses + // that `Omit` to a bare index signature, so the spread no longer proves the + // required `name` to `InputField`. Types 17 and 18 hand `Props` through + // untouched; the explicit prop satisfies all three. + const { label, labelEndAdornment, titleCase, name, ...rest } = props const { multiline, rows, rowsMax } = props const alignment = useMemo(() => { @@ -58,6 +64,7 @@ export const Input = React.forwardRef((props, ref) => { return ( {...rest} + name={name} setHasMultilineCounter={setHasMultilineCounter} label={ label ? ( diff --git a/packages/picasso-forms/src/NumberInput/test.tsx b/packages/picasso-forms/src/NumberInput/test.tsx index 829d41f3f9..c687a799af 100644 --- a/packages/picasso-forms/src/NumberInput/test.tsx +++ b/packages/picasso-forms/src/NumberInput/test.tsx @@ -36,9 +36,6 @@ describe('Form.NumberInput', () => { name: 'test-input', }) ) - // react-final-form v7 registers the field in an effect and re-renders - // once after mount (v6 registered during render and rendered once) - expect(numberInputMock).toHaveBeenCalledTimes(2) }) }) }) diff --git a/packages/picasso-forms/src/Rating/__snapshots__/test.tsx.snap b/packages/picasso-forms/src/Rating/__snapshots__/test.tsx.snap index e078bd743b..52f6202926 100644 --- a/packages/picasso-forms/src/Rating/__snapshots__/test.tsx.snap +++ b/packages/picasso-forms/src/Rating/__snapshots__/test.tsx.snap @@ -145,7 +145,6 @@ exports[`Rating Thumbs renders default Stars 1`] = ` id="stars-reset" name="stars" type="radio" - value="" />
diff --git a/packages/picasso-forms/src/utils/use-form-input-reset/test.ts b/packages/picasso-forms/src/utils/use-form-input-reset/test.ts index f7fbde387e..6252bfeb70 100644 --- a/packages/picasso-forms/src/utils/use-form-input-reset/test.ts +++ b/packages/picasso-forms/src/utils/use-form-input-reset/test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import useFormInputReset from './use-form-input-reset' diff --git a/packages/picasso-pictograms/package.json b/packages/picasso-pictograms/package.json index a2b54c0896..a78e22fda3 100644 --- a/packages/picasso-pictograms/package.json +++ b/packages/picasso-pictograms/package.json @@ -23,7 +23,7 @@ "homepage": "https://github.com/toptal/picasso/tree/master/packages/picasso-pictograms#readme", "peerDependencies": { "@toptal/picasso-shared": "workspace:^", - "react": ">=17.0.0 < 19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "typescript": "^5.5.0" }, "devDependencies": { diff --git a/packages/picasso-provider/package.json b/packages/picasso-provider/package.json index 27cd196c32..c66d5f6ae4 100644 --- a/packages/picasso-provider/package.json +++ b/packages/picasso-provider/package.json @@ -24,7 +24,7 @@ "url": "https://github.com/toptal/picasso/issues" }, "peerDependencies": { - "react": ">=17.0.0 < 19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "typescript": "^5.5.0" }, "dependencies": { diff --git a/packages/picasso-provider/src/Favicon/Favicon.tsx b/packages/picasso-provider/src/Favicon/Favicon.tsx index d74ef49b3d..647a44b51f 100644 --- a/packages/picasso-provider/src/Favicon/Favicon.tsx +++ b/packages/picasso-provider/src/Favicon/Favicon.tsx @@ -13,12 +13,10 @@ export interface Props { environment?: EnvironmentType<'test' | 'temploy'> } +type Icons = Awaited> + export const Favicon = ({ environment }: Props) => { - const [icons, setIcons] = useSafeState<{ - icon16?: string - icon32?: string - icon180?: string - }>({}) + const [icons, setIcons] = useSafeState(null) const { environment: configEnvironment } = useAppConfig() @@ -52,6 +50,13 @@ export const Favicon = ({ environment }: Props) => { return null } + // React 19 decides whether a `` can be hoisted into `` when it + // mounts, and one mounted with an undefined `href` stays in the body for + // good — so the links wait for the asynchronously loaded icons. + if (!icons) { + return null + } + const { icon16, icon32, icon180 } = icons // favicon.ico will be loaded automatically by browser from "public" folder diff --git a/packages/picasso-provider/src/Favicon/__snapshots__/test.tsx.snap b/packages/picasso-provider/src/Favicon/__snapshots__/test.tsx.snap deleted file mode 100644 index db0652bafa..0000000000 --- a/packages/picasso-provider/src/Favicon/__snapshots__/test.tsx.snap +++ /dev/null @@ -1,51 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Favicon renders 1`] = ` -NodeList [ - , - , - , -] -`; - -exports[`Favicon renders with environment specified 1`] = ` -NodeList [ - , - , - , -] -`; diff --git a/packages/picasso-provider/src/Favicon/test.tsx b/packages/picasso-provider/src/Favicon/test.tsx index e550844e8b..8b5b37a662 100644 --- a/packages/picasso-provider/src/Favicon/test.tsx +++ b/packages/picasso-provider/src/Favicon/test.tsx @@ -3,6 +3,9 @@ import { render as baseRender, waitFor } from '@testing-library/react' import Picasso from '../Picasso' import { Favicon } from './Favicon' +import { getIcons } from './icons' + +jest.mock('./icons') const render: typeof baseRender = ui => baseRender( @@ -16,25 +19,54 @@ const render: typeof baseRender = ui => ) +// Each link has to reach `` carrying its href — React 19 only hoists a +// link whose href exists when the element mounts, and the icons load +// asynchronously. +const expectFaviconLinks = async (environment: string) => { + await waitFor(() => { + expect(document.querySelectorAll('head > link')).toHaveLength(3) + }) + + const linkFor = (selector: string) => + document.querySelector(`head > link${selector}`) + + expect(linkFor('[rel="apple-touch-icon"][sizes="180x180"]')).toHaveAttribute( + 'href', + `${environment}-180` + ) + expect( + linkFor('[rel="icon"][type="image/png"][sizes="32x32"]') + ).toHaveAttribute('href', `${environment}-32`) + expect( + linkFor('[rel="icon"][type="image/png"][sizes="16x16"]') + ).toHaveAttribute('href', `${environment}-16`) +} + describe('Favicon', () => { - it('renders', async () => { - render() + // The real icons all resolve to the same file stub under jest, which would + // make every environment look alike, so the loader answers with hrefs that + // carry the environment it was asked for. Set per test: the jest config + // resets mock implementations between tests. + beforeEach(() => { + jest.mocked(getIcons).mockImplementation(async environment => ({ + icon16: `${environment}-16`, + icon32: `${environment}-32`, + icon180: `${environment}-180`, + })) + }) - await waitFor(() => { - expect(document.querySelectorAll('head > link')).not.toHaveLength(0) - }) + it('renders the icons of the provider environment', async () => { + render() - expect(document.querySelectorAll('head > link')).toMatchSnapshot() + await expectFaviconLinks('development') + expect(getIcons).toHaveBeenCalledWith('development') }) - it('renders with environment specified', async () => { + it('renders the icons of the environment prop over the provider one', async () => { render() - await waitFor(() => { - expect(document.querySelectorAll('head > link')).not.toHaveLength(0) - }) - - expect(document.querySelectorAll('head > link')).toMatchSnapshot() + await expectFaviconLinks('staging') + expect(getIcons).toHaveBeenCalledWith('staging') }) it('renders nothing in a test environment', async () => { @@ -42,5 +74,6 @@ describe('Favicon', () => { const picassoRoot = container.firstChild as Element expect(picassoRoot.children).toHaveLength(0) + expect(getIcons).not.toHaveBeenCalled() }) }) diff --git a/packages/picasso-provider/src/Picasso/FixViewport.test.tsx b/packages/picasso-provider/src/Picasso/FixViewport.test.tsx index dbd2bb7c34..fda22f996e 100644 --- a/packages/picasso-provider/src/Picasso/FixViewport.test.tsx +++ b/packages/picasso-provider/src/Picasso/FixViewport.test.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { render, waitFor } from '@testing-library/react' +import { cleanup, render, waitFor } from '@testing-library/react' import { HelmetProvider } from 'react-helmet-async' import FixViewport from './FixViewport' @@ -18,6 +18,10 @@ const getViewportContent = () => describe('FixViewport', () => { afterEach(() => { + // Unmount first: on React 19 the viewport meta is hoisted into `` + // by React itself, so removing it by hand before unmount leaves React + // deleting a node that no longer has a parent. + cleanup() document .querySelectorAll('meta[name="viewport"]') .forEach(tag => tag.remove()) diff --git a/packages/picasso-provider/src/Picasso/RootContext.ts b/packages/picasso-provider/src/Picasso/RootContext.ts index 13a6097b62..576cef8e92 100644 --- a/packages/picasso-provider/src/Picasso/RootContext.ts +++ b/packages/picasso-provider/src/Picasso/RootContext.ts @@ -5,7 +5,7 @@ import type { TextLabelProps, EnvironmentType } from '../types' import type { BreakpointKeys } from './config' export interface RootContextProps extends TextLabelProps { - rootRef?: RefObject + rootRef?: RefObject hasTopBar: boolean setHasTopBar: (value: boolean) => void hasSidebar: boolean diff --git a/packages/picasso-query-builder/package.json b/packages/picasso-query-builder/package.json index 83887ce298..885b055a14 100644 --- a/packages/picasso-query-builder/package.json +++ b/packages/picasso-query-builder/package.json @@ -23,28 +23,28 @@ }, "peerDependencies": { "@toptal/picasso-provider": "workspace:^", - "react": ">=17.0.0 < 19.0.0", "@toptal/picasso-tailwind": "workspace:^", - "react-dom": ">=17.0.0 < 19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "typescript": "^5.5.0" }, "dependencies": { "@react-querybuilder/dnd": "6.5.4", "@toptal/picasso-button": "workspace:*", - "@toptal/picasso-select": "workspace:*", - "@toptal/picasso-typography": "workspace:*", "@toptal/picasso-container": "workspace:*", - "@toptal/picasso-radio": "workspace:*", "@toptal/picasso-icons": "workspace:*", - "@toptal/picasso-tooltip": "workspace:*", - "@toptal/picasso-tagselector": "workspace:*", "@toptal/picasso-input": "workspace:*", "@toptal/picasso-list": "workspace:*", "@toptal/picasso-loader": "workspace:*", + "@toptal/picasso-notification": "workspace:*", "@toptal/picasso-number-input": "workspace:*", "@toptal/picasso-prompt-modal": "workspace:*", - "@toptal/picasso-notification": "workspace:*", + "@toptal/picasso-radio": "workspace:*", + "@toptal/picasso-select": "workspace:*", + "@toptal/picasso-tagselector": "workspace:*", "@toptal/picasso-tailwind-merge": "workspace:*", + "@toptal/picasso-tooltip": "workspace:*", + "@toptal/picasso-typography": "workspace:*", "@toptal/picasso-utils": "workspace:*", "classnames": "^2.5.1", "react-dnd": "^16.0.1", @@ -53,7 +53,7 @@ "use-debounce": "^10.0.0" }, "devDependencies": { - "@testing-library/react-hooks": "^8.0.1", + "@testing-library/react": "^14.1.2", "@toptal/picasso-test-utils": "workspace:*", "@types/classnames": "^2.3.1" }, diff --git a/packages/picasso-query-builder/src/CombinatorSelector/CombinatorSelector.test.tsx b/packages/picasso-query-builder/src/CombinatorSelector/CombinatorSelector.test.tsx index 3a36b4ad09..cd9f99805d 100644 --- a/packages/picasso-query-builder/src/CombinatorSelector/CombinatorSelector.test.tsx +++ b/packages/picasso-query-builder/src/CombinatorSelector/CombinatorSelector.test.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { render } from '@toptal/picasso-test-utils' +import { render, renderedProps } from '@toptal/picasso-test-utils' import type { ComponentProps } from 'react' import { Typography } from '@toptal/picasso-typography' @@ -51,9 +51,8 @@ describe('CombinatorSelector', () => { }) expect(TypographyMock).toHaveBeenCalledTimes(1) - expect(TypographyMock).toHaveBeenCalledWith( - expect.objectContaining({ children: 'Query' }), - {} + expect(renderedProps(TypographyMock)).toContainEqual( + expect.objectContaining({ children: 'Query' }) ) }) }) @@ -65,10 +64,6 @@ describe('CombinatorSelector', () => { }) expect(TypographyMock).toHaveBeenCalledTimes(0) - expect(TypographyMock).not.toHaveBeenCalledWith( - expect.objectContaining({ children: 'Query' }), - {} - ) }) }) }) diff --git a/packages/picasso-query-builder/src/CombinatorSelector/RadioOptions.test.tsx b/packages/picasso-query-builder/src/CombinatorSelector/RadioOptions.test.tsx index e6b079ae5d..27b8460d6e 100644 --- a/packages/picasso-query-builder/src/CombinatorSelector/RadioOptions.test.tsx +++ b/packages/picasso-query-builder/src/CombinatorSelector/RadioOptions.test.tsx @@ -1,6 +1,6 @@ import React from 'react' import { Radio } from '@toptal/picasso-radio' -import { render } from '@toptal/picasso-test-utils' +import { render, renderedProps } from '@toptal/picasso-test-utils' import type { OptionGroup, Option } from 'react-querybuilder' import { RadioOptions } from './RadioOptions' @@ -43,23 +43,15 @@ describe('RadioOptions', () => { it('renders radio with correct properties', () => { render(<>{RadioOptions({ options: optionGroups })}) - expect(RadioMock).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ - value: 'and', - label: 'AND', - }), - {} - ) + expect(renderedProps(RadioMock)[0]).toMatchObject({ + value: 'and', + label: 'AND', + }) - expect(RadioMock).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - value: 'or', - label: 'OR', - }), - {} - ) + expect(renderedProps(RadioMock)[1]).toMatchObject({ + value: 'or', + label: 'OR', + }) }) }) @@ -67,23 +59,15 @@ describe('RadioOptions', () => { it('renders radio with correct properties', () => { render(<>{RadioOptions({ options })}) - expect(RadioMock).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ - value: 'and', - label: 'AND', - }), - {} - ) + expect(renderedProps(RadioMock)[0]).toMatchObject({ + value: 'and', + label: 'AND', + }) - expect(RadioMock).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - value: 'or', - label: 'OR', - }), - {} - ) + expect(renderedProps(RadioMock)[1]).toMatchObject({ + value: 'or', + label: 'OR', + }) }) }) }) diff --git a/packages/picasso-query-builder/src/RangeInput/RangeInput.test.tsx b/packages/picasso-query-builder/src/RangeInput/RangeInput.test.tsx index eb5a70febd..01faa6d72f 100644 --- a/packages/picasso-query-builder/src/RangeInput/RangeInput.test.tsx +++ b/packages/picasso-query-builder/src/RangeInput/RangeInput.test.tsx @@ -2,7 +2,7 @@ import React from 'react' import { Container } from '@toptal/picasso-container' import { NumberInput } from '@toptal/picasso-number-input' import { Typography } from '@toptal/picasso-typography' -import { render } from '@toptal/picasso-test-utils' +import { render, renderedProps } from '@toptal/picasso-test-utils' import type { ComponentProps } from 'react' import { RangeInput } from './' @@ -43,18 +43,16 @@ describe('RangeInput', () => { max: 1000, }) - expect(NumberInputMock).toHaveBeenCalledWith( + expect(renderedProps(NumberInputMock)).toContainEqual( expect.objectContaining({ value: 0, - }), - {} + }) ) - expect(NumberInputMock).toHaveBeenCalledWith( + expect(renderedProps(NumberInputMock)).toContainEqual( expect.objectContaining({ value: 100, - }), - {} + }) ) }) }) @@ -74,18 +72,16 @@ describe('RangeInput', () => { }, }) - expect(NumberInputMock).toHaveBeenCalledWith( + expect(renderedProps(NumberInputMock)).toContainEqual( expect.objectContaining({ status: 'error', - }), - {} + }) ) - expect(NumberInputMock).toHaveBeenCalledWith( + expect(renderedProps(NumberInputMock)).toContainEqual( expect.objectContaining({ status: 'default', - }), - {} + }) ) }) }) @@ -102,18 +98,16 @@ describe('RangeInput', () => { validation: { reasons: [undefined, 'Invalid to value'], valid: false }, }) - expect(NumberInputMock).toHaveBeenCalledWith( + expect(renderedProps(NumberInputMock)).toContainEqual( expect.objectContaining({ status: 'default', - }), - {} + }) ) - expect(NumberInputMock).toHaveBeenCalledWith( + expect(renderedProps(NumberInputMock)).toContainEqual( expect.objectContaining({ status: 'error', - }), - {} + }) ) }) }) @@ -133,18 +127,16 @@ describe('RangeInput', () => { }, }) - expect(NumberInputMock).toHaveBeenCalledWith( + expect(renderedProps(NumberInputMock)).toContainEqual( expect.objectContaining({ status: 'error', - }), - {} + }) ) - expect(NumberInputMock).toHaveBeenCalledWith( + expect(renderedProps(NumberInputMock)).toContainEqual( expect.objectContaining({ status: 'error', - }), - {} + }) ) }) }) diff --git a/packages/picasso-query-builder/src/ValidationErrors/ValidationErrors.test.tsx b/packages/picasso-query-builder/src/ValidationErrors/ValidationErrors.test.tsx index 6d60c62bbc..bfb9a15ad3 100644 --- a/packages/picasso-query-builder/src/ValidationErrors/ValidationErrors.test.tsx +++ b/packages/picasso-query-builder/src/ValidationErrors/ValidationErrors.test.tsx @@ -2,7 +2,7 @@ import React, { Fragment } from 'react' import type { ComponentProps } from 'react' import { Container } from '@toptal/picasso-container' import { List } from '@toptal/picasso-list' -import { render } from '@toptal/picasso-test-utils' +import { render, renderedProps } from '@toptal/picasso-test-utils' import { ValidationErrors } from './' @@ -90,17 +90,15 @@ describe('ValidationErrors', () => { expect(ListMock).toHaveBeenCalledTimes(1) expect(ListItemMock).toHaveBeenCalledTimes(2) - expect(ListItemMock).toHaveBeenCalledWith( + expect(renderedProps(ListItemMock)).toContainEqual( expect.objectContaining({ children: 'reason1', - }), - {} + }) ) - expect(ListItemMock).toHaveBeenCalledWith( + expect(renderedProps(ListItemMock)).toContainEqual( expect.objectContaining({ children: 'reason2', - }), - {} + }) ) }) }) diff --git a/packages/picasso-query-builder/src/utils/use-handle-touched.test.ts b/packages/picasso-query-builder/src/utils/use-handle-touched.test.ts index b6dc7ad75a..01cf0609e8 100644 --- a/packages/picasso-query-builder/src/utils/use-handle-touched.test.ts +++ b/packages/picasso-query-builder/src/utils/use-handle-touched.test.ts @@ -1,4 +1,4 @@ -import { act, renderHook } from '@testing-library/react-hooks' +import { act, renderHook } from '@testing-library/react' import useHandleTouched from './use-handle-touched' diff --git a/packages/picasso-query-builder/src/utils/use-query-builder-validation.test.ts b/packages/picasso-query-builder/src/utils/use-query-builder-validation.test.ts index 50f2920332..8bcc416b47 100644 --- a/packages/picasso-query-builder/src/utils/use-query-builder-validation.test.ts +++ b/packages/picasso-query-builder/src/utils/use-query-builder-validation.test.ts @@ -1,4 +1,4 @@ -import { act, renderHook } from '@testing-library/react-hooks' +import { act, renderHook } from '@testing-library/react' import type { RuleGroupTypeAny, RuleType } from 'react-querybuilder' import type { Field } from '../types/query-builder' diff --git a/packages/picasso-rich-text-editor/package.json b/packages/picasso-rich-text-editor/package.json index 933187b1d8..a1f48ae065 100644 --- a/packages/picasso-rich-text-editor/package.json +++ b/packages/picasso-rich-text-editor/package.json @@ -29,30 +29,30 @@ "@lexical/text": "0.11.3", "@lexical/utils": "0.11.3", "@toptal/picasso-shared": "workspace:^", - "react": ">=17.0.0 < 19.0.0", - "react-dom": ">=17.0.0 < 19.0.0", - "typescript": "^5.5.0", + "@toptal/picasso-tailwind": "workspace:^", "@toptal/picasso-tailwind-merge": "workspace:^", - "@toptal/picasso-tailwind": "workspace:^" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "typescript": "^5.5.0" }, "dependencies": { "@emoji-mart/data": "^1.2.1", "@lexical/html": "0.11.3", "@lexical/react": "0.11.3", - "@toptal/picasso-container": "workspace:*", "@toptal/picasso-button": "workspace:*", - "@toptal/picasso-icons": "workspace:*", + "@toptal/picasso-container": "workspace:*", "@toptal/picasso-file-input": "workspace:*", + "@toptal/picasso-form": "workspace:*", + "@toptal/picasso-icons": "workspace:*", "@toptal/picasso-image": "workspace:*", - "@toptal/picasso-outlined-input": "workspace:*", - "@toptal/picasso-list": "workspace:*", + "@toptal/picasso-input": "workspace:*", + "@toptal/picasso-input-adornment": "workspace:*", "@toptal/picasso-link": "workspace:*", + "@toptal/picasso-list": "workspace:*", + "@toptal/picasso-modal": "workspace:*", + "@toptal/picasso-outlined-input": "workspace:*", "@toptal/picasso-select": "workspace:*", - "@toptal/picasso-input-adornment": "workspace:*", "@toptal/picasso-typography": "workspace:*", - "@toptal/picasso-modal": "workspace:*", - "@toptal/picasso-form": "workspace:*", - "@toptal/picasso-input": "workspace:*", "@toptal/picasso-utils": "workspace:*", "classnames": "^2.5.1", "emoji-mart": "^5.5.2", @@ -64,12 +64,12 @@ "lexical": "0.11.3" }, "devDependencies": { - "@types/jsdom": "^21.1.7", - "jsdom": "^26.1.0", - "@testing-library/react-hooks": "^8.0.1", - "@toptal/picasso-test-utils": "workspace:*", + "@testing-library/react": "^14.1.2", "@toptal/picasso-tailwind-merge": "workspace:*", - "@types/classnames": "^2.3.1" + "@toptal/picasso-test-utils": "workspace:*", + "@types/classnames": "^2.3.1", + "@types/jsdom": "^21.1.7", + "jsdom": "^26.1.0" }, "sideEffects": false, "exports": { diff --git a/packages/picasso-rich-text-editor/src/LexicalEditor/LexicalEditor.test.tsx b/packages/picasso-rich-text-editor/src/LexicalEditor/LexicalEditor.test.tsx index 4f8a5c7249..21ed5e4522 100644 --- a/packages/picasso-rich-text-editor/src/LexicalEditor/LexicalEditor.test.tsx +++ b/packages/picasso-rich-text-editor/src/LexicalEditor/LexicalEditor.test.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { render, waitFor } from '@toptal/picasso-test-utils' +import { render, renderedProps, waitFor } from '@toptal/picasso-test-utils' import type { OmitInternalProps } from '@toptal/picasso-shared' import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin' import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin' @@ -131,12 +131,9 @@ describe('LexicalEditor', () => { renderLexicalEditor() await waitFor(() => - expect(mockedLexicalTextLengthPlugin).toHaveBeenCalledWith( - { - onTextLengthChange, - }, - {} - ) + expect(renderedProps(mockedLexicalTextLengthPlugin)[0]).toEqual({ + onTextLengthChange, + }) ) }) @@ -157,27 +154,21 @@ describe('LexicalEditor', () => { it('renders OnChangePlugin with correct props', () => { renderLexicalEditor() - expect(mockedOnChangePlugin).toHaveBeenCalledWith( - { - ignoreSelectionChange: true, - onChange: expect.any(Function), - }, - {} - ) + expect(renderedProps(mockedOnChangePlugin)[0]).toEqual({ + ignoreSelectionChange: true, + onChange: expect.any(Function), + }) }) it('renders ToolbarPlugin with correct props', () => { renderLexicalEditor() - expect(mockedToolbarPlugin).toHaveBeenCalledWith( - { - id: 'id', - toolbarRef: { - current: null, - }, + expect(renderedProps(mockedToolbarPlugin)[0]).toEqual({ + id: 'id', + toolbarRef: { + current: null, }, - {} - ) + }) }) }) @@ -188,15 +179,12 @@ describe('LexicalEditor', () => { plugins: ['link'], }) - expect(mockedToolbarPlugin).toHaveBeenCalledWith( - { - id: 'id', - toolbarRef: { - current: null, - }, + expect(renderedProps(mockedToolbarPlugin)[0]).toEqual({ + id: 'id', + toolbarRef: { + current: null, }, - {} - ) + }) }) }) }) diff --git a/packages/picasso-rich-text-editor/src/LexicalEditor/LexicalEditor.tsx b/packages/picasso-rich-text-editor/src/LexicalEditor/LexicalEditor.tsx index d729639e16..0885455c72 100644 --- a/packages/picasso-rich-text-editor/src/LexicalEditor/LexicalEditor.tsx +++ b/packages/picasso-rich-text-editor/src/LexicalEditor/LexicalEditor.tsx @@ -14,7 +14,7 @@ import { Container } from '@toptal/picasso-container' import { Typography } from '@toptal/picasso-typography' import type { BaseProps } from '@toptal/picasso-shared' import { noop } from '@toptal/picasso-utils' -import type { LexicalEditor as LexicalEditorType } from 'lexical' +import type { EditorState, LexicalEditor as LexicalEditorType } from 'lexical' import { $getRoot } from 'lexical' import ToolbarPlugin from '../LexicalEditorToolbarPlugin' @@ -141,7 +141,7 @@ const LexicalEditor = forwardRef(function LexicalEditor( const classes = styles - const toolbarRef = useRef(null) + const toolbarRef = useRef(null) const theme = useLexicalTheme(classes) @@ -166,7 +166,7 @@ const LexicalEditor = forwardRef(function LexicalEditor( ) const handleChange = useCallback( - (editorState, editor) => { + (editorState: EditorState, editor: LexicalEditorType) => { editorState.read(() => { const root = $getRoot() const topLevelChildren = root.getChildren() diff --git a/packages/picasso-rich-text-editor/src/LexicalEditor/hooks/useOnFocus/test.ts b/packages/picasso-rich-text-editor/src/LexicalEditor/hooks/useOnFocus/test.ts index 22e5635123..2971689301 100644 --- a/packages/picasso-rich-text-editor/src/LexicalEditor/hooks/useOnFocus/test.ts +++ b/packages/picasso-rich-text-editor/src/LexicalEditor/hooks/useOnFocus/test.ts @@ -1,4 +1,4 @@ -import { renderHook, act } from '@testing-library/react-hooks' +import { renderHook, act } from '@testing-library/react' import useOnFocus from './use-on-focus' diff --git a/packages/picasso-rich-text-editor/src/LexicalEditor/hooks/useOnFocus/use-on-focus.ts b/packages/picasso-rich-text-editor/src/LexicalEditor/hooks/useOnFocus/use-on-focus.ts index f10c0009cf..1cb7a24388 100644 --- a/packages/picasso-rich-text-editor/src/LexicalEditor/hooks/useOnFocus/use-on-focus.ts +++ b/packages/picasso-rich-text-editor/src/LexicalEditor/hooks/useOnFocus/use-on-focus.ts @@ -2,7 +2,7 @@ import { useCallback, useState } from 'react' import { noop } from '@toptal/picasso-utils' export type Props = { - internalRefs?: React.RefObject[] + internalRefs?: React.RefObject[] onFocus?: () => void onBlur?: () => void } diff --git a/packages/picasso-rich-text-editor/src/LexicalEditor/hooks/useTypographyClasses/test.ts b/packages/picasso-rich-text-editor/src/LexicalEditor/hooks/useTypographyClasses/test.ts index cda448cded..4f4d1133f9 100644 --- a/packages/picasso-rich-text-editor/src/LexicalEditor/hooks/useTypographyClasses/test.ts +++ b/packages/picasso-rich-text-editor/src/LexicalEditor/hooks/useTypographyClasses/test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import type { TypographyOptions } from '../../../utils' import { getTypographyClassName } from '../../../utils' diff --git a/packages/picasso-rich-text-editor/src/LexicalEditorToolbarPlugin/LexicalEditorToolbarPlugin.tsx b/packages/picasso-rich-text-editor/src/LexicalEditorToolbarPlugin/LexicalEditorToolbarPlugin.tsx index 100bf81db8..6cf3cf34fd 100644 --- a/packages/picasso-rich-text-editor/src/LexicalEditorToolbarPlugin/LexicalEditorToolbarPlugin.tsx +++ b/packages/picasso-rich-text-editor/src/LexicalEditorToolbarPlugin/LexicalEditorToolbarPlugin.tsx @@ -14,6 +14,7 @@ import { import type { ChangeEvent } from 'react' import React, { useEffect, useReducer } from 'react' import { $createHeadingNode, $isHeadingNode } from '@lexical/rich-text' +import type { NullableRefObject } from '@toptal/picasso-utils' import { registerLexicalEvents, @@ -30,7 +31,7 @@ import { $isCodeBlockNode } from '../plugins/CodeBlockPlugin/nodes' type Props = { id: string - toolbarRef: React.RefObject + toolbarRef: NullableRefObject testIds?: { wrapper?: string editor?: string diff --git a/packages/picasso-rich-text-editor/src/RichText/hooks/useRichText/test.tsx b/packages/picasso-rich-text-editor/src/RichText/hooks/useRichText/test.tsx index db02953515..d7982b1225 100644 --- a/packages/picasso-rich-text-editor/src/RichText/hooks/useRichText/test.tsx +++ b/packages/picasso-rich-text-editor/src/RichText/hooks/useRichText/test.tsx @@ -1,5 +1,5 @@ import type { ReactElement } from 'react' -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import type { ASTType } from '../../types' import useRichText from './useRichText' diff --git a/packages/picasso-rich-text-editor/src/RichText/hooks/useRichText/useRichText.tsx b/packages/picasso-rich-text-editor/src/RichText/hooks/useRichText/useRichText.tsx index 51f89982a1..eab68d0952 100644 --- a/packages/picasso-rich-text-editor/src/RichText/hooks/useRichText/useRichText.tsx +++ b/packages/picasso-rich-text-editor/src/RichText/hooks/useRichText/useRichText.tsx @@ -93,7 +93,7 @@ const componentMap: Record = { } as const const picassoMapper = (child: ReactNode): ReactNode => { - if (!isValidElement(child)) { + if (!isValidElement<{ children?: ReactNode[] }>(child)) { return child } @@ -114,7 +114,9 @@ const useRichText = (value: ASTType): ReactNode[] | ReactNode => { } const isSingleChild = astChildren.length === 1 - const reactElement = toH(createElement, value) as ReactElement + const reactElement = toH(createElement, value) as ReactElement<{ + children: ReactNode[] + }> if (isSingleChild) { return picassoMapper(reactElement) diff --git a/packages/picasso-rich-text-editor/src/RichTextEditor/hooks/useCounter/test.ts b/packages/picasso-rich-text-editor/src/RichTextEditor/hooks/useCounter/test.ts index 31b59805f0..340f196e3f 100644 --- a/packages/picasso-rich-text-editor/src/RichTextEditor/hooks/useCounter/test.ts +++ b/packages/picasso-rich-text-editor/src/RichTextEditor/hooks/useCounter/test.ts @@ -1,4 +1,4 @@ -import { act, renderHook } from '@testing-library/react-hooks' +import { act, renderHook } from '@testing-library/react' import useCounter from './useCounter' import { RichTextEditor } from '../../RichTextEditor' diff --git a/packages/picasso-rich-text-editor/src/RichTextEditor/hooks/useOnFocus/test.ts b/packages/picasso-rich-text-editor/src/RichTextEditor/hooks/useOnFocus/test.ts index 42a0a7fd8e..d25d86cb2c 100644 --- a/packages/picasso-rich-text-editor/src/RichTextEditor/hooks/useOnFocus/test.ts +++ b/packages/picasso-rich-text-editor/src/RichTextEditor/hooks/useOnFocus/test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import { act } from '@toptal/picasso-test-utils' import type React from 'react' diff --git a/packages/picasso-rich-text-editor/src/RichTextEditor/hooks/useOnFocus/useOnFocus.tsx b/packages/picasso-rich-text-editor/src/RichTextEditor/hooks/useOnFocus/useOnFocus.tsx index d82e3ddee7..b211b04277 100644 --- a/packages/picasso-rich-text-editor/src/RichTextEditor/hooks/useOnFocus/useOnFocus.tsx +++ b/packages/picasso-rich-text-editor/src/RichTextEditor/hooks/useOnFocus/useOnFocus.tsx @@ -6,7 +6,7 @@ import type { ActionsType } from '../../store' type Props = { autoFocus: boolean editorRef: React.RefObject - toolbarRef: React.RefObject + toolbarRef: React.RefObject wrapperRef: React.RefObject onFocus: () => void onBlur: () => void diff --git a/packages/picasso-rich-text-editor/src/RichTextEditor/hooks/useTextEditorState/test.ts b/packages/picasso-rich-text-editor/src/RichTextEditor/hooks/useTextEditorState/test.ts index bedcf645d5..1933940d8b 100644 --- a/packages/picasso-rich-text-editor/src/RichTextEditor/hooks/useTextEditorState/test.ts +++ b/packages/picasso-rich-text-editor/src/RichTextEditor/hooks/useTextEditorState/test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' +import { renderHook } from '@testing-library/react' import { act } from '@toptal/picasso-test-utils' import useTextEditorState from './useTextEditorState' diff --git a/packages/picasso-rich-text-editor/src/RichTextEditor/test.tsx b/packages/picasso-rich-text-editor/src/RichTextEditor/test.tsx index 53779a2f1a..907bd6ebe6 100644 --- a/packages/picasso-rich-text-editor/src/RichTextEditor/test.tsx +++ b/packages/picasso-rich-text-editor/src/RichTextEditor/test.tsx @@ -1,6 +1,6 @@ import React from 'react' import type { OmitInternalProps } from '@toptal/picasso-shared' -import { render } from '@toptal/picasso-test-utils' +import { render, renderedProps } from '@toptal/picasso-test-utils' import { InputMultilineAdornment } from '@toptal/picasso-input-adornment' import { RichTextEditor } from './RichTextEditor' @@ -77,22 +77,19 @@ describe('RichTextEditor', () => { it('renders active RichTextEditor', () => { renderRichTextEditor() - expect(mockedLexicalEditor).toHaveBeenCalledWith( - { - disabled: false, - id: 'test-editor', - onChange: expect.any(Function), - onFocus: expect.any(Function), - onBlur: expect.any(Function), - onTextLengthChange: handleCounterMessage, - placeholder: 'placeholder+1', - testIds: { - wrapper: 'wrapper-test-id-1', - }, - autoFocus: false, + expect(renderedProps(mockedLexicalEditor)[0]).toEqual({ + disabled: false, + id: 'test-editor', + onChange: expect.any(Function), + onFocus: expect.any(Function), + onBlur: expect.any(Function), + onTextLengthChange: handleCounterMessage, + placeholder: 'placeholder+1', + testIds: { + wrapper: 'wrapper-test-id-1', }, - {} - ) + autoFocus: false, + }) expect(mockedUseCounter).toHaveBeenCalledWith({ minLength: undefined, maxLength: undefined, @@ -105,22 +102,19 @@ describe('RichTextEditor', () => { it('renders disabled RichTextEditor', () => { renderRichTextEditor({ disabled: true }) - expect(mockedLexicalEditor).toHaveBeenCalledWith( - { - disabled: true, - id: 'test-editor', - onChange: expect.any(Function), - onFocus: expect.any(Function), - onBlur: expect.any(Function), - onTextLengthChange: handleCounterMessage, - placeholder: 'placeholder+1', - testIds: { - wrapper: 'wrapper-test-id-1', - }, - autoFocus: false, + expect(renderedProps(mockedLexicalEditor)[0]).toEqual({ + disabled: true, + id: 'test-editor', + onChange: expect.any(Function), + onFocus: expect.any(Function), + onBlur: expect.any(Function), + onTextLengthChange: handleCounterMessage, + placeholder: 'placeholder+1', + testIds: { + wrapper: 'wrapper-test-id-1', }, - {} - ) + autoFocus: false, + }) }) }) @@ -150,13 +144,10 @@ describe('RichTextEditor', () => { maxLengthMessage, }) - expect(mockedInputMultilineAdornment).toHaveBeenCalledWith( - { - error: true, - children: 'COUNTER_MESSAGE+1', - }, - {} - ) + expect(renderedProps(mockedInputMultilineAdornment)[0]).toEqual({ + error: true, + children: 'COUNTER_MESSAGE+1', + }) }) }) @@ -182,13 +173,10 @@ describe('RichTextEditor', () => { maxLengthMessage, }) - expect(mockedInputMultilineAdornment).toHaveBeenCalledWith( - { - error: false, - children: 'COUNTER_MESSAGE+1', - }, - {} - ) + expect(renderedProps(mockedInputMultilineAdornment)[0]).toEqual({ + error: false, + children: 'COUNTER_MESSAGE+1', + }) }) }) }) diff --git a/packages/picasso-rich-text-editor/src/RichTextEditorButton/RichTextEditorButton.tsx b/packages/picasso-rich-text-editor/src/RichTextEditorButton/RichTextEditorButton.tsx index 156918a8ec..66bfcea52e 100644 --- a/packages/picasso-rich-text-editor/src/RichTextEditorButton/RichTextEditorButton.tsx +++ b/packages/picasso-rich-text-editor/src/RichTextEditorButton/RichTextEditorButton.tsx @@ -1,13 +1,12 @@ -import type { ReactElement } from 'react' import React from 'react' import { twMerge } from '@toptal/picasso-tailwind-merge' -import type { BaseProps } from '@toptal/picasso-shared' +import type { BaseProps, IconElement } from '@toptal/picasso-shared' import { ButtonCircular } from '@toptal/picasso-button' type Props = BaseProps & { active?: boolean disabled?: boolean - icon: ReactElement + icon: IconElement onClick?: (e: React.MouseEvent) => void id?: string } diff --git a/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/EmojiMartPicker.test.tsx b/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/EmojiMartPicker.test.tsx index 50d978e287..b3ce2288cb 100644 --- a/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/EmojiMartPicker.test.tsx +++ b/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/EmojiMartPicker.test.tsx @@ -1,5 +1,6 @@ -import React from 'react' +import React, { StrictMode } from 'react' import data from '@emoji-mart/data' +import { Picker } from 'emoji-mart' import { render, waitFor } from '@toptal/picasso-test-utils' import EmojiMartPicker from './EmojiMartPicker' @@ -17,4 +18,38 @@ describe('EmojiMartPicker', () => { expect(container.querySelector('em-emoji-picker')).toBeInTheDocument() ) }) + + it('pushes prop changes into the mounted picker', async () => { + const update = jest.spyOn(Picker.prototype, 'update') + const onClickOutside = jest.fn() + const { rerender } = renderEmojiMartPicker() + + rerender( + + ) + + await waitFor(() => + expect(update).toHaveBeenCalledWith( + expect.objectContaining({ onClickOutside }) + ) + ) + + update.mockRestore() + }) + + it('renders a single picker when StrictMode replays the mount effect', async () => { + const { container } = render( + + + + ) + + await waitFor(() => + expect(container.querySelectorAll('em-emoji-picker')).toHaveLength(1) + ) + }) }) diff --git a/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/EmojiMartPicker.tsx b/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/EmojiMartPicker.tsx index 5384883c93..a197050524 100644 --- a/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/EmojiMartPicker.tsx +++ b/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/EmojiMartPicker.tsx @@ -19,7 +19,9 @@ interface Props { * * `Picker` appends itself to the element passed as `ref` and is fed prop * changes through `update`, so it is constructed after the first commit and - * updated on every commit after that. + * updated on every commit after that. An effect replay on the same instance + * (StrictMode, Fast Refresh) finds the picker still attached and updates it + * too, instead of constructing a second one. */ const EmojiMartPicker = (props: Props) => { const containerRef = useRef(null) @@ -35,13 +37,6 @@ const EmojiMartPicker = (props: Props) => { pickerRef.current.update(props) }) - useEffect( - () => () => { - pickerRef.current = null - }, - [] - ) - return
} diff --git a/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/RichTextEditorEmojiPicker.tsx b/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/RichTextEditorEmojiPicker.tsx index ffce788c9a..f443030d9d 100644 --- a/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/RichTextEditorEmojiPicker.tsx +++ b/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/RichTextEditorEmojiPicker.tsx @@ -1,8 +1,8 @@ /* eslint-disable no-inline-styles/no-inline-styles */ import React, { useEffect } from 'react' import data from '@emoji-mart/data' -import cx from 'classnames' import { Container } from '@toptal/picasso-container' +import { twMerge } from '@toptal/picasso-tailwind-merge' import RichTextEditorButton from '../RichTextEditorButton' import EmojiMartPicker from './EmojiMartPicker' @@ -16,21 +16,6 @@ interface Props { const TRIGGER_EMOJI_PICKER_ID = 'trigger-emoji-picker' -const classes = { - emojiPicker: 'absolute top-[34px] left-0 z-10 opacity-0 pointer-events-none', - activeOpacity: 'opacity-100', - activePointers: '[pointer-events:all]', -} - -const handleEmojiPickerEscBehaviour = ( - event: KeyboardEvent, - setShowEmojiPicker: React.Dispatch> -) => { - if (event.key === 'Escape') { - setShowEmojiPicker(false) - } -} - export const RichTextEditorEmojiPicker = ({ customEmojis, onInsertEmoji, @@ -56,15 +41,18 @@ export const RichTextEditorEmojiPicker = ({ return } - const handleKeyUp = (event: KeyboardEvent) => - handleEmojiPickerEscBehaviour(event, setShowEmojiPicker) + const closeOnEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setShowEmojiPicker(false) + } + } - document.body.addEventListener('keyup', handleKeyUp) + document.body.addEventListener('keyup', closeOnEscape) return () => { - document.body.removeEventListener('keyup', handleKeyUp) + document.body.removeEventListener('keyup', closeOnEscape) } - }, [showEmojiPicker, setShowEmojiPicker]) + }, [showEmojiPicker]) return ( @@ -75,10 +63,12 @@ export const RichTextEditorEmojiPicker = ({ disabled={disabled} /> { return null } -export class CustomEmojiNode extends DecoratorNode { +export class CustomEmojiNode extends DecoratorNode { src: string id: string diff --git a/packages/picasso-rich-text-editor/src/plugins/ImagePlugin/nodes/ImageNode.tsx b/packages/picasso-rich-text-editor/src/plugins/ImagePlugin/nodes/ImageNode.tsx index bdf5c8fa9a..b9652b7aa1 100644 --- a/packages/picasso-rich-text-editor/src/plugins/ImagePlugin/nodes/ImageNode.tsx +++ b/packages/picasso-rich-text-editor/src/plugins/ImagePlugin/nodes/ImageNode.tsx @@ -45,7 +45,7 @@ const convertImageElement = (domNode: Node): null | DOMConversionOutput => { return null } -export class ImageNode extends DecoratorNode { +export class ImageNode extends DecoratorNode { src: string alt?: string diff --git a/packages/picasso-tailwind-merge/package.json b/packages/picasso-tailwind-merge/package.json index 7f475e315c..3f048d87cd 100644 --- a/packages/picasso-tailwind-merge/package.json +++ b/packages/picasso-tailwind-merge/package.json @@ -26,7 +26,7 @@ }, "sideEffects": false, "peerDependencies": { - "react": ">=17.0.0 < 19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "exports": { ".": "./dist-package/src/index.js" diff --git a/packages/picasso-tailwind-merge/src/test.ts b/packages/picasso-tailwind-merge/src/test.ts index 6abb1b724f..1004e90942 100644 --- a/packages/picasso-tailwind-merge/src/test.ts +++ b/packages/picasso-tailwind-merge/src/test.ts @@ -62,7 +62,9 @@ describe('twMerge', () => { }) it('passes unknown classes through untouched', () => { - expect(twMerge('min-h-auto dellay-225')).toBe('min-h-auto dellay-225') + expect(twMerge('min-h-auto not-a-utility')).toBe( + 'min-h-auto not-a-utility' + ) }) }) diff --git a/packages/picasso/package.json b/packages/picasso/package.json index 46239807ca..4c3537adb3 100644 --- a/packages/picasso/package.json +++ b/packages/picasso/package.json @@ -30,8 +30,8 @@ "@toptal/picasso-provider": "workspace:^", "@toptal/picasso-tailwind": "workspace:^", "notistack": "3.0.2", - "react": ">=17.0.0 < 19.0.0", - "react-dom": ">=17.0.0 < 19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "typescript": "^5.5.0" }, "dependencies": { @@ -45,6 +45,7 @@ "@toptal/picasso-avatar-upload": "workspace:*", "@toptal/picasso-badge": "workspace:*", "@toptal/picasso-breadcrumbs": "workspace:*", + "@toptal/picasso-button": "workspace:*", "@toptal/picasso-calendar": "workspace:*", "@toptal/picasso-carousel": "workspace:*", "@toptal/picasso-checkbox": "workspace:*", @@ -63,6 +64,7 @@ "@toptal/picasso-helpbox": "workspace:*", "@toptal/picasso-icons": "workspace:*", "@toptal/picasso-image": "workspace:*", + "@toptal/picasso-input": "workspace:*", "@toptal/picasso-input-adornment": "workspace:*", "@toptal/picasso-link": "workspace:*", "@toptal/picasso-list": "workspace:*", @@ -73,11 +75,13 @@ "@toptal/picasso-note": "workspace:*", "@toptal/picasso-notification": "workspace:*", "@toptal/picasso-number-input": "workspace:*", + "@toptal/picasso-outlined-input": "workspace:*", "@toptal/picasso-overview-block": "workspace:*", "@toptal/picasso-page": "workspace:*", "@toptal/picasso-pagination": "workspace:*", "@toptal/picasso-paper": "workspace:*", "@toptal/picasso-password-input": "workspace:*", + "@toptal/picasso-popper": "workspace:*", "@toptal/picasso-prompt-modal": "workspace:*", "@toptal/picasso-quote": "workspace:*", "@toptal/picasso-radio": "workspace:*", @@ -94,6 +98,7 @@ "@toptal/picasso-tabs": "workspace:*", "@toptal/picasso-tag": "workspace:*", "@toptal/picasso-tagselector": "workspace:*", + "@toptal/picasso-test-utils": "workspace:*", "@toptal/picasso-timeline": "workspace:*", "@toptal/picasso-timepicker": "workspace:*", "@toptal/picasso-tooltip": "workspace:*", @@ -101,12 +106,7 @@ "@toptal/picasso-typography": "workspace:*", "@toptal/picasso-typography-overflow": "workspace:*", "@toptal/picasso-user-badge": "workspace:*", - "@toptal/picasso-button": "workspace:*", - "@toptal/picasso-input": "workspace:*", - "@toptal/picasso-outlined-input": "workspace:*", "@toptal/picasso-utils": "workspace:*", - "@toptal/picasso-test-utils": "workspace:*", - "@toptal/picasso-popper": "workspace:*", "ap-style-title-case": "^1.1.2", "classnames": "^2.5.1", "d3": "^7.8.2", @@ -125,7 +125,6 @@ "@babel/types": "^7.26.8", "@testing-library/jest-dom": "^6.2.0", "@testing-library/react": "^14.1.2", - "@testing-library/react-hooks": "^8.0.1", "@types/classnames": "^2.3.1", "@types/d3": "^7.4.0", "@types/glider-js": "^1.7.8", diff --git a/packages/picasso/src/index.ts b/packages/picasso/src/index.ts index 91f7c67cc7..de2a48ffbc 100644 --- a/packages/picasso/src/index.ts +++ b/packages/picasso/src/index.ts @@ -237,7 +237,13 @@ export type { ValueType as SelectValueType, } from '@toptal/picasso-select' export { BaseProps } from '@toptal/picasso-shared' -export type { ColorType, SizeType, SpacingType } from '@toptal/picasso-shared' +export type { + ColorType, + IconElement, + SizeType, + SpacingType, + TransitionChild, +} from '@toptal/picasso-shared' export { ShowMore } from '@toptal/picasso-show-more' export type { ShowMoreProps } from '@toptal/picasso-show-more' export { SkeletonLoader } from '@toptal/picasso-skeleton-loader' diff --git a/packages/shared/package.json b/packages/shared/package.json index e20859b11c..55670f7c59 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -25,11 +25,11 @@ "url": "https://github.com/toptal/picasso/issues" }, "peerDependencies": { - "react": ">=17.0.0 < 19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "typescript": "^5.5.0", "@toptal/picasso-provider": "workspace:^", "notistack": "3.0.2", - "react-dom": ">=17.0.0 < 19.0.0" + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "dependencies": { "classnames": "^2.5.1", diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 3f1d4285ee..e8e481fa2a 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -2,6 +2,9 @@ import type { CSSProperties, AnchorHTMLAttributes, ButtonHTMLAttributes, + ExoticComponent, + ReactElement, + Ref, } from 'react' import type { Classes } from './styles' @@ -24,6 +27,9 @@ export interface BaseProps { 'data-private'?: boolean | 'lipsum' } +/** An ``-like element the host decorates through `cloneElement` */ +export type IconElement = ReactElement<{ className?: string }> + export interface JssProps { classes: Classes } @@ -71,9 +77,15 @@ export interface NamedComponent

{ // have required fields (Page.Article, Breadcrumbs.Item, OverviewBlock). // Those sites broke the previous generic call-signature shape under the // TS 5.5 variance change. +// +// The call signature returns `ReturnType>` instead of a +// literal `ReactElement | null` so one declaration serves both type majors: +// under @types/react 17 it resolves to `ReactElement | null`, which keeps the +// interface a valid JSX element type, and under 19 to `ReactNode`, which is +// what `forwardRef` components return there. export interface OverridableComponent

extends NamedComponent

{ // eslint-disable-next-line @typescript-eslint/no-explicit-any - (props: P & { [key: string]: any }): JSX.Element | null + (props: P & { [key: string]: any }): ReturnType> } type BaseEnvironments = 'development' | 'staging' | 'production' @@ -100,8 +112,15 @@ export type ColorType = | 'inherit' export interface TransitionProps { - /* Callback fired when the component has exited */ + /** Callback fired when the exit transition settles */ onExited?: (node: HTMLElement) => void - /* The duration for the transition, in milliseconds */ + /** The duration for the transition, in milliseconds; one value, or one per phase */ timeout?: number | { enter?: number; exit?: number; appear?: number } } + +/** The single child Fade/Slide clone with transition classes, style and a ref */ +export type TransitionChild = ReactElement<{ + className?: string + style?: CSSProperties + ref?: Ref +}> diff --git a/packages/topkit-analytics-charts/package.json b/packages/topkit-analytics-charts/package.json index a5bb40c00e..5998c5ccda 100644 --- a/packages/topkit-analytics-charts/package.json +++ b/packages/topkit-analytics-charts/package.json @@ -21,7 +21,7 @@ "url": "https://github.com/toptal/picasso/issues" }, "peerDependencies": { - "react": ">=17.0.0 < 19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@toptal/picasso-tailwind": "workspace:^", "typescript": "^5.5.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93b97ee5d0..fa8374d2ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,7 +31,6 @@ overrides: prismjs: ^1.30.0 react: ^18.2.0 react-dom: ^18.2.0 - react-test-renderer: ^19.2.0 semver-regex: ^3.1.3 trim: ^0.0.3 unset-value: ^2.0.1 @@ -90,6 +89,9 @@ importers: '@changesets/get-github-info': specifier: ^0.6.0 version: 0.6.0(patch_hash=d7524dd8be0ab936b7eea9993dc7df46c25d273d041f70e2f82d5b3916f55593)(encoding@0.1.13) + '@cypress/request': + specifier: 3.0.9 + version: 3.0.9 '@cypress/skip-test': specifier: ^2.6.1 version: 2.6.1 @@ -143,7 +145,7 @@ importers: version: 15.0.0(@tailwindcss/postcss@4.2.1)(@types/babel__core@7.1.19)(@types/node@24.5.2)(@types/react@17.0.39)(@types/webpack@4.41.32)(babel-plugin-macros@3.1.0)(encoding@0.1.13)(eslint@8.57.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(styled-components@6.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(tailwindcss@4.2.1)(type-fest@4.15.0)(typescript@5.5.4)(webpack-hot-middleware@2.25.1) '@toptal/davinci-qa': specifier: ^19.1.0 - version: 19.1.0(@swc/core@1.13.5)(@toptal/davinci-engine@15.0.0(@tailwindcss/postcss@4.2.1)(@types/babel__core@7.1.19)(@types/node@24.5.2)(@types/react@17.0.39)(@types/webpack@4.41.32)(babel-plugin-macros@3.1.0)(encoding@0.1.13)(eslint@8.57.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(styled-components@6.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(tailwindcss@4.2.1)(type-fest@4.15.0)(typescript@5.5.4)(webpack-hot-middleware@2.25.1))(@types/node@24.5.2)(babel-loader@9.1.2(@babel/core@7.28.4)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)))(babel-plugin-macros@3.1.0)(cypress@13.6.0)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(styled-components@6.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.5.2)(typescript@5.5.4))(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)) + version: 19.1.0(@swc/core@1.13.5)(@toptal/davinci-engine@15.0.0(@tailwindcss/postcss@4.2.1)(@types/babel__core@7.1.19)(@types/node@24.5.2)(@types/react@17.0.39)(@types/webpack@4.41.32)(babel-plugin-macros@3.1.0)(encoding@0.1.13)(eslint@8.57.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(styled-components@6.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(tailwindcss@4.2.1)(type-fest@4.15.0)(typescript@5.5.4)(webpack-hot-middleware@2.25.1))(@types/node@24.5.2)(babel-loader@9.1.2(@babel/core@7.28.4)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)))(babel-plugin-macros@3.1.0)(cypress@14.5.4)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(styled-components@6.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.5.2)(typescript@5.5.4))(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)) '@toptal/davinci-syntax': specifier: ^25.0.0 version: 25.0.0(encoding@0.1.13)(eslint-config-standard@17.1.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.61.1(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1))(eslint-plugin-n@16.6.2(eslint@8.57.1))(eslint-plugin-promise@6.1.1(eslint@8.57.1))(eslint@8.57.1))(jest@29.7.0(@types/node@24.5.2)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.5.2)(typescript@5.5.4)))(postcss@8.4.32)(typescript@5.5.4) @@ -454,14 +456,14 @@ importers: specifier: ^7.0.3 version: 7.0.3 cypress: - specifier: ^13.6.0 - version: 13.6.0 + specifier: ^14.5.4 + version: 14.5.4 cypress-plugin-tab: specifier: ^1.0.5 version: 1.0.5 cypress-real-events: - specifier: ^1.12.0 - version: 1.12.0(cypress@13.6.0) + specifier: ^1.15.0 + version: 1.15.0(cypress@14.5.4) date-fns: specifier: ^4.1.0 version: 4.4.0 @@ -570,9 +572,6 @@ importers: react-storybook-addon-chapters: specifier: ^3.1.7 version: 3.1.7(@babel/core@7.28.4)(@emotion/core@10.3.1(react@18.2.0))(@storybook/builder-webpack5@6.5.16(@swc/core@1.13.5)(eslint@8.57.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@storybook/manager-webpack5@6.5.16(@swc/core@1.13.5)(encoding@0.1.13)(eslint@8.57.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@swc/core@1.13.5)(@types/react@17.0.39)(@types/webpack@4.41.32)(encoding@0.1.13)(eslint@8.57.1)(prop-types@15.8.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(regenerator-runtime@0.13.11)(require-from-string@2.0.2)(type-fest@4.15.0)(typescript@5.5.4)(webpack-dev-server@4.15.1(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)))(webpack-hot-middleware@2.25.1) - react-test-renderer: - specifier: ^19.2.0 - version: 19.2.3(react@18.2.0) remark-gfm: specifier: ^4.0.0 version: 4.0.0 @@ -1064,7 +1063,7 @@ importers: version: 18.2.0 react-day-picker: specifier: ^8.10.2 - version: 8.10.2(date-fns@2.30.0)(react@18.2.0) + version: 8.10.2(date-fns@4.4.0)(react@18.2.0) devDependencies: '@toptal/picasso-provider': specifier: workspace:* @@ -1158,6 +1157,9 @@ importers: '@toptal/picasso-utils': specifier: workspace:* version: link:../Utils + classnames: + specifier: ^2.5.1 + version: 2.5.1 react: specifier: ^18.2.0 version: 18.2.0 @@ -1362,6 +1364,9 @@ importers: '@toptal/picasso-icons': specifier: workspace:* version: link:../Icons + '@toptal/picasso-shared': + specifier: workspace:* + version: link:../../shared '@toptal/picasso-tailwind': specifier: workspace:^ version: link:../../picasso-tailwind @@ -3375,9 +3380,6 @@ importers: '@testing-library/react': specifier: ^14.1.2 version: 14.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@testing-library/react-hooks': - specifier: ^8.0.1 - version: 8.0.1(@types/react@17.0.39)(react-dom@18.2.0(react@18.2.0))(react-test-renderer@19.2.3(react@18.2.0))(react@18.2.0) '@types/classnames': specifier: ^2.3.1 version: 2.3.1 @@ -3463,7 +3465,7 @@ importers: dependencies: cypress: specifier: '>=13.0.0 < 16.0.0' - version: 13.6.0 + version: 14.5.4 packages/picasso-forms: dependencies: @@ -3579,9 +3581,9 @@ importers: specifier: ^5.5.0 version: 5.5.4 devDependencies: - '@testing-library/react-hooks': - specifier: ^8.0.1 - version: 8.0.1(@types/react@17.0.39)(react-dom@18.2.0(react@18.2.0))(react-test-renderer@19.2.3(react@18.2.0))(react@18.2.0) + '@testing-library/react': + specifier: ^14.1.2 + version: 14.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@toptal/picasso-test-utils': specifier: workspace:* version: link:../base/Test-Utils @@ -3726,9 +3728,9 @@ importers: specifier: ^10.0.0 version: 10.0.0(react@18.2.0) devDependencies: - '@testing-library/react-hooks': - specifier: ^8.0.1 - version: 8.0.1(@types/react@17.0.39)(react-dom@18.2.0(react@18.2.0))(react-test-renderer@19.2.3(react@18.2.0))(react@18.2.0) + '@testing-library/react': + specifier: ^14.1.2 + version: 14.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@toptal/picasso-test-utils': specifier: workspace:* version: link:../base/Test-Utils @@ -3850,9 +3852,9 @@ importers: specifier: ^5.5.0 version: 5.5.4 devDependencies: - '@testing-library/react-hooks': - specifier: ^8.0.1 - version: 8.0.1(@types/react@17.0.39)(react-dom@18.2.0(react@18.2.0))(react-test-renderer@19.2.3(react@18.2.0))(react@18.2.0) + '@testing-library/react': + specifier: ^14.1.2 + version: 14.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@toptal/picasso-tailwind-merge': specifier: workspace:* version: link:../picasso-tailwind-merge @@ -5136,8 +5138,8 @@ packages: cypress: '*' webpack: ^5.0.0 - '@cypress/request@3.0.1': - resolution: {integrity: sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==} + '@cypress/request@3.0.9': + resolution: {integrity: sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw==} engines: {node: '>= 6'} '@cypress/skip-test@2.6.1': @@ -7152,22 +7154,6 @@ packages: resolution: {integrity: sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - '@testing-library/react-hooks@8.0.1': - resolution: {integrity: sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==} - engines: {node: '>=12'} - peerDependencies: - '@types/react': '17' - react: ^18.2.0 - react-dom: ^18.2.0 - react-test-renderer: ^19.2.0 - peerDependenciesMeta: - '@types/react': - optional: true - react-dom: - optional: true - react-test-renderer: - optional: true - '@testing-library/react@14.3.1': resolution: {integrity: sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==} engines: {node: '>=14'} @@ -7557,9 +7543,6 @@ packages: '@types/node@17.0.35': resolution: {integrity: sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg==} - '@types/node@18.19.17': - resolution: {integrity: sha512-SzyGKgwPzuWp2SHhlpXKzCX0pIOfcI4V2eF37nNBJOhwlegQ83omtVQ1XxZpDE06V/d6AQvfQdPfnw0tRC//Ng==} - '@types/node@20.4.7': resolution: {integrity: sha512-bUBrPjEry2QUTsnuEjzjbS7voGWCc30W0qzgMf90GPeDGFRakvrz47ju+oqDAKCXLUCe39u57/ORMl/O/04/9g==} @@ -8882,10 +8865,6 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} - ci-info@4.3.0: - resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==} - engines: {node: '>=8'} - ci-info@4.3.1: resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} engines: {node: '>=8'} @@ -8952,6 +8931,10 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} + cli-table3@0.6.1: + resolution: {integrity: sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==} + engines: {node: 10.* || >= 12.*} + cli-table3@0.6.5: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} engines: {node: 10.* || >= 12.*} @@ -9457,14 +9440,14 @@ packages: cypress-plugin-tab@1.0.5: resolution: {integrity: sha512-QtTJcifOVwwbeMP3hsOzQOKf3EqKsLyjtg9ZAGlYDntrCRXrsQhe4ZQGIthRMRLKpnP6/tTk6G0gJ2sZUfRliQ==} - cypress-real-events@1.12.0: - resolution: {integrity: sha512-oiy+4kGKkzc2PT36k3GGQqkGxNiVypheWjMtfyi89iIk6bYmTzeqxapaLHS3pnhZOX1IEbTDUVxh8T4Nhs1tyQ==} + cypress-real-events@1.15.0: + resolution: {integrity: sha512-in98xxTnnM9Z7lZBvvVozm99PBT2eEOjXRG5LKWyYvQnj9mGWXMiPNpfw7e7WiraBFh7XlXIxnE9Cu5o+52kQQ==} peerDependencies: - cypress: ^4.x || ^5.x || ^6.x || ^7.x || ^8.x || ^9.x || ^10.x || ^11.x || ^12.x || ^13.x + cypress: ^4.x || ^5.x || ^6.x || ^7.x || ^8.x || ^9.x || ^10.x || ^11.x || ^12.x || ^13.x || ^14.x || ^15.x - cypress@13.6.0: - resolution: {integrity: sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==} - engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} + cypress@14.5.4: + resolution: {integrity: sha512-0Dhm4qc9VatOcI1GiFGVt8osgpPdqJLHzRwcAB5MSD/CAAts3oybvPUPawHyvJZUd8osADqZe/xzMsZ8sDTjXw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true d3-array@3.2.2: @@ -10150,9 +10133,6 @@ packages: encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} - end-of-stream@1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -11644,8 +11624,8 @@ packages: resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} engines: {node: '>=8.0.0'} - http-signature@1.3.6: - resolution: {integrity: sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==} + http-signature@1.4.0: + resolution: {integrity: sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==} engines: {node: '>=0.10'} http2-wrapper@1.0.3: @@ -14995,10 +14975,6 @@ packages: (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) - qs@6.10.4: - resolution: {integrity: sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==} - engines: {node: '>=0.6'} - qs@6.13.0: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} @@ -15227,9 +15203,6 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - react-is@19.2.3: - resolution: {integrity: sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==} - react-lifecycles-compat@3.0.4: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} @@ -15301,11 +15274,6 @@ packages: peerDependencies: react: ^18.2.0 - react-test-renderer@19.2.3: - resolution: {integrity: sha512-TMR1LnSFiWZMJkCgNf5ATSvAheTT2NvKIwiVwdBPHxjBI7n/JbWd4gaZ16DVd9foAXdvDz+sB5yxZTwMjPRxpw==} - peerDependencies: - react: ^18.2.0 - react-textarea-autosize@7.1.2: resolution: {integrity: sha512-uH3ORCsCa3C6LHxExExhF4jHoXYCQwE5oECmrRsunlspaDAbS4mGKNlWZqjLfInWtFQcf0o1n1jC/NGXFdUBCg==} peerDependencies: @@ -15789,9 +15757,6 @@ packages: scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - schema-utils@2.7.0: resolution: {integrity: sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==} engines: {node: '>= 8.9.0'} @@ -16191,8 +16156,8 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - sshpk@1.17.0: - resolution: {integrity: sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==} + sshpk@1.18.0: + resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} engines: {node: '>=0.10.0'} hasBin: true @@ -16712,10 +16677,6 @@ packages: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} - tmp@0.2.1: - resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} - engines: {node: '>=8.17.0'} - tmp@0.2.6: resolution: {integrity: sha512-5sJPdPjfI5Kx+qbrDesxkglRBxW//g7hCsqspEjwkewGvBMGIKMOTKzLt1hFVJzyadba3lDUN20O9qhvbQUSTA==} engines: {node: '>=14.14'} @@ -17009,9 +16970,6 @@ packages: unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - undici-types@7.12.0: resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==} @@ -17861,7 +17819,7 @@ snapshots: '@babel/code-frame@7.27.1': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 @@ -17875,7 +17833,7 @@ snapshots: '@babel/core@7.12.9': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.7 '@babel/generator': 7.28.3 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.12.9) '@babel/helpers': 7.28.4 @@ -18080,7 +18038,7 @@ snapshots: dependencies: '@babel/core': 7.12.9 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.29.7 '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color @@ -18089,7 +18047,7 @@ snapshots: dependencies: '@babel/core': 7.28.4 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.29.7 '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color @@ -18615,7 +18573,7 @@ snapshots: '@babel/helper-hoist-variables': 7.22.5 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.29.7 transitivePeerDependencies: - supports-color @@ -19072,7 +19030,7 @@ snapshots: '@babel/template@7.27.2': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.7 '@babel/parser': 7.28.4 '@babel/types': 7.28.4 @@ -19084,7 +19042,7 @@ snapshots: '@babel/traverse@7.28.4': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.7 '@babel/generator': 7.28.3 '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.4 @@ -19096,7 +19054,7 @@ snapshots: '@babel/traverse@7.28.4(supports-color@7.2.0)': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.7 '@babel/generator': 7.28.3 '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.4 @@ -19108,7 +19066,7 @@ snapshots: '@babel/traverse@7.28.4(supports-color@8.1.1)': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.7 '@babel/generator': 7.28.3 '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.4 @@ -19182,7 +19140,7 @@ snapshots: outdent: 0.5.0 prettier: 2.8.8 resolve-from: 5.0.0 - semver: 7.7.2 + semver: 7.8.5 '@changesets/assemble-release-plan@6.0.9': dependencies: @@ -19191,7 +19149,7 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 - semver: 7.7.2 + semver: 7.8.5 '@changesets/changelog-git@0.2.1': dependencies: @@ -19257,7 +19215,7 @@ snapshots: '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 picocolors: 1.1.1 - semver: 7.7.2 + semver: 7.8.5 '@changesets/get-github-info@0.6.0(patch_hash=d7524dd8be0ab936b7eea9993dc7df46c25d273d041f70e2f82d5b3916f55593)(encoding@0.1.13)': dependencies: @@ -19485,14 +19443,14 @@ snapshots: dependencies: postcss-selector-parser: 6.1.2 - '@cypress/code-coverage@3.12.10(@babel/core@7.28.4)(@babel/preset-env@7.24.0(@babel/core@7.28.4))(babel-loader@9.1.2(@babel/core@7.28.4)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)))(cypress@13.6.0)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5))': + '@cypress/code-coverage@3.12.10(@babel/core@7.28.4)(@babel/preset-env@7.24.0(@babel/core@7.28.4))(babel-loader@9.1.2(@babel/core@7.28.4)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)))(cypress@14.5.4)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5))': dependencies: '@babel/core': 7.28.4 '@babel/preset-env': 7.24.0(@babel/core@7.28.4) '@cypress/webpack-preprocessor': 6.0.0(@babel/core@7.28.4)(@babel/preset-env@7.24.0(@babel/core@7.28.4))(babel-loader@9.1.2(@babel/core@7.28.4)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)))(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)) babel-loader: 9.1.2(@babel/core@7.28.4)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)) chalk: 4.1.2 - cypress: 13.6.0 + cypress: 14.5.4 dayjs: 1.11.10 debug: 4.3.4 execa: 4.1.0 @@ -19504,7 +19462,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@cypress/request@3.0.1': + '@cypress/request@3.0.9': dependencies: aws-sign2: 0.7.0 aws4: 1.11.0 @@ -19513,15 +19471,15 @@ snapshots: extend: 3.0.2 forever-agent: 0.6.1 form-data: 2.5.5 - http-signature: 1.3.6 + http-signature: 1.4.0 is-typedarray: 1.0.0 isstream: 0.1.2 json-stringify-safe: 5.0.1 mime-types: 2.1.35 performance-now: 2.1.0 - qs: 6.10.4 + qs: 6.14.0 safe-buffer: 5.2.1 - tough-cookie: 4.1.4 + tough-cookie: 5.1.2 tunnel-agent: 0.6.0 uuid: 8.3.2 @@ -20246,7 +20204,7 @@ snapshots: read-cmd-shim: 4.0.0 resolve-from: 5.0.0 rimraf: 4.4.1 - semver: 7.7.2 + semver: 7.8.5 set-blocking: 2.0.0 signal-exit: 3.0.7 slash: 3.0.0 @@ -20539,7 +20497,7 @@ snapshots: promise-all-reject-late: 1.0.1 promise-call-limit: 3.0.1 read-package-json-fast: 3.0.2 - semver: 7.7.2 + semver: 7.8.5 ssri: 10.0.6 treeverse: 3.0.0 walk-up-path: 3.0.1 @@ -20578,7 +20536,7 @@ snapshots: proggy: 3.0.0 promise-all-reject-late: 1.0.1 promise-call-limit: 3.0.1 - semver: 7.7.2 + semver: 7.8.5 ssri: 12.0.0 treeverse: 3.0.0 walk-up-path: 4.0.0 @@ -20588,19 +20546,19 @@ snapshots: '@npmcli/fs@1.1.1': dependencies: '@gar/promisify': 1.1.3 - semver: 7.7.2 + semver: 7.8.5 '@npmcli/fs@3.1.1': dependencies: - semver: 7.7.2 + semver: 7.8.5 '@npmcli/fs@4.0.0': dependencies: - semver: 7.7.2 + semver: 7.8.5 '@npmcli/fs@5.0.0': dependencies: - semver: 7.7.2 + semver: 7.8.5 '@npmcli/git@4.1.0': dependencies: @@ -20610,7 +20568,7 @@ snapshots: proc-log: 3.0.0 promise-inflight: 1.0.1 promise-retry: 2.0.1 - semver: 7.7.2 + semver: 7.8.5 which: 3.0.1 transitivePeerDependencies: - bluebird @@ -20623,7 +20581,7 @@ snapshots: proc-log: 3.0.0 promise-inflight: 1.0.1 promise-retry: 2.0.1 - semver: 7.7.2 + semver: 7.8.5 which: 4.0.0 transitivePeerDependencies: - bluebird @@ -20636,7 +20594,7 @@ snapshots: npm-pick-manifest: 10.0.0 proc-log: 5.0.0 promise-retry: 2.0.1 - semver: 7.7.2 + semver: 7.8.5 which: 5.0.0 '@npmcli/git@7.0.2': @@ -20647,7 +20605,7 @@ snapshots: lru-cache: 11.3.5 npm-pick-manifest: 11.0.3 proc-log: 6.1.0 - semver: 7.7.2 + semver: 7.8.5 which: 6.0.1 '@npmcli/installed-package-contents@2.1.0': @@ -20685,7 +20643,7 @@ snapshots: json-parse-even-better-errors: 3.0.2 pacote: 18.0.6 proc-log: 4.2.0 - semver: 7.7.2 + semver: 7.8.5 transitivePeerDependencies: - bluebird - supports-color @@ -20696,7 +20654,7 @@ snapshots: json-parse-even-better-errors: 5.0.0 pacote: 21.5.0 proc-log: 6.1.0 - semver: 7.7.2 + semver: 7.8.5 transitivePeerDependencies: - supports-color @@ -20725,7 +20683,7 @@ snapshots: json-parse-even-better-errors: 3.0.2 normalize-package-data: 6.0.2 proc-log: 4.2.0 - semver: 7.7.2 + semver: 7.8.5 transitivePeerDependencies: - bluebird @@ -20736,7 +20694,7 @@ snapshots: hosted-git-info: 9.0.2 json-parse-even-better-errors: 5.0.0 proc-log: 6.1.0 - semver: 7.7.2 + semver: 7.8.5 validate-npm-package-license: 3.0.4 '@npmcli/promise-spawn@6.0.2': @@ -20814,8 +20772,8 @@ snapshots: ignore: 5.2.4 minimatch: 9.0.3 nx: 22.7.5(@swc/core@1.13.5) - semver: 7.7.2 - tmp: 0.2.1 + semver: 7.8.5 + tmp: 0.2.6 tslib: 2.8.1 yargs-parser: 21.1.1 @@ -20826,7 +20784,7 @@ snapshots: enquirer: 2.3.6 minimatch: 10.2.4 nx: 22.7.5(@swc/core@1.13.5) - semver: 7.7.2 + semver: 7.8.5 tslib: 2.8.1 yargs-parser: 21.1.1 @@ -20837,7 +20795,7 @@ snapshots: enquirer: 2.3.6 minimatch: 10.2.5 nx: 22.7.5(@swc/core@1.13.5) - semver: 7.7.2 + semver: 7.8.5 tslib: 2.8.1 yargs-parser: 21.1.1 @@ -20915,7 +20873,7 @@ snapshots: enquirer: 2.3.6 nx: 22.7.5(@swc/core@1.13.5) picomatch: 4.0.4 - semver: 7.7.2 + semver: 7.8.5 tslib: 2.8.1 yargs-parser: 21.1.1 transitivePeerDependencies: @@ -20945,7 +20903,7 @@ snapshots: natural-orderby: 2.0.3 object-treeify: 1.1.33 password-prompt: 1.1.3 - semver: 7.7.2 + semver: 7.8.5 string-width: 4.2.3 strip-ansi: 6.0.1 supports-color: 8.1.1 @@ -21433,7 +21391,7 @@ snapshots: read-pkg: 9.0.1 registry-auth-token: 5.1.0 semantic-release: 24.2.8(typescript@5.5.4) - semver: 7.7.2 + semver: 7.8.5 tempy: 3.1.0 '@semantic-release/release-notes-generator@14.1.0(semantic-release@24.2.8(typescript@5.5.4))': @@ -22837,8 +22795,8 @@ snapshots: '@testing-library/dom@9.3.0': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.28.4 + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.2 '@types/aria-query': 5.0.1 aria-query: 5.0.0 chalk: 4.1.2 @@ -22855,19 +22813,9 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react-hooks@8.0.1(@types/react@17.0.39)(react-dom@18.2.0(react@18.2.0))(react-test-renderer@19.2.3(react@18.2.0))(react@18.2.0)': - dependencies: - '@babel/runtime': 7.28.4 - react: 18.2.0 - react-error-boundary: 3.1.4(react@18.2.0) - optionalDependencies: - '@types/react': 17.0.39 - react-dom: 18.2.0(react@18.2.0) - react-test-renderer: 19.2.3(react@18.2.0) - '@testing-library/react@14.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@testing-library/dom': 9.3.0 '@types/react-dom': 18.0.0 react: 18.2.0 @@ -22906,7 +22854,7 @@ snapshots: ora: 5.4.1 proper-lockfile: 4.1.2 read-pkg-up: 7.0.1 - semver: 7.7.2 + semver: 7.8.5 zx: 7.2.4(encoding@0.1.13) transitivePeerDependencies: - encoding @@ -23067,13 +23015,13 @@ snapshots: - encoding - supports-color - '@toptal/davinci-qa@19.1.0(@swc/core@1.13.5)(@toptal/davinci-engine@15.0.0(@tailwindcss/postcss@4.2.1)(@types/babel__core@7.1.19)(@types/node@24.5.2)(@types/react@17.0.39)(@types/webpack@4.41.32)(babel-plugin-macros@3.1.0)(encoding@0.1.13)(eslint@8.57.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(styled-components@6.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(tailwindcss@4.2.1)(type-fest@4.15.0)(typescript@5.5.4)(webpack-hot-middleware@2.25.1))(@types/node@24.5.2)(babel-loader@9.1.2(@babel/core@7.28.4)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)))(babel-plugin-macros@3.1.0)(cypress@13.6.0)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(styled-components@6.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.5.2)(typescript@5.5.4))(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5))': + '@toptal/davinci-qa@19.1.0(@swc/core@1.13.5)(@toptal/davinci-engine@15.0.0(@tailwindcss/postcss@4.2.1)(@types/babel__core@7.1.19)(@types/node@24.5.2)(@types/react@17.0.39)(@types/webpack@4.41.32)(babel-plugin-macros@3.1.0)(encoding@0.1.13)(eslint@8.57.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(styled-components@6.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(tailwindcss@4.2.1)(type-fest@4.15.0)(typescript@5.5.4)(webpack-hot-middleware@2.25.1))(@types/node@24.5.2)(babel-loader@9.1.2(@babel/core@7.28.4)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)))(babel-plugin-macros@3.1.0)(cypress@14.5.4)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(styled-components@6.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.5.2)(typescript@5.5.4))(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5))': dependencies: '@babel/core': 7.28.4 '@babel/preset-env': 7.24.0(@babel/core@7.28.4) '@babel/preset-react': 7.24.1(@babel/core@7.28.4) '@babel/preset-typescript': 7.29.7(@babel/core@7.28.4) - '@cypress/code-coverage': 3.12.10(@babel/core@7.28.4)(@babel/preset-env@7.24.0(@babel/core@7.28.4))(babel-loader@9.1.2(@babel/core@7.28.4)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)))(cypress@13.6.0)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)) + '@cypress/code-coverage': 3.12.10(@babel/core@7.28.4)(@babel/preset-env@7.24.0(@babel/core@7.28.4))(babel-loader@9.1.2(@babel/core@7.28.4)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)))(cypress@14.5.4)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)) '@cypress/webpack-preprocessor': 5.12.0(@babel/core@7.28.4)(@babel/preset-env@7.24.0(@babel/core@7.28.4))(babel-loader@9.1.2(@babel/core@7.28.4)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)))(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)) '@swc/jest': 0.2.39(@swc/core@1.13.5) '@swc/plugin-styled-components': 9.1.0 @@ -23084,7 +23032,7 @@ snapshots: '@toptal/davinci-monorepo': 13.0.0(@swc/core@1.13.5)(@types/node@24.5.2)(babel-plugin-macros@3.1.0)(encoding@0.1.13) '@types/jest': 29.5.14 babel-jest: 28.1.3(@babel/core@7.28.4) - cypress: 13.6.0 + cypress: 14.5.4 fs-extra: 11.3.2 glob: 8.1.0 happo-cypress: 4.3.1(happo-e2e@5.0.0(@babel/core@7.28.4)(babel-loader@9.1.2(@babel/core@7.28.4)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)))(encoding@0.1.13)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5))) @@ -23464,7 +23412,7 @@ snapshots: '@types/happo-cypress@4.1.3': dependencies: - cypress: 13.6.0 + cypress: 14.5.4 '@types/hast@2.3.4': dependencies: @@ -23565,10 +23513,6 @@ snapshots: '@types/node@17.0.35': {} - '@types/node@18.19.17': - dependencies: - undici-types: 5.26.5 - '@types/node@20.4.7': {} '@types/node@24.5.2': @@ -23807,7 +23751,7 @@ snapshots: debug: 4.4.3(supports-color@7.2.0) globby: 11.1.0 is-glob: 4.0.3 - semver: 7.7.2 + semver: 7.8.5 tsutils: 3.21.0(typescript@3.9.10) optionalDependencies: typescript: 3.9.10 @@ -23821,7 +23765,7 @@ snapshots: debug: 4.4.3(supports-color@7.2.0) globby: 11.1.0 is-glob: 4.0.3 - semver: 7.7.2 + semver: 7.8.5 tsutils: 3.21.0(typescript@4.9.5) optionalDependencies: typescript: 4.9.5 @@ -23835,7 +23779,7 @@ snapshots: debug: 4.4.3(supports-color@7.2.0) globby: 11.1.0 is-glob: 4.0.3 - semver: 7.7.2 + semver: 7.8.5 ts-api-utils: 1.0.3(typescript@5.5.4) optionalDependencies: typescript: 5.5.4 @@ -23866,7 +23810,7 @@ snapshots: '@typescript-eslint/types': 6.11.0 '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.5.4) eslint: 8.57.1 - semver: 7.7.2 + semver: 7.8.5 transitivePeerDependencies: - supports-color - typescript @@ -25175,8 +25119,6 @@ snapshots: ci-info@3.9.0: {} - ci-info@4.3.0: {} - ci-info@4.3.1: {} cjs-module-lexer@1.2.2: {} @@ -25232,6 +25174,12 @@ snapshots: cli-spinners@2.9.2: {} + cli-table3@0.6.1: + dependencies: + string-width: 4.2.3 + optionalDependencies: + colors: 1.4.0 + cli-table3@0.6.5: dependencies: string-width: 4.2.3 @@ -25498,7 +25446,7 @@ snapshots: handlebars: 4.7.7 json-stringify-safe: 5.0.1 meow: 8.1.2 - semver: 7.7.2 + semver: 7.8.5 split: 1.0.1 conventional-changelog-writer@8.2.0: @@ -25506,7 +25454,7 @@ snapshots: conventional-commits-filter: 5.0.0 handlebars: 4.7.7 meow: 13.2.0 - semver: 7.7.2 + semver: 7.8.5 conventional-commits-filter@3.0.0: dependencies: @@ -25710,7 +25658,7 @@ snapshots: postcss-modules-values: 4.0.0(postcss@8.4.32) postcss-value-parser: 4.2.0 schema-utils: 3.3.0 - semver: 7.7.2 + semver: 7.8.5 webpack: 5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5) css-loader@6.8.1(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)): @@ -25722,7 +25670,7 @@ snapshots: postcss-modules-scope: 3.0.0(postcss@8.4.32) postcss-modules-values: 4.0.0(postcss@8.4.32) postcss-value-parser: 4.2.0 - semver: 7.7.2 + semver: 7.8.5 webpack: 5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5) css-select@4.1.3: @@ -25803,15 +25751,14 @@ snapshots: dependencies: ally.js: 1.4.1 - cypress-real-events@1.12.0(cypress@13.6.0): + cypress-real-events@1.15.0(cypress@14.5.4): dependencies: - cypress: 13.6.0 + cypress: 14.5.4 - cypress@13.6.0: + cypress@14.5.4: dependencies: - '@cypress/request': 3.0.1 + '@cypress/request': 3.0.9 '@cypress/xvfb': 1.2.4(supports-color@8.1.1) - '@types/node': 18.19.17 '@types/sinonjs__fake-timers': 8.1.1 '@types/sizzle': 2.3.3 arch: 2.2.0 @@ -25821,8 +25768,9 @@ snapshots: cachedir: 2.3.0 chalk: 4.1.2 check-more-types: 2.24.0 + ci-info: 4.3.1 cli-cursor: 3.1.0 - cli-table3: 0.6.5 + cli-table3: 0.6.1 commander: 6.2.1 common-tags: 1.8.2 dayjs: 1.11.10 @@ -25835,7 +25783,7 @@ snapshots: figures: 3.2.0 fs-extra: 9.1.0 getos: 3.2.1 - is-ci: 3.0.1 + hasha: 5.2.2 is-installed-globally: 0.4.0 lazy-ass: 1.6.0 listr2: 3.14.0(enquirer@2.4.1) @@ -25847,9 +25795,10 @@ snapshots: process: 0.11.10 proxy-from-env: 1.0.0 request-progress: 3.0.0 - semver: 7.7.2 + semver: 7.8.5 supports-color: 8.1.1 - tmp: 0.2.1 + tmp: 0.2.6 + tree-kill: 1.2.2 untildify: 4.0.0 yauzl: 2.10.0 @@ -26075,7 +26024,7 @@ snapshots: date-fns@2.30.0: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 date-fns@4.4.0: {} @@ -26236,7 +26185,7 @@ snapshots: prompts: 2.4.2 rechoir: 0.8.0 safe-regex: 2.1.1 - semver: 7.7.2 + semver: 7.8.5 teamcity-service-messages: 0.1.14 tsconfig-paths-webpack-plugin: 4.2.0 watskeburt: 4.2.3 @@ -26446,7 +26395,7 @@ snapshots: dom-helpers@5.1.3: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 csstype: 2.6.19 dom-serializer@1.3.2: @@ -26592,10 +26541,6 @@ snapshots: iconv-lite: 0.6.3 optional: true - end-of-stream@1.4.4: - dependencies: - once: 1.4.0 - end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -27508,7 +27453,7 @@ snapshots: fork-ts-checker-webpack-plugin@4.1.6(eslint@8.57.1)(typescript@5.5.4)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)): dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.7 chalk: 2.4.2 micromatch: 4.0.8 minimatch: 3.1.4 @@ -27522,7 +27467,7 @@ snapshots: fork-ts-checker-webpack-plugin@6.5.3(eslint@8.57.1)(typescript@5.5.4)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)): dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.7 '@types/json-schema': 7.0.15 chalk: 4.1.2 chokidar: 3.6.0 @@ -27533,7 +27478,7 @@ snapshots: memfs: 3.6.0 minimatch: 3.1.2 schema-utils: 2.7.0 - semver: 7.7.2 + semver: 7.8.5 tapable: 1.1.3 typescript: 5.5.4 webpack: 5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5) @@ -27542,7 +27487,7 @@ snapshots: fork-ts-checker-webpack-plugin@9.0.2(typescript@5.5.4)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)): dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.7 chalk: 4.1.2 chokidar: 3.6.0 cosmiconfig: 8.3.6(typescript@5.5.4) @@ -27552,7 +27497,7 @@ snapshots: minimatch: 3.1.4 node-abort-controller: 3.1.1 schema-utils: 3.3.0 - semver: 7.7.2 + semver: 7.8.5 tapable: 2.3.0 typescript: 5.5.4 webpack: 5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5) @@ -27832,7 +27777,7 @@ snapshots: git-semver-tags@5.0.1: dependencies: meow: 8.1.2 - semver: 7.7.2 + semver: 7.8.5 git-up@7.0.0: dependencies: @@ -28132,7 +28077,7 @@ snapshots: babel-loader: 9.1.2(@babel/core@7.28.4)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)) babel-plugin-dynamic-import-node: 2.3.3 commander: 2.20.3 - form-data: 4.0.4 + form-data: 4.0.5 glob: 10.4.5 https-proxy-agent: 7.0.6(supports-color@8.1.1) jose: 5.10.0 @@ -28500,11 +28445,11 @@ snapshots: transitivePeerDependencies: - debug - http-signature@1.3.6: + http-signature@1.4.0: dependencies: assert-plus: 1.0.0 jsprim: 2.0.2 - sshpk: 1.17.0 + sshpk: 1.18.0 http2-wrapper@1.0.3: dependencies: @@ -28706,7 +28651,7 @@ snapshots: npm-package-arg: 11.0.3 promzard: 1.0.0 read: 3.0.1 - semver: 7.7.2 + semver: 7.8.5 validate-npm-package-license: 3.0.4 validate-npm-package-name: 5.0.1 transitivePeerDependencies: @@ -28718,7 +28663,7 @@ snapshots: npm-package-arg: 13.0.1 promzard: 2.0.0 read: 4.1.0 - semver: 7.7.2 + semver: 7.8.5 validate-npm-package-license: 3.0.4 validate-npm-package-name: 6.0.2 @@ -28748,7 +28693,7 @@ snapshots: mute-stream: 0.0.8 ora: 5.4.1 run-async: 2.4.1 - rxjs: 7.8.1 + rxjs: 7.8.2 string-width: 4.2.3 strip-ansi: 6.0.1 through: 2.3.8 @@ -29108,7 +29053,7 @@ snapshots: '@babel/parser': 7.28.4 '@istanbuljs/schema': 0.1.2 istanbul-lib-coverage: 3.2.0 - semver: 7.7.2 + semver: 7.8.5 transitivePeerDependencies: - supports-color @@ -29373,7 +29318,7 @@ snapshots: jest-message-util@29.7.0: dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.7 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.0 chalk: 4.1.2 @@ -29497,7 +29442,7 @@ snapshots: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.7.2 + semver: 7.8.5 transitivePeerDependencies: - supports-color @@ -29645,7 +29590,7 @@ snapshots: decimal.js: 10.6.0 domexception: 4.0.0 escodegen: 2.1.0 - form-data: 4.0.4 + form-data: 4.0.5 html-encoding-sniffer: 3.0.0 http-proxy-agent: 5.0.0 https-proxy-agent: 5.0.1 @@ -29672,7 +29617,7 @@ snapshots: cssstyle: 4.6.0 data-urls: 5.0.0 decimal.js: 10.6.0 - form-data: 4.0.4 + form-data: 4.0.5 html-encoding-sniffer: 4.0.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -29843,7 +29788,7 @@ snapshots: jws: 3.2.3 lodash: 4.17.21 ms: 2.1.3 - semver: 7.7.2 + semver: 7.8.5 jsprim@2.0.2: dependencies: @@ -30121,7 +30066,7 @@ snapshots: npm-package-arg: 13.0.1 npm-registry-fetch: 19.1.0 proc-log: 5.0.0 - semver: 7.7.2 + semver: 7.8.5 sigstore: 4.1.0 ssri: 12.0.0 transitivePeerDependencies: @@ -30129,12 +30074,12 @@ snapshots: libnpmpublish@9.0.9: dependencies: - ci-info: 4.3.0 + ci-info: 4.3.1 normalize-package-data: 6.0.2 npm-package-arg: 11.0.3 npm-registry-fetch: 17.1.0 proc-log: 4.2.0 - semver: 7.7.2 + semver: 7.8.5 sigstore: 2.2.2 ssri: 10.0.6 transitivePeerDependencies: @@ -30223,7 +30168,7 @@ snapshots: log-update: 4.0.0 p-map: 4.0.0 rfdc: 1.4.1 - rxjs: 7.8.1 + rxjs: 7.8.2 through: 2.3.8 wrap-ansi: 7.0.0 optionalDependencies: @@ -30491,7 +30436,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.2 + semver: 7.8.5 make-error@1.3.6: {} @@ -31342,7 +31287,7 @@ snapshots: make-fetch-happen: 13.0.0 nopt: 7.2.1 proc-log: 3.0.0 - semver: 7.7.2 + semver: 7.8.5 tar: 6.2.1 which: 4.0.0 transitivePeerDependencies: @@ -31355,7 +31300,7 @@ snapshots: graceful-fs: 4.2.11 nopt: 9.0.0 proc-log: 6.1.0 - semver: 7.7.2 + semver: 7.8.5 tar: 7.5.11 tinyglobby: 0.2.15 undici: 6.25.0 @@ -31371,7 +31316,7 @@ snapshots: nopt: 6.0.0 npmlog: 6.0.2 rimraf: 3.0.2 - semver: 7.7.2 + semver: 7.8.5 tar: 6.2.1 which: 2.0.2 transitivePeerDependencies: @@ -31420,20 +31365,20 @@ snapshots: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.16.1 - semver: 7.7.2 + semver: 7.8.5 validate-npm-package-license: 3.0.4 normalize-package-data@5.0.0: dependencies: hosted-git-info: 6.1.1 is-core-module: 2.16.1 - semver: 7.7.2 + semver: 7.8.5 validate-npm-package-license: 3.0.4 normalize-package-data@6.0.2: dependencies: hosted-git-info: 7.0.2 - semver: 7.7.2 + semver: 7.8.5 validate-npm-package-license: 3.0.4 normalize-path@3.0.0: {} @@ -31492,7 +31437,7 @@ snapshots: rc-config-loader: 4.1.3 remote-git-tags: 3.0.0 rimraf: 5.0.10 - semver: 7.7.2 + semver: 7.8.5 semver-utils: 1.1.4 source-map-support: 0.5.21 spawn-please: 2.0.2 @@ -31506,15 +31451,15 @@ snapshots: npm-install-checks@6.3.0: dependencies: - semver: 7.7.2 + semver: 7.8.5 npm-install-checks@7.1.2: dependencies: - semver: 7.7.2 + semver: 7.8.5 npm-install-checks@8.0.0: dependencies: - semver: 7.7.2 + semver: 7.8.5 npm-normalize-package-bin@3.0.1: {} @@ -31526,35 +31471,35 @@ snapshots: dependencies: hosted-git-info: 6.1.1 proc-log: 3.0.0 - semver: 7.7.2 + semver: 7.8.5 validate-npm-package-name: 5.0.1 npm-package-arg@11.0.2: dependencies: hosted-git-info: 7.0.2 proc-log: 4.2.0 - semver: 7.7.2 + semver: 7.8.5 validate-npm-package-name: 5.0.1 npm-package-arg@11.0.3: dependencies: hosted-git-info: 7.0.2 proc-log: 4.2.0 - semver: 7.7.2 + semver: 7.8.5 validate-npm-package-name: 5.0.1 npm-package-arg@12.0.1: dependencies: hosted-git-info: 8.1.0 proc-log: 5.0.0 - semver: 7.7.2 + semver: 7.8.5 validate-npm-package-name: 6.0.2 npm-package-arg@13.0.1: dependencies: hosted-git-info: 9.0.2 proc-log: 5.0.0 - semver: 7.7.2 + semver: 7.8.5 validate-npm-package-name: 6.0.2 npm-packlist@10.0.3: @@ -31575,28 +31520,28 @@ snapshots: npm-install-checks: 7.1.2 npm-normalize-package-bin: 4.0.0 npm-package-arg: 12.0.1 - semver: 7.7.2 + semver: 7.8.5 npm-pick-manifest@11.0.3: dependencies: npm-install-checks: 8.0.0 npm-normalize-package-bin: 5.0.0 npm-package-arg: 13.0.1 - semver: 7.7.2 + semver: 7.8.5 npm-pick-manifest@8.0.2: dependencies: npm-install-checks: 6.3.0 npm-normalize-package-bin: 3.0.1 npm-package-arg: 10.1.0 - semver: 7.7.2 + semver: 7.8.5 npm-pick-manifest@9.1.0: dependencies: npm-install-checks: 6.3.0 npm-normalize-package-bin: 3.0.1 npm-package-arg: 11.0.3 - semver: 7.7.2 + semver: 7.8.5 npm-registry-fetch@14.0.5: dependencies: @@ -32096,14 +32041,14 @@ snapshots: got: 11.8.6 registry-auth-token: 4.2.2 registry-url: 5.1.0 - semver: 7.7.2 + semver: 7.8.5 package-json@8.1.1: dependencies: got: 12.6.1 registry-auth-token: 5.1.0 registry-url: 6.0.1 - semver: 7.7.2 + semver: 7.8.5 package-manager-detector@0.2.11: dependencies: @@ -32265,14 +32210,14 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.7 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 parse-json@8.1.0: dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.7 index-to-position: 0.1.2 type-fest: 4.15.0 @@ -32477,7 +32422,7 @@ snapshots: loader-utils: 2.0.4 postcss: 8.4.32 schema-utils: 3.3.0 - semver: 7.7.2 + semver: 7.8.5 webpack: 5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5) postcss-loader@7.3.3(postcss@8.4.32)(typescript@5.5.4)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)): @@ -32787,7 +32732,7 @@ snapshots: pump@3.0.0: dependencies: - end-of-stream: 1.4.4 + end-of-stream: 1.4.5 once: 1.4.0 punycode@2.3.1: {} @@ -32800,10 +32745,6 @@ snapshots: q@1.5.1: {} - qs@6.10.4: - dependencies: - side-channel: 1.1.0 - qs@6.13.0: dependencies: side-channel: 1.1.0 @@ -32894,7 +32835,7 @@ snapshots: dependencies: react: 18.2.0 - react-day-picker@8.10.2(date-fns@2.30.0)(react@18.2.0): + react-day-picker@8.10.2(date-fns@4.4.0)(react@18.2.0): dependencies: date-fns: 4.4.0 react: 18.2.0 @@ -32903,7 +32844,7 @@ snapshots: react-dev-utils@12.0.1(eslint@8.57.1)(typescript@5.5.4)(webpack@5.98.0(patch_hash=e2c4e4364d7f63ce4220ac564a0eb0a6789525608c04a20b13e962e3eccec20c)(@swc/core@1.13.5)): dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.7 address: 1.2.2 browserslist: 4.26.2 chalk: 4.1.2 @@ -33081,8 +33022,6 @@ snapshots: react-is@18.3.1: {} - react-is@19.2.3: {} - react-lifecycles-compat@3.0.4: {} react-markdown@9.0.0(@types/react@17.0.39)(react@18.2.0): @@ -33232,12 +33171,6 @@ snapshots: react: 18.2.0 refractor: 2.10.1 - react-test-renderer@19.2.3(react@18.2.0): - dependencies: - react: 18.2.0 - react-is: 19.2.3 - scheduler: 0.27.0 - react-textarea-autosize@7.1.2(react@18.2.0): dependencies: '@babel/runtime': 7.29.2 @@ -33255,7 +33188,7 @@ snapshots: react-transition-group@4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 dom-helpers: 5.1.3 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -33809,8 +33742,6 @@ snapshots: dependencies: loose-envify: 1.4.0 - scheduler@0.27.0: {} - schema-utils@2.7.0: dependencies: '@types/json-schema': 7.0.15 @@ -33876,7 +33807,7 @@ snapshots: p-reduce: 3.0.0 read-package-up: 11.0.0 resolve-from: 5.0.0 - semver: 7.7.2 + semver: 7.8.5 semver-diff: 4.0.0 signale: 1.4.0 yargs: 17.7.2 @@ -33886,7 +33817,7 @@ snapshots: semver-diff@4.0.0: dependencies: - semver: 7.7.2 + semver: 7.8.5 semver-regex@3.1.4: {} @@ -34333,7 +34264,7 @@ snapshots: sprintf-js@1.0.3: {} - sshpk@1.17.0: + sshpk@1.18.0: dependencies: asn1: 0.2.6 assert-plus: 1.0.0 @@ -34979,10 +34910,6 @@ snapshots: dependencies: os-tmpdir: 1.0.2 - tmp@0.2.1: - dependencies: - rimraf: 3.0.2 - tmp@0.2.6: {} tmpl@1.0.5: {} @@ -35281,8 +35208,6 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.0.2 - undici-types@5.26.5: {} - undici-types@7.12.0: {} undici@5.28.5: @@ -35459,7 +35384,7 @@ snapshots: is-yarn-global: 0.4.1 latest-version: 7.0.0 pupa: 3.3.0 - semver: 7.7.2 + semver: 7.8.5 semver-diff: 4.0.0 xdg-basedir: 5.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 475069d209..ba0efd56bf 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -53,7 +53,6 @@ overrides: prismjs: '^1.30.0' react: '^18.2.0' react-dom: '^18.2.0' - react-test-renderer: '^19.2.0' semver-regex: '^3.1.3' trim: '^0.0.3' unset-value: '^2.0.1' @@ -75,6 +74,9 @@ peerDependencyRules: patchedDependencies: '@changesets/get-github-info@0.6.0': patches/@changesets__get-github-info@0.6.0.patch '@storybook/react-docgen-typescript-plugin@1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0': 'patches/@storybook__react-docgen-typescript-plugin@1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0.patch' + # 3.0.1 publishes its declarations at dist/src/index.d.ts but `types` still + # points at dist/index.d.ts, so the package resolves as untyped. Drop the + # patch once a release ships the corrected path. react-final-form-listeners@3.0.1: patches/react-final-form-listeners@3.0.1.patch 'webpack@5.98.0': 'patches/webpack@5.98.0.patch' resolvePeersFromWorkspaceRoot: true diff --git a/react19/package.json b/react19/package.json index d3bb0b9cd7..96d5601d68 100644 --- a/react19/package.json +++ b/react19/package.json @@ -2,11 +2,15 @@ "name": "picasso-react19-harness", "version": "0.0.0", "private": true, - "description": "Standalone React 19 install for pnpm test:react19 and STORYBOOK_REACT_19=1 (PF-2262 workstream E). Deliberately NOT a pnpm workspace member: inside the workspace its React 19 packages join hoisting/peer resolution and can win the root react-dom slot on a fresh install, breaking react-transition-group (findDOMNode) for the React 18 suite. Installed via `pnpm install --ignore-workspace` in this directory (the test:react19 script does it automatically); jest.react19.mjs and .storybook/main.js resolve react/react-dom from react19/node_modules explicitly.", + "description": "Standalone React 19 install for `pnpm test:react19`, `pnpm typecheck:react19`, `pnpm test:integration:react19` and `STORYBOOK_REACT_19=1`, installed with `pnpm react19:install`. Deliberately not a pnpm workspace member; the header of jest.react19.mjs says why, and when it can go.", "dependencies": { "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.3.0", "react": "^19.2.0", "react-dom": "^19.2.0" + }, + "devDependencies": { + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0" } } diff --git a/react19/pnpm-lock.yaml b/react19/pnpm-lock.yaml index 7da10fee25..d159a6c171 100644 --- a/react19/pnpm-lock.yaml +++ b/react19/pnpm-lock.yaml @@ -13,13 +13,20 @@ importers: version: 10.4.1 '@testing-library/react': specifier: ^16.3.0 - version: 16.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: specifier: ^19.2.0 version: 19.2.8 react-dom: specifier: ^19.2.0 version: 19.2.8(react@19.2.8) + devDependencies: + '@types/react': + specifier: ^19.2.0 + version: 19.2.18 + '@types/react-dom': + specifier: ^19.2.0 + version: 19.2.5(@types/react@19.2.18) packages: @@ -57,6 +64,14 @@ packages: '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/react-dom@19.2.5': + resolution: {integrity: sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -68,6 +83,9 @@ packages: aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -127,15 +145,26 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 - '@testing-library/react@16.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@testing-library/react@16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@babel/runtime': 7.29.7 '@testing-library/dom': 10.4.1 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) '@types/aria-query@5.0.4': {} + '@types/react-dom@19.2.5(@types/react@19.2.18)': + dependencies: + '@types/react': 19.2.18 + + '@types/react@19.2.18': + dependencies: + csstype: 3.2.3 + ansi-regex@5.0.1: {} ansi-styles@5.2.0: {} @@ -144,6 +173,8 @@ snapshots: dependencies: dequal: 2.0.3 + csstype@3.2.3: {} + dequal@2.0.3: {} dom-accessibility-api@0.5.16: {} diff --git a/react19/sanity.test.tsx b/react19/sanity.test.tsx index 9288e5246c..6e0424a09f 100644 --- a/react19/sanity.test.tsx +++ b/react19/sanity.test.tsx @@ -1,4 +1,4 @@ -import React, { StrictMode, useEffect, useState } from 'react' +import React, { StrictMode, useEffect, useId, useState } from 'react' import { version as reactDomVersion } from 'react-dom' import { render } from '@testing-library/react' @@ -13,6 +13,20 @@ describe('react 19 test runtime', () => { expect(reactDomVersion).toBe(React.version) }) + it('rewrites useId output to the React 18 format the snapshots use', () => { + let id = '' + + const Probe = () => { + id = useId() + + return null + } + + render() + + expect(id).toMatch(/^:r[0-9a-z]*:$/) + }) + it('renders and re-renders under StrictMode', () => { const consoleError = jest.spyOn(console, 'error').mockImplementation() diff --git a/tsconfig.react19.json b/tsconfig.react19.json new file mode 100644 index 0000000000..732dce24e4 --- /dev/null +++ b/tsconfig.react19.json @@ -0,0 +1,47 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "composite": false, + "incremental": false, + "declaration": false, + "declarationMap": false, + "sourceMap": false, + "typeRoots": [ + "./react19/node_modules/@types", + "./node_modules/@types", + "./@types" + ], + "types": [ + "node", + "jest", + "global", + "ap-style-title-case", + "hast-util-from-dom", + "react-deprecate", + "react-source-render" + ], + "paths": { + "react": ["./react19/node_modules/@types/react"], + "react/*": ["./react19/node_modules/@types/react/*"], + "react-dom": ["./react19/node_modules/@types/react-dom"], + "react-dom/*": ["./react19/node_modules/@types/react-dom/*"] + } + }, + // The sanity test is the one test written against @types/react 19; `files` + // is not subject to `exclude`, so it is checked while the rest stay out. + "files": ["react19/sanity.test.tsx"], + "include": ["packages/*/src", "packages/base/*/src"], + "exclude": [ + "**/test.tsx", + "**/*.test.tsx", + "**/test.ts", + "**/*.test.ts", + "**/__tests__", + "**/__testfixtures__", + "**/story", + "**/*.example.tsx", + "packages/picasso-codemod", + "packages/picasso-cypress-utils" + ] +} From ad0398b873e80be8304e8c26f3a3f7674b19de2a Mon Sep 17 00:00:00 2001 From: Vedran Ivanac Date: Mon, 7 Sep 2026 16:17:16 +0200 Subject: [PATCH 14/14] Drop the emoji picker changeset already released in #5107 Co-Authored-By: Claude Fable 5.1 --- .changeset/rte-emoji-picker-click-through.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .changeset/rte-emoji-picker-click-through.md diff --git a/.changeset/rte-emoji-picker-click-through.md b/.changeset/rte-emoji-picker-click-through.md deleted file mode 100644 index bda52cfcb6..0000000000 --- a/.changeset/rte-emoji-picker-click-through.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@toptal/picasso-rich-text-editor': patch ---- - -### RichTextEditor - -- fix the emoji picker ignoring every emoji click: the open picker kept `pointer-events: none`, so clicks fell through to the editor underneath and only closed the picker. Its hidden-state classes are now dropped while it is open, and picking an emoji inserts it again. Regression from the Tailwind migration in `v100`