Skip to content

feat(react,ui): PretableTextInput and PretableCheckbox — the fourteen fields and checkboxes on the kit (components SP3) - #585

Merged
blove merged 20 commits into
mainfrom
blove/components-sp3-inputs
Sep 8, 2026
Merged

feat(react,ui): PretableTextInput and PretableCheckbox — the fourteen fields and checkboxes on the kit (components SP3)#585
blove merged 20 commits into
mainfrom
blove/components-sp3-inputs

Conversation

@blove

@blove blove commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Components SP3, the third slice of the kit: the grid's plain fields and its
checkboxes stop being ad-hoc markup and become two components a consumer can
replace, on the contract SP1 wrote and SP2 already follows.

What ships

PretableTextInput — an <input> carrying data-pretable-text-input
and PretableCheckbox — a button[role="checkbox"] carrying
data-pretable-checkbox and aria-checked — with their props types, their
…Component aliases, and TextInput / Checkbox slots on
PretableComponents. components={{ TextInput, Checkbox }} replaces them
everywhere, the same way Button, IconButton and Select already do.

Fourteen controls migrate. Seven text fields: the filter dialog's value and
its two range bounds, the tool panel's three filter-row values, and the Columns
search box. Seven checkboxes: row select, header select-all, the boolean cell,
the column visibility toggle, "hide grouped columns", and the two enum
checklists. Three of those checkboxes were <input type="checkbox"> and are now
buttons; four already were buttons and simply stopped hand-rolling the markup.

The site vocabulary is renamed. PretableButtonSitePretableSite,
PretableBuiltInButtonSitePretableBuiltInSite: it names where any kit
control sits, not where a button does. The union gains ten names —
filter-value, filter-row-value, tool-search, row-select,
row-select-all, bool-cell, tool-column-toggle, hide-grouped,
filter-choice, filter-row-choice. Ten names cover fourteen controls because
the range twins share their sibling's site: the theme keys on the site, and a
from/to pair is one thing to style.

CSS collapses onto the kit contract. grid.css loses the per-site field and
checkbox rules and gains one [data-pretable-text-input] block and one
[data-pretable-checkbox] block, with the existing :where() and cascade-order
guards extended to cover them.

Docs. The Components page documents both, and the option-attribute rename
follows through the docs and the e2e drivers.

Measurement

A Playwright probe reads the computed style of all fourteen controls at rest,
checked, and focused, before and after, and diffs every property. Every
difference below is deliberate; the probe's "finding" class — a change nobody
asked for — came back empty.

Site Box before → after Differing props
filter-value / filter-min / filter-max 218×28 → 218×28 0 of 35
filter-row-value 211×24 → 211×24 0 of 35
filter-row-min 239×24 → 239×24 0 of 35
filter-row-max 211×24 → 211×24 0 of 35
tool-search 247×33 → 247×33 2 (focus ring only)
row-select 16×16 → 16×16 0 of 65
bool-cell 16×16 → 16×16 0 of 65
row-select-all 16×16 → 16×16 2 (focus ring only)
tool-column-toggle 16×16 → 16×16 4 (focus ring only)
filter-choice 13×13 → 16×16 37 — by design
filter-row-choice 13×13 → 16×16 35 — by design
hide-grouped 13×13 → 16×16 36 — by design

The six text fields report zero differing properties: same box, same type,
same spacing, same ring. row-select and bool-cell are byte-identical across
all 65 measured properties — bool-cell keeps its margin: 0 auto centring,
which resolves to 0px top/bottom and 15.5px left/right on both sides.

The three ex-native checkboxes go 13×13 → 16×16 on purpose: they take the kit's
16px square and the --pretable-checkbox-* tokens in place of the browser's
~13px default. Their wrapping label boxes are unchanged — 218×21 and 239×21
exactly, with hide-grouped at 168×24 against 165×24, +3px wide and +0 tall.

Focus rings. The six fields keep rgb(37, 84, 207) solid 2px @ -2px
untouched. tool-search moves off the user agent's rgb(0, 95, 204) auto 1px @ 0px onto that same product ring. row-select-all and tool-column-toggle move
off the UA ring onto the product ring at +1px; the three ex-native checkboxes
end there too (filter-choice's offset moves from -2px, and the other two
already sat at +1px). A 16px square paints to its own edge, so an inset ring
would draw on the border — the ring sits outside it instead.

Guards, each mutation-checked

  • The site sweep now walks all fourteen controls, asserting each renders the
    kit element and carries its own data-pretable-site — and that a consumer
    override removes the kit attribute from the whole tree, portal included.
  • Slot-resolution guards for both components: identity, memo stability, and the
    four-part edit (comparison, defaults, literal, deps) SP1's review flagged.
  • "No element-type selector targets a kit control" — the rule that stops
    input[type=checkbox] creeping back into grid.css.
  • Cascade order: the checkbox's ring-offset rule must precede the shared ring.
    Both are :where()-flattened to (0,0,0), so order is the whole of what
    makes it win
    ; above the shared rule it is silently undone and every checkbox
    rings inside its own box again.
  • The disabled checkbox rule declares no color. On a checkbox color is
    the check glyph, not label ink: --pretable-text-dim on the checked fill is
    about 1.9:1, and a read-only boolean column renders disabled on every row — so
    that would be the entire column's ticks.
  • Attribute-namespace guard extended to the kit fields and both checklists.

Consumer-visible DOM

.checked, .check() and input[type=checkbox] must move to aria-checked on
the three ex-native checkboxes (toBeChecked() still works — it reads ARIA).
The two checklists gain data-pretable-filter-choice /
data-pretable-filter-row-choice plus data-pretable-option-value per choice.
The builder's checklist wrapper is now data-pretable-filter-row-set; the
field beside it keeps data-pretable-filter-row-value, which the wrapper used
to borrow. The listbox option's data-value is now
data-pretable-option-value — which option an element is, as against the
select trigger's data-pretable-value, which is what that picker has committed.
Full detail in the changeset.

Verification

All from a clean tree on this branch:

Gate Result
format / lint / typecheck / build / api / api:check / typecheck:public exit 0
pnpm api left git status clean — report already current
pnpm test exit 0, green first pass, no flakes (packages/react 136 files / 1868 tests)
pnpm bench:e2e exit 0, 29/29
website e2e, both projects, --workers=1 against next build + next start exit 0, 352 passed, 4 skipped

The 4 skips are the dev-build-only StrictMode suite, which cannot run against a
production next start.

Review provenance

Every task in the plan ran spec review and quality review with fix rounds before
its commit; the fix rounds are visible as their own commits (the disabled-tick
contrast catch, the slot-ref docs correction, the option-identity rename). The
final whole-branch review's findings are addressed in 0df8e5d6.

🤖 Generated with Claude Code

blove and others added 19 commits September 7, 2026 08:06
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e input and checkbox sites

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The kit's checkbox, one component for the model the row-select cell, the
column toggle and the boolean cell already drew by hand: a
`button[role="checkbox"]` carrying `data-pretable-checkbox` and
`aria-checked`, with the grid's CheckIcon / MinusIcon as the glyph. A
button rather than a styled native input because mixed state, the roving
tabindex in body cells and a themed glyph are all plain on a button and a
fight on an `<input>`.

The consumer's `onClick` runs first and may `preventDefault()` to veto the
toggle — how a shift-click range select keeps its click without a second
write. Keyboard is the native button's, so no key handler.

A name can come from a wrapping `<label>`, which props cannot see, so
`hasAccessibleName` reads the DOM once after mount and warns when nothing
names the control.

Three mutation checks, each restored:
- removing the `defaultPrevented` guard fails the veto test
- `checked !== true` → `!checked` fails the mixed→true assertion
- `hasAccessibleName` ignoring `closest("label")` fails the warning
  test's label case

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The kit's text field is the native `<input>` and nothing more. A wrapper
would have to reinterpret `type`, and with it goes the platform's date
picker, `inputMode`'s soft keyboard and the autofill the native element
gets for free. What a theme needs is only the box, which grid.css draws
through `data-pretable-text-input`; `site` lands as `data-pretable-site`
after the spread, and a site's own attribute still arrives through it.

No wrapper, no clear button, no debounce: the three chrome fields that
will use it each own their value semantics, and one owner for all three
would be wrong for two.

The name check is `PretableCheckbox`'s — a merged callback ref so the
component can read its own node, and one post-mount `hasAccessibleName`
call, because a `<label for>` is a fact about the tree and not the props.
That branch is what the new test's label-for case covers; the checkbox
suite only exercises the wrapping label.

Mutation-checked: dropping the `label[for]` branch from
`hasAccessibleName` makes the label-for case warn and the test fail.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…r covered

Every glyph in the set carries the same `data-pretable-icon` hook, so
asserting its presence proved only that some icon rendered — the tick and
the minus were interchangeable and the test would not have noticed. It
now reads the drawn `<path d>` and anchors each state against the icon
rendered on its own, plus asserts the two differ.

Mutation-checked: swapping CheckIcon and MinusIcon in checkbox.tsx fails
the test (expected 'M4 8h8' to be 'M3.5 8.5 6.5 11.5 12.5 4.5'); before
this change it passed.

Also adds a `<label for>` case to the warning test, the naming branch the
wrapping-label case does not reach.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Mirrors SP2's Select slot (f10cbe0) for two more kit components:
components.TextInput and components.Checkbox now resolve through the
same context, with member JSDoc on PretableComponents and the two new
Pretable*Component types.

Two mutation checks confirm the four-part edit is complete: dropping
Checkbox from useResolvedComponents' deps array made its rerender test
fail (stale hit), and dropping TextInput from the identity comparison
made its replacement test fail too. Both were reverted after
confirming.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s fourteen controls

The field box the three text fields each drew for themselves, and the 16px
square four checkbox sites hand-kept a copy of, become two kit rules at the
top of the layer — beside the push-button and select rules, ahead of every
site rule so a site's size still wins at equal specificity. Both join the
state section's ring and disabled list, and the checked checkbox joins the
forced-colours block: its fill and its glyph colour are both author values
the UA erases, which between them are the whole of what the control says.

The old four-site checkbox rules stay for now. They come AFTER the kit rules
in source order and declare the same values, so nothing moves visually until
Task 8 puts [data-pretable-checkbox] on the sites and deletes them.

The site guard grows a third and fourth kind — TEXT_INPUT_SITES (three, one
of which is really three attributes: the dialog's value field and its
between-range min/max twins) and CHECKBOX_SITES (seven) — each with its own
forbidden list and its own OWN entry. `display` stays legal at bool-cell
alone: `display: flex` there places a 16px control in a table cell rather
than copying the kit's box. The bracket guard anchors on both new kit base
rules against their own sites.

Two intended failures, red until Task 8 migrates the sites: the site guard
(the fields and checkboxes still declare the kit's box) and the new
no-element-type-selector guard (twelve `input[data-pretable-…]` /
`button[data-pretable-…]` selectors). Everything else green.

Mutation-checked both new guards: deleting `border-radius: 3px` from the kit
checkbox rule fails the kit checkbox guard (the surviving copy in the old
four-site rule does not vouch for it); moving the kit checkbox base rule
below the tool-search rule fails the bracket guard.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The three chrome text fields and the seven checkboxes now render
`PretableTextInput` / `PretableCheckbox`. Every site attribute is kept, so
nothing that identified one of these controls stops identifying it; what
goes is the second hand-kept copy of the box in grid.css.

The wrapper rename. The builder's set-shape checklist wrapper wore
`data-pretable-filter-row-value` — the FIELD's attribute — and was told
apart from the field only by `div[…]` vs `input[…]`. Both are kit controls
now, so the element type can no longer separate them and one shared
attribute would hand the wrapper the field's `block-size: 24px`. The
wrapper is `data-pretable-filter-row-set`.

Where the row-select writes live. In `onCheckedChange` for the plain
toggle, in `onClick` for the shift-click range — which calls
`preventDefault()` to veto the kit's toggle, because the range IS the
write and a toggle landing on top of it unselects the row just clicked.
The old handler called `preventDefault()` UNCONDITIONALLY; it dated from
the control's first commit with no stated reason, a `<button type=button>`
click has no default action to prevent, and nothing in the surface reads
`defaultPrevented` on a click that also stops propagating. So it is now
the shift branch's alone, and the writes stay where they read best rather
than moving wholesale into `onClick` behind a no-op `onCheckedChange`.

The kit-level checkbox ring offset. `:where([data-pretable-checkbox]
:focus-visible) { outline-offset: 1px }`, after the shared ring: a 16px
square paints to its own edge, so the shared `-2px` would land ON the box
and read as a recoloured border. The two checklists each argued this in a
rule of their own; every checkbox gets it now — including the four button
sites, which had the UA's ring before the kit existed.

Three guard fixes in css-cascade.test.ts.
- The element-type guard builds its pattern from the SITE LISTS instead of
  a hand-kept alternation, and catches `…) input:focus-visible` as well as
  `…] input`. `button[data-pretable-filter-join]` is deliberately excluded
  rather than stripped as the task sketched: the join is no kit control,
  and the SAME attribute rides a `<span>` (`Where`) that must not take the
  button's border — the element type there is the selector doing its job.
- The checklist test pinned the old `…) input:focus-visible` rule. It now
  asserts the wrapper still takes its own line under its new attribute,
  that the ring comes from the kit's offset rule, and that no site rule
  draws a ring for `filter-row-choice`.
- `DISPLAY_ALLOWED` was vacuous — bool-cell declares `display: flex` and
  the forbidden pattern is `inline-flex`, so the carve-out never fired.
  Deleted; the pattern already names only the kit's value.

Plus a new ordering guard: the checkbox offset rule must come AFTER the
shared ring in source. Both are :where()-flattened to (0,0,0), so order is
the whole of what makes it win.

The choice checkboxes carry `data-pretable-value`, not `data-value`: the
attribute-contract sweep caught the namespace escape, and the kit select
already established that name for "a button has no `.value`".

Mutation-checked, each restored: a `border` on the builder field rule
fails the site guard; the offset rule above the shared ring fails the new
ordering guard; dropping the shift-branch `preventDefault()` fails both
the new `row-selection-callback` range test and the existing 100k
`indexed-rendering` shift-span test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… data-pretable-option-value

Six review findings from the fourteen-site migration (74609cf).

1. A disabled, checked checkbox lost its tick. The kit's shared
   `:disabled` rule set `color: var(--pretable-text-dim)`, and on a
   checkbox `color` is not label ink — it is the CHECK GLYPH, painted on
   `--pretable-checkbox-checked-bg` at about 1.9:1. A read-only boolean
   column renders `BooleanCellControl` disabled on EVERY row, so that was
   every tick in the column. The checkbox is split out of the shared rule:
   the four text-drawing controls keep the dim ink, the checkbox keeps
   only `cursor: default` and lets its own tokens (and the checked rule
   above it) say what disabled looks like. Guarded in `css-cascade.test.ts`
   — no rule matching `[data-pretable-checkbox]:disabled` outside the
   forced-colours block may declare `color`; mutation-checked by re-adding
   the declaration, which fails the guard, then reverting.

2. `data-pretable-value` on the two checklists' choices conflated identity
   with state: on a select trigger that attribute is the one COMMITTED
   value. Both checklists now write `data-pretable-option-value`, and
   `listbox.tsx`'s options, which wrote plain `data-value` outside the
   `data-pretable-` namespace, write the same name. Selectors updated in
   `select-helpers.ts`, `components-listbox`, `components-select`,
   `filter-builder` and `tool-panel-aggregates-picker`;
   `attribute-contract.test.tsx` pins the new attribute on both checklists.

3. `filter-min` / `filter-max` were unpinned in `components-override`. The
   fixture gains a number column and both tests open its funnel, pick
   `between` through `chooseOption`, and assert the twins are the kit
   `TextInput` at `site="filter-value"` — and that an override replaces
   them too.

4. The "no `onSelectionChange` emit here" argument had ended up inside the
   `onClick` shift-range branch while the plain toggle moved to
   `onCheckedChange`. Hoisted above the `<Checkbox>` and reworded for the
   pair; only the shift-veto note stays inline.

5. The `resolvedComponents` comment had the context relation inverted. It
   now says what is true: a hook call in this body would run above its own
   provider and read an ancestor's context, ignoring this surface's prop.

6. Five suites hand-rolled `getAttribute("aria-checked")` reads that
   `checkboxState` exists for; all folded onto the helper, which leaves
   `checkbox-helpers.ts` as the only reader of the attribute in the suite.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t types

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…bute is namespaced

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rivers follow the rename

`chooseOption` was selecting `[data-pretable-option][data-value=...]`, which
the listbox stopped writing when the option attribute was namespaced — every
website e2e that picks a select option was failing on a locator that matched
nothing. Verified by mutation: restoring the old selector fails the funnel
dialog test at helpers.ts:371.

The components fixture now replaces `TextInput` and `Checkbox` too, and gains
an enum column and a row-selection column so the two sites only those reach —
the filter checklist and the row-select cell — are actually rendered. The
override test asserts all four new sites and that nothing the kit draws itself
is left, at the end, once every site has rendered at least once.

A new real-grid test covers the two checkbox claims jsdom can only
approximate: Space activates a `button[role=checkbox]` through the native
button's own activation behaviour, and a wrapping `<label>` forwards a click
on its TEXT. Both are asserted against a visible consequence rather than the
attribute alone — `/fixtures/grouping` hides its grouped columns by default,
so the header cell for `sector` goes 0 → 1 → 0 as the switch is driven.

`.check()` needs an `<input>`; the kit's checkbox is a button, so the three
smoke.spec drivers move to a new `toggleCheckbox` helper — a click plus a wait
on `aria-checked`. `toBeChecked()` is unaffected and stays. The radios in
server-data.spec.ts are native and keep `.check()`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`content-metadata.test.ts` had been red since 7f0d2d5: the new description
was 175 characters. Trimmed to 143; nothing else changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… claim, and two docs facts

`toggleCheckbox`'s comment said Playwright's `.check()` / `.uncheck()` throw on
a `button[role=checkbox]`. They do not — `getChecked` honours `aria-checked` for
that role. The two real reasons to hand-roll the driver are that the same read
collapses `aria-checked="mixed"` to `false` (so `.check()` on a mixed select-all
throws "did not change its state" on a box that changed) and that `_setChecked`
reads the final state once with no retry, racing the React commit that writes the
attribute. The comment now says that. The helper also took a `page` it never
used; dropped, with a clause on why `chooseOption` still needs one.

The components spec's closing zero-kit sweep claimed to cover "every site above,
the builder's own field and checklist included". Both dialogs are closed by then,
so their sites are unmounted, and the new builder row defaults to a text column,
so no checklist is ever drawn. The comment now says what the sweep sees, and the
gap it left is closed at the source: a page-wide zero count for the kit's field
while the name dialog is open, and for the kit's checkbox while the status one
is — a stray built-in inside a portalled dialog is invisible to the count at the
top of the test.

Docs: `warnOnce` is not gated on a build flag, so "warns in development" was
wrong in four places; and "seven grid fields" (instances) beside "the three
chrome fields" (kinds) read as a contradiction on one page, so the second now
names the kinds.

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

vercel Bot commented Sep 8, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated
pretable Ignored Ignored Sep 8, 2026 2:42am UTC

Request Review

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Vercel preview ready

Preview: https://pretable-a8pb3y1mv-cacheplane.vercel.app
Commit: eebaf984dfb9c5ea3c0ba60809d428596a1c1181

Updated automatically by the deploy-preview job.

…wo hook lists

The changeset is the published CHANGELOG, and it under-reported the one change
that breaks a selector a consumer could already have written against 0.17.0:
the listbox option's `data-value` is now `data-pretable-option-value`. It was
the tail of the Attributes paragraph, framed as naming consistency; it now has
its own bolded paragraph beside the "no longer `<input type=checkbox>`" one,
saying plainly that a `[data-value="x"]` selector matches nothing. This
branch's own e2e broke on exactly that.

The same file's "Look" paragraph named three sites as leaving the UA focus ring
and then said the four already-button checkboxes keep everything "exactly".
True of the box, but `row-select` and `bool-cell` match
`[data-pretable-checkbox]:focus-visible` too and also leave the UA's ring —
they were simply never measured, because `tabIndex={-1}` puts them out of a
Tab's reach. The sentence now says that rather than implying their ring is
unchanged.

The consumer-`onClick`-veto contract was stated in checkbox.tsx's module
comment, which does not reach the `.d.ts`, and in the docs — but in no exported
symbol's JSDoc. `onCheckedChange` now carries it.

`warnOnce` is explicitly not gated on a build flag, so four JSDoc blocks
claiming the missing-name check "warns in development" were wrong the way the
website's four were before 0df8e5d. They now say "warns once per page", the
phrasing that commit settled on.

`packages/react/react.api.md` is UNCHANGED, contrary to what the review
expected: `pnpm api` regenerated it byte-identically and `api:check` passes.
The report carries release tags and signatures, not JSDoc prose, and
`onCheckedChange` was already documented — so no `(undocumented)` marker moved
either.

Two docs reference lists were a revision behind: tool-panel.mdx's filter-row
hooks lacked `data-pretable-filter-row-set` and `-row-choice`, and
filtering.mdx's funnel-menu hooks lacked `data-pretable-filter-choice`; both
now note the per-box `data-pretable-option-value`.

grid.css's invalid boolean cell draws an `outline`, the same property the kit
focus ring at the end of the layer now claims for every
`[data-pretable-checkbox]:focus-visible`. A comment records that the collision
is latent (the control is `tabIndex={-1}`, so a click never gives it
`:focus-visible`) and that the precedence would be correct if it ever fired —
a focus ring should outrank an error outline while focused, and `aria-invalid`
with `aria-errormessage` carries the error to assistive tech regardless. No
declaration changed.

Cosmetic: a css-cascade guard's skip explained itself as a wait for "Task 8"
rather than describing what an empty site list means; a helper's doc said it
was read by two guards when one reads it; and a helpers.ts comment kept an
awkward line break left by the rename.

No behaviour changes anywhere, and nothing a browser test reads: helpers.ts's
edit is comment-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@blove
blove enabled auto-merge (squash) September 8, 2026 02:42
@blove
blove merged commit 8ce28ca into main Sep 8, 2026
21 checks passed
@blove
blove deleted the blove/components-sp3-inputs branch September 8, 2026 03:04
@blove blove mentioned this pull request Sep 8, 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