Extract a shared input-state module behind the control recipes (#60)
* Extract a shared input-state module behind the control recipes
text-input.css.ts and combobox.css.ts each maintained their own copy of
the disabled / hover / focus-within / invalid / read-only selector
constants and state treatments, and the copies had drifted: different
focus-ring tokens, forced-colors support on only one of them. The new
recipes/input-states.css.ts owns the state definitions and the full
control chrome; both recipes consume it, and the combobox extends state
detection explicitly for its input+trigger anatomy and keeps its visible
disabled border as a deliberate override.
Deliberate behaviour changes:
- The combobox focus ring now uses the focusRingColor token (was
paletteThemePrimary200, one shade lighter).
- The combobox control gains the forced-colors treatments the text input
already had.
- Read-only detection is scoped to :has(input:read-only). Bare
:read-only matches any non-editable element, so every combobox
control (via its trigger button) and every adorned text input group
rendered with the read-only background unconditionally.
- Read-only styling no longer applies to disabled controls (a disabled
input also matches :read-only; the two backgrounds happen to share a
value today, so this pins semantics rather than changing pixels).
- The combobox focus-within state drops [data-focused="true"]: RAC
Group never emits data-focused, so the selector was dead.
The browser vitest project now loads vanillaExtractPlugin (matching the
unit and storybook projects) so the new regression test can assert
computed styles against the compiled recipes in real Chromium.
* Align the combobox item focus ring with the control ring
comboboxItem's focus-visible outline was the last focus ring still
hardcoded to paletteThemePrimary200; the global reset and the shared
input chrome both use the focusRingColor token, so a focused listbox
item rendered one shade lighter than the focused control above it.
* Add combobox selection indicators and rework the option focus treatment
The keyboard-active listbox option is now indicated by a background one
step stronger than hover (neutralPressed) instead of a focus ring — DOM
focus stays on the input under aria-activedescendant, so the control
shows the only ring. Forced-colors mode keeps a 2px inset ring on the
active option, since background highlights are stripped there.
Selection surfaces, following the Park UI shape:
- The selected option shows a trailing check icon in the listbox.
ComboboxItem re-derives textValue for string children because the
checkmark wrapper is a render function, which disables RAC's own
inference.
- New ComboboxClearButton primitive (exported from the kit) clears the
selection and input text via the modern ComboBoxState value API
(state.value / setValue — selectedKey/setSelectedKey are deprecated).
It renders nothing while there is no value, and opts out of the RAC
button slot so it doesn't toggle the popover. The composed
ComboboxField shows it before the trigger unless disabled/read-only.
- The trigger's non-first-child background is transparent instead of
subtle, so the chevron box no longer reads darker than the input.
Icon sizing moved behind the seam that owns it: COMBOBOX_ICON_SIZE in
sizing/ (deliberately distinct from BUTTON_ICON_SIZE), provided by the
trigger, clear button, and item via IconSizeProvider — callers no
longer pass icon sizes, and consumer icons inside those parts inherit
the right size.
Extract a shared input-state module behind the control recipes (#60)
* Extract a shared input-state module behind the control recipes
text-input.css.ts and combobox.css.ts each maintained their own copy of
the disabled / hover / focus-within / invalid / read-only selector
constants and state treatments, and the copies had drifted: different
focus-ring tokens, forced-colors support on only one of them. The new
recipes/input-states.css.ts owns the state definitions and the full
control chrome; both recipes consume it, and the combobox extends state
detection explicitly for its input+trigger anatomy and keeps its visible
disabled border as a deliberate override.
Deliberate behaviour changes:
- The combobox focus ring now uses the focusRingColor token (was
paletteThemePrimary200, one shade lighter).
- The combobox control gains the forced-colors treatments the text input
already had.
- Read-only detection is scoped to :has(input:read-only). Bare
:read-only matches any non-editable element, so every combobox
control (via its trigger button) and every adorned text input group
rendered with the read-only background unconditionally.
- Read-only styling no longer applies to disabled controls (a disabled
input also matches :read-only; the two backgrounds happen to share a
value today, so this pins semantics rather than changing pixels).
- The combobox focus-within state drops [data-focused="true"]: RAC
Group never emits data-focused, so the selector was dead.
The browser vitest project now loads vanillaExtractPlugin (matching the
unit and storybook projects) so the new regression test can assert
computed styles against the compiled recipes in real Chromium.
* Align the combobox item focus ring with the control ring
comboboxItem's focus-visible outline was the last focus ring still
hardcoded to paletteThemePrimary200; the global reset and the shared
input chrome both use the focusRingColor token, so a focused listbox
item rendered one shade lighter than the focused control above it.
* Add combobox selection indicators and rework the option focus treatment
The keyboard-active listbox option is now indicated by a background one
step stronger than hover (neutralPressed) instead of a focus ring — DOM
focus stays on the input under aria-activedescendant, so the control
shows the only ring. Forced-colors mode keeps a 2px inset ring on the
active option, since background highlights are stripped there.
Selection surfaces, following the Park UI shape:
- The selected option shows a trailing check icon in the listbox.
ComboboxItem re-derives textValue for string children because the
checkmark wrapper is a render function, which disables RAC's own
inference.
- New ComboboxClearButton primitive (exported from the kit) clears the
selection and input text via the modern ComboBoxState value API
(state.value / setValue — selectedKey/setSelectedKey are deprecated).
It renders nothing while there is no value, and opts out of the RAC
button slot so it doesn't toggle the popover. The composed
ComboboxField shows it before the trigger unless disabled/read-only.
- The trigger's non-first-child background is transparent instead of
subtle, so the chevron box no longer reads darker than the input.
Icon sizing moved behind the seam that owns it: COMBOBOX_ICON_SIZE in
sizing/ (deliberately distinct from BUTTON_ICON_SIZE), provided by the
trigger, clear button, and item via IconSizeProvider — callers no
longer pass icon sizes, and consumer icons inside those parts inherit
the right size.