Skip to content

CORE-2008: Migrate feature components (MessageBox, Banner, Tree, ToggleButtonGroup) - #146

Draft
OpenStaxClaude wants to merge 2 commits into
CORE-2720-global-css-theme-tokensfrom
CORE-2008-migrate-feature-components
Draft

CORE-2008: Migrate feature components (MessageBox, Banner, Tree, ToggleButtonGroup)#146
OpenStaxClaude wants to merge 2 commits into
CORE-2720-global-css-theme-tokensfrom
CORE-2008-migrate-feature-components

Conversation

@OpenStaxClaude

Copy link
Copy Markdown
Contributor

Migrates the last four feature components off styled-components and onto plain CSS bound to the --ox-* theme tokens, following the pattern set by the Button, Modal and composite-component migrations.

Jira: https://openstax.atlassian.net/browse/CORE-2008 — the full implementation plan is in the ticket description.

Based on CORE-2720-global-css-theme-tokens, not main, as requested on the ticket. Review the diff against that branch.

What changed

Component New stylesheet Classes
MessageBox MessageBox.css .message-box, .message-box-heading, .message-box-body, .message-box-event-id
Banner Banner.css .banner, .banner-note / .banner-warning / .banner-error, .banner-severity, .banner-close-button
Tree Tree.css .tree, .tree-item, .tree-chevron
ToggleButtonGroup ToggleButtonGroup.css .toggle-button-group, .toggle-button

Tree/TreeCheckbox.tsx is listed on the ticket but was already migrated with the Checkbox work and hardened by CORE-2708/CORE-2710. It is verified here, not re-migrated.

Decisions worth a reviewer's attention

  • Banner severity is a tone class, not a prop-driven inline style. .banner-note / .banner-warning / .banner-error set --banner-bg, --banner-color and --banner-border-color. The tone class goes on both the banner and its close button, so CloseButton keeps its severity colour when a consumer renders it outside a StyledBanner — which assignments does not do today, but the old styled(Button) supported.
  • CloseButton is now a self-contained <button class="banner-close-button"> rather than a wrapped Button. The original wrapped Button and then unset every style Button gave it, and only won because styled-components injects its sheet last. In plain CSS the two selectors have equal specificity, so the result would depend on module evaluation order. A self-contained class removes the cascade fight entirely.
  • Four new allowlist entries. Banner uses #fff5e0, #976502, #fdbd3e and #f8e8ea. Two are near-misses for palette entries (tangerine #ffbd3e, paleRed #fbe7ea), so snapping them would be a visual change rather than a refactor. They go in KNOWN_OFF_PALETTE with a reason each. Happy to promote them to palette.ts as banner design tokens instead if you'd rather — that felt like a design call, not a migration one.
  • One rule carried over that I believe is dead. .tree[data-expanded] .react-aria-Button[slot=chevron] svg cannot match: TreeChevron always supplies a className to its RAC.Button, so the button never carries RAC's default react-aria-Button class. The rotation you actually see comes from .tree-item[data-expanded] svg. Kept verbatim with a comment, because deleting it would be a behaviour change rather than a refactor. Say the word and I'll drop it.
  • TreeChevron's button is always given a class (tree-chevron-button) for the same reason — under styled-components it always had one, and letting RAC fall back to its default class would make the rule above suddenly live.
  • CloseButton no longer leaks severity to the DOM. styled(Button) forwarded unknown props through to a custom component, so the rendered <button> carried severity="warning". It doesn't any more; visible in the snapshot diff.
  • type is deliberately left unset on CloseButton. The original inherited <button>'s submit default. Defaulting a dismiss button to submit inside a form is a latent bug, but fixing it is a behaviour change and belongs in its own ticket.

Breaking change for openstax/assessments

BoxWrapper is no longer a styled-component, so it can't be used as a styled-components component selector. assessments does exactly that in packages/frontend/src/assessments/screens/Preview/styled.tsx:

${BoxWrapper} {
  border: none;
  padding: ${containerSpacing};
}

That needs to become a .message-box { ... } selector. It is a separate PR in assessments, needed when it next bumps its ui-components tag — this PR does not touch assessments.

Everything else is source-compatible. I checked every use of these exports in openstax/assignments and openstax/assessments: StyledBanner, CloseButton, Tree, TreeItem, TreeItemContent, TreeChevron, MessageBox and ToggleButtonGroup keep their props and rendered structure. In particular assignments wraps the group with styled(UI.ToggleButtonGroup) in two places, so the wrapper composes the caller's className rather than replacing it.

Testing

  • Snapshots updated for all four components (they recorded styled-components hashes such as sc-bczRLJ hWiIYF).
  • Targeted assertions added alongside them for the parts that matter — see the spec diffs.
  • src/theme/tokens.spec.ts is the real guard: it fails on any literal in the new stylesheets that duplicates a theme value, on any new off-palette colour, and on any --ox-* token that doesn't exist.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Ref-forwarding regressions and Banner close-button visual and accessibility regressions must be addressed.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Migrates MessageBox, Banner, Tree, and ToggleButtonGroup from styled-components to token-backed plain CSS.

Changes:

  • Replaces styled wrappers with class-composing React components.
  • Adds component stylesheets and theme-token validation.
  • Updates tests, snapshots, stories, and migration documentation.
File summaries
File Description
src/theme/tokens.spec.ts Allows preserved Banner colors.
src/components/Tree/Tree.tsx Replaces styled Tree wrappers; ref forwarding must be restored.
src/components/Tree/Tree.spec.tsx Tests classes and composition.
src/components/Tree/Tree.css Adds Tree styles.
src/components/Tree/__snapshots__/Tree.spec.tsx.snap Updates Tree snapshot.
src/components/ToggleButtonGroup/ToggleButtonGroup.css Adds toggle styles.
src/components/ToggleButtonGroup/styles.tsx Adds class-based wrappers; ref forwarding must be restored.
src/components/ToggleButtonGroup/styles.ts Removes styled-components implementation.
src/components/ToggleButtonGroup.spec.tsx Tests class composition.
src/components/MessageBox/MessageBox.tsx Replaces styled elements; ref forwarding must be restored.
src/components/MessageBox/MessageBox.spec.tsx Tests margin-variable binding.
src/components/MessageBox/MessageBox.css Adds MessageBox styles.
src/components/MessageBox/__snapshots__/MessageBox.spec.tsx.snap Updates MessageBox snapshots.
src/components/Banner/Banner.tsx Replaces styled elements; ref forwarding must be restored.
src/components/Banner/Banner.stories.tsx Removes styled story container.
src/components/Banner/Banner.spec.tsx Tests severity classes and props.
src/components/Banner/Banner.css Adds severity and close-button styles; missing inherited layout, disabled, and focus styles must be restored.
src/components/Banner/__snapshots__/Banner.spec.tsx.snap Updates Banner snapshots.
src/components/__snapshots__/ToggleButtonGroup.spec.tsx.snap Updates toggle snapshots.
src/components/__snapshots__/ErrorModal.spec.tsx.snap Updates MessageBox-derived classes.
src/components/__snapshots__/Error.spec.tsx.snap Updates error-component classes.
CHANGELOG.md Documents migration and breaking changes.
Review details

Suppressed comments (3)

src/components/MessageBox/MessageBox.tsx:33

  • The old styled.h3 export forwarded refs; this ordinary function component no longer does under React 17. Wrap it in React.forwardRef<HTMLHeadingElement, BoxProps<'h3'>> and pass the ref to the heading.
export const BoxHeading = ({ className, ...props }: BoxProps<'h3'>) => (
  <h3 {...props} className={classNames('message-box-heading', className)} />
);

src/components/MessageBox/MessageBox.tsx:37

  • The old styled.div export forwarded refs; this ordinary function component no longer does under React 17. Wrap it with forwardRef so existing consumers can still access the body element.
export const BoxBody = ({ className, ...props }: BoxProps<'div'>) => (
  <div {...props} className={classNames('message-box-body', className)} />
);

src/components/MessageBox/MessageBox.tsx:41

  • The old styled.div export forwarded refs; this ordinary function component no longer does under React 17. Wrap it with forwardRef so the event-id element remains accessible through the public ref API.
export const BoxEventId = ({ className, ...props }: BoxProps<'div'>) => (
  <div {...props} className={classNames('message-box-event-id', className)} />
);
  • Files reviewed: 22/22 changed files
  • Comments generated: 6
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +59 to +70
.banner-close-button {
color: var(--banner-color);
overflow: visible;
background: none;
border: none;
padding: 0;
font: inherit;
cursor: pointer;
outline: inherit;
box-shadow: none;
margin-left: 2.4rem;
}
Comment on lines +67 to +70
outline: inherit;
box-shadow: none;
margin-left: 2.4rem;
}
Comment on lines +16 to +18
export const Severity = ({ className, ...props }: WithVariableStyle<'span'>) => (
<span {...props} className={classNames('banner-severity', className)} />
);
`;
}

export const BoxWrapper = ({ margin, className, style, ...props }: BoxWrapperProps) => (
Comment on lines +15 to +19
export const StyledToggleButtonGroup = ({ className, ...props }: ToggleButtonGroupProps) => (
<ToggleButtonGroup
{...props}
className={composeRenderProps(className, (resolved) => classNames('toggle-button-group', resolved))}
/>
Comment on lines +9 to +13
export const Tree = <T extends object>({ className, ...props }: RAC.TreeProps<T>) => (
<RAC.Tree
{...props}
className={RAC.composeRenderProps(className, (resolved) => classNames('tree', resolved))}
/>
OpenStaxClaude and others added 2 commits September 10, 2026 19:02
…plain CSS

Replaces the styled-components definitions in these four components with sibling
stylesheets bound to the --ox-* theme tokens, following the pattern established by
the Button, Modal and composite-component migrations.

The exported styled pieces (BoxWrapper, BoxHeading, BoxBody, BoxEventId,
StyledBanner, Severity, CloseButton, Tree, TreeItem, TreeChevron,
StyledToggleButtonGroup, StyledToggleButton) keep their names and props; the
react-aria wrappers compose className through composeRenderProps so a caller's
render-callback form is not dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Snapshots record the shape; these assert the parts that carry meaning — the
banner tone class landing on both the banner and its close button, the margin
binding on MessageBox, the tree chevron structure, and className composition on
the react-aria wrappers that consumers wrap with styled().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@OpenStaxClaude

Copy link
Copy Markdown
Contributor Author

Rebased — history rewritten, base unchanged. To pick it up: git fetch && git reset --hard origin/CORE-2008-migrate-feature-components.

Roy asked for #143 to be split for review, so its branch was rewritten and this PR had to move with it. The stack is now main#149 (colour audit engine) → #150 (token file + enforcement) → #143 (the CSS sweep) → this.

This one stays on #143 rather than moving to #150 like #140 and #141 did, because it genuinely overlaps the sweep: Button.css, the Tree snapshot and the ManageCookies snapshot.

This branch was carrying ten stale commits. It forked from an older state of the CORE-2720 branch and was never rebased through the fifth- and sixth-round rewrites, so it held its own copies of #137 and the whole CORE-2720 series — including duplicates of Roy's three commits. Those are gone; what is replayed is the two commits that are actually this PR's work. 12 commits → 2, effcf24ae2a97fd.

496 tests / 41 suites / 114 snapshots pass; typecheck clean, no conflicts, no snapshot regeneration.

Worth a look at the diff before picking this back up — it should now show only MessageBox, Banner, Tree, ToggleButtonGroup and the CHANGELOG, where before it showed the token work as well.

🤖 Generated with Claude Code

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.

2 participants