CORE-2003: Migrate composite UI components (Tooltip, Toast, Tabs, etc.) - #128
CORE-2003: Migrate composite UI components (Tooltip, Toast, Tabs, etc.)#128OpenStaxClaude wants to merge 12 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Migrates several composite UI components from styled-components to plain CSS files, using CSS custom properties (bound to the theme palette) plus classnames for variant/state styling to support the ongoing styled-components removal.
Changes:
- Replaced styled-components implementations for Tooltip, Toast, ToastContainer, Tabs, and ButtonBar with
className+ imported CSS files. - Introduced per-component CSS variables (set via inline
style) to keep theme/palette values configurable. - Added new CSS stylesheets for the migrated components and updated component structure accordingly.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/components/Tooltip.tsx | Switched Tooltip/TooltipGroup/CustomTooltip to class-based styling with CSS variables. |
| src/components/Tooltip.css | New stylesheet implementing Tooltip layout/placement/trigger styles. |
| src/components/ToastContainer.tsx | Replaced styled toast container wrappers with classnames + CSS variable for z-index. |
| src/components/ToastContainer.css | New stylesheet for toast container layout and fixed positioning behavior. |
| src/components/Toast.tsx | Replaced styled toast with classnames + CSS variables for variant colors and animation delay. |
| src/components/Toast.css | New stylesheet for toast base layout, variants, and dismissal animation. |
| src/components/Tabs.tsx | Replaced styled Tabs wrapper with classnames + CSS variables for borders/backgrounds. |
| src/components/Tabs.css | New stylesheet for default and button-bar variants and size variants. |
| src/components/ButtonBar.tsx | Replaced styled wrapper with a plain div + classnames + CSS variables. |
| src/components/ButtonBar.css | New stylesheet for button bar layout, sizes, and selected/hover states. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
5e05817 to
1885218
Compare
This comment was marked as resolved.
This comment was marked as resolved.
46c6099 to
3275462
Compare
3275462 to
41d94ea
Compare
41d94ea to
ebda986
Compare
85cc7cc to
1773b96
Compare
|
@RoyEJohnson Copilot's One unrelated thing I ran into and want to flag rather than quietly leave. While verifying I hit an intermittent failure in I checked out unmodified I've left it alone since it's outside this PR's scope, but it's a real latent flake — happy to open a ticket if you want it chased. |
RoyEJohnson
left a comment
There was a problem hiding this comment.
Open a separate bug-fix ticket for the testing issue.
|
@RoyEJohnson Filed as CORE-2715: Fix flaky ProfileMenu snapshot test in ui-components — Bug, component REX, It carries the reproduction command, the exact failing diff, and the evidence it's pre-existing. I re-verified against current Two things to flag:
Nothing else outstanding: both review threads are resolved and |
Migrated Tooltip, Toast, ToastContainer, Tabs, DropdownMenu, and ButtonBar components from styled-components to plain CSS with CSS variables. Changes: - Converted styled-components to className-based styling - Created separate CSS files for each component - Bound theme values to CSS variables - Used classnames library for conditional styling - Maintained all existing functionality and variants Related to CORE-2003 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Fix ButtonBar to accept standard HTML div attributes Extends ButtonBarProps with React.HTMLAttributes<HTMLDivElement> to allow passing standard div attributes like className, style, id, etc. Also properly merges custom className and style props with component defaults. Addresses code review comment about breaking TS API change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Restore data-selected styling Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
BodyPortal never uses style, so remove the style parameter from where ToastContainer calls it.
Have Tabs merge style parameters
Address Copilot code review comments
1. Fix Tabs style prop merge order
- Changed from {...style, ...additionalStyle} to {...additionalStyle, ...style}
- Allows consumers to override CSS variables via the style prop
2. Add style prop support to BodyPortal
- Added style to BodyPortalProps type
- Apply style to portal element in useLayoutEffect
- Clean up styles on unmount
- Pass style from BodyPortalToastContainer to BodyPortal
- Fixes z-index CSS variable binding for toast containers
3. Remove unused icon prop from Tooltip
- The icon prop is only used in TooltipGroup, not in Tooltip itself
- Removed from Tooltip destructuring to avoid unused variable
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Fix BodyPortal to avoid teardown/reinsert on style changes
Split the useLayoutEffect into two separate effects:
1. One for portal creation/destruction and non-style props (doesn't depend on style)
2. One for style updates only (depends on style but doesn't remove/reinsert the portal)
This prevents unnecessary DOM churn when only CSS variables/styles change,
which could cause focus loss or UI flicker in portal-based components like
ToastContainer.
Also added comprehensive tests for:
- CSS variable application
- Regular CSS property application
- Style updates without portal removal
- Style cleanup on unmount
- Handling of null/undefined style values
Addresses code review comments about performance and test coverage.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Address Copilot review comments for style handling and prop forwarding
Fix three issues identified in code review:
1. BodyPortal: Convert camelCase to kebab-case for style cleanup
- React style objects use camelCase (e.g., backgroundColor)
- CSS removeProperty expects kebab-case (e.g., background-color)
- Added conversion to prevent style property leaks on unmount
2. BodyPortalToastContainer: Memoize style object
- Fresh style object on each render caused unnecessary portal remounts
- Wrapped style object in useMemo to prevent BodyPortal useLayoutEffect
from triggering on every toast list change
3. Tooltip: Prevent invalid props from reaching AriaTooltip
- icon and ariaLabel are trigger-only props (used by TooltipGroup)
- Now explicitly destructured to avoid spreading them to AriaTooltip
- Prevents invalid DOM attributes and unexpected react-aria behavior
All fixes maintain backward compatibility.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Handle CSS custom property copying
Use variable for animation time
Extract zIndexStyle
Co-Authored-By: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Use CSSPropertiesWithVariables (added to src/types on main) instead of casting CSS-variable style objects to React.CSSProperties. BodyPortal now requires this type, so ToastContainer would not typecheck otherwise. - Carry over the forced-colors selected-tab styles from #135 into Tabs.css. - Narrow Tooltip's props so icon/ariaLabel are only accepted by TooltipGroup, which owns the trigger button (Copilot review feedback). - CustomTooltip: merge caller className/style instead of dropping them. - Refresh Tabs/ButtonBar snapshots. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ButtonBar reused buttonBarItemCss from Tabs on main, so it also picked up the forced-colors fix from #135. Preserve that after the CSS split. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tabs: TabsProps intersected RAC.TabsProps with static className/style/children,
which collapses react-aria's `T | ((values) => T)` unions to `T` and rejects
render callbacks at compile time. Spreading a style callback also produced an
empty object at runtime. Stop narrowing the type and resolve each callback
against the render props before merging the variant classes and CSS variables.
RAC merges its own defaultStyle for us, so we only merge the caller's.
Tooltip: StyledTooltip and StyledTrigger were public API via the wildcard
export in src/index.ts. openstax/assignments imports UI.StyledTrigger, so
removing them breaks that build. Re-added as plain-CSS components, deprecated
in favour of Tooltip/TooltipGroup.
Tooltip also set className="tooltip" after {...props}, dropping a caller's
className. styled-components used to merge it, and assignments relies on that
via styled(UI.TooltipGroup). Both now merge.
Also drops the palette.darkerGreen workaround now that 2035082 fixed the
value, and documents the removed Tabs css fragments in CHANGELOG.md. A search
of the openstax org found no consumers of those fragments outside this repo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
styled-components has forwarded refs since v4, so the styled(AriaTooltip) and
styled(Button) exports these replaced passed a ref through to the underlying
element. The plain-function replacements dropped that: a consumer attaching a
ref got a type error ("Property 'ref' does not exist") and no DOM node at
runtime.
Wrapped both in forwardRef following the pattern the other migrated react-aria
wrappers use (NavBarButton, NavBarPopover), including displayName. Ref types
come from React.ElementRef<typeof ...> so they track react-aria.
Adds tests covering both; each fails to compile against the previous version.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
src/components/Tooltip.spec.tsx:12
- This test suite overwrites
ReactDOM.createPortalinbeforeAllbut never restores it, which can leak the mock into other test files and cause hard-to-debug failures. Capture the original implementation and restore it inafterAll.
describe('Tooltip', () => {
beforeAll(() => {
ReactDOM.createPortal = jest.fn((element) => element) as any;
})
These components are themed through CSS variables, but their style props were typed React.CSSProperties, so overriding a documented variable required a cast. Widened to CSSPropertiesWithVariables on Tabs, ButtonBar and Tooltip (which covers StyledTooltip and StyledTrigger via ClassNameAndStyle). Copilot flagged ButtonBar and Tooltip; Tabs has the same problem via RAC.TabsProps and is included so the three read consistently. Tabs needed Omit<RAC.TabsProps, 'style'> rather than a plain intersection: intersecting a narrower style would collapse react-aria's `T | ((values) => T)` union and break the render-callback form, which is the bug fixed earlier in this PR. The existing callback tests cover that. Adds a test per component overriding a documented variable with no cast; each fails to compile against the previous types with "does not exist in type Properties<...>". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Minor formatting and documentation clariification Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Summary
Migrated composite UI components from styled-components to plain CSS with CSS variables as part of the migration away from styled-components.
Components migrated:
Migration approach:
CSS Variables used:
Each component now uses CSS custom properties that reference the palette:
--tooltip-bg,--tooltip-color,--tooltip-border-color--toast-success-*,--toast-neutral-*,--toast-failure-*--toast-container-z-index--tabs-border-color,--tabs-active-border-color,--tabs-button-*--button-bar-border-color,--button-bar-selected-bg,--button-bar-hover-bgAPI compatibility
I searched the
openstaxorg for every export this PR removes, so the calls below are based on actual usage rather than guesswork.Kept (would have broken consumers):
StyledTooltip/StyledTriggerare still exported, now as plain-CSS components, both@deprecatedin favour ofTooltip/TooltipGroup.openstax/assignmentsusesUI.StyledTriggerinLicenseNotice.tsx.Removed (no consumers outside this repo):
tabListBaseCss/tabBaseCss/buttonBarWrapperCss/buttonBarItemCssfragments fromTabs. Documented inCHANGELOG.mdunder the existing[Unreleased]→ BREAKING CHANGES section, following the CORE-1999 Button migration format.Public
styleprops accept CSS custom properties:Tabs,ButtonBar,Tooltip,StyledTooltipandStyledTriggertypestyleasCSSPropertiesWithVariablesrather thanReact.CSSProperties, so callers can override the documented--tabs-*/--button-bar-*/--tooltip-*variables without a cast.ToastandToastContainerexpose nostyleprop, so they're unaffected. This is a widening — existingstyleusage is unchanged.Three regressions found and fixed during review:
Tabsnarrowed react-aria'sclassName/style/childrenrender-callback unions to static values, so callbacks were rejected at compile time and astylecallback spread to{}at runtime. It now resolves callbacks against the render props before merging.TooltipsetclassName="tooltip"after{...props}, discarding a caller'sclassName. styled-components used to merge it, andopenstax/assignmentsrelies on that viastyled(UI.TooltipGroup). Both now merge.StyledTooltip/StyledTriggercompat wrappers didn't forward refs, which the styled-components they replaced did (styled-components has forwarded refs since v4; this repo is on 5.3.5). Both are nowforwardRef, matching theNavBarButton/NavBarPopoverpattern from CORE-2004: Migrate NavBar components to plain CSS #130.Each has a regression test that fails against the previous implementation.
Notes for reviewers
main(2026-08-26).Tabs.cssandButtonBar.css. OnmainButtonBarpicked those up implicitly by interpolatingbuttonBarItemCss; now that the CSS is split per component they are declared in both files.CSSPropertiesWithVariables(added tosrc/typesonmainby CORE-2004: Migrate NavBar components to plain CSS #130) instead of casting toReact.CSSProperties.BodyPortalrequires this type onmain, soToastContainerwould not typecheck without it.Tooltip's props are narrowed soicon/ariaLabelare only accepted byTooltipGroup, which owns the trigger button.CustomTooltipmerges a caller-suppliedclassName/stylerather than dropping them.palette.darkerGreenfix in 2035082 — thestartsWith('#')workaround inToast.tsxis gone now that the palette value is correct.Testing
npm run lint,npx tsc --noEmit,npm run test(39 suites / 250 tests),npm run build, andnpm run ladle-buildall pass locally.All components have existing Ladle stories that demonstrate:
Related
🤖 Generated with Claude Code