Skip to content

feat!: support react 19, storybook 10 and styled-components 6 - #576

Merged
Mukesh-FN merged 64 commits into
nextfrom
react-19-upgrade
Oct 14, 2026
Merged

Mukesh-FN merged 64 commits into
nextfrom
react-19-upgrade

Conversation

@Mukesh-FN

@Mukesh-FN Mukesh-FN commented Mar 24, 2026 •

Copy link
Copy Markdown
Contributor

What

Wave 3: Wave works with React 19 as well as React 18. Apps need styled-components 6 to use it.

Media

Nothing should look or work differently. Every component example and documentation page was compared with today's site, in light and dark mode.

Why

Teams moving their apps to React 19 can't use Wave 2.

How

  • Test version first. Merging this publishes a release candidate, 3.0.0-rc.1, that app teams can try with npm i @freenow/wave@next styled-components@^6. Wave 2 stays the default version, and wave.free-now.com doesn't change.
  • Then the launch. Once teams have tested it, Wave 3 becomes the default version and wave.free-now.com shows its documentation. The Wave 2 documentation moves to wave.free-now.com/v2/.
  • Wave 2 keeps getting fixes, during testing and after the launch.
  • Upgrading: see Migrating to Wave 3. For most apps it's styled-components 6 and updated snapshots.

Try it now: npm i https://pkg.pr.new/@freenow/wave@576 styled-components@^6

Checklist

  • Tests cover the change, and npm test passes without console errors or warnings
  • New or changed components have a story, and their documentation is updated
  • New form components are added to src/components/FormComponents.spec.tsx
  • Checked with React 18 and React 19 (npm run test:fixture runs the consumer apps)
  • Breaking changes have a BREAKING CHANGE: footer in the commit message that explains what consumers need to change

Merging

  • Merge into next, after the PR that pauses the automatic main → next merge.
  • Squash and merge with the prepared title and commit message, so it's released as version 3 and not 2.67.

@Mukesh-FN
Mukesh-FN force-pushed the react-19-upgrade branch 3 times, most recently from 36c4f59 to 6d99c6e Compare March 24, 2026 18:47
@pkg-pr-new

pkg-pr-new Bot commented Mar 25, 2026 •

Copy link
Copy Markdown

commit: 3cc5f75

@pkg-pr-new

pkg-pr-new Bot commented Mar 25, 2026

Copy link
Copy Markdown

commit: 4516c68

@Mukesh-FN
Mukesh-FN force-pushed the react-19-upgrade branch 2 times, most recently from 8b14e56 to dda17f5 Compare May 6, 2026 14:31
Phase 1 — Tooling upgrades:
- TypeScript 4 → 5.8.3, typescript-plugin-styled-components 1 → 3
- ts-jest 26 → 29, Jest 26 → 29, @types/jest 25 → 29
- Add jest-environment-jsdom (Jest 29 no longer defaults to jsdom)
- eslint 7 → 8, @typescript-eslint v4 → v7, eslint-plugin-unicorn/jest/airbnb-typescript
- jest.config.js: add testEnvironment jsdom, ts-jest tuple transform format
- .eslintrc.js: rename recommended-requiring-type-checking → recommended-type-checked
- src/utils/testing.ts: update jest-dom import path (extend-expect removed in v6)
- Build scripts: ttsc → tsc (ttypescript incompatible with TS5)

Phase 2 — React 19.2.4:
- react/react-dom 18.2.0 → 19.2.4, @types/react v18 → v19
- @testing-library/react v14 → v16, jest-dom v5 → v6
- peerDependencies broadened to ^18.2.0 || ^19.0.0 for library consumers
- overrides: react-refresh 0.14.2, react-is 19.2.4, windowed-select types

Phase 3 — Remove defaultProps (removed in React 19 for FC/forwardRef):
- Styled-components (Button, TextButton, HelperText, Label, experimental/Text):
  defaultProps → .attrs<Props>({...})
- Function components (Checkbox, Modal, SelectList):
  defaultProps → destructuring defaults
- forwardRef components (Input, InnerInput):
  defaultProps → destructuring defaults

Phase 4 — TypeScript type fixes for @types/react@19:
- JSX global namespace removed: JSX.Element → React.JSX.Element
  (Breadcrumbs, TabBar/Link, Tooltip, DateField, DatePicker, Calendar,
   InfoBannerCard, experimental/Tooltip, withDeprecatedMessage, LabelWrapper)
- ReactDOM.findDOMNode removed: add nodeRef to all CSSTransition usages
  (Banner, Modal×2, Offcanvas×3) using react-transition-group v4 nodeRef API
- useRef<T>() requires arg: → useRef<T | null>(null)
  (Password, PhoneInput, DatepickerRangeInput, InnerInput)
- Render-prop functions no longer assignable to ReactNode:
  Chip: remove unnecessary Fragment wrapper
  RadioButton: narrow children type to ReactNode via Omit
- React.cloneElement stricter typing: cast to HTMLAttributes & RefAttributes
  (Tooltip)

Storybook fix:
- @storybook/react-dom-shim preset only enables createRoot shim for React 18
  (version.startsWith("18")). React 19 falls through to legacy react-16.js
  shim which calls ReactDOM.render() — removed in React 19.
- Fix: webpackFinal alias in .storybook/main.ts forces react-18 shim
  (createRoot) which is fully compatible with React 19.

Result: 58/58 test suites pass, build succeeds, tsc --noEmit clean,
Storybook renders correctly.
- Use useDarkMode() from storybook-dark-mode in withColorScheme decorator
  (works in Storybook preview hook context, same as main branch)
- Fix docs.container dark mode using React hooks + addons.getChannel()
  to subscribe to DARK_MODE_EVENT_NAME, avoiding storybook preview-api
  hooks which throw outside decorator/story context
- Add missing ESLint plugins (import, react, react-hooks, jsx-a11y) to
  plugins array so ESLint 8 correctly resolves rules from extended configs
- Disable newly-triggered strict rules that fire on pre-existing code
- Fix no-floating-promises in useLocaleObject.ts
- Upgrade pretty-quick v3 to v4 for Prettier v3 compatibility
- Fix commit-msg hook for Husky v9 (use --edit $1 instead of HUSKY_GIT_PARAMS)
- Fix type-only exports across all components (export type {}) for
  Vite/esbuild compatibility
- Add import type for all type-only imports (ESLint consistent-type-imports)
- Add @vite-ignore to dynamic imports in Flag.tsx and useLocaleObject.ts
- Fix CSSTransition nodeRef for React 19 (Modal, Offcanvas, Banner)
- Fix React 19 breaking changes: defaultProps, JSX namespace, useRef types,
  ReactNode functions, cloneElement typing
- Migrate react-popper to @floating-ui/react for Popover and Tooltip
- Update date-fns v3 locale dynamic imports
- Add Husky v9 pre-commit hook
- Update jscodeshift v17 codemod type casts
- Add UPGRADE_PLAN.md documenting all migration phases
… warnings

- Replace legacy .eslintrc.js with ESLint 10 flat config (eslint.config.mjs)
- Remove unmaintained eslint-config-airbnb-typescript; replace with
  typescript-eslint unified package + @eslint-react/eslint-plugin
- Add eslint-plugin-import-x (maintained fork), globals, eslint-import-resolver-typescript
- Fix all 14 ESLint errors: conditional hooks, missing keys, leaked conditional
  rendering, stale disable comments, array sort, autofocus, prefer-ternary
- Fix 56 of 103 warnings: lazy useState init, ref naming convention,
  import naming, forwardRef (turned off for React 18+19 compat), complex
  dep-array expressions extracted to variables
- Bump tsconfig.json lib to es2023 (required for Array#toSorted)
- Add UPGRADE_PLAN_2026.md with 10-phase upgrade plan
- Upgrade styled-components ^5.3.8 → ^6.3.12; remove @types/styled-components (v6 ships own types)
- Add shouldForwardProp: isPropValid to all styled HTML element components to restore v5
  prop-filtering behavior (Box, Text, Headline, Button, Card, Link, Table, TabBar,
  Skeleton, Input, Select, Banner, Tag, Breadcrumbs, Datepicker, Popover, and more)
- Fix removed v6 types: StyledComponent→IStyledComponent, drop GlobalStyleComponent
  and ThemeProps, ReadonlyArray<string>→string[], use RuleSet for css return type
- Fix react-select components: replace styled(components.X) wrappers with inner styled
  divs to prevent theme-prop stripping (SingleValue, Option, DynamicWidthMenu)
- Update snapshots for stylis v4 CSS output (fewer vendor prefixes, gradient formatting)
- Bump typescript devDep to ^6.0.2
- Add ignoreDeprecations: "6.0" to tsconfig for node10 moduleResolution deprecation
- Exclude src/codemods entirely from tsconfig type-checking (uses Node.js globals)
- Add src/global.d.ts for CSS module, warning, @styled-system/theme-get, and process declarations

TS6 breaking changes fixed across ~55 files:
- Implicit-any now enforced (TS7053/TS7006): typed object index expressions, callback params
- Optional callbacks require optional-chaining invocation (TS2722)
- React.createContext default value must match generic type
- useRef<T>(null) → RefObject<T | null>, requiring casts for useDay library compatibility
- FocusedInput null vs undefined strictness in @datepicker-react/hooks
- String indexing on typed objects requires keyof typeof cast
- Boolean coercion required for mixed-type setHasValue calls
- isValidDateText updated to accept Date | undefined
- SelectList triggerReference typed as HTMLDivElement | null with explicit callback ref
- Tooltip: fix potential leaked conditional rendering
Remove ignoreDeprecations workaround by fixing all deprecated features:

- tsconfig.json: moduleResolution "node" → "bundler" (correct for a bundler-consumed library;
  avoids node16/nodenext requirement for explicit .js import extensions)
- package.json build:cjs: --target es5 → --target es2015 (ES5 target deprecated in TS6;
  consumers use bundlers that handle transpilation anyway)
- SelectList.tsx/styles.ts: fix size > 0 comparisons (size is 'medium'|'small', not a number;
  previously type errors were masked, now correctly use truthiness checks)
moduleResolution "bundler" requires module to be "esnext" or "preserve".
Without an explicit module setting, TypeScript defaults to CommonJS which
is incompatible. The build scripts override --module on the CLI anyway,
so this only affects IDE type-checking.
No config changes required — Storybook manages Vite internally.
Storybook 8 starts cleanly with Vite 8, library build unaffected.

Node ≥20.19 required by Vite 7+; running Node 24.13.1.
…nd @types/node v22

Root causes:
- moduleResolution: "bundler" couldn't resolve jscodeshift/dist/testUtils (no exports field)
- @types/node v22 removed CJS globals (__dirname, module) from global scope

Fix:
- Add tsconfig.jest.json with moduleResolution: "node" + module: "commonjs" for codemods
- Add src/codemods/global.d.ts declaring __dirname, __filename, jscodeshift/dist/testUtils
- Split jest.config.js into two projects: components (jsdom) and codemods (node, diagnostics off)
- Re-add storybook-dark-mode@5.0.0 (compatible with storybook@^10.0.0)
- Restore useDarkMode() hook in withColorScheme decorator
- Restore channel-based dark mode tracking in docs container
- Restore darkMode parameters with lightClass/darkClass/stylePreview
- Fix preview background by using CSS variable directly on wrapper div
  instead of relying on background:inherit through EnforcedColorScheme
- Remove custom manager.ts (was for native colorScheme global approach)
…o Node 24

- ESLint config: add parserOptions blocks for .storybook/, docs/, scripts/;
  disableTypeChecked for root JS/MJS config files; ignore .github/, storybook-static/
- tsconfig.eslint.json: add rootDir='.', noEmit=true, fix .storybook glob pattern
- Convert import styled from 'styled-components' to named import { styled }
  across 120+ files (v6 exports styled as named export)
- Breadcrumbs: replace deprecated Children.toArray/map/cloneElement with plain array map
- useOnChange: replace useState+useEffect anti-pattern with useRef for prev value
- Modal, Offcanvas: wrap handleClose in useCallback, add to effect deps
- Fix missing exhaustive-deps across Search, MonthRangePicker, useMutationObserver,
  DatepickerRangeInput, DatepickerSingleInput, DatePicker (experimental)
- Add eslint-disable comments with explanations for intentional set-state-in-effect
  patterns (datepicker sync, input label float, color scheme sync, popover gate)
- Delete stale generated JS files: .storybook/main.js, preview.js,
  docs/components/*.js, docs/components/migration/*.js
- package.json: add npm overrides for @csstools/css-tokenizer and
  @csstools/css-parser-algorithms to fix Jest jsdom module resolution (hoisting bug)
- package.json: update lint:eslint script to eslint . (full project)
- CI: upgrade docs.yml from Node 18 to 24, gh-pages v3 to v4
- CI: add node-version 24 + cache npm to preview.yml (was missing entirely)
- CI: add --legacy-peer-deps to all npm ci install steps
- CI: add timeout-minutes and explicit permissions to all jobs
- CI: enable preview template in preview.yml (package is React 19 compatible)
…on snapshot

styled-components 6.4 made attrs-supplied props optional and exposed `as` in component props,
which broke the FC-annotated `styled(BoxedInput).attrs({ as: 'textarea' })` (TS2322) and with
it the build, the tests and the preview workflow.

jest-styled-components 7.4 normalises keyframe names to `k0` and adds the @Keyframes block to
snapshots, so the Skeleton snapshot is regenerated.
…n works

The tsconfig exclude had become `src/codemods`, so lib/esm/codemods was never built and
`npx @freenow/wave run_v2_migration` exited with "The 'codemods' folder does not exist".
Only the test folders (`src/codemods/__*`) are excluded again, as on main.

Under TypeScript 6 the codemods hit the new defaults (`strict` on, `types: []`): implicit any
arrays, nullable AST fields and the CommonJS `module` global. The three CommonJS codemods now use
`export default` like the other four (jscodeshift and its test utils accept both), and the
lookup maps are typed as string records.

Also drops the non-existent `codemods/` entry from the package files.
… preview template

The pull request preview app didn't declare the styled-components peer dependency, and its
experimental Button rendered without colours because no global style loaded the CSS variables.
The README explains what the template is for and how to run it.
…conventions

- "Migration to v3": requirements, upgrade steps and every consumer-visible change since 2.66.0
- Get Started, Versioning Policy and Browser Support: supported React, styled-components, Node and
  browser versions, the peer dependency policy and the minimum browser versions
- CONTRIBUTING: Node 24 and npm ci, the console guard, form component specs, consumer fixtures,
  the styled import and shouldForwardProp conventions, the toolchain, BREAKING CHANGE footers and
  squash merges
- README, issue and pull request templates: requirements, a working build badge, the versions
  bug reports need, and a pull request checklist
- Fixes: ModernColors is the maroon theme, the v2 guide's React prerequisite and broken sample,
  Popper references, and the Button `inverted` prop on the landing page, now DarkScheme
…oads

release.config.js uses the conventionalcommits preset, which semantic-release loads from the root
node_modules. It was never declared and came from @commitlint/config-conventional. Since the
upgrade to commitlint 21 that is preset 10, which needs conventional-changelog-writer 9, while the
latest @semantic-release/commit-analyzer and release-notes-generator still use writer 8. The
release on main would have stopped while generating the release notes, with "Missing helper:
conventional-changelog-conventionalcommits requires conventional-changelog-writer@9 or newer".

Preset 9.3.1 renders the notes with writer 8. commitlint keeps its own preset 10.
styled-components 6 gives styled components a generic call signature for the `as` prop before the
plain one, and react-docgen-typescript reads the first, which only has `as` and `forwardedAs`. So
the documentation of Button, TextButton, Label, Text, Card, Skeleton, Link and the experimental
Text lost the descriptions and types of their props, or the props themselves. A patch in
.storybook/docgen.ts makes the parser read the plain signature; it only runs in Storybook.

The defaults of variant and size came from defaultProps, which are gone, so they are now @default
tags. Compared with the published docs of 2.66.0, every prop default is back.
… so that webpack builds Wave

useLocaleObject imported `date-fns/locale/${locale}`. date-fns 4 exports each locale under its own
path and has no pattern for them, so webpack can't resolve the template literal: a production
build of any app that imports Wave failed with "Package path ./locale is exported from package
date-fns, but no valid target file was found", and a development build crashed the legacy
Datepickers with "Cannot find module 'undefined'". Vite built it but couldn't load the locale, so
locale="de" showed an English calendar, which already happened with Wave 2.

localeLoaders.ts has one literal import per supported locale and reads the locale by its name:
the CommonJS builds of the date-fns locales have no default export. TypeScript says they do,
because date-fns 2, which @datepicker-react/hooks installs, declares these modules globally.
webpack resolves every import at build time, including dynamic ones, against the paths a
dependency exports. Vite builds an import of a path that isn't exported, and it only fails in the
browser, so the Vite fixtures missed the date-fns locale import that broke every webpack build of
Wave 3. The new app bundles the shared sources for production and fails on any error or warning;
it fails like that against the build before the previous commit.
styled-components passes every prop to a wrapped component, and react-aria's Text spreads them on
its element, so Text rendered e.g. <p variant="body2">, and so did ListBox (Select, ComboBox),
Checkbox and Dialog. The shouldForwardProp sweep only covered styled HTML elements. Neither React
(the attribute is lowercase) nor styled-components (only when NODE_ENV is development) warns
in the tests, so FormComponents.spec now also checks that no element has a variant attribute.
Checked with consumer apps on TypeScript 5.0.4, 5.4.5 and 5.9.3: no errors with skipLibCheck.
Without it, some dependencies' declarations report errors on TypeScript 6 too, and
moduleResolution node needs esModuleInterop for the default React imports in the declarations.
…ements again

styled-components 5 read a nested `:last-child` or `::-webkit-search-cancel-button` without `&`
as a pseudo-class of the element itself, styled-components 6 reads it as a descendant selector.
So the last Accordion lost its bottom divider, and Chrome showed its own clear button in a
Search with a value.
The TypeScript 6 upgrade passed `padding ?? 0` to PopoverContent to satisfy its required
`padding` prop, so its default padding never applied and the content touched the edges of its
card. The prop is optional now and Popover passes `padding` on as before.
Since the options no longer wrap react-select's Option with styled(), an inline-flex wrapper
sits inside the Option's line box, which made each option 5px higher. A block flex wrapper
lays the flag and the name out as before.
…ments again

Wave 2 typed `placement` with react-popper's placements, which include the auto ones. Floating
UI has no such placements, so moving to it removed them. They map to Floating UI's
autoPlacement middleware now, which like react-popper picks the side with the most space, and
the migration guide no longer lists them as removed.
…e background now

The TypeScript 6 upgrade replaced the classic dark scheme's page background, which pointed to a
missing shade and so was `undefined` in Wave 2, with the darkest blue of the palette. Apps that
use ClassicColors with the dark scheme see the change.
…ation again

A comparison of every story and docs page with wave.free-now.com, in the light and the dark
scheme, found these differences:

- Wrap stories in LightScheme and DarkScheme and read the dark mode with useDarkMode() again,
  in the decorator, the docs container, Source and the colors table. Source watched the class
  of the body instead and could miss it, which left code samples light in the dark scheme.
- Read the theme of docs pages from `store.userGlobals`, as Storybook 10 has no
  `store.globals`. The Colors page crashed and the Theme toolbar didn't change docs pages.
- Render the Breakpoints example from a story hidden from the sidebar, as Canvas no longer
  renders children.
- Load the flags: Vite can't analyse the import of `./flags/${code}` without a file extension,
  so a Vite plugin of Storybook adds it. The published code stays the same for apps.
- Don't inline SVG files, which <use href> doesn't load from data: URLs, so the Wave logo shows
  on the Welcome and Get Started pages again, and hide the table of contents of Storybook 10 on
  the Welcome page.
- Repeat Storybook's default docgen options next to `tsconfigPath`, which replaced them, and
  leave out props declared only by dependencies, so the ArgTypes tables have no `style` row and
  no `| undefined` types again.
- Don't underline links in docs pages, as Storybook 10's rule also overrode Wave's Link.
@Mukesh-FN Mukesh-FN changed the title React 19 upgrade feat!: support react 19, story-book 10 and styled-components 6 Oct 14, 2026
@Mukesh-FN
Mukesh-FN marked this pull request as ready for review October 14, 2026 09:11
@Mukesh-FN
Mukesh-FN changed the base branch from main to next October 14, 2026 13:46
@Mukesh-FN Mukesh-FN changed the title feat!: support react 19, story-book 10 and styled-components 6 feat!: support react 19, storybook 10 and styled-components 6 Oct 14, 2026
@Mukesh-FN
Mukesh-FN merged commit 83ba98d into next Oct 14, 2026
13 checks passed
@Mukesh-FN
Mukesh-FN deleted the react-19-upgrade branch October 14, 2026 13:57
github-actions Bot pushed a commit that referenced this pull request Oct 14, 2026
## [3.0.0-rc.1](v2.66.0...v3.0.0-rc.1) (2026-10-14)

### ⚠ BREAKING CHANGES

* styled-components 6 is required (peer dependency `^6.0.0`). With styled-components
5, Wave crashes on import. React 18.2+ and React 19 are supported.
* the experimental `Calendar` passes its props on to react-day-picker 10, which
removes the props deprecated in v9. Use `startMonth` instead of `fromMonth` and `fromYear`,
`endMonth` instead of `toMonth` and `toYear`, `hidden={{ before: date }}` and `hidden={{ after: date
}}` instead of `fromDate` and `toDate`, and `autoFocus` instead of `initialFocus`. The
`onWeekNumberClick`, `onDayKeyUp`, `onDayKeyPress`, `onDayPointerEnter`, `onDayPointerLeave` and
`onDayTouch*` handlers and the deprecated `classNames` and `styles` keys (e.g. `day_selected`) are
removed; use custom `DayButton` and `WeekNumber` components instead of the handlers.
* Tooltip, Popover, Datepicker and the month range picker are positioned by Floating
UI instead of react-popper, with the same placements. `react-popper`, `@popperjs/core` and
`wave-react-windowed-select` are no longer installed with Wave, so add them to your own dependencies
if your app imports them.
* snapshots of Wave components change: generated class names are shorter
(`Box-sc-lqdr92` becomes e.g. `sc-bdvwhi`), component names are e.g. `styled.div`, vendor prefixes
are gone (pass `enableVendorPrefixes` to `StyleSheetManager` to keep them), and Wave props no longer
appear as DOM attributes. Use jest-styled-components 7.
* Wave's components no longer set `defaultProps`, which React 19 ignores. The default
values are the same, but e.g. `Button.defaultProps` is `undefined`.
* the CommonJS build is compiled to ES2015 instead of ES5, like the ES module build.

### Features

* support react 19, storybook 10 and styled-components 6 ([#576](#576)) ([83ba98d](83ba98d))
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.0.0-rc.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

1 participant