Skip to content

Commit 0637971

Browse files
frankieyanclaude
andauthored
feat: support React 19 (#1074)
* refactor: drop findDOMNode and dead defaultProps Replace ReactDOM.findDOMNode(this) in the deprecated ColorPicker dropdown Box with a typed root ref (removed in React 19), and delete the redundant Button.defaultProps block (ignored for forwardRef in React 19; already shadowed by the destructuring defaults). Both fixes are version-safe under React 18. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: support React 19 Bump dev react/react-dom/@types/react/@types/react-dom/react-is to 19.x and widen the peer ranges to >=18 <20 (additive for existing React 18 consumers). Run types-react-codemod preset-19 for the React.JSX scoping and useRef initial argument, then hand-fix the React 19 type and runtime breakages: - tooltip: read the child ref from props.ref under React 19 (falls back to element.ref on 18) instead of the removed element.ref. - polymorphism: cast the forwardRef render fn to React 19's PropsWithoutRef render signature. - stack: widen react-keyed-flatten-children's React 19-removed ReactChild[] return to ReactNode[]. - revert the codemod's ReactElement<any> additions (the repo bans explicit any; bare ReactElement now defaults to unknown). - add @types/prop-types, previously pulled in transitively by @types/react 18. Also drive the hover-opened submenu test selection via keyboard: jsdom has no layout, so ariakit closes the submenu when the pointer moves onto an item, and React 19's synchronous flushing unmounts it before the click lands. Real-browser hover + click works (verified in Storybook); keyboard selection is layout- independent and version-stable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: add React 18 jest run Add aliased react-18 / react-dom-18 dev dependencies and a second Jest config that remaps react/react-dom to them, so the suite runs against React 18 from a single install. Wire test:react18 into validate (and therefore the pre-push hook), sharing the same committed snapshots. react-dom-18 peers on react ^18 while the top-level react is 19; an overrides entry points its react peer at the top-level version so npm resolves the tree (Jest remaps everything to the 18 aliases at run time regardless). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: run tests against React 18 and 19 Add a second unit-testing step running npm run test:react18 off the same install, so CI exercises the suite under both React versions. Two sequential steps on the single runner; a strategy matrix is unnecessary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: remove prop-types usage React 19 ignores propTypes, so drop the only two uses: the import + propTypes block in ThreeDotsIcon and the propTypes member in the deprecated polymorphism types. Remove prop-types from peerDependencies and the @types/prop-types dev dependency (added in the React 19 bump only to type the now-deleted usage). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: drop the ref-masking cast in polymorphicComponent PolymorphicComponentProps is built on React.ComponentProps, which includes ref under React 19. A forwardRef render never receives ref in props, so move the strip into the ForwardRefFunction contract via PropsWithoutRef and ForwardedRef, dropping the cast and the now-unused PolymorphicRenderFunction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: type ThreeDotsIcon's color prop Extract a named ThreeDotsIconProps type and switch to a function declaration so the color prop is explicitly typed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: document the keyboard-driven submenu selection Explain why the submenu test drives selection via keyboard: jsdom can't give Ariakit the DOMRects it needs to keep the hovercard open, and React 19 flushes the close render before a click can land. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: type-check against React 18 typings Add a tsconfig.react18.json that remaps react/react-dom to aliased @types/react@18 and @types/react-dom@18, wired into a type-check:react18 script, the validate chain, and CI. This catches typings regressions against the older React the library still supports. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: reject string refs in Tooltip Throw on string refs instead of silently forwarding them, so React 18 consumers get a clear unsupported-usage error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 53777ff commit 0637971

26 files changed

Lines changed: 224 additions & 87 deletions

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
lint-staged.config.js
22
jest.config.js
3+
jest.config.react18.js
34
babel.config.js
45
plopfile.mjs
56
release.config.js

.github/workflows/check-ci-validation.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ jobs:
8888
run: |
8989
npm run type-check
9090
91+
- name: Perform type checking with TypeScript (React 18 typings)
92+
run: |
93+
npm run type-check:react18
94+
9195
- name: Check React Compiler compatibility
9296
if: ${{ github.event_name == 'pull_request' }}
9397
run: |
@@ -129,10 +133,14 @@ jobs:
129133
run: |
130134
npm ci
131135
132-
- name: Test codebase correctness
136+
- name: Test codebase correctness (React 19)
133137
run: |
134138
npm run test
135139
140+
- name: Test codebase correctness (React 18)
141+
run: |
142+
npm run test:react18
143+
136144
build-package:
137145
name: Build Package
138146
runs-on: ubuntu-latest

jest.config.react18.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const baseConfig = require('./jest.config')
2+
3+
// Runs the Jest suite against React 18 by remapping react/react-dom to the aliased 18 packages.
4+
module.exports = {
5+
...baseConfig,
6+
moduleNameMapper: {
7+
...baseConfig.moduleNameMapper,
8+
'^react$': 'react-18',
9+
'^react/(.*)$': 'react-18/$1',
10+
'^react-dom$': 'react-dom-18',
11+
'^react-dom/(.*)$': 'react-dom-18/$1',
12+
},
13+
}

package-lock.json

Lines changed: 85 additions & 27 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"scripts": {
3939
"postinstall": "patch-package",
4040
"setup": "npm install && npm run validate",
41-
"validate": "npm run lint && npm run type-check && npm run test",
41+
"validate": "npm run lint && npm run type-check && npm run type-check:react18 && npm run test && npm run test:react18",
4242
"start": "ON_SUCCESS=\"./scripts/organize-styles.sh\" rollup -c --watch --no-watch.clearScreen",
4343
"prestart:yalc": "npm run clean && yalc publish",
4444
"start:yalc": "ON_SUCCESS=\"npm run start:yalc:success\" rollup -c --watch --no-watch.clearScreen",
@@ -49,8 +49,11 @@
4949
"clean": "rimraf es lib styles dist",
5050
"test": "jest --passWithNoTests",
5151
"test:watch": "npm run test -- --watch",
52+
"test:react18": "jest --config jest.config.react18.js",
53+
"test:react18:watch": "npm run test:react18 -- --watch",
5254
"test:coverage": "npm run test -- --coverage",
5355
"type-check": "tsc --noEmit -p ./tsconfig.json",
56+
"type-check:react18": "tsc --noEmit -p ./tsconfig.react18.json",
5457
"lint": "eslint --format codeframe --cache --ext js,jsx,ts,tsx ./",
5558
"storybook": "storybook dev -p 6006",
5659
"prettify": "prettier --write \"./**/*.{js,jsx,ts,tsx,json,css,scss,less,md,mdx}\"",
@@ -59,10 +62,9 @@
5962
"peerDependencies": {
6063
"@ariakit/react": "~0.4.19",
6164
"classnames": "^2.2.5",
62-
"prop-types": "^15.6.2",
63-
"react": ">=18.0.0 <19.0.0",
65+
"react": ">=18.0.0 <20.0.0",
6466
"react-compiler-runtime": "^1.0.0",
65-
"react-dom": ">=18.0.0 <19.0.0"
67+
"react-dom": ">=18.0.0 <20.0.0"
6668
},
6769
"devDependencies": {
6870
"@ariakit/react": "0.4.19",
@@ -95,8 +97,10 @@
9597
"@types/jest": "28.1.8",
9698
"@types/jest-axe": "^3.5.3",
9799
"@types/marked": "^4.0.8",
98-
"@types/react": "18.3.1",
99-
"@types/react-dom": "18.3.0",
100+
"@types/react": "19.2.17",
101+
"@types/react-18": "npm:@types/react@^18.3",
102+
"@types/react-dom": "19.2.3",
103+
"@types/react-dom-18": "npm:@types/react-dom@^18.3",
100104
"@typescript-eslint/eslint-plugin": "8.46.2",
101105
"@typescript-eslint/parser": "8.46.2",
102106
"@vitejs/plugin-react": "5.2.0",
@@ -125,11 +129,13 @@
125129
"mockdate": "^3.0.2",
126130
"plop": "^3.1.1",
127131
"prettier": "3.6.2",
128-
"react": "18.3.1",
132+
"react": "19.2.7",
133+
"react-18": "npm:react@18.3.1",
129134
"react-compiler-runtime": "1.0.0",
130-
"react-dom": "18.3.1",
131135
"react-docgen-typescript": "2.4.0",
132-
"react-is": "18.3.1",
136+
"react-dom": "19.2.7",
137+
"react-dom-18": "npm:react-dom@18.3.1",
138+
"react-is": "19.2.7",
133139
"rimraf": "^3.0.2",
134140
"rollup": "2.79.2",
135141
"rollup-plugin-styles": "4.0.0",
@@ -154,6 +160,12 @@
154160
"pretty-format": {
155161
"react-is": "$react-is"
156162
},
157-
"react-element-to-jsx-string": "15.0.0"
163+
"react-element-to-jsx-string": "15.0.0",
164+
"react-dom-18": {
165+
"react": "$react"
166+
},
167+
"@types/react-dom-18": {
168+
"@types/react": "$@types/react"
169+
}
158170
}
159171
}

src/checkbox-field/checkbox-field.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { useForkRef } from './use-fork-ref'
88

99
import styles from './checkbox-field.module.css'
1010

11+
import type { JSX } from 'react'
12+
1113
interface CheckboxFieldProps
1214
extends Omit<
1315
JSX.IntrinsicElements['input'],

src/components/color-picker/deprecated-button/deprecated-button.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,4 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
8383

8484
Button.displayName = 'Button'
8585

86-
Button.defaultProps = {
87-
size: 'default',
88-
loading: false,
89-
disabled: false,
90-
}
91-
9286
export { Button }

src/components/color-picker/deprecated-dropdown/dropdown.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import './dropdown.less'
22

33
import * as React from 'react'
4-
import ReactDOM from 'react-dom'
54

65
import classNames from 'classnames'
76

87
import Button from '../deprecated-button'
98

9+
import type { JSX } from 'react'
10+
1011
type BoxProps = {
1112
onShowBody?: () => void
1213
onHideBody?: () => void
@@ -29,6 +30,8 @@ type BoxState = {
2930
class Box extends React.Component<BoxProps, BoxState> {
3031
public static displayName: string
3132

33+
private rootRef = React.createRef<HTMLDivElement>()
34+
3235
constructor(props: BoxProps, context: React.Context<unknown>) {
3336
super(props, context)
3437
this.state = {
@@ -48,7 +51,7 @@ class Box extends React.Component<BoxProps, BoxState> {
4851
_timeout?: ReturnType<typeof setTimeout>
4952

5053
_handleClickOutside = (event: MouseEvent) => {
51-
const dropdownDOMNode = ReactDOM.findDOMNode(this)
54+
const dropdownDOMNode = this.rootRef.current
5255

5356
if (dropdownDOMNode && !dropdownDOMNode.contains(event.target as Node))
5457
this._toggleShowBody()
@@ -93,13 +96,12 @@ class Box extends React.Component<BoxProps, BoxState> {
9396
)
9497

9598
if (scrollingParent) {
96-
const dropdown = ReactDOM.findDOMNode(this)
99+
const dropdown = this.rootRef.current
97100
if (!dropdown) {
98101
return
99102
}
100-
const dropdownVerticalPosition = (ReactDOM.findDOMNode(this) as HTMLElement)
101-
.offsetTop
102-
const dropdownTrigger = (dropdown as Element).querySelector('.trigger')
103+
const dropdownVerticalPosition = dropdown.offsetTop
104+
const dropdownTrigger = dropdown.querySelector('.trigger')
103105
if (!dropdownTrigger) {
104106
return
105107
}
@@ -160,6 +162,7 @@ class Box extends React.Component<BoxProps, BoxState> {
160162

161163
return (
162164
<div
165+
ref={this.rootRef}
163166
style={{ display: 'inline-block' }}
164167
className={className}
165168
data-testid="reactist-dropdown-box"

0 commit comments

Comments
 (0)