Skip to content

Form control roundout - #50

Merged
microbit-matt-hillsdon merged 11 commits into
mainfrom
form-control-roundout
Aug 6, 2026
Merged

Form control roundout#50
microbit-matt-hillsdon merged 11 commits into
mainfrom
form-control-roundout

Conversation

@microbit-matt-hillsdon

Copy link
Copy Markdown
Contributor

No description provided.

- Remove select's label slot; Select and ComboBox render labels from the
  field recipe like every other field (the two had drifted).
- Fix the dead disabled-label rule: RAC never marks the Label, so
  "&[data-disabled]" matched nothing; now "[data-disabled] > &" plus
  FormLabel's opacity transition (playbook gotcha #45).
- Export FieldLabel (isRequired, css override, id/htmlFor passthrough)
  and render all six fields through it.
- Disabled story on Forms/Field chrome and an it.each asserting every
  field's label carries field().label.
Chakra FormLabel's `medium` never rendered: no font in the family's
stack has a 500 face, so macOS/Windows drew 400 regardless, and the only
call sites that cared (settings rows) overrode to `normal` — as their
Chakra originals did. The default moves, the overrides go at the next
release. Visible only on platforms whose sans-serif has a real Medium
(Roboto/Noto) or under a future webfont brand preset; accepted without a
device check.

Playbook: expected-deltas entry, and the field-chrome roadmap bullet
updated to the decisions in docs/form-controls.md (it predated them and
argued for keeping `medium`).
Decision record and implementation plan from the 2026-08-05/06 sessions:
size variants with label auto-follow, labelPosition="side" on the four
single-control fields, NativeSelectField alongside the bare export,
context-free FieldHelperText/FieldErrorMessage, and the app-PR list.
The missing half of the size scale (docs/form-controls.md):

- NumberField takes `size` (input recipe), as TextField does. The
  stepper column stays 24px at every size, as Chakra's did; only the
  arrow glyphs scale (0.75 x the field's font size).
- The select recipe's trigger gets the input recipe's size ladder, step
  for step, so a Select sits level with a TextField at every size. Its
  hard-coded h/px move into the variants; fontSize md at the default is
  new but a no-op at every call site (non-dense apps inherit 1rem, and
  classroom's `classroom` variant sets its own and wins ties — verified
  against the generated CSS order).
- The field recipe gets a size variant driving label fontSize, threaded
  from the same prop, so one `size` scales the whole row. FieldLabel
  takes `size` for the composites. Helper/error text stays sm, as
  Chakra's did.
- staticCss entries for field and numberField now they have variants.
- Sizes story on Forms/Field chrome; tests that size reaches the label
  on all four fields and the input on NumberField.
The settings-row pattern as a field variant (docs/form-controls.md): the
label absorbs the free space, the control keeps its own width (give it
one via groupCss/wrapperCss/css), and helper/error text drops to a
full-width line below the pair. Named after React Spectrum's prop —
`orientation` is RAC RadioGroup's radio-layout axis, which is also why
the group fields don't take it.

The field recipe's root becomes the single owner of field-root layout:
Select, ComboBox and NumberField wear it alongside their own root slot,
which now carries none — two recipes fighting over flexDirection would
leave the variant at the mercy of emission order. NumberField's root
gains width:100% from it, matching the other fields (its one call site
already set that by hand).

SideLabels story; tests that the variant reaches root, label and helper
on all four fields. Both new stories screenshot-checked.
NativeSelectField pairs the bare NativeSelect with the field chrome, as
TextField pairs with Input: label association, aria-describedby and the
root's data-disabled wired by hand, since there is no RAC context to do
it. With labelPosition="side" it is the settings row python-editor's and
ml-trainer's SelectFormControls each hand-rolled (docs/form-controls.md);
the bare export stays for the six control-only call sites.

FieldHelperText/FieldErrorMessage are the non-RAC escape hatch decided
over documenting the recipe's slot classes: plain styled elements, the
caller owning the wiring RAC would otherwise do (aria-describedby, when
the error shows). No errorMessage on NativeSelectField yet - no consumer
needs one.

NativeSelectField joins the Field chrome stories (native
required/disabled props there); tests cover the hand-wiring piece by
piece.
The plan is done to the release boundary, so the working doc goes; what
still matters lives in the playbook's field-chrome roadmap bullet,
recast around what remains: the decisions taken (weight, size follows,
labelPosition, NativeSelectField beside the bare export, context-free
helper/error) and the app-PR checklist (python-editor's dialog aligns
on md, both SelectFormControls deleted, the data-microbit-org
adoptions). Source comments now point at the playbook.
Five unrelated fields crammed on one row clipped and wrapped; now it is
one small form per column at sm/md/lg, so the scale is compared like
for like. The TextField/Select pair keeps the cross-recipe level check
as the first row.
The tier-2 gap: standalone Checkbox and Switch had no helper-text
story, so both settings dialogs hand-rolled a Text underneath (never
reaching aria-describedby, and drifting to gray.700 against the
chrome's gray.600), and ml-trainer bent Switch into a settings row by
reaching into .switch__label. Now:

- helperText on Checkbox and Switch, wired to aria-describedby and
  rendered by FieldHelperText; the component gains a wrapping div.
- labelPosition="start" on Switch: label first, switch at the row's
  end. Values are start/end, not the field's top/side - a toggle's
  label is already beside it. The label keeps an end margin (the
  SelectFormControl translation lesson).
- Radio stays helper-less (always grouped); Slider/Textarea parked
  deliberately - no consumer exists.

Naming, while breaking is cheap (owner-approved):
- css no longer means a different element per component: Select and
  ComboBox css -> triggerCss (pairs with contentCss); NumberField css
  -> rootCss (pairs with NativeSelectField). Bare controls keep css
  for their root, which is the control there. TS-guided rename;
  classroom's four Select/ComboBox sites are the ones outside the
  queued app PRs.
- Files follow their contents: Field.recipe.ts (the field recipe was
  in TextField.recipe.ts), Field.tsx (FieldSupport.tsx held the whole
  chrome family), RadioGroup.tsx split out to mirror CheckboxGroup.
  switchRecipe keeps its name - switch is a reserved word.

Stories: standalone toggles join HelperText, the analytics Switch row
joins SideLabels; both screenshot-checked. Tests cover the toggle
aria-describedby wiring and the start-position classes.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 6, 2026

Copy link
Copy Markdown

Deploying ui with  Cloudflare Pages  Cloudflare Pages

Latest commit: cfb2b2a
Status: ✅  Deploy successful!
Preview URL: https://74a52971.ui-2wg.pages.dev
Branch Preview URL: https://form-control-roundout.ui-2wg.pages.dev

View logs

@microbit-matt-hillsdon
microbit-matt-hillsdon merged commit f4da301 into main Aug 6, 2026
2 checks passed
@microbit-matt-hillsdon
microbit-matt-hillsdon deleted the form-control-roundout branch August 6, 2026 09:37
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