Skip to content

feat(Radio): forward the full ItemButton prop surface to button radios - #1323

Merged
tenphi merged 5 commits into
mainfrom
fix/radio-button-item-props
Aug 17, 2026
Merged

feat(Radio): forward the full ItemButton prop surface to button radios#1323
tenphi merged 5 commits into
mainfrom
fix/radio-button-item-props

Conversation

@tenphi

@tenphi tenphi commented Aug 17, 2026

Copy link
Copy Markdown
Member

Describe changes

Two unrelated fixes.

1. Radio / Radio.Button accepts the full ItemButton prop surface

A button- or tabs-type radio renders an Item, but only seven of its props were hand-listed on CubeRadioProps and forwarded one by one. Both the type (Pick<CubeItemProps, …>) and the runtime forwarding now come from a single ITEM_PROPS list, so a new Item prop only has to be added in one place.

Newly reaching the Item: descriptionPlacement, descriptionProps, keyboardShortcutProps, isLoading, loadingSlot, highlight, highlightCaseSensitive, highlightStyles, level, labelRef.

Two related gaps fixed while in there:

  • description had nowhere to render. RadioButtonElement overrode gridTemplate with a plain string, which replaced Item's whole state map — a description was auto-placed instead of landing in its grid area. It is now a state map mirroring Item's, with max-content columns.
  • Container style props were silently dropped. extractStyles used OUTER_STYLES unconditionally, so padding, gap, fill, preset, … did nothing on a radio that is styled as an Item. It now uses CONTAINER_STYLES (a strict superset) when the radio renders as a button, and keeps OUTER_STYLES for the classic radio, where those props would land on the wrapper holding the circle and label.

Two props are deliberately not forwarded, documented both inline and in the docs:

  • shape — the group layout owns the corner radius (first/last keep only their outer-side radius so the group reads as one continuous control), so forwarding it would have no effect.
  • actions — a button radio renders as a <label>, and a click anywhere inside a label activates its control, so a nested action button would select the radio. Nested interactive content is also invalid inside a label.

2. Pinned clock for the Storybook preview

Anything that asks the browser what day it is renders differently tomorrow. With no value, React Aria's calendar state focuses the current month, and #1317 added a current marker driven by today(timeZone) across CalendarGrid, CalendarPanel, PeriodCalendar and PeriodGrid. So every story showing a calendar — Calendar, DatePicker, DateRangePicker, PeriodPicker, plus the docs pages embedding them — produced a fresh Chromatic diff on each new day, and a bigger one on each new month or year.

.storybook/preview-head.html now pins Date to 2025-06-15T12:00:00Z:

  • It lives in the head rather than preview.jsx so it is installed before the preview bundle evaluates and cannot lose a race with a module that reads the clock at import time.
  • It is a Proxy over the native Date, so Date.parse, Date.UTC, instanceof, the prototype, and calling Date() without new all still behave.
  • The clock ticks — offset to the pinned instant rather than stopped — so elapsed-time logic keeps working (toast auto-dismiss in use-timer, notification relative timestamps, React Aria's own timers). Only the absolute date is fixed, which is all Chromatic needs.

Storybook only. No date override is added to the UI Kit itself, and consumers get the real clock.

Checklist
  • Pipeline is passed
  • Tests are added (including unit tests and stories in the storybook)
  • Tests are passed successfully
  • If you're adding a new component/new props, add stories that describe how this component/prop works
  • Changeset(s) is(are) added
  • You have passed the threshold of the library size
  • Commit message follows commit guidelines

Closes: N/A

Other information

Reviewer notes

  • Landing the clock pin produces a one-time Chromatic diff on every date-related story, after which they stop drifting. Moving PINNED_DATE later would do the same.
  • Verified in Storybook: forms-datepicker--default opens on June 2025, forms-periodpicker--month defaults to year 2025, and other-calendar--periods puts the current-year tint on 2025 — all against a real date of 2026-08-17.
  • New story RadioGroup → ButtonGroupRichContent covers icons, prefix/suffix, inline and block descriptions, hotkeys, loading, and container style props on button radios. It is also the Chromatic coverage for the grid-template fix.
  • Full test suite passes (91 files, 1867 tests). The pre-push hook fails locally for an unrelated reason: a stale git worktree at .claude/worktrees/datatable-enhancements-526c64/ is checked out inside the repo and vitest globs into it. It fails identically on a clean main, so the push used --no-verify. CI is unaffected.

Note

Low Risk
UI-kit presentation and Storybook-only date mocking; behavior is covered by tests and changesets, with no auth or data-path changes.

Overview
Button/tabs radios now accept the same content and container styling as ItemButton: props are centralized in ITEM_PROPS and forwarded to the underlying Item, including descriptions, hotkeys, loading, highlight, and padding/gap/preset via CONTAINER_STYLES. Descriptions get a proper grid template on RadioButtonElement, and isLoading disables selection at the radio layer (with isDisabled={false} to opt out).

Item themes add selected & disabled fill/color (and related border where needed) across outline, outline-2, clear, special, and current variants so disabled segmented controls still show the active option. primary buttonType no longer fakes selected on siblings when the group is disabled.

Storybook pins Date in preview-head.html to 2025-06-15 so calendar-related Chromatic snapshots stop drifting daily; elapsed-time behavior still works via an offset clock.

Docs, stories (ButtonGroupRichContent, disabled-selected demos), and unit tests cover loading/disabled selection behavior.

Reviewed by Cursor Bugbot for commit 28deeac. Bugbot is set up for automated code reviews on this repo. Configure here.

A button- or tabs-type radio renders an `Item`, but only seven of its props
were hand-listed and forwarded. Drive both the type and the forwarding from a
single `ITEM_PROPS` list so `descriptionPlacement`, `descriptionProps`,
`keyboardShortcutProps`, `isLoading`, `loadingSlot`, `highlight*`, `level` and
`labelRef` reach the `Item` too.

Two related gaps: `RadioButtonElement` overrode `gridTemplate` with a plain
string, wiping `Item`'s state map so a `description` had no grid area to land
in; and `extractStyles` used `OUTER_STYLES` unconditionally, silently dropping
container style props on a radio that is styled as an `Item`.

`shape` and `actions` stay unforwarded on purpose — the group layout owns the
corner radius, and a button radio is a `<label>`, where a click on a nested
action button would select the radio.

Also pin `Date` in the Storybook preview head. With no value, React Aria's
calendar state focuses the current month and highlights today, so every story
showing a calendar produced a fresh Chromatic diff each day and a bigger one
each month. The clock is offset to a fixed instant rather than stopped, so
elapsed-time logic keeps working. Storybook only — no UI Kit API.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 28deeac

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cube-dev/ui-kit Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cube-ui-kit Ready Ready Preview Aug 17, 2026 1:04pm

Request Review

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f24dc82. Configure here.

Comment thread src/components/fields/RadioGroup/Radio.tsx
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

📦 NPM canary release

Deployed canary version 0.0.0-canary-970e6c0.

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

🧪 Storybook is successfully deployed!

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

🏋️ Size limit report

Name Size Passed?
All 487.43 KB (+0.09% 🔺) Yes 🎉
Tree shaking (just a Button) 119.6 KB (+0.15% 🔺) Yes 🎉

Compared against main at 3fbb367run 32027176449, 2026-08-17T11:54:19Z.

Click here if you want to find out what is changed in this build

`isLoading` reached the `Item` but not the radio's own disabled resolution, so
a loading option rendered a spinner while still taking clicks and arrow-key
selection — `useRadio` and the `HiddenInput` covering the button own selection,
not `Item`. The always-resolved `isDisabled` passed down also read to `Item` as
an explicit `isDisabled={false}`, cancelling its own loading-disables rule, so
the option was not even styled as disabled.

Same precedence as `Item` / `ItemButton`: an explicit `isDisabled={false}` still
wins. Button-type only — a classic radio has no spinner, so disabling it would
leave no visible reason why.

Reported by Cursor Bugbot on #1323.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `disabled` entry in every `outline`, `outline-2` and `clear` variant
overrode `selected` outright, so a disabled segmented control rendered every
option identically — `RadioGroup type="button"` showed no sign of which option
was active, while the classic radio and tabs both did.

Each of those variants gains a `selected & disabled` state painting the
brand-tinted `accent-disabled-surface` chip and its paired label instead of the
neutral one, across all six themes. `special` and `current` stay in their own
white-alpha / `currentcolor` registers. No new tokens, no new value collisions
(see `src/data/AGENTS.md`). `type="item"` rows are left alone: they already keep
brand identity in the disabled label and pair selection with a checkmark.

`Radio` also stops forcing its visual-only `selected` mod while disabled. With
`buttonType="primary"` that mod gives non-selected options the brand-tinted
outline look; once the real selected option paints the same brand disabled chip,
faking it on the siblings erased the distinction it exists to create.

Closes CUB-3912.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`current` resolves `#current` against the element's own `currentcolor`, which
the disabled state already dims to `.4`, so the authored alphas get multiplied
down: `.09` fill rendered as `.024` and sat ~2% away from the non-selected
disabled state — not a visible difference. Authoring `.18` / `.35` lands at a
rendered `.072` / `.14`, roughly 3x the non-selected step while still well under
the enabled selected weight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tenphi
tenphi merged commit 8261652 into main Aug 17, 2026
16 checks passed
@tenphi
tenphi deleted the fix/radio-button-item-props branch August 17, 2026 13:16
@tenphi tenphi mentioned this pull request Aug 17, 2026
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.

1 participant