Skip to content

beta: React 18 + 19 compatibility#3328

Open
dreamwasp wants to merge 30 commits intomainfrom
cass-gmt-1473-beta
Open

beta: React 18 + 19 compatibility#3328
dreamwasp wants to merge 30 commits intomainfrom
cass-gmt-1473-beta

Conversation

@dreamwasp
Copy link
Copy Markdown
Contributor

@dreamwasp dreamwasp commented Apr 15, 2026

Overview

Upgrades the Gamut monorepo for React 19 while keeping React 18 in the supported peer range for consumers. This includes:

  • Workspace dependencies on React 19 (react, react-dom, react-test-renderer) and aligned Yarn resolutions.
  • Peer dependency updates on published packages (e.g. @codecademy/gamut) so apps can use React 18.3 or 19.
  • react-hook-form upgrade and follow-on fixes in Connected Form / Grid Form.
  • React 19–oriented ref handling (notably Popover / PopoverContainer and related floating UI) and small utilities (isNullish / isDefined) where strict equality is required.
  • Tooling updates (ESLint, TypeScript-ESLint, Testing Library, Jest config tweaks) and eslint-plugin-gamut rule/test updates.

PR Checklist

  • Related to designs:
  • Related to JIRA ticket: GMT-1473
  • Version plan added/updated (or not needed)
  • I have run this code to verify it works
  • This PR includes unit tests for the code change
  • This PR includes testing instructions tests for the code change
  • [tbd] The alpha package of this PR is passing end-to-end tests in all relevant Codecademy repositories

Testing Instructions

Component / area Location What to verify
Popover / positioning AtomsPopoverContainer Open/close, alignment controls, scroll/resize; repeat with RTL on.
Tooltips / floating tips MoleculesTipsToolTip (and other Tip stories as needed) Open states, anchor behavior.
Modal / focus MoleculesModalsModal Open, focus trap, close; Dark mode.
Menu MoleculesMenu Keyboard nav, disabled items if covered by stories.
Select AtomsFormInputsSelectDropdown Default, multi-select, and searchable examples if present.
Forms (RHF) OrganismsConnectedFormConnectedForm / ConnectedFormGroup / ConnectedFormInputs Submit/validation flows still work.
Grid Form OrganismsGridForm (e.g. Usage, States, Layout) Field groups and validation.

Accessibility: On a subset of the above (PopoverContainer, Modal, Menu, SelectDropdown, ConnectedForm), run VoiceOver : focus order, labels, and escape to dismiss.

PR Previews

mono on 18
mono on 19
monolith on 18

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Apr 15, 2026

View your CI Pipeline Execution ↗ for commit a0a3ad0


☁️ Nx Cloud last updated this comment at 2026-04-29 16:23:26 UTC

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 15, 2026

Codecov Report

❌ Patch coverage is 20.77922% with 61 lines in your changes missing coverage. Please review.
✅ Project coverage is 32.84%. Comparing base (14aab61) to head (a0a3ad0).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/gamut/src/PopoverContainer/hooks.ts 0.00% 19 Missing ⚠️
...es/gamut/src/PopoverContainer/PopoverContainer.tsx 0.00% 8 Missing ⚠️
packages/gamut/src/Popover/Popover.tsx 0.00% 6 Missing ⚠️
packages/gamut/src/Tag/index.tsx 0.00% 5 Missing ⚠️
...s/gamut/src/Form/SelectDropdown/SelectDropdown.tsx 0.00% 4 Missing ⚠️
packages/gamut/src/Modals/Dialog.tsx 0.00% 4 Missing ⚠️
packages/gamut/src/Modals/Modal.tsx 0.00% 4 Missing ⚠️
...ges/gamut/src/ConnectedForm/ConnectedFormGroup.tsx 0.00% 2 Missing ⚠️
packages/gamut/src/Menu/MenuItem.tsx 0.00% 2 Missing ⚠️
packages/gamut/src/Tip/shared/FloatingTip.tsx 0.00% 2 Missing ⚠️
... and 5 more

❗ There is a different number of reports uploaded between BASE (14aab61) and HEAD (a0a3ad0). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (14aab61) HEAD (a0a3ad0)
main 1 0
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #3328       +/-   ##
===========================================
- Coverage   89.98%   32.84%   -57.15%     
===========================================
  Files         380      347       -33     
  Lines        5674     4856      -818     
  Branches     1845     1664      -181     
===========================================
- Hits         5106     1595     -3511     
- Misses        560     3261     +2701     
+ Partials        8        0        -8     
Flag Coverage Δ
main ?
pull-request 32.84% <20.77%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@dreamwasp dreamwasp marked this pull request as ready for review April 15, 2026 21:09
@dreamwasp dreamwasp requested a review from a team as a code owner April 15, 2026 21:09
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Upgrades the Gamut monorepo to run on React 19 internally while widening published package peer dependency ranges to continue supporting React 18.3, and applies follow-on compatibility fixes across refs, tests, and tooling.

Changes:

  • Bumps workspace React/tooling deps (Testing Library, TypeScript-ESLint, framer-motion, react-hook-form) and updates peer ranges to include React 19.
  • Refactors several components/utilities for React 19 ref/type behavior (Popover/PopoverContainer, MenuItem, ButtonBase/Anchor, Tip, nullish helpers).
  • Updates tests/docs/stories for new render behavior, stricter typing, and React key/ref expectations.

Reviewed changes

Copilot reviewed 87 out of 88 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tools/gamut-release/src/main.ts Reorders node imports.
packages/variance/src/utils/responsive.ts Simplifies optional param type.
packages/variance/src/types/config.ts Removes duplicate union type.
packages/variance/integration/tests/component.test.tsx Switches from test-renderer to RTL.
packages/styleguide/src/lib/Organisms/Lists & Tables/List/List.stories.tsx Adds missing React keys.
packages/styleguide/src/lib/Organisms/ConnectedForm/ConnectedFormInputs/ConnectedFormInputsTable.tsx Adds missing React key.
packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx Adds missing React key.
packages/styleguide/src/lib/Molecules/Tips/TipsTable.tsx Adds missing React keys.
packages/styleguide/src/lib/Meta/Best practices.mdx Doc formatting/quotes cleanup.
packages/styleguide/src/lib/Layouts/LayoutGrid/examples.tsx Safer display of size value.
packages/styleguide/src/lib/Foundations/shared/elements.tsx Adds eslint disable for namespace access.
packages/styleguide/src/lib/Foundations/Layout.mdx Adds missing keys in examples.
packages/styleguide/src/lib/Atoms/UtilityComponents/DelayedRenderWrapper/DelayedRenderWrapper.mdx Clarifies delay wording.
packages/styleguide/src/lib/Atoms/PopoverContainer/PopoverContainer.stories.tsx Adds missing key in map.
packages/styleguide/src/lib/Atoms/PopoverContainer/PopoverContainer.mdx Formats inline code snippet.
packages/styleguide/src/lib/Atoms/FormInputs/SelectDropdown/SelectDropdown.mdx Flattens Callout formatting.
packages/gamut/src/utils/react.ts Uses isNullish; tighter prop typing.
packages/gamut/src/utils/nullish.ts Adds isNullish/isDefined helpers.
packages/gamut/src/utils/tests/nullish.test.ts Unit tests for nullish helpers.
packages/gamut/src/Tip/shared/utils.tsx Simplifies aria-expanded check.
packages/gamut/src/Tip/shared/types.tsx Broadens ref prop types for React 19.
packages/gamut/src/Tip/shared/InlineTip.tsx Ref casting updates for new types.
packages/gamut/src/Tip/shared/FloatingTip.tsx Ref casting + explicit timeout refs.
packages/gamut/src/Tip/tests/helpers.tsx Ref type update + keyboard open helper refactor.
packages/gamut/src/Tag/index.tsx Safer aria-label generation.
packages/gamut/src/PopoverContainer/types.ts Changes targetRef to Ref type.
packages/gamut/src/PopoverContainer/hooks.ts Adds ref resolver helpers + shared target type.
packages/gamut/src/PopoverContainer/tests/PopoverContainer.test.tsx Adds useRef-based targetRef test.
packages/gamut/src/PopoverContainer/PopoverContainer.tsx Uses new ref helpers throughout.
packages/gamut/src/Popover/types.tsx Broadens targetRef/container ref types.
packages/gamut/src/Popover/tests/Popover.test.tsx Adds useRef-based targetRef test.
packages/gamut/src/Popover/Popover.tsx Uses new ref helpers + ref casts.
packages/gamut/src/Pagination/utils.tsx Centralizes animation timing constants.
packages/gamut/src/Modals/Modal.tsx Avoids stringifying non-primitive title for aria props.
packages/gamut/src/Modals/Dialog.tsx Avoids stringifying non-primitive title for aria props.
packages/gamut/src/Menu/tests/Menu.test.tsx Adds ref-forwarding tests.
packages/gamut/src/Menu/MenuItem.tsx Ref narrowing helper for render branches.
packages/gamut/src/GridForm/GridForm.tsx JSX generic removal for ConnectedForm.
packages/gamut/src/Form/tests/snapshots/utils.test.tsx.snap Updates snapshots for React 19 element shape.
packages/gamut/src/Form/SelectDropdown/utils.tsx Replaces != null with isDefined.
packages/gamut/src/Form/SelectDropdown/types/internal.ts Ref type update for internal focus refs.
packages/gamut/src/Form/SelectDropdown/elements/multi-value.tsx Ref casts + focus call tweaks.
packages/gamut/src/Form/SelectDropdown/elements/controls.tsx Ref casts + focus call tweaks.
packages/gamut/src/Form/SelectDropdown/elements/containers.tsx Optional chaining for role check.
packages/gamut/src/Form/SelectDropdown/SelectDropdown.tsx Tightens types for callbacks/options.
packages/gamut/src/FeatureShimmer/index.tsx Adds as const easing typing.
packages/gamut/src/FeatureShimmer/tests/FeatureShimmer.test.tsx Updates IntersectionObserver mock shape.
packages/gamut/src/ConnectedForm/utils.tsx Simplifies validation rules access.
packages/gamut/src/ConnectedForm/ConnectedFormGroup.tsx Stabilizes field id usage.
packages/gamut/src/ConnectedForm/ConnectedForm.tsx Updates generic forwardRef typing.
packages/gamut/src/Coachmark/index.tsx Uses React.JSX.Element return type.
packages/gamut/src/Card/styles.tsx Adds as const easing typing.
packages/gamut/src/ButtonBase/tests/ButtonBase.test.tsx Adds ref-forwarding tests.
packages/gamut/src/ButtonBase/ButtonBase.tsx Ref type changes + narrowing helper.
packages/gamut/src/Button/shared/types.ts React.JSX IntrinsicElements typing + eslint disable.
packages/gamut/src/Breadcrumbs/index.tsx Replaces Object with object bound.
packages/gamut/src/Box/GridBox.tsx Re-exports GridBoxProps type.
packages/gamut/src/BarChart/utils/hooks.ts RefObject nullability updates + width measure tweak.
packages/gamut/src/Anchor/index.tsx Ref narrowing + AnchorBase typing changes.
packages/gamut/src/Anchor/tests/Anchor.test.tsx Adds ref-forwarding tests.
packages/gamut/src/AccordionButtonDeprecated/ButtonDeprecated/index.tsx Renames themes const to _themes.
packages/gamut/package.json Updates deps + React 19 peer range.
packages/gamut-tests/src/index.tsx React.JSX IntrinsicAttributes typing update.
packages/gamut-tests/package.json Pins component-test-setup + React 19 peer.
packages/gamut-styles/tsconfig.lib.json Excludes tests from lib build.
packages/gamut-styles/src/variance/utils.ts Uses React.JSX IntrinsicElements typing.
packages/gamut-styles/src/tests/fontUtilsMock.ts Adds reusable getFonts mock export.
packages/gamut-styles/src/tests/fontLoading.test.tsx Stabilizes font mocks + preload link lookup.
packages/gamut-styles/src/tests/AssetProvider.test.tsx Stabilizes font mocks + preload link lookup.
packages/gamut-styles/package.json React 19 peer range + framer-motion bump.
packages/gamut-patterns/package.json React 19 peer range.
packages/gamut-illustrations/package.json React 19 peer range.
packages/gamut-icons/package.json React 19 peer range.
packages/eslint-plugin-gamut/src/prefer-themed.ts Updates AST node type checks.
packages/eslint-plugin-gamut/src/prefer-themed.test.ts Migrates RuleTester import/usage.
packages/eslint-plugin-gamut/src/no-kbd-element.ts Uses AST_NODE_TYPES constants.
packages/eslint-plugin-gamut/src/no-kbd-element.test.ts Migrates RuleTester import/usage.
packages/eslint-plugin-gamut/src/no-inline-style.ts Uses AST_NODE_TYPES constants.
packages/eslint-plugin-gamut/src/no-inline-style.test.ts Migrates RuleTester import/usage.
packages/eslint-plugin-gamut/src/no-css-standalone.ts Meta docs tweak (recommended removed).
packages/eslint-plugin-gamut/src/no-css-standalone.test.ts Migrates RuleTester import/usage.
packages/eslint-plugin-gamut/src/gamut-import-paths.ts Meta docs tweak (recommended removed).
packages/eslint-plugin-gamut/package.json Bumps @typescript-eslint/utils to v8.
package.json React 19 workspace deps + tooling bumps/resolutions.
jest.config.base.ts Import ordering cleanup.
.nx/version-plans/version-plan-1775236316343.md Adds version plan for majors.
.eslintrc.js Adds import parser settings + enables rule config.

Comment on lines 89 to +96
export const useScrollingParents = (
targetRef: React.RefObject<HTMLElement | null>
targetRef: React.Ref<PopoverTargetElement | null>
): HTMLElement[] => {
return useMemo(() => {
if (!targetRef.current) {
return [];
}
return findAllAdditionalScrollingParents(targetRef.current);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [targetRef.current]);
const target = getRefElement(targetRef);
if (!target) return [];
return findAllAdditionalScrollingParents(getTargetAsElement(target)!);
}, [targetRef]);
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useScrollingParents memoizes based on [targetRef], but ref object identity typically doesn't change when .current is set. This can permanently cache an empty scrollingParents array from the initial render (when ref.current is still null), which breaks closeOnViewportExit behavior. Derive a stable element value (e.g. const target = getRefElement(targetRef) outside the memo) and include that in the dependency list (or otherwise re-compute when the resolved element becomes available).

Copilot uses AI. Check for mistakes.
Comment on lines 83 to 87
* The target element around which the popover will be positioned.
* Only ref objects (e.g. from useRef) are supported at runtime; RefCallback is not.
*/
targetRef: RefObject<TargetRef>;
targetRef: Ref<TargetRef | null>;
/**
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The public prop type targetRef: Ref<...> allows callback refs, but the implementation explicitly treats function refs as unsupported (getRefElement returns null) and the docs comment says RefCallback isn't supported. To avoid a type/runtime mismatch for consumers, consider changing the type to React.RefObject<...> (or implement callback-ref support by storing the node in state).

Copilot uses AI. Check for mistakes.
Comment on lines 98 to 108
/**
* The target element around which the popover will be positioned.
* Only ref objects (e.g. from useRef) are supported at runtime; RefCallback is not.
*/
targetRef: React.RefObject<
Pick<HTMLDivElement, 'getBoundingClientRect' | 'contains'>
>;
targetRef: React.Ref<HTMLElement | null>;

/**
* The PopoverContainer which contents will be rendered into.
*/
popoverContainerRef?:
| React.RefObject<HTMLDivElement>
| React.RefCallback<HTMLDivElement>;
popoverContainerRef?: React.Ref<HTMLDivElement | null>;

Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

targetRef is typed as React.Ref<HTMLElement | null>, which includes callback refs, but the underlying positioning hooks treat callback refs as unsupported (and the docstring states RefCallback isn't supported). Consider narrowing this prop type to React.RefObject<HTMLElement | null> (or add runtime support for callback refs) to prevent consumers from passing a ref shape that won't work.

Copilot uses AI. Check for mistakes.
Comment on lines 111 to +119
<ModalContainer
aria-hidden="false"
aria-label={ariaLabel}
aria-labelledby={titleText ? String(titleText) : undefined}
aria-labelledby={
!isNullish(titleText) &&
(typeof titleText === 'string' || typeof titleText === 'number')
? String(titleText)
: undefined
}
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aria-labelledby must reference the id of an element that labels the dialog (e.g. the heading’s id), but here it’s being set to the title text itself. This likely results in an invalid reference and the dialog not being properly labelled for screen readers. Consider generating a stable heading id (or using an existing id prop) and applying it to the title element, then set aria-labelledby to that id (or omit aria-labelledby and rely on aria-label).

Copilot uses AI. Check for mistakes.
Comment on lines 66 to +74
<ModalContainer
aria-hidden="false"
aria-label="dialog"
aria-labelledby={String(title)}
aria-labelledby={
!isNullish(title) &&
(typeof title === 'string' || typeof title === 'number')
? String(title)
: undefined
}
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aria-labelledby should be an element id reference, but it’s currently set to the title text value. This likely points to a non-existent id and prevents the dialog from being labelled correctly for assistive tech. Consider giving the <Text as="h2"> a stable id and using that id for aria-labelledby (or remove aria-labelledby if aria-label is sufficient).

Copilot uses AI. Check for mistakes.
Comment on lines 16 to 21
/**
* Ref type for programmatic focus management.
* Ref type for programmatic focus management (internal refs from useRef).
* Used for managing focus on select input and remove all button.
*/
export type ProgramaticFocusRef =
| React.MutableRefObject<HTMLDivElement>
| React.MutableRefObject<null>;
export type ProgramaticFocusRef = React.RefObject<HTMLDivElement | null>;

Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type name ProgramaticFocusRef appears misspelled ("Programmatic"). Since this type is only referenced within this file, consider renaming it to ProgrammaticFocusRef for clarity and consistency.

Copilot uses AI. Check for mistakes.
Comment on lines 18 to 23
mockIntersectionObserver.mockReturnValue({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
unobserve: jest.fn(),
});
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mocked IntersectionObserver object literal defines unobserve twice, which TypeScript flags as a duplicate property name and can fail compilation. Remove the duplicate entry (keep a single unobserve mock).

Copilot uses AI. Check for mistakes.
@codecademydev
Copy link
Copy Markdown
Collaborator

📬 Published Alpha Packages:

Package Version npm Diff
@codecademy/gamut 68.3.1-alpha.910133.0 npm diff
@codecademy/gamut-icons 9.57.4-alpha.910133.0 npm diff
@codecademy/gamut-illustrations 0.58.11-alpha.910133.0 npm diff
@codecademy/gamut-kit 0.6.594-alpha.910133.0 npm diff
@codecademy/gamut-patterns 0.10.30-alpha.910133.0 npm diff
@codecademy/gamut-styles 17.14.1-alpha.910133.0 npm diff
@codecademy/gamut-tests 5.3.5-alpha.910133.0 npm diff
@codecademy/variance 0.26.2-alpha.910133.0 npm diff
eslint-plugin-gamut 2.4.4-alpha.910133.0 npm diff

@github-actions
Copy link
Copy Markdown
Contributor

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants