Skip to content

feat: BigNumberField onValueChange and value={null} (#97), DropdownSelect multiple selection (#106), react-aria-components 1.20.0; fix read-only steppers (related to #98), dropdown trigger overflow (#105), CustomRadio arrow keys (#109), stray CSS utilities (#110), Slider input names (#111) - #107

Open
kemuru wants to merge 26 commits into
mainfrom
fix/issues-97-and-98

Conversation

@kemuru

@kemuru kemuru commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Closes #97, closes #105, closes #106, closes #109, closes #110, closes #111. Refs #98, which can be closed as not planned (see below).

#103 (released in 3.10.0) fixed #97 points 2 and 4. This PR covers points 1 and 3 without changing onChange.

BigNumberField (#97)

  • onValueChange?: (value: BigNumber | null) => void gets the same number as onChange, or null when the input holds none: emptied, or only -, . or -.. A field controlled through it tells a cleared input from a typed 0 and stays empty when cleared.
  • value={null} empties a controlled field, like value="". It threw a TypeError before.
  • Typing . or -. keeps that text instead of showing NaN, so .5 gives 0.5 when the value echoes back. 3.10.0 gave 5. onChange still gets NaN for that text.
  • Typing . over a selection that holds the decimal point replaces it. Before, selecting 2.7 and typing .5 gave 5.
  • Blur empties a leftover -, . or -. and reports it as an emptied input: onChange gets 0 and onValueChange gets null, and a required field shows its error. Before, a lone - survived blur and passed the required check.
  • A typed . or -. survives a re-render of a parent that builds value={new BigNumber(amount)} and so passes the NaN onChange reported again, so .5 gives 0.5 there too. A number given again still replaces the text, as on 3.10.0, since it can be a deliberate reset.
  • The number a parent answers blur's report with, such as an echoed 0, is validated: blur validated the empty text it replaces.
  • When onChange throws while the user types, the field keeps the number and still calls onValueChange. The error is logged as Error in BigNumberField onChange, and still not thrown: 3.10.0 caught it too, and futarchy-ui's onChange runs parseUnits, which throws for ..
  • ⌘A, ⌘C, ⌘V and ⌘X work on macOS.
  • A value set from outside after blur formatted the text, such as a reset to "", leaves the field unformatted. Before, the field kept treating its text as formatted: a pasted 1,5 lost its comma as a group separator and gave 15 where a fresh field rejects it, and the idle formatting skipped later values set from outside. kleros-v2 hits this once it resets through value="" instead of remounting (fix(frontend): read BigNumberField values through onValueChange, validate gated token IDs, drop library workarounds (#2617, #2319, #2353) kleros-v2#2613): after a transaction popup resets the appeal field, a pasted 0,5 funded 5 ETH. Merged here from fix(bignumber-field): leave the field unformatted when its value is set from outside #108.

NumberField (#98)

A read-only NumberField no longer reveals its disabled stepper buttons on hover; BigNumberField already hid them. A read-only or disabled NumberField no longer fills its background on hover either. hideStepper is not added: the kleros-v2#2281 review asked for a validated use case beyond ID and suffix fields, and none turned up in the Kleros repos.

CustomRadio (#109)

Arrow keys pressed in a control inside the group now stay with that control. That covers a field such as a TextField or BigNumberField next to the options, as the docs recommend, a DropdownSelect, and a native select: the keys move the field's caret or value, or open the dropdown's list. Before, react-aria's RadioGroup took them. It moved the selection, which could unmount the control, and cancelled the key. react-aria 1.20 made nested dropdowns hit this too, since its Select lets ArrowDown and ArrowUp reach the group. In kleros-v2 that hit the case ID field and the gated token address and ID fields. Arrow keys on the radios themselves still move the selection. The options now sit in a display: contents element with the keydown handler.

Stylesheet (#110)

Tailwind scanned the whole repository, so class-like words in stories, tests and workflow files ended up in the published stylesheet as global utilities, such as .container (a max-width per breakpoint) and .collapse (visibility: collapse). global.css now scans only src/lib. Storybook, the browser tests and the dev page load the new stories.css, which adds the stories, native and test sources.

Slider (#111)

  • Names: each slider's input is named after its own aria-label or aria-labelledby.
    • Before, the value label above the thumb and the captions under the track were react-aria labels with fixed ids. Every input on a page was named after the first slider's value label and value, and the aria-label passed had no effect. In Chrome, Firefox and Safari, two sliders labelled "A" and "B" were both named "400 $M 400". This was the same on 3.10.0 and on this branch's react-aria 1.20.0.
    • The value label and the captions stay <label> elements, now plain ones that label nothing. curate-v2's global label rule still styles them.
    • react-aria names the input after the slider's label, or else after the group. Browsers read the group's text there, not its aria-labelledby. With aria-labelledby, an empty hidden label takes the group's place, and the thumb gets the aria-labelledby and the aria-label. Every combination of the two props now names the input like its group in Chrome, Firefox and Safari, including both props together and an id that matches nothing.
    • Without aria-labelledby, aria-label reaches the input through the group. Forwarding it to the thumb as well, as the issue suggests, doubled the name ("A A").
  • Value: screen readers announce the formatter's text, such as 0.005 ETH where curate-v2's Deposit sliders read 5,000,000,000,000,000.
    • react-aria has no prop for this, so a small layout effect writes the value label's text to aria-valuetext after each commit, like the dropdowns' SelectStateBridge.
    • A formatter that returns "" still announces react-aria's number.
    • The value label is aria-hidden, since the input announces the same text.
  • Captions: they read as their text, without aria-label="min-label" and "max-label".
  • Styling hooks: each part has a data-slot named like its id, such as [data-slot=slider-label].
    • The ids stay, because foresight's OptionCard and futarchy-ui's AmountSlider and PredictionSlider style through #slider-label, #slider-track and #slider-fill. Removing them would silently change those sliders.
    • The ids no longer take part in labelling. Their only remaining cost is invalid HTML when a page has several sliders, so the issue's check for no duplicate ids still fails until the next major drops them.
    • #slider-thumb never reached the DOM, on 3.10.0 too: react-aria drops a thumb's id.
  • Focus: the thumb glows (shadow-input) while it has keyboard focus, as it already did while dragged. Nothing showed which slider had focus.

react-aria-components 1.20.0

The library bundles react-aria-components, and it was on 1.7.1 (March 2025). #106 needs multiple selection in Select, which arrived in 1.13. Apps already compile against 1.13 to 1.21 types, so the runtime lagged what their types allowed. ~1.20.0 replaces ^1.7.1, so the types apps compile against are those of the version they run. ^1.20.0 would resolve to 1.22.1 types, which allow props that the bundled 1.20.0 ignores.

  • Why not newer:
  • Adapting to it:
    • Both dropdowns commit validation from their onChange. react-aria's setSelectedKey no longer does, so a required field kept its error after a valid pick. That happened with the cascader's Select button and with an arrow key or typeahead on the closed trigger, and a second submit then went through with the error still shown. A form reset sets the value too, so they skip the commit then and leave validation to react-aria; otherwise a reset after an awaited submit showed the required error.
    • The bundled react-aria is patched to keep 1.7.1's handling of overlays that apps open on top. A Yarn patch (.yarn/patches/react-aria-npm-3.51.0-*.patch) changes two things in the bundled copy:
      • react-aria 1.20 makes the page behind an open Modal or popover inert, including elements added while it is open. The patch makes it use aria-hidden only, as 1.7.1 did, and closing the overlay leaves alone an inert the app set itself.
      • 1.20 also stops the click that closes a dismissable Modal. The patch ends that interaction on pointerup, as 1.7.1 did, so the click still reaches what it lands on.
      • Without both, a wallet modal opened from a library Modal, as the mobile menus of futarchy-ui, governor-v2 and foresight do, and toasts ignored taps. Patched, Chromium, Firefox and WebKit behave as on 3.10.0.
      • Browser tests fail if the patch stops applying, for example after a react-aria bump.
    • A single selection keeps ignoring defaultValue, as 1.7.1 did. curate-v2's chain field passes one and writes its form value only from callback. Honouring it showed Ethereum Mainnet before any pick, and picking it again reported nothing, so the step's Next button stayed disabled.
    • Modal gives the consumer's props to the overlay only. react-aria warns when the nested Modal also gets the open state. It now forwards DOM events too, so a caller's onClick fired twice.
    • The Storybook docs filter allows react-stately, where Select's selectedKey and isOpen now live, and leaves out the ~70 global DOM props every component gained. Every component keeps the props its table documented.
  • Size: the dist's JS grows 3.9% gzipped (+23 KB). One DropdownSelect import pulls 9 KB more.

DropdownSelect and DropdownCascader trigger (#105)

  • buttonClassName styles the trigger, for example buttonClassName="w-full"; className still styles the wrapper.
    • Apps reach the button through descendant selectors today: [&_button]:w-full in curate-v2, > button { width: 100% } in kleros-v2, atlas-dashboard's truncation set. The DOM is unchanged, so those keep working.
  • Long text stays on one line, cut with an ellipsis, and the arrow, icon and dot keep their size.
    • Before, words wrapped onto a second line and the trigger grew from 45 to 69px.
    • A long token such as an address ran 194px past the border and squeezed the arrow to 0px.
  • Width: the trigger is never wider than its field.
  • The select's list is 240px but never narrower than the bordered trigger, and long options wrap.
    • Narrow w-fit triggers keep their 238px list: the token pickers in futarchy, foresight and governor.
    • The simple button stretches across its column, so its list stays 240px. This covers escrow-v2's mobile Policies menu.
  • The 240px default stays, since apps size skeletons and grid columns from it.

DropdownSelect multiple selection (#106)

  • selectionMode="multiple" keeps the list open while picking. callback gets the selected items, and value, defaultValue and onChange take arrays of keys; react-aria calls only onChange in this mode.
    • With several items picked, the button shows react-aria's locale-aware list ("A, B, and C"), cut with an ellipsis when long.
    • In a form, isRequired asks for at least one item and name submits every selected key.
  • Selected rows keep the single-selection style, a 3px left border rather than a check, as the design system describes. Since several rows can look selected, the row with keyboard focus also gets an outline, as in DraggableList.
  • Typed with overloads, not a generic, ordered single, multiple, single. ComponentProps<typeof DropdownSelect> reads the last overload, so it keeps resolving to the single-selection props, which kleros-v2's LabeledDropdown and foresight's wrapper build on. The docs read the first. A dynamic selectionMode (a union) has no overload.
  • callback gets the items in the order they were picked. selectedKey, defaultSelectedKey and onSelectionChange don't apply in multiple mode, and the types reject them.
  • atlas-dashboard's ScopesSelect could move to it, except for its per-option descriptions, which IItem doesn't have.

CI

The component tests job now runs on ubuntu-24.04. GitHub's ubuntu-latest image became Ubuntu 26.04, where the repo's Playwright 1.56.1 can't install Chromium, so the job failed before any test ran. Playwright supports Ubuntu 26.04 from 1.61, so the pin can go when Playwright is upgraded.

For consumers

  • No API is removed. The CHANGELOG's Breaking changes section lists the behaviour changes that can still affect an app. onChange sends exactly what 3.10.0 sent for the same typed text, and blur now also reports a -, . or -. it empties. value's type gains null, which only affects code that reads the type back through ComponentProps<typeof BigNumberField>. The Slider keeps its props, types, ids, elements and behaviour; besides what screen readers get, only its keyboard focus glow is new.
  • react-aria-components 1.20.0: no prop is removed, but some behaviour changes. The CHANGELOG's Changed section lists each change, checked against 1.7.1:
    • Re-picking the selected option no longer calls callback.
    • react-aria's value, defaultValue and onChange now work.
    • A Checkbox or Switch label no longer toggles from a click by @testing-library/user-event before 14.6, which Storybook 8's @storybook/test bundles. 14.6 and later, which kleros-v2, foresight and atlas-dashboard use, are unaffected.
    • Enter on a radio submits its form, as it already did on a Checkbox or Switch.
    • Button onClick also fires on keyboard presses. Checkbox, Switch, TextField, NumberField and Modal now call a caller's onClick, and other DOM event handlers and attributes such as dir or lang now reach the element.
    • While a Modal or popover is open, react-aria keeps the scrollbar's space with scrollbar-gutter. In Chromium, a page with a custom ::-webkit-scrollbar (escrow-ui, veashi-scanner) shifts by the scrollbar's width while it is open.
    • Clearing a DatePicker segment no longer reports null, and a disabled NumberField is left out of FormData.
    • RadioGroup's focus ring doesn't show after a clicked submit with an invalid group.
    • In jsdom, tabbing through a form-less RadioGroup inside a Modal needs a CSS.escape polyfill.
    • curate-v2's ChainInput passes defaultValue, which a single selection keeps ignoring, so its chain field works as before.
  • Dropdowns: buttonClassName replaces the descendant selectors apps use to size the trigger, though those still work. selectionMode must be a literal, since a union has no overload.
  • Recommended wiring: value={amount} with onValueChange={(v) => setAmount(v?.toString() ?? "")}. In kleros-v2 this retires useBigNumberFieldReset, the isZero() mappings and CreationCard's DOM read. It also stops the crash in Fund and InputDisplay when . reaches parseUnits as "NaN". Code that stays on onChange works as before, crash included.
  • CustomRadio: the options are inside a display: contents element. The group still lays them out, but a child selector such as [role="radiogroup"] > label no longer matches them. kleros-v2 has none.
  • Stylesheet: 20 utilities that no component uses are gone (see the CHANGELOG). An app that relied on one of them by accident needs its own.
  • Slider:
    • Give each Slider an aria-label or aria-labelledby. curate-v2's two Deposit sliders have neither. Without one, an input has no name of its own: Chrome and Firefox read its captions, Safari reads nothing, and react-aria warns on every render in development.
    • Prefer [data-slot=slider-label] to #slider-label in new styles.
  • Known limits:
    • A parent that passes the value back in a later update, such as a transition or an effect relay, loses a leading . or -.
    • While the field holds only -, . or -., setting the value to empty again keeps that text: with onValueChange wiring, or as new BigNumber("") after onChange's NaN. Remount with a key where that matters.
  • Next major: make onChange nullable and drop the duplicate callback. Drop the Slider's fixed ids in favour of data-slot.

Verification

Added after the review of f553238, in 92c7b4a…0c0b3b9:

  • Fixed: an inert the app set was cleared when a library overlay closed (the patch's unhide path); the BigNumberField items above, with handleChange's two copies merged; the multiple-selection focus outline; the CHANGELOG's breaking-changes wording and the missing Modal entry; the Slider focus glow, the NumberField hover fill and the simple button's arrow.
  • Left as is:
    • A form reset of a single selection doesn't call callback, as on 3.10.0: callback takes an item and can't get null without breaking apps' types. onSelectionChange gets the null.
    • A single selection still ignores defaultValue, as on 3.10.0, though its type accepts it. curate-v2 passes one, so removing it from the type would fail their type-check, and honouring it is what broke their chain step. The CHANGELOG and the prop's JSDoc say so.
    • Multiple selection has no checkbox: the design system marks selected rows with the 3px left border, "not a check". The closed button lists the picks and has no count, which would be a design decision.
    • The Slider label stays optional, since curate-v2 renders two sliders without one, and the Slider's fixed ids stay, since foresight and futarchy-ui style the parts through them.
    • No props type is exported for multiple selection, as none is exported for any component.
    • The cascader's confirm button is clipped at 375px, as on 3.10.0, in code this PR doesn't touch.
  • Independent review: a fresh-context review of these changes found three issues, each reproduced and fixed: an echoed 0 after blur skipped validate, a number given again over a lone - was ignored, and an onChange error from blur's report skipped validation.
  • Tests: each fix has one that fails on f553238: 16 jsdom tests and 7 native ones, in real Chromium where browser behaviour matters. Others pin the unchanged behaviour around them in useBigNumberField.test.tsx, and the pick order of callback in dropdown.native.test.tsx.
  • Evidence:
    • A differential fuzz of the hook against f553238, with the intended changes reverted: 8,400 random edit sequences across 7 parent wirings and 4 prop sets, identical, and 4,200 more under StrictMode. With the changes, every difference is one of them.
    • 16 mutants of the hook, each caught by a test.
    • The built dist no longer writes inert. The bundled .d.ts is unchanged; the per-module ones gain an explicit defaultValue with the type it inherited.
  • Gates: yarn test:unit:coverage 232, yarn test:stories 314, test:native 36, yarn test:storybook-plays 314 stories × 2 themes, tsc, eslint and yarn build.
  • Screenshots: apart from sub-pixel anti-aliasing, the published Storybooks of f553238 and 0c0b3b9, compared story by story in both themes, differ only in the Slider's focus glow, in the four stories that end with keyboard focus on a thumb. No axe result changes. DraggableList's Keyboard reorder story ends with focus on Acrobat or on Premiere at random, on both builds, so Chromatic may flag it on any build.

Added after a whole-PR audit:

  • How:
    • Four independent auditors covered issue coverage, react-aria 1.20's behaviour against the 12 consumer apps on ^3.x, the library's own changes against those apps, and a fresh review of the full diff.
    • A story-by-story comparison of this branch against main: all 298 common stories in both themes, screenshots plus axe, in Chromium.
  • Fixed:
    • The bundled react-aria made the page behind a library Modal or popover inert, and swallowed the click that closes a dismissable Modal. A wallet modal opened from futarchy-ui's, governor-v2's or foresight's mobile menu, and toasts, ignored taps. It's now patched as described above. The failure reproduced in Chromium, Firefox and WebKit.
    • curate-v2's chain field stayed stuck on its Next button: a single selection now ignores defaultValue, as before.
    • A required dropdown showed its error after a form reset that followed an awaited submit.
    • ArrowDown on a DropdownSelect nested in CustomRadio moved the group's selection, which unmounted the dropdown. A nested native select had the same problem.
    • One long word widened the select's list past its 240px, clipping every option. The LongText story's check compared an option with itself, so it couldn't fail.
    • An icon passed as an element shrank beside long selected text.
    • The dropdowns' and the Slider's new layout-effect bridges logged React 18 server-render warnings.
    • The CHANGELOG got corrections: Enter submits only from radios now; other DOM events pass through; scrollbar-gutter; the BigNumberField caveats about echoes and resets; fix(bignumber-field): leave the field unformatted when its value is set from outside #108's wording; a Deprecated entry for the Slider ids.
  • Tests: each fix has one that fails without it, in real Chromium where browser behaviour matters:
    • src/native/dropdown.native.test.tsx
    • src/native/custom-radio.native.test.tsx
    • src/native/modal.native.test.tsx, for the patch
    • src/test/ssr.test.tsx
    • the corrected LongText story
    • a type check that selectedKey is rejected in multiple selection
  • No breaking changes found:
    • The published .d.ts: the changes since 3.10.0 only add things, and compile against consumer code (including vea's FC<DropdownSelectProps> assignment).
    • CSS: only the 20 stray utilities removed and 4 used ones added. No consumer app uses them in a way that changes, and curate-v2's and escrow-v2's compiled cascades were checked.
    • Runtime imports: still only react and react-dom.
    • The story comparison against main: no unexpected visual change and no new accessibility violation.
    • The other differences are documented and intended: stories that changed, tooltip positions, and RadioGroup's focus ring.

Added with #111:

  • Gates: all pass with no unhandled errors.
    • yarn test:unit:coverage: 205 tests
    • yarn test:stories: 314
    • test:native: 14
    • yarn test:storybook-plays: 314 stories × 2 themes
    • tsc, eslint and yarn build
  • New tests fail on the old code:
    • a jsdom test of the names, the value text and the hooks;
    • the SeveralSliders and LabelledBy stories, plus name and aria-valuetext checks in the existing stories.
    • Twelve mutants each fail a test, for example: dropping the bridge, giving it dependencies or dropping its empty-text fallback; dropping the empty label or rendering it always; dropping the thumb's aria-labelledby or aria-label, or always forwarding the aria-label; unhiding the value label or making it a react-aria Label again; dropping a data-slot; putting back a caption's aria-label.
  • Independent review: a fresh-context review found three low-severity defects and a wrong doc comment. Each was reproduced.
    • Fixed, each with a test:
      • an aria-label next to aria-labelledby was lost on the input, which had no name at all when the id matched nothing;
      • a formatter that returns "" left no announced value;
      • the doc comment.
    • The value-text bridge, like the dropdowns' state bridge, uses the library's isomorphic layout effect (usehooks-ts), so React 18's server renderer logs no warning for them. A test renders both on the server.
  • Real browsers: Chrome, Firefox and Safari's accessibility trees were read with Playwright for 3.10.0, this branch before the fix and after it.
    • The cases: the issue's two sliders, one slider, curate-v2's unlabelled pair, aria-labelledby with and without captions, futarchy's slider, foresight's disabled sliders, an empty formatter, StrictMode and RTL.
    • Every labelled case now gives exactly its label, with the formatter's text as the value.
    • Switching between aria-label, aria-labelledby, both, two ids, a missing id and none at runtime gives the same names as a fresh load.
  • No breaking changes:
    • The emitted .d.ts files, the CSS files and the export list are identical to this branch before the fix. The Slider chunk grows by 81 bytes gzipped.
    • Screenshots of 11 cases are pixel-identical to this branch before the fix in all three browsers, in both themes, focused and not. The cases include curate-v2's global label rule, futarchy-ui's #slider-* overrides, a custom theme and RTL. Eight of them were also compared with 3.10.0 in Chromium, and match.
    • Track clicks, thumb drags, a click on the value label, and the arrow, End and PageDown keys give the same values and focus as 3.10.0 in all three browsers.
    • foresight's OptionCard test is the only consumer test that GitHub code search finds for the Slider. It was ported. Its checks, which find the captions by text and the slider as disabled, pass before and after the fix.
    • Code search finds no Kleros test setup that fails on console output. curate-v2, the only unlabelled consumer, has no tests.

Added with #105, #106 and the react-aria upgrade (branch head c5c2050):

  • Gates: all pass with no unhandled errors.
    • yarn test:unit:coverage: 198 tests
    • yarn test:stories: 312
    • test:native: 14
    • yarn test:storybook-plays: 312 stories × 2 themes
    • tsc, eslint and yarn build
  • New tests fail on the old code:
  • Independent review: a fresh-context review of the upgrade, DropdownSelect button has a fixed width and its content overflows #105 and DropdownSelect does not support selecting multiple items #106 found 8 issues. Each was reproduced, and each is fixed in 3d151c8, c9b1ef1 and c5c2050.
  • kleros-v2 against c5c2050:
    • #2613 resolves react-aria-components to 1.20.0. check-types gives 0 errors, 184 tests pass and the vite build succeeds. 69 component scenarios are unchanged.
    • kleros-v2's dev branch still has LabeledDropdown, built on ComponentProps<typeof DropdownSelect>. It gives 0 type errors and 172 tests pass.
    • On 046a2ff, the cases and jurors dropdowns behaved the same on 1.7.1 and 1.20.0, and none of the behaviour changes above reaches kleros-v2.
  • Screenshots: every story was captured in both themes before and after. Besides the new stories, two things differ, and Chromatic will show both:
    • the tooltips, by under a pixel (react-aria's positioning);
    • the RadioGroup RequiredOptions focus ring.
  • Consumer types: these compile against the new .d.ts with react-aria-components 1.20.0 and 1.22.1 types:
    • kleros-v2's LabeledDropdown and uiComponentsTypes;
    • foresight's wrapper;
    • the atlas-dashboard, curate-v2, escrow-v2, futarchy and governor call sites.
      The .d.ts changes only by buttonClassName, DropdownSelect's overloads and the exported SelectProps.
  • Version choice: probes with real input on 1.7.1, 1.19.0, 1.20.0, 1.21.1 and 1.22.1 covered Enter and Space selection, popover reveal time with touch points, overlays closed during their enter animation, and the Modal in jsdom. Each behaviour change in the CHANGELOG was checked on 1.7.1 and 1.20.0.

Added with the fixes for #108, #109 and #110:

  • Two new unit tests (outside reset after blur, then paste or set) fail before the fix. yarn test:unit:coverage now runs 196 tests.
  • The extended Composition story (arrow keys in the nested field) fails without the CustomRadio fix. yarn test:stories still runs 302 tests, and test:native runs 10.
  • The published CSS loses exactly the 20 unused utilities and gains nothing. Storybook's CSS is unchanged except .p-2 and .text-red-500, which only a unit test of cn used.
  • kleros-v2 (#2613, with its onValueChange migration) against this branch:
    • 69 component scenarios for stake, appeal fund, case ID and gated token ID are unchanged, except the reset and arrow-key cases, which are now fixed.
    • Real Chromium: the stake suite passes 15/15, and the Resolver cards keep a pixel-identical layout.

Earlier checks:

  • yarn test:unit:coverage: 194 tests. They include a test of the echo check under real React scheduling, without act(), which caught a timing bug in an earlier draft. yarn test:mutation: 100%.
  • yarn test:stories: 302 tests. yarn test:storybook-plays: 302 stories × 2 themes. The new and extended stories fail on 3.10.0.
  • tsc, eslint, prettier and yarn build pass. The emitted .d.ts changes only by value | null, onValueChange and NumberField's prop destructuring.
  • Local checks, not committed:
    • A differential fuzz against 3.10.0, over thousands of random edit sequences across 8 consumer wirings × 9 prop sets, gave identical onChange calls.
    • A kleros-v2 simulation ported the field code from InputDisplay (the same wiring as Fund), CreationCard, GatedErc1155 and Jurors, built it against 3.9.0, 3.10.0 and this branch, and typed into it in Chromium. With today's kleros-v2 code there are no regressions, and .5 now gives 0.5. Migrated to onValueChange, it passes every BigNumberField: controlled mode cannot represent an empty field #97 point, which 3.9.0 and 3.10.0 fail.

🤖 Generated with Claude Code


PR-Codex overview

This PR focuses on updating styles and functionality across the codebase, particularly in the src and stories directories, enhancing accessibility, and improving the behavior of various components.

Detailed summary

  • Changed stylesheet imports from global.css to stories.css.
  • Updated react-aria-components version to ~1.20.0.
  • Enhanced modal functionality to prevent double event firing.
  • Improved accessibility checks in stories.
  • Refined dropdown and slider components for better usability.
  • Added new tests for dropdowns, modals, and sliders.
  • Introduced SelectStateBridge for managing select states.
  • Adjusted BigNumberField for clearer value handling.
  • Modified CSS classes for better responsiveness and styling.
  • Added support for handling arrow key navigation within nested controls.

The following files were skipped due to too many changes: src/lib/form/slider.test.tsx, src/stories/slider.stories.tsx, src/lib/form/slider.tsx, src/stories/dropdown-select.stories.tsx, src/native/dropdown.native.test.tsx, src/stories/dropdown-select-multiple.stories.tsx, CHANGELOG.md, src/lib/form/bignumber-field/useBigNumberField.tsx, src/lib/form/bignumber-field/useBigNumberField.test.tsx, yarn.lock

✨ Ask PR-Codex anything about this PR by commenting with /codex {your question}

Summary by CodeRabbit

  • New Features

    • Dropdowns now support multiple selection, controlled values, and customizable trigger styling.
    • Big number fields support null for empty values and provide a callback that distinguishes empty or incomplete input from numeric values.
    • Dropdown triggers handle long labels with truncation and offer improved width options.
  • Bug Fixes

    • Read-only number fields no longer show increment and decrement controls.
    • Arrow keys in nested editable fields no longer change radio selection.
    • Improved dropdown selection, validation, and form submission behavior.

…ead-only steppers (#98)

BigNumberField (#97 points 1 and 3; 3.10.0 shipped points 2 and 4):
- onValueChange gets the same number as onChange, or null when the input holds none ("", "-",
  ".", "-."). onChange is unchanged: 0 when emptied, NaN for "." and "-.".
- value={null} empties a controlled field like value="". It threw a TypeError.
- "." and "-." keep their text instead of showing NaN, so ".5" gives 0.5 when the value
  echoes back. An empty value counts as an echo only when it comes in the same commit as the
  edit. The check compares committed text, so React's effect timing can't break it.
- "." over a selection that holds the decimal point replaces it.
- A required field flags the empty text that blur leaves after "." or "-.".

NumberField (#98): a read-only field no longer reveals its disabled steppers on hover.
hideStepper is not added: the kleros-v2 review asked for a use case beyond ID and suffix
fields, and none exists yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@netlify

netlify Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for kleros-v2-ui-storybook ready!

Name Link
🔨 Latest commit 0c0b3b9
🔍 Latest deploy log https://app.netlify.com/projects/kleros-v2-ui-storybook/deploys/6ae215b5bc4cc37e0850cb50
😎 Deploy Preview https://deploy-preview-107--kleros-v2-ui-storybook.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review.

ℹ️ Recent review info
⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 67ed45fb-2fba-4f00-be15-b0fb2b900c1e
📥 Commits

Reviewing files that changed from the base of the PR and between 046a2ff and c5c2050.

⛔ Files ignored due to path filters: 1
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing: 10
  • CHANGELOG.md
  • package.json
  • src/lib/container/modal.test.tsx
  • src/lib/container/modal.tsx
  • src/lib/dropdown/cascader/index.tsx
  • src/lib/dropdown/select-state.tsx
  • src/lib/dropdown/select/dropdown-container.tsx
  • src/lib/dropdown/select/index.test.tsx
  • src/lib/dropdown/select/index.tsx
  • src/stories/dropdown-select.stories.tsx

Included review availability: This review used your included allowance. 0 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 5 reviews per hour.

Walkthrough

The changes update BigNumberField controlled values and callbacks, add multiple selection and trigger sizing to dropdowns, and adjust read-only NumberField and CustomRadio behavior. They also upgrade react-aria-components and update stylesheet source scanning and related interaction coverage.

Changes

BigNumberField controlled input

Layer / File(s) Summary
Controlled values and synchronization
src/lib/form/bignumber-field/useBigNumberField.tsx, src/lib/form/bignumber-field/useBigNumberField.test.tsx
value accepts null for an empty controlled field. Controlled updates preserve in-progress text for numeric echoes and some same-update empty echoes; later empty updates can reset the field.
Input and value callbacks
src/lib/form/bignumber-field/useBigNumberField.tsx, src/lib/form/bignumber-field/useBigNumberField.test.tsx, src/lib/form/bignumber-field/useBigNumberField.scheduling.test.tsx, src/stories/bignumber-field.stories.tsx
onValueChange reports a BigNumber or null, while onChange retains its separate empty and nonnumeric reporting. Input, stepper, and Home/End updates use shared callback logic.
Blur validation and decimal-key handling
src/lib/form/bignumber-field/useBigNumberField.tsx, src/lib/form/bignumber-field/useBigNumberField.test.tsx
Blur validation checks the resulting text. Decimal-key handling accounts for selected text and formatted values.

Dropdown selection and layout

Layer / File(s) Summary
Selection modes and callback wiring
src/lib/dropdown/select/index.tsx, src/lib/dropdown/select/select-state.tsx, src/lib/dropdown/select/index.test.tsx, src/stories/dropdown-select-multiple.stories.tsx, src/stories/dropdown-select.stories.tsx
DropdownSelect supports single and multiple selection with mode-specific callback types. Stories cover selection, controlled values, required forms, and multiple selected values.
Trigger sizing and selected-text display
src/lib/dropdown/select/dropdown-button.tsx, src/lib/dropdown/select/dropdown-container.tsx, src/lib/dropdown/select/item.tsx, src/lib/dropdown/select/simple-button.tsx, src/lib/dropdown/cascader/button.tsx, src/stories/dropdown-cascader.stories.tsx, src/stories/utils.tsx
Dropdown buttons accept custom classes and constrain long selected text. Popovers and list boxes use trigger-aware sizing; stories check widths and truncation.
DropdownCascader styling and validation
src/lib/dropdown/cascader/index.tsx, src/lib/dropdown/select-state.tsx
DropdownCascader accepts a trigger class, forwards selection changes, and commits validation through retained React Aria state.

Form control behavior

Layer / File(s) Summary
Read-only NumberField
src/lib/form/number-field.tsx, src/stories/number-field.stories.tsx
NumberField forwards isReadOnly and hides stepper controls when read-only. The story checks that typing and arrow keys do not change the value.
Nested editable fields in CustomRadio
src/lib/form/custom-radio.tsx, src/stories/customRadio.stories.tsx
CustomRadio stops arrow-key propagation from nested editable fields. The story checks that field focus and radio selection remain unchanged during text editing.

React Aria compatibility

Layer / File(s) Summary
Dependency and component integration
package.json, src/lib/container/modal.tsx, src/lib/container/modal.test.tsx, .storybook/main.ts, CHANGELOG.md
The react-aria-components range changes to ~1.20.0. Modal props are no longer spread onto AriaModal; the changelog records the dependency update and listed behavior changes.
Control and modal interaction coverage
src/native/toggle.native.test.tsx, src/stories/checkbox.stories.tsx, src/stories/switch.stories.tsx, src/stories/modal.stories.tsx
Native tests cover enabled and disabled checkbox-label and switch-track clicks. Story interactions use accessible control elements, and the modal story checks for an inert page ancestor.

Stylesheet source scanning

Layer / File(s) Summary
Component and story stylesheet sources
src/styles/global.css, src/styles/stories.css, .storybook/preview.tsx, src/native/setup.ts, index.html
global.css scans src/lib; stories.css adds story, native, and test sources. Storybook, native setup, and the dev page load stories.css.

Priority: Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Feature · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant DropdownSelect
  participant ReactAriaSelectState
  participant ParentState
  User->>DropdownSelect: Select or deselect options
  DropdownSelect->>ReactAriaSelectState: Update selected keys
  DropdownSelect->>ParentState: Report selected values and matching items
  ParentState->>DropdownSelect: Send controlled selection
Loading

Possibly related issues

  • BigNumberField controlled empty-value issue: this change adds null controlled values and an onValueChange callback for empty or incomplete input.
  • DropdownSelect trigger sizing issue: the trigger accepts custom classes and truncates long selected text.
  • DropdownSelect multiple-selection issue: the component adds multiple-selection props and array-based callbacks.
  • CustomRadio arrow-key issue: the component stops arrow-key events from nested editable fields.
  • Tailwind utility leakage issue: global source scanning is limited to src/lib, with separate sources for stories, native code, and tests.
  • BigNumberField numeric validation issue in kleros/kleros-v2: this change adds onValueChange and nullable values, but does not add whole-number validation.

Possibly related PRs

Suggested reviewers: tractorss

Merge Risk: Minimal · up to c5c20

No actionable issue remains in the reviewed changes; the PR is mergeable after normal checks.

Pre-merge checks: Passed 3 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Out of Scope Changes check Warning The PR contains a demonstrated unrelated feature change in src/lib/form/number-field.tsx: read-only NumberField now hides its stepper controls, with a dedicated ReadOnly story. This change does … Move the read-only NumberField implementation, its story, and related changelog entry to a separate pull request, or link an active coding requirement that requires this behavior.
Passed Passed checks: 3 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check Passed The PR meets the coding requirements for active linked issues #97, #105, #106, #109, and #110. For #97, BigNumberField adds onValueChange with BigNumber | null, accepts value={null}, preserves…
Title check Passed The title accurately covers the pull request’s main changes, including BigNumberField updates, DropdownSelect multiple selection, the React Aria upgrade, and the listed fixes. It is overly long but re…
Full details: Linked Issues check

Explanation

The PR meets the coding requirements for active linked issues #97, #105, #106, #109, and #110. For #97, BigNumberField adds onValueChange with BigNumber | null, accepts value={null}, preserves incomplete decimal text, resets formatting after external updates, and adds unit, scheduling, and Storybook tests. onChange remains compatible. For #105, dropdown triggers accept buttonClassName, constrain long text with ellipsis, and keep the list within the required width; stories test these cases. For #106, DropdownSelect adds typed selectionMode="multiple", array callbacks and values, ordered item callbacks, required form submission, and multiple-selection stories and type tests. For #109, CustomRadio blocks arrow keys from editable descendants while preserving radio navigation, with composition-story coverage. For #110, global.css scans only src/lib, while stories.css supplies sources for Storybook, native tests, and the dev page.

Full details: Title check

Explanation

The title accurately covers the pull request’s main changes, including BigNumberField updates, DropdownSelect multiple selection, the React Aria upgrade, and the listed fixes. It is overly long but remains specific and relevant.

Full details: Out of Scope Changes check

Explanation

The PR contains a demonstrated unrelated feature change in src/lib/form/number-field.tsx: read-only NumberField now hides its stepper controls, with a dedicated ReadOnly story. This change does not implement or support active linked issues #97, #105, #106, #109, or #110. The react-aria upgrade, Modal compatibility change, toggle tests, and dropdown adaptations have a direct connection to the upgrade and dropdown objectives.

Resolution

Move the read-only NumberField implementation, its story, and related changelog entry to a separate pull request, or link an active coding requirement that requires this behavior.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

A rabbit taps a number key,
Then leaves the empty field as null.
A dropdown gathers choices three,
Long labels tuck inside their hull.
The radio waits while arrows steer,
And stylesheet sources stay clear.

Comment @coderabbitai help to get the list of available commands.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Oct 22, 2026
…et from outside

The controlled-value effect writes the raw text of a value set from outside but kept
`isFormatted`. After blur formatted the text, a reset to "" therefore left the field in
formatted mode:

- the next change was unformatted first, so a pasted "1,5" lost its comma as a group
  separator and gave 15, where a fresh field rejects it;
- the idle formatter skipped every later value set from outside.

Consumers that reset by remounting (kleros-v2 until its onValueChange migration) never hit
it. Clear the flag whenever the effect applies a value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

🔇 Additional comments: 3
src/lib/form/bignumber-field/useBigNumberField.tsx: 1

206-206: LGTM!

Also applies to: 209-209, 214-214

src/lib/form/bignumber-field/useBigNumberField.test.tsx: 1

509-550: LGTM!

CHANGELOG.md-23-23: 1

23-23: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

⚠️ Unverified finding
Verification ran but could not confirm this finding. It is shown for review, not as a verified issue.

Use American English in the changelog.

Change “afterwards” to “afterward” on Line [23] to resolve the reported locale violation.

Source: Linters/SAST tools


ℹ️ Review info
⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 1d868317-2825-4e67-8ac5-7444b8d108e1
📥 Commits

Reviewing files that changed from the base of the PR and between 8bb8ecb and 1997866.

📒 Files selected for processing: 3
  • CHANGELOG.md
  • src/lib/form/bignumber-field/useBigNumberField.test.tsx
  • src/lib/form/bignumber-field/useBigNumberField.tsx

Included review availability: This review used your included allowance. 3 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 5 reviews per hour.

kemuru and others added 3 commits October 23, 2026 17:05
…t field

CustomRadio's docs ask for fields (conditional content next to an option) to be rendered inside
the group, and react-aria's RadioGroup handles the arrow keys of every keydown that reaches it.
An arrow key typed in such a field moved the selection, which could unmount the field, and was
cancelled, so the caret didn't move (kleros-v2's case ID and token address fields).

The options now sit in a `display: contents` element whose keydown handler stops arrow keys from
editable targets after their own handlers ran, so BigNumberField still steps on ArrowUp/Down.
Arrow keys on the radios themselves still move the selection.

Closes #109

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tailwind's automatic source detection scanned the whole repository, so class-like words in
stories, tests and workflow files ended up in the published stylesheet as global utilities, such
as `.container` and `.collapse { visibility: collapse }`, styling any app element that had those
class names.

global.css now scans only src/lib. Storybook, the browser tests and the dev page load the new
stories.css, which imports it and adds the stories, native and test sources, so their stylesheet
keeps every class they used (only `.p-2` and `.text-red-500`, from a unit test of `cn`, drop out).

Closes #110

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Button uses it, so the stylesheet still ships it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kemuru kemuru changed the title feat: add BigNumberField onValueChange and value={null} (#97), hide read-only steppers (#98) feat: BigNumberField onValueChange and value={null} (#97); fix read-only steppers (#98), CustomRadio arrow keys (#109), stray CSS utilities (#110) Oct 23, 2026
kemuru and others added 5 commits October 23, 2026 17:18
The library bundles react-aria-components, and it was on 1.7.1. Select
supports multiple selection from 1.13 on, which #106 needs, and apps
already compile against 1.13 to 1.21 types, so the runtime lagged the
types they see.

1.21.1 and 1.22.1 are skipped. In 1.21.1, preventFocus throws
"Failed to execute 'contains' on 'Node'" when the window gets a focus
event during a press, which shows up as unhandled errors in the story
tests. In 1.22.1, opening a Modal or popover calls CSS.escape, which
jsdom lacks, so apps' jsdom tests crash. 1.20.0 has neither.

Adapting to it:
- DropdownCascader commits validation after its Select button sets the
  key. setSelectedKey no longer does, so a required field kept its error.
- Modal keeps isOpen and the other overlay props off the nested Modal,
  which react-aria now warns about on every render.
- Stories expect the rest of the page to be inert under a modal, and
  click the Checkbox and Switch controls: react-aria reads the 0x0
  pointer events of a synthetic label click as a screen reader's. A
  native test covers real clicks on the label and track.

The behaviour apps may notice is listed in the changelog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#105)

DropdownSelect's and DropdownCascader's trigger is a fixed w-60 button,
and `className` styles the wrapper, so apps reached the button through
descendant selectors. A long selected text wrapped onto a second line,
and a long word ran past the border with the arrow squeezed to nothing.

- `buttonClassName` merges over the trigger's classes.
- The trigger is never wider than its field. Its value stays on one
  line and truncates; the arrow, icon and dot keep their size.
- The select's list fills its popover, which is 240px wide but never
  narrower than the trigger. Long options wrap, breaking long words.

The 240px default stays, since apps size skeletons and grid columns
from it, and the DOM is unchanged, so their selector workarounds keep
working.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iple" (#106)

DropdownSelect wraps react-aria's Select, which supports multiple
selection from 1.13 on and is now bundled at 1.20.0.

- `selectionMode="multiple"` keeps the list open while picking.
  `callback` gets the selected items, and react-aria's `value`,
  `defaultValue` and `onChange` take arrays of keys. react-aria calls
  only `onChange` in this mode, so the component maps its keys to items
  for `callback`.
- With several items picked, the button shows their texts as
  react-aria's locale-aware list, truncated like any long value. One
  item shows as in single selection.
- Two overloads type it, so `callback` takes an array only with
  `selectionMode="multiple"`. With a generic,
  `ComponentProps<typeof DropdownSelect>` would resolve to both modes,
  and kleros-v2 builds its LabeledDropdown props from that type.

Selected rows keep the single-selection style, a left border rather
than a check, as the design system describes. A single-selection story
covers react-aria's `value` and `onChange`, which the upgrade enables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
react-aria-components 1.20 declares Select's selectedKey,
onSelectionChange, isOpen and other props in react-stately's types,
which the docgen filter dropped. The new GlobalDOMEvents and
GlobalDOMAttributes added about 70 DOM props to every table. Allow
react-stately and leave the global DOM props out, so each component
keeps the props it documented and gains only its new ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since react-aria 1.15, a field that react-aria focuses after a clicked
submit fails validation doesn't show its focus ring, because useFocusRing
subscribes to the modality change too late (react-spectrum#10751). In
this library only RadioGroup draws its ring from that state, as the
RequiredOptions story's final screenshot shows. Submitting with the
keyboard is unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kemuru kemuru changed the title feat: BigNumberField onValueChange and value={null} (#97); fix read-only steppers (#98), CustomRadio arrow keys (#109), stray CSS utilities (#110) feat: BigNumberField onValueChange and value={null} (#97), DropdownSelect multiple selection (#106), react-aria-components 1.20.0; fix read-only steppers (#98), dropdown trigger overflow (#105), CustomRadio arrow keys (#109), stray CSS utilities (#110) Oct 23, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes Oct 23, 2026
kemuru and others added 3 commits October 23, 2026 17:52
- A key pressed on the closed trigger (an arrow or typeahead) that
  picked an item left a required field's error in place, and a second
  submit went through with the error still shown. react-aria commits
  validation for picks in the list but not for those keys. 3.10.0
  committed for every change. Both dropdowns now commit from their
  onChange, reaching react-aria's state through a small bridge inside
  the Select. The cascader's Select button no longer needs its own
  commit.
- The simple button stretches across its column, so the list's
  min-width followed it there. A 360px mobile column gave a 352px
  list. The list matches the trigger width only for the bordered button.
- The multiple-selection overload omits selectedKey,
  defaultSelectedKey and onSelectionChange, which react-aria ignores
  in that mode. Adding selectionMode="multiple" to an existing select
  now flags its defaultSelectedKey instead of silently dropping it.
- The overloads are ordered single, multiple, single. The docs read
  the first and ComponentProps the last, so both show single selection.
  The Select docs page had shown the multiple-selection types.
- The changelog scopes the ellipsis to the bordered button, and notes
  the order callback receives items in.

A story covers the keyboard pick, SimpleSelect checks its list stays
240px, and a type test locks the overloads. Each fails on 046a2ff.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The nested react-aria Modal got every prop the overlay got, apart from
the open state. react-aria 1.20 passes DOM events on to the element,
so a caller's onClick fired twice for a click inside the modal, and an
id landed on two elements. The nested Modal needs none of these props.
Kleros apps pass only className, isOpen, isDismissable and
onOpenChange.

The changelog lists the components that now call a caller's onClick,
which react-aria 1.7.1 dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The library bundles 1.20.0, but its .d.ts files take their types from
the app's react-aria-components, and ^1.20.0 resolves to 1.22.1 on a
fresh install. Apps then compile against props the bundled code
ignores, such as Modal's onEnter and onExit. ~1.20.0 gives them the
types of the version they run.

The changelog also scopes the label-click note. Only
@testing-library/user-event before 14.6, which Storybook 8's
@storybook/test bundles, sends the 0x0 pointer events react-aria
ignores. Apps on 14.6 are unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Oct 23, 2026
The value label and the captions under the track were react-aria Labels with fixed ids, so
every input on a page was named after the first slider's value label and value ("400 $M 400"
for two sliders labelled "A" and "B"), and an aria-label had no effect.

- The value label and the captions are plain <label> elements that label nothing, so apps'
  label styles still apply. The captions lose aria-label="min-label" and "max-label".
- With aria-labelledby, an empty hidden Label takes the group's place in react-aria's
  labelling, and the thumb gets aria-labelledby and aria-label. Browsers read the group's text
  rather than its aria-labelledby, so the input is otherwise named after the captions.
- The input announces the formatter's text as aria-valuetext, written after each commit since
  react-aria has no prop for it. The value label is aria-hidden.
- Each part gets a data-slot named like its id. The ids stay: foresight and futarchy-ui style
  through them.

Closes #111

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kemuru kemuru changed the title feat: BigNumberField onValueChange and value={null} (#97), DropdownSelect multiple selection (#106), react-aria-components 1.20.0; fix read-only steppers (#98), dropdown trigger overflow (#105), CustomRadio arrow keys (#109), stray CSS utilities (#110) feat: BigNumberField onValueChange and value={null} (#97), DropdownSelect multiple selection (#106), react-aria-components 1.20.0; fix read-only steppers (#98), dropdown trigger overflow (#105), CustomRadio arrow keys (#109), stray CSS utilities (#110), Slider input names (#111) Oct 26, 2026
kemuru and others added 6 commits October 26, 2026 12:52
GitHub's ubuntu-latest image is now Ubuntu 26.04, and Playwright 1.56.1 can't install Chromium
there ("Playwright does not support chromium on ubuntu26.04-x64"), so the job failed before
any test ran. Ubuntu 26.04 support arrived in Playwright 1.61. Pin the job to the image of its
last green run until Playwright is upgraded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… top

react-aria 1.20 makes the page behind an open Modal or popover inert, including elements added
while it is open, and ends a click outside a dismissable Modal on `click`, which it stops. A wallet
modal opened from a library Modal, as the mobile menus of futarchy-ui, governor-v2 and foresight
do, and toasts therefore ignored taps, in Chromium, Firefox and WebKit.

A Yarn patch on the bundled react-aria 3.51.0 restores 1.7.1's behaviour: aria-hidden only, and
the outside interaction ends on pointerup, so the click still reaches what it lands on. The Modal
story checks aria-hidden again, as it did on main, and a browser test fails if the patch stops
applying, for example after a react-aria bump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by the audit of the whole PR, each reproduced against 3.10.0:
- A required dropdown showed its error after a form reset that followed an awaited submit: the
  reset sets the value, and the dropdowns committed validation on every change. They skip that
  commit during a reset and leave validation to react-aria.
- A single selection keeps ignoring defaultValue, as react-aria 1.7.1 did. curate-v2's chain field
  passes one and writes its form value only from callback, so it showed Ethereum Mainnet before
  any pick, and picking it again reported nothing: the step's Next button stayed disabled.
- One long word widened the list's implicit grid column past the 240px list, clipping every
  option. The list has one column now, and the LongText story checks each option's right edge
  (its old check compared an option with itself).
- An icon passed as an element shrank beside long text; every child but the text keeps its size.
- The state bridge uses the isomorphic layout effect, so server rendering logs no warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
react-aria 1.20's Select lets ArrowDown and ArrowUp reach ancestors, so ArrowDown on a
DropdownSelect nested in CustomRadio, as the docs recommend for conditional fields, moved the
group's selection and unmounted the dropdown. A nested native select had the same problem on
3.10.0. The guard now stops arrow keys from any target that isn't one of the group's radios;
radio-to-radio navigation and BigNumberField stepping are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The value-text bridge uses the library's isomorphic layout effect, like useElementSize, so React
18's server renderer logs no warning. A test renders the Slider and both dropdowns on the server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The react-aria 1.20 entry now describes the patch, says Enter submits only from radios now, as
Checkbox and Switch already did, and covers DOM events, scrollbar-gutter and defaultValue in a
single selection. The BigNumberField entries and the value prop's JSDoc gain the caveats about late
echoes and resetting a lone "-", "." or "-.", and #108's wording now says only the idle formatting
skipped values. CustomRadio and the dropdown icons are updated, and a Deprecated section lists the
Slider's fixed ids.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kemuru kemuru changed the title feat: BigNumberField onValueChange and value={null} (#97), DropdownSelect multiple selection (#106), react-aria-components 1.20.0; fix read-only steppers (#98), dropdown trigger overflow (#105), CustomRadio arrow keys (#109), stray CSS utilities (#110), Slider input names (#111) feat: BigNumberField onValueChange and value={null} (#97), DropdownSelect multiple selection (#106), react-aria-components 1.20.0; fix read-only steppers (related to #98), dropdown trigger overflow (#105), CustomRadio arrow keys (#109), stray CSS utilities (#110), Slider input names (#111) Oct 26, 2026
@tractorss

Copy link
Copy Markdown
Contributor

Review of f553238: a quick pass, a UI pass (Storybook in Chromium at 375 and 1280 px, light and dark, axe) and a standard code pass. Every item below was reproduced; the probe output is in each Proof line.

What already checks out: unit (208), stories (314) and native (26) suites pass, the react-aria patch is bundled into dist, and Slider names, CustomRadio arrow keys, read-only steppers and Modal drag-to-dismiss all behave as described.

1. Introduced by this PR

Medium

Closing a library overlay clears an inert the app set itself
.yarn/patches/react-aria-npm-3.51.0-7c39ac129d.patch:105
The patch turns off the hide side of inert, but the unhide path (setHidden(el, false)) still runs element.inert = false. 3.10.0 never wrote inert.

  • Proof: while open: inert true aria-hidden true → after close: inert false aria-hidden null
  • Fix: drop that line, or guard it with shouldUseInert, in all three hunks.

onValueChange runs inside the parse try
src/lib/form/bignumber-field/useBigNumberField.tsx:490
new BigNumber() never throws, so the catch only catches errors thrown by the app's own handlers. Those are logged as "Error parsing input as BigNumber", onValueChange is skipped when onChange throws, and blur erases the value.

  • Proof: thrown: null | console.error: ['Error parsing input as BigNumber: Error: consumer bug'] | onValueChange calls: 0, then after type: "12" → after blur: ""
  • Fix: keep only the parse inside the try, and call emitChange after it.

The .5 → 5 fix doesn't hold when the parent re-renders
src/lib/form/bignumber-field/useBigNumberField.tsx:195
The value sync is keyed on object identity. With value={new BigNumber(amount)}, any unrelated parent re-render after typing . wipes it. Typing - has the same problem: −5 becomes 125.

  • Proof: afterRerender="" parent=5 and afterRerender="12" parent=125
  • Fix: depend on value?.toString() (or a last-applied ref), and add the re-render case to the FreshBigNumber test.

Blur clears . / -. without telling the parent
src/lib/form/bignumber-field/useBigNumberField.tsx:524
The field shows empty but the last onChange was NaN. Backspacing to empty reports 0. At base the field showed "NaN", so the mismatch is new.

  • Proof: "." text="" lastOnChange=NaN vs control text="" lastOnChange=0
  • Fix: call emitChange(null) in that branch.

Multi-select: keyboard focus is invisible among selected rows
src/lib/dropdown/select/item.tsx:32
focus: and selected: both use bg-klerosUIComponentsMediumBlue, so the focused row looks like every other selected row (WCAG 2.4.7).

  • Proof: focused hello 3 and unfocused hello 4 both compute bg rgb(240, 249, 255), border-left 3px
  • Fix: give focus its own outline or inset ring.

Single-mode form reset never reaches callback
src/lib/dropdown/select/index.tsx:85
react-aria 1.20 sends a null key on reset, and it is dropped. Multiple mode calls callback([]).

  • Proof: single callback: [1], new after reset: 0 vs multiple callback: [[1],[]]

Single-mode defaultValue type-checks but is ignored
src/lib/dropdown/select/index.tsx:113, src/lib/dropdown/cascader/index.tsx:50
The 1.7.1 types rejected it (TS2353). The 1.20 types accept it, but it's still dropped at runtime.

  • Proof: defaultValue=2 → "Pick", value=2 → "two"
  • Fix: Omit it on the single-mode overload, or support it.

Low

  • Multi mode doesn't look like multi-select (select/item.tsx:32). Rows reuse the single-select style, with no check or checkbox.
  • Closed multi trigger hides how many are picked (select/dropdown-button.tsx:28-31). There's no count and no title, and the newest pick is the part the ellipsis cuts. Proof: "hello 2, hello 4, and hello 1", clientWidth 169 < scrollWidth 190, title null.
  • Pick order is untested (select/index.tsx:101). The JSDoc promises pick order, but a mutant returning list order still passes dropdown.native.test.tsx.
  • The CHANGELOG says "Breaking changes: None", but each of these can break an app on a minor bump:
    • re-picking the selected item no longer fires callback;
    • .container and other utilities left the shipped CSS;
    • Modal style/id/data-* now reach only the overlay;
    • [role="radiogroup"] > label no longer matches;
    • Enter on a radio now submits the form.
  • Stale comment in the patch. It keeps upstream's "Use click instead of pointerup to avoid Android Chrome issue" above the reverted line. A note that this trade-off is deliberate would help.
  • Slider props don't require a label. Neither aria-label nor aria-labelledby is required, so an unlabelled Slider is now nameless and react-aria warns on every render.
  • No exported type for multiple-mode props. A union-typed selectionMode fails with TS2769.

@tractorss

Copy link
Copy Markdown
Contributor

Second part of the review of f553238: things that were already there, but in code this PR rewrote, so they're cheap to fix here.

2. Already there, but in code this PR rewrote (cheap to fix here)

Medium

Required field accepts a lone -
src/lib/form/bignumber-field/useBigNumberField.tsx:528
The new required-on-blur check treats - as filled, while onValueChange reported null.

  • Proof: text "-", ariaInvalid "false", valid true, onValueChange [null]
  • Fix: clear - on blur, the same way . and -. are cleared.

Cmd+A / C / V / X blocked on macOS
src/lib/form/bignumber-field/useBigNumberField.tsx:554
The keydown allow-list only lets Ctrl shortcuts through.

  • Proof: Meta+a is not prevented … expected false to be true (Ctrl passes)
  • Fix: e.metaKey || e.ctrlKey.

Slider thumb has no focus-visible style
src/lib/form/slider.tsx:127
With two sliders on a page, you can't see which one has keyboard focus.

  • Proof: focused vs unfocused screenshots report no visual difference

Low

  • simpleButton trigger has no overflow handling (select/simple-button.tsx:48). It got buttonClassName but not the overflow fix. In a 160px column it wraps to 8 lines and the arrow shrinks to about 2.7px.
  • Cascader confirm button is clipped at 375px (cascader/dropdown-container.tsx:98). "Select " is 464px wide inside the 240px popover.
  • Read-only NumberField still shows the hover fill (number-field.tsx:78). It keeps the !important hover fill, so it looks editable. Read-only BigNumberField drops it.
  • handleChange has two duplicate bodies (useBigNumberField.tsx:404). Both copies are edited identically here. Merging them is −92/+23 lines, with all 208 unit tests green.
  • Slider ids stay static (slider-track, slider-label, …), so they repeat when two sliders render.

Older issues and out-of-scope items are filed separately and don't block this PR: #112, #113, #114, #115, #116, #117, #118, #119.

kemuru and others added 6 commits October 28, 2026 16:32
The patch stopped react-aria from making the page behind an overlay inert, but its unhide path
still set `inert = false`. Closing a library Modal or DropdownSelect therefore cleared an `inert`
that the app had set itself, such as on its root while its own drawer is open. react-aria 1.7.1
never wrote `inert`. The line is gone from the three builds, and a native test checks both
overlays. The comment above the pointerup listener now says that giving up upstream's Android
Chrome fix is deliberate.

Raised in the review of f553238.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ortcuts

Raised in the review of f553238, each reproduced:
- Blur empties a `-`, `.` or `-.` and reports it as an emptied input: onChange gets 0 and
  onValueChange null. Before, blur emptied `.` and `-.` while the parent's last onChange value
  was NaN, and a lone `-` stayed and passed a required field. The number a parent answers that
  report with, such as an echoed 0, is validated, since blur validated the empty text.
- A `-`, `.` or `-.` survives a re-render of a parent that builds `value={new BigNumber(amount)}`
  while that value holds no number. Before, any re-render emptied it, so `.5` gave 5. A number
  given again still replaces the text, as on 3.10.0, since it can be a reset such as a Max
  button.
- Only the parse is inside handleChange's `try`, and its two copies are merged. When onChange
  throws while the user types, the field keeps the number, still calls onValueChange and logs
  the error as an onChange error. It is still logged rather than thrown, as on 3.10.0:
  futarchy-ui's onChange runs viem's parseUnits, which throws for `.`.
- Cmd+A, Cmd+C, Cmd+V and Cmd+X work on macOS; only their Ctrl versions did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ton's arrow

Raised in the review of f553238:
- In a multiple selection, the row with keyboard focus looked like every other selected row. It
  gets DraggableList's focus outline; a single selection, with one selected row, is unchanged.
- Beside long text, the simple button's arrow shrank to a few pixels. It keeps its size, and the
  text still wraps, since the simple button has no fixed width.
- `defaultValue` is documented as ignored by a single selection, as it always has been, though
  its type accepts it. Removing it from the type would break curate-v2, which passes one.
- A native test checks that `callback` gets the items in the order they were picked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A read-only NumberField filled its background on hover, so it looked editable. As in
BigNumberField, a field that can't be edited no longer reacts to hover. Raised in the review of
f553238.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With two sliders on a page, nothing showed which one had keyboard focus. The thumb now glows like
a focused input, in blue on themed sliders too, as the design system keeps focus blue. Raised in
the review of f553238.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Breaking changes: None" now reads "None in the API", followed by each behaviour change that can
affect an app on this minor release, including blur's new report and the Modal props, which had
no entry. The fixes from the review of f553238 get their entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

2 participants