Repository navigation
docs(spec): Tokenizer records the experimental touch sheet presentation (DEC-3) - #7976
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report
|
There was a problem hiding this comment.
This spec change is on hold for a maintainer decision. component:Tokenizer DEC-3 adds requirements the current record does not have (FR16–FR24, AR6, PR6), so an engineering owner needs to approve them at this head, including the choice of the selection controls' adaptive over the adaptive-bottom-sheet value the other input fields use. Whatever that decision, the record needs these fixes before it merges:
- Scope FR1–FR15 and the existing design rows to the
popoverpresentation: FR17 removes the input FR1 keeps mounted, and FR19 lists the committed items FR13 excludes. - In the sheet presentation, send
refand the other base props to the trigger (family:input-fieldsFR10), and make the suppression of the eight inert props visible rather than silent. - Give the sheet a pending-search state, so FR20 does not show "no results" before results arrive.
- Say what happens when the field becomes disabled with the sheet open, and update
approved_by/approved_atwhen DEC-3 is approved.
[Automated review]
| | FR14 | With `hasEntriesOnFocus` enabled, committing a choice from the current settled empty-query bootstrap cohort MUST keep the input focused and the popup open when another eligible loaded choice remains. The active option advances to the next remaining choice in displayed order; when the committed choice was last, the nearest preceding choice becomes active. `aria-activedescendant` MUST reference that rendered option. | Approved rapid multi-select lifecycle | Current contract; implementation and verification pending | | ||
| | FR15 | The retained popup MUST close and clear its active descendant when no eligible loaded choice remains, `maxEntries` is reached, the control becomes disabled, focus leaves, or the person dismisses it. Disabled Tokenizer remains non-interactive. Pending or stale source work MUST NOT reopen a terminal popup or expose stale choices; loading has no selectable placeholder and its busy state clears when work is cancelled or settles. | Approved terminal-state safety boundary | Current contract; implementation and verification pending | | ||
| | FR16 | `experimental.presentation` selects the surface. `popover`, the value when `experimental` or the option is omitted, is the inline combobox with its anchored suggestion menu, unchanged. `bottom-sheet` is the trigger and modal sheet of FR17–FR24 on every device. `adaptive` resolves to `bottom-sheet` on compact coarse-pointer screens and to `popover` otherwise, through the same shared resolution the selection controls use (`component:Selector/FR4`). A resolution change while the sheet is open takes effect after it closes. | DEC-3; touch-flow evidence from the Lab prototype | Current contract. Implementation gap: the shipped Tokenizer has no `experimental` prop and renders the inline combobox on every device. | | ||
| | FR17 | In the sheet presentation the control surface is one button, named by the field label, with `aria-haspopup="dialog"` and `aria-expanded`. It shows the selected tokens without remove controls, or the placeholder when empty, wraps tokens onto further lines, and keeps a block size of at least 44px. Activating it opens one tall modal BottomSheet named by the field label; the suggestion popover, inline input, and end lane do not render. While disabled the trigger stays focusable with `aria-disabled="true"`, does not open, and shows `disabledMessage` as the inline field does (`spec:AST-085/FR1`). | DEC-3; Lab touch-flow evidence; `spec:AST-085` | Current contract. Implementation gap: no trigger exists. | |
There was a problem hiding this comment.
This removes the inline input, but FR1 (line 161) still says Tokenizer MUST keep one native combobox input mounted, and FR13 (line 173) still excludes committed items that FR19 lists as checked rows. One sentence scoping FR1–FR15, AR1–AR5 and the existing design rows to the popover presentation would remove the contradiction.
| surface to act on and are inert: `renderItem`, `renderToken`, `endContent`, | ||
| `hasClear` (the sheet's own Clear all replaces it), `hasEntriesOnFocus` (the | ||
| sheet always lists the bootstrap set), `menuWidth`, `tokenOverflowBehavior`, | ||
| `hasAutoFocus`, and the input `ref`. `handleRef.focus()` focuses the trigger. |
There was a problem hiding this comment.
family:input-fields FR10 sends ref, id, data-* and aria-* to the "combobox/trigger element that carries the field's accessible name and value", and architecture:public-component-api INV8 connects ref to the element the contract promises. In the sheet presentation that element is the trigger, so an inert ref contradicts both. The eight dropped props are also suppressed silently (INV3).
| | FR17 | In the sheet presentation the control surface is one button, named by the field label, with `aria-haspopup="dialog"` and `aria-expanded`. It shows the selected tokens without remove controls, or the placeholder when empty, wraps tokens onto further lines, and keeps a block size of at least 44px. Activating it opens one tall modal BottomSheet named by the field label; the suggestion popover, inline input, and end lane do not render. While disabled the trigger stays focusable with `aria-disabled="true"`, does not open, and shows `disabledMessage` as the inline field does (`spec:AST-085/FR1`). | DEC-3; Lab touch-flow evidence; `spec:AST-085` | Current contract. Implementation gap: no trigger exists. | | ||
| | FR18 | The sheet is a single surface: a search row at the top using the panel search field the selection controls share, a scrolling list of rows, and, while the query is empty, a footer with Clear all and Done. There is no second sheet. Done closes the sheet and clears the query; swipe, scrim, and Escape dismissal follow BottomSheet `purpose="info"`; focus returns to the trigger after the sheet closes. | DEC-3; Lab touch-flow evidence; `component:BottomSheet` | Current contract. Implementation gap: no sheet exists. | | ||
| | FR19 | Each row is a `checkbox` named by the item label whose `aria-checked` mirrors membership in `value`. Activating an unchecked row commits an `add` change; a checked row commits a `remove` change; both apply immediately and are announced as the inline field announces them. Row order is stable while the sheet is open: the items selected when it opened, in `value` order, then selected items absent from the results, then the results in source order; a toggle never moves an existing row. The next open takes a fresh snapshot. The query filters the selected rows by case-insensitive label substring. | DEC-3; Lab touch-flow evidence | Current contract. Implementation gap: no rows exist. | | ||
| | FR20 | An empty query lists the source's bootstrap set; a query shorter than `minQueryLength` lists no source results; otherwise the source is searched after `debounceMs` and the results are capped at `maxMenuItems`. `onChangeQuery` fires on every query change. With no rows and no create row, an empty query shows a "No items available" empty state and a non-empty query shows the `emptySearchText` empty state (its default message when omitted). | DEC-3; Lab touch-flow evidence | Current contract. Implementation gap: no sheet search exists. | |
There was a problem hiding this comment.
As written, a pending search with no rows shows the emptySearchText empty state before results arrive. The Lab prototype holds the empty state back while searching, and family:input-fields FR7 requires named source-busy feedback, so this row needs a pending state.
| | FR15 | The retained popup MUST close and clear its active descendant when no eligible loaded choice remains, `maxEntries` is reached, the control becomes disabled, focus leaves, or the person dismisses it. Disabled Tokenizer remains non-interactive. Pending or stale source work MUST NOT reopen a terminal popup or expose stale choices; loading has no selectable placeholder and its busy state clears when work is cancelled or settles. | Approved terminal-state safety boundary | Current contract; implementation and verification pending | | ||
| | FR16 | `experimental.presentation` selects the surface. `popover`, the value when `experimental` or the option is omitted, is the inline combobox with its anchored suggestion menu, unchanged. `bottom-sheet` is the trigger and modal sheet of FR17–FR24 on every device. `adaptive` resolves to `bottom-sheet` on compact coarse-pointer screens and to `popover` otherwise, through the same shared resolution the selection controls use (`component:Selector/FR4`). A resolution change while the sheet is open takes effect after it closes. | DEC-3; touch-flow evidence from the Lab prototype | Current contract. Implementation gap: the shipped Tokenizer has no `experimental` prop and renders the inline combobox on every device. | | ||
| | FR17 | In the sheet presentation the control surface is one button, named by the field label, with `aria-haspopup="dialog"` and `aria-expanded`. It shows the selected tokens without remove controls, or the placeholder when empty, wraps tokens onto further lines, and keeps a block size of at least 44px. Activating it opens one tall modal BottomSheet named by the field label; the suggestion popover, inline input, and end lane do not render. While disabled the trigger stays focusable with `aria-disabled="true"`, does not open, and shows `disabledMessage` as the inline field does (`spec:AST-085/FR1`). | DEC-3; Lab touch-flow evidence; `spec:AST-085` | Current contract. Implementation gap: no trigger exists. | | ||
| | FR18 | The sheet is a single surface: a search row at the top using the panel search field the selection controls share, a scrolling list of rows, and, while the query is empty, a footer with Clear all and Done. There is no second sheet. Done closes the sheet and clears the query; swipe, scrim, and Escape dismissal follow BottomSheet `purpose="info"`; focus returns to the trigger after the sheet closes. | DEC-3; Lab touch-flow evidence; `component:BottomSheet` | Current contract. Implementation gap: no sheet exists. | |
There was a problem hiding this comment.
Nothing says what happens if the field becomes disabled while the sheet is open. The rows (FR19), the create row (FR22) and Clear all (FR24) would still commit, which spec:AST-085 FR2 forbids. Saying the sheet closes and focus returns to the trigger, as FR15 does for the inline popup, would cover it.
e33be18 to
0beb6ba
Compare
0beb6ba to
1edddb0
Compare
|
/approve-spec 1edddb0 |
There was a problem hiding this comment.
An engineering owner approved component:Tokenizer DEC-3 at this head, so the direction is settled. Four record fixes are still needed before it merges:
- Say what element and type
refhas in the sheet presentation. It is released asRef<HTMLInputElement>, and the record keeps stable types unchanged, but it now goes to the trigger button. - Say what a sheet row does with an item's
action, whichspec:AST-058FR9 expects Tokenizer to handle through its one panel. - Update the Compatibility bullet that still says the public API is unchanged.
- In AR6, name the WCAG 2.2 success criterion and the row activation keys that
spec:AST-020FR1 and FR15 ask for.
[Automated review]
| In the sheet presentation the trigger is the control: `ref`, `id`, `data-*`, | ||
| `aria-*`, and the other base props reach it (`family:input-fields/FR10`, | ||
| `architecture:public-component-api/INV8`), `handleRef.focus()` and `blur()` | ||
| act on it, and `onFocus`, `onBlur`, and `onKeyDown` report its events. The |
There was a problem hiding this comment.
ref now reaches the trigger button here, but Tokenizer.tsx still declares ref?: React.Ref<HTMLInputElement> ("Ref forwarded to the combobox input"), and the summary table says the stable types are unchanged. As written, a caller's input-typed ref receives an HTMLButtonElement on phones under adaptive. Could the record say which element and type ref has in the sheet presentation? One option is a props type that applies only when experimental is passed, so callers who omit it keep Ref<HTMLInputElement>.
| | FR16 | `experimental.presentation` selects the surface. `popover`, the value when `experimental` or the option is omitted, is the inline combobox with its anchored suggestion menu, unchanged. `bottom-sheet` is the trigger and modal sheet of FR17–FR24 on every device. `adaptive` resolves to `bottom-sheet` on compact coarse-pointer screens and to `popover` otherwise, through the same shared resolution the selection controls use (`component:Selector/FR4`). A resolution change while the sheet is open takes effect after it closes. | DEC-3; touch-flow evidence from the Lab prototype | Current contract. Implementation gap: the shipped Tokenizer has no `experimental` prop and renders the inline combobox on every device. | | ||
| | FR17 | In the sheet presentation the control surface is one button, named by the field label, with `aria-haspopup="dialog"` and `aria-expanded`. It shows the selected tokens without remove controls, or the placeholder when empty, wraps tokens onto further lines, and keeps the inline field's block size for its `size` step. Activating it opens one tall modal BottomSheet named by the field label; the suggestion popover, inline input, and end lane do not render. While disabled the trigger stays focusable with `aria-disabled="true"`, does not open, and shows `disabledMessage` as the inline field does (`spec:AST-085/FR1`); a field that becomes disabled while its sheet is open closes the sheet and returns focus to the trigger (`spec:AST-085/FR2`). | DEC-3; Lab touch-flow evidence; `spec:AST-085` | Current contract. Implementation gap: no trigger exists. | | ||
| | FR18 | The sheet is a single surface: a search row at the top using the panel search field the selection controls share, a scrolling list of rows, and, while the query is empty, a footer with Clear all and Done. There is no second sheet. Done closes the sheet and clears the query; swipe, scrim, and Escape dismissal follow BottomSheet `purpose="info"`; focus returns to the trigger after the sheet closes. | DEC-3; Lab touch-flow evidence; `component:BottomSheet` | Current contract. Implementation gap: no sheet exists. | | ||
| | FR19 | Each row is a `checkbox` named by the item label whose `aria-checked` mirrors membership in `value`. Activating an unchecked row commits an `add` change; a checked row commits a `remove` change; both apply immediately and are announced as the inline field announces them. Row order is stable while the sheet is open: the items selected when it opened, in `value` order, then selected items absent from the results, then the results in source order; a toggle never moves an existing row. The next open takes a fresh snapshot. The query filters the selected rows by case-insensitive label substring. | DEC-3; Lab touch-flow evidence | Current contract. Implementation gap: no rows exist. | |
There was a problem hiding this comment.
SearchableItem carries action?: ReactNode, and spec:AST-058 FR9 names Tokenizer as an adopter "through the one panel BaseTypeahead renders". The sheet is a second panel, and FR19 doesn't say what a row does with an item's action. One sentence would close the gap: render the item without it and warn in development, as BaseTypeahead does today, until adoption covers the sheet rows.
|
|
||
| ## Compatibility and migration | ||
|
|
||
| - The public API and the default `hasEntriesOnFocus={false}` path are unchanged. |
There was a problem hiding this comment.
This bullet still says "The public API … unchanged", while Public concepts now introduces the experimental experimental prop and the bullet at lines 75-80 describes it. Maybe "The stable public API …"?
| - **AR6 — Sheet semantics.** In the sheet presentation the trigger is a button | ||
| named by the field label with `aria-haspopup="dialog"` and `aria-expanded`, | ||
| and its accessible description lists the selected token labels, so assistive | ||
| technology hears the selection the sighted person sees (`spec:AST-020/FR1`); |
There was a problem hiding this comment.
spec:AST-020 FR1 asks each WCAG expectation to cite a success criterion. Other records write "WCAG 2.2 SC 1.3.1 and 4.1.2 through spec:AST-020/FR1". FR15 also has the record name the activation keys a role adopts. Could AR6 name the criterion for the token description and the keys that toggle a row?
1edddb0 to
d19e19f
Compare
There was a problem hiding this comment.
This spec change is on hold for an engineering owner's decision. The only /approve-spec names the previous head, and this head changes the normative text of component:Tokenizer DEC-3 (FR16, FR19, FR20, AR6 and the ref rule), so an engineering owner needs to record approval by commenting /approve-spec with the full head SHA, which the author may do as an engineering owner. Two record fixes are needed either way:
- AR6's in-dialog live region should also announce how a search ended (the result count or the empty-state message), because the page-level announcement channel is not exposed inside the native modal sheet.
- The Sheet trigger and Sheet footer design rows cite
component:Tokenandcomponent:Button, which are not records.
[Automated review]
| open, additions, removals, and the search's busy start are announced through | ||
| a persistent polite live region inside the sheet's dialog, because the shared | ||
| page-body channel is not exposed while a native modal is open | ||
| (`spec:AST-086/FR10`, Platform support), and that region follows FR3, FR5, | ||
| and FR6 of that record; after the sheet closes focus returns to the trigger |
There was a problem hiding this comment.
This region carries toggles and the busy start, but not how a search ended. The inline field announces the result count and the empty-search message today (BaseTypeahead.tsx:736-742), and the page-level channel isn't exposed inside the native modal, so someone typing in the sheet's search never hears the outcome (WCAG 2.2 SC 4.1.3). Could the region also announce the settled outcome once per search, with those announcements added to the sheet verification row? spec:AST-086 FR10 covers owners rehosted above a modal, so Platform support is the closer citation.
| | Suggestion menu | Retains current outer-field anchoring and exact preferred `menuWidth`; focus bootstrap keeps only eligible choices and a valid active option. | Current BaseTypeahead source | Prominent | FR2, FR13–FR15, AR5 | | ||
| | Overflow placeholder/layer | Preserves a usable outer-field fallback while live content may reparent or collapse. | Current Tokenizer/Layer composition | Supporting | FR3, FR7–FR8 | | ||
| | Dependent narrower surface | Uses the descriptor only after its owning component resolves a width narrower than the field. | Consuming component plus Layer | Prominent | FR9–FR12 | | ||
| | Sheet trigger | The field's control surface in the sheet presentation: tokens or placeholder, the inline field's size step, opens the sheet. | Tokenizer source; `component:Token` | Prominent | FR17, AR6 | |
There was a problem hiding this comment.
component:Token here and component:Button in the Sheet footer row (line 356) aren't record ids on main or at this head, so a reader following them finds nothing. Naming the components without the record-id form, as the other rows do, would fix it.
d19e19f to
d7ee224
Compare
d7ee224 to
a3bbf10
Compare
9623a37 to
4030612
Compare
There was a problem hiding this comment.
This spec change needs an engineering owner's decision and one record fix before it can be approved. An engineering owner has to approve component:Tokenizer DEC-3 at the current head, because it adds the experimental presentation option and the sheet requirements FR16–FR24, AR6, PR6 and AV7 to a record whose main version has none of them, and the only /approve-spec so far names an earlier commit; any engineering owner, including the author, records that decision by commenting /approve-spec followed by the full head SHA. Before that, FR16's new hydration sentence says nothing is focused before an adaptive field swaps to its trigger, but a field with hasAutoFocus server-renders its input with autofocus, which takes focus on load, so FR16 should say whether adaptive defers hasAutoFocus or moves focus to the trigger, and the sheet verification row should list that case.
[Automated review]
| | FR13 | Tokenizer MUST exclude every choice whose `SearchableItem.id` is already present in its committed `value` from displayed bootstrap and search cohorts, including results that settle after the controlled value changes. A caller-rejected proposed value remains eligible because it was not committed. Distinct IDs remain eligible even when their labels match. | Approved Tokenizer multi-selection contract | Current contract; implementation and verification pending | | ||
| | FR14 | With `hasEntriesOnFocus` enabled, committing a choice from the current settled empty-query bootstrap cohort MUST keep the input focused and the popup open when another eligible loaded choice remains. The active option advances to the next remaining choice in displayed order; when the committed choice was last, the nearest preceding choice becomes active. `aria-activedescendant` MUST reference that rendered option. | Approved rapid multi-select lifecycle | Current contract; implementation and verification pending | | ||
| | FR15 | The retained popup MUST close and clear its active descendant when no eligible loaded choice remains, `maxEntries` is reached, the control becomes disabled, focus leaves, or the person dismisses it. Disabled Tokenizer remains non-interactive. Pending or stale source work MUST NOT reopen a terminal popup or expose stale choices; loading has no selectable placeholder and its busy state clears when work is cancelled or settles. | Approved terminal-state safety boundary | Current contract; implementation and verification pending | | ||
| | FR16 | `experimental.presentation` selects the surface. `popover`, the value when `experimental` or the option is omitted, is the inline combobox with its anchored suggestion menu, unchanged. `bottom-sheet` is the trigger and modal sheet of FR17–FR24 on every device. `adaptive` resolves to `bottom-sheet` on compact coarse-pointer screens and to `popover` otherwise, through the same shared resolution the selection controls use (`component:Selector/FR4`). `adaptive` renders on the server, and hydrates, as the inline field, because the resolver answers `popover` where there is no `matchMedia` and in the hydrating render; on a compact coarse-pointer client the field swaps to the trigger in the first render after hydration, with nothing focused before interaction, so focus is undisturbed (`document.activeElement` stays the body). The trigger keeps the field's box — the same width, top, label, and `size` block size — except the inline input's own row: when the tokens leave the input no room on their row, the inline field wraps the input onto a row of its own, and the trigger, which has no input, drops that row (one `md` row, 27px, on a 390 and a 414px phone with five tokens); with room for the input beside the tokens, nothing shifts. A resolution change while the sheet is open takes effect after it closes and focus has returned to the trigger. Whenever the presentation changes while its control holds focus — then, or while the sheet is closed — focus moves to the new presentation's control, the combobox input or the trigger, so it never lands on the page body (`spec:AST-085/FR4`). | DEC-3; touch-flow evidence from the Lab prototype | Current contract. Implementation gap: the shipped Tokenizer has no `experimental` prop and renders the inline combobox on every device. | |
There was a problem hiding this comment.
This says the swap happens "with nothing focused before interaction", and the sheet verification row expects the body still active, but a field with hasAutoFocus server-renders its input with autofocus (BaseTypeahead renders autoFocus={hasAutoFocus}, and React 19's server renderer writes <input autofocus="">). On a phone that input holds focus before any interaction, so this row's later rule then moves focus to the trigger. Could FR16 name the case, for example "unless hasAutoFocus focused the input, in which case focus moves to the trigger by the rule below", or say that adaptive applies hasAutoFocus only once the presentation resolves, and could the sheet verification row list it?
4030612 to
104a95e
Compare
There was a problem hiding this comment.
This spec change needs one decision before it can be approved: an engineering owner has to approve component:Tokenizer DEC-3 at the current head, because it adds the experimental presentation option and the sheet requirements FR16–FR24, AR6, PR6 and AV7 to a record whose main version has none of them, and the only /approve-spec so far names an earlier commit. The new FR16 clause for hasAutoFocus across hydration settles the last record fix, so nothing else needs to change first. Any engineering owner, including the author, records the decision by commenting /approve-spec followed by the full head SHA.
[Automated review]
104a95e to
d227c88
Compare
There was a problem hiding this comment.
This spec change needs one decision before it can be approved: an engineering owner has to approve component:Tokenizer DEC-3 at the current head, because it adds the experimental presentation option and the sheet requirements FR16–FR24, AR6, PR6 and AV7 to a record whose main version has none of them, and the only /approve-spec so far names an earlier commit. The new FR24 sentence for an empty-selection Clear all (focusable, aria-disabled, a tooltip, no action) matches spec:AST-085, so no record fix is needed first; with Button's default tooltip, a tap on a phone does not show that tooltip, which the owner may accept or refine. Any engineering owner, including the author, records the decision by commenting /approve-spec followed by the full head SHA.
[Automated review]
| | FR21 | Long lists render progressively: 50 rows at a time, the next batch when the list scrolls within 200px of its end, with the window reset to the first batch on open and on each query change. The rendered and total counts are exposed as `data-rendered-count` and `data-total-count` on the list, and the rows sit in a `list` whose `listitem` wrappers carry `aria-setsize` with the total and `aria-posinset` with the row's position — the `checkbox` role itself admits neither — so a person moving through the rows can learn that more exist before the list has scrolled; whether a screen reader speaks the position ("n of total") from the focused checkbox depends on the screen reader (`spec:AST-009/FR8`), and the accessibility tree proves the `list`, `listitem`, `aria-setsize`, and `aria-posinset` exposure; focus reaching the last rendered row renders the next batch (WCAG 2.2 SC 1.3.1 Info and Relationships and 4.1.2 Name, Role, Value through `spec:AST-020/FR1`). | DEC-3; Lab touch-flow evidence | Current contract. Implementation gap: no list exists. | | ||
| | FR22 | With `hasCreate`, a non-empty trimmed query that matches no selected id, no selected label, and no result label (case-insensitive) adds a create row above the list showing `Create "{text}"` with a trailing Add action named `Add {text}`. Activating it commits a `create` change whose item has the text as both id and label, clears the query, and moves focus to the sheet's search field, since the row it was on is gone. The create row is absent at `maxEntries`. | DEC-3; Lab touch-flow evidence; `hasCreate` contract of the inline field | Current contract. Implementation gap: no create row exists. | | ||
| | FR23 | At `maxEntries`, unchecked rows carry `aria-disabled="true"`, stay focusable, and ignore activation; checked rows remain toggleable so the person can make room (`spec:AST-085/FR1`). | DEC-3; Lab touch-flow evidence; `spec:AST-085` | Current contract. Implementation gap: no rows exist. | | ||
| | FR24 | Clear all is disabled while `value` is empty: `aria-disabled`, still in the focus order, with a tooltip saying nothing is selected, and a click opens nothing (`spec:AST-085/FR1`). Otherwise it opens an AlertDialog confirmation naming the field; confirming commits `[]` with a `remove` change naming the last selected item, closes the confirmation, and — once the confirmation has closed — moves focus to the sheet's search field, the destination this record names (`spec:AST-085/FR4`): the clear is spent, and the search field is where the next selection starts (WCAG 2.2 SC 2.4.3 through `spec:AST-020/FR1`), and announces "All items removed" inside the sheet (AR6); cancelling changes nothing and focus returns to Clear all. | DEC-3; Lab touch-flow evidence | Current contract. Implementation gap: no footer exists. | |
There was a problem hiding this comment.
Optional, not blocking: if this footer's Clear all uses Button's tooltip, the tooltip runs with touchTrigger auto, which keeps it shut when a button is tapped and ignores focus that comes from a touch. On a phone the "nothing is selected" reason would then show only to keyboard and mouse users (and to screen readers as the button's description). If touch users should see why Clear all is unavailable, this row could say a tap shows the tooltip.
d227c88 to
9fc2235
Compare
There was a problem hiding this comment.
This spec change needs one decision before it can be approved: an engineering owner has to approve component:Tokenizer DEC-3 at the current head, because it adds the experimental presentation option and the sheet requirements FR16–FR24, AR6, PR6 and AV7 to a record whose main version has none of them, and the only /approve-spec so far names an earlier commit. The record text is unchanged from that commit and no record fix is needed first. Any engineering owner, including the author, records the decision by commenting /approve-spec followed by the full head SHA.
[Automated review]
| interface and keeps `ref?: Ref<HTMLInputElement>` as released, so `interface | ||
| X extends TokenizerProps<T>` and `Omit<TokenizerProps<T>, …>` wrappers keep | ||
| compiling; the component's own parameter type is where the widening lives: | ||
| with `experimental` passed, `ref` accepts `Ref<HTMLInputElement | |
There was a problem hiding this comment.
Optional, not blocking: with the repository's TypeScript 6.0.3 and @types/react 19.2.17, an input-only RefObject<HTMLInputElement | null> or callback ref still compiles where Ref<HTMLInputElement | HTMLButtonElement> is expected, so a caller who adds experimental={{presentation: 'adaptive'}} to a field holding useRef<HTMLInputElement>(null) gets an HTMLButtonElement on a phone without a type error. If that is the intent, a clause saying the wider type documents the trigger case rather than rejecting input-only refs would set callers' expectations.
9fc2235 to
f52d9d3
Compare
There was a problem hiding this comment.
This spec change needs two record fixes and an engineering owner's decision before it can be approved. FR20 puts the sheet's busy semantics on the list, but family:input-fields FR7 on main still requires Tokenizer's source-busy feedback to expose them on the combobox, so FR7 needs a matching amendment in this pull request; and the ownership bullet should name spec:AST-099, which main now makes the owner of BottomSheet's on-screen keyboard behavior. Once those land, any engineering owner, including the author, records the decision on component:Tokenizer DEC-3 by commenting /approve-spec followed by the full head SHA.
[Automated review]
| | FR17 | In the sheet presentation the control surface is one button, named by the field label, with `aria-haspopup="dialog"` and `aria-expanded`. It shows the selected tokens without remove controls, or the placeholder when empty, wraps tokens onto further lines, and keeps the inline field's block size for its `size` step. Activating it opens one tall modal BottomSheet named by the field label; the suggestion popover, inline input, and end lane do not render. While disabled the trigger stays focusable with `aria-disabled="true"`, does not open, and shows `disabledMessage` as the inline field does (`spec:AST-085/FR1`); a field that becomes disabled while its sheet is open closes the sheet and returns focus to the trigger (`spec:AST-085/FR2`). | DEC-3; Lab touch-flow evidence; `spec:AST-085` | Current contract. Implementation gap: no trigger exists. | | ||
| | FR18 | The sheet is a single surface: a search row at the top using the panel search field the selection controls share, a scrolling list of rows, and, while the query is empty, a footer with Clear all and Done. There is no second sheet. On open, focus moves to the sheet's panel, not the search field, so the reading order starts at the top of the sheet and the on-screen keyboard rises only when the person reaches for the search (WCAG 2.2 SC 2.4.3 Focus Order and 3.2.1 On Focus through `spec:AST-020/FR1`). Done closes the sheet; swipe, scrim, and Escape dismissal follow BottomSheet `purpose="info"`; each of those closes clears the query and reports it through `onChangeQuery`, so the next open lists the bootstrap set, while a close the field forces (becoming disabled, FR17) keeps the query, since nothing the person did changed it; focus returns to the trigger after the sheet closes. | DEC-3; Lab touch-flow evidence; `component:BottomSheet` | Current contract. Implementation gap: no sheet exists. | | ||
| | FR19 | Each row is a `checkbox` named by the item label whose `aria-checked` mirrors membership in `value`. Activating an unchecked row commits an `add` change; a checked row commits a `remove` change; both apply immediately and are announced with the inline field's words through the sheet's own live region (AR6). Row order is stable while the sheet is open: the items selected when it opened, in `value` order, then selected items absent from the results, then the results in source order; a toggle never moves an existing row. The next open takes a fresh snapshot. The query filters the selected rows by case-insensitive label substring. A row renders an item without its `action` and warns once in development that the sheet does not render it, as the inline panel does (`spec:AST-058/FR9`). | DEC-3; Lab touch-flow evidence | Current contract. Implementation gap: no rows exist. | | ||
| | FR20 | An empty query lists the source's bootstrap set, requested at once on open and on clearing the query, so an instant source shows no busy feedback and announces no busy start — the outcome below still applies, so an instant source with no bootstrap items shows and announces the "No items available" empty state; a query shorter than `minQueryLength` is not searched: it lists only the selected rows it matches and, with `hasCreate`, the create row, with no busy feedback, no empty state, and no outcome announcement, as the inline field shows nothing for such a query; otherwise the source is searched `debounceMs` after the query change and the results are capped at `maxMenuItems`. `onChangeQuery` fires on every query change. From open, and from each query change, until that search settles, the list is pending: it shows named busy feedback above any rows still listed from the previous search, the list element carries the busy semantics (`aria-busy`) since the sheet has no combobox (`family:input-fields/FR7`), and no empty state shows; when the settled search removes the row that holds focus, focus moves to the search field (`spec:AST-085/FR4`). Once settled, the outcome is announced through the sheet's live region (AR6): the result count for a searched query, or the rendered text of the empty state when it shows (`spec:AST-056/FR3`). A source that rejects settles the search to no results, as the inline field does. With no rows and no create row, an empty query shows a "No items available" empty state and a searched query shows the `emptySearchText` empty state (its default message when omitted). | DEC-3; Lab touch-flow evidence; `family:input-fields` | Current contract. Implementation gap: no sheet search exists. | |
There was a problem hiding this comment.
This puts the sheet's busy semantics on the list "since the sheet has no combobox", but family:input-fields FR7 on main still says Tokenizer's visible source-busy feedback must "expose busy semantics on the combobox", and architecture:knowledge-contracts DEC-1 resolves a conflict between current records at the canonical owner rather than in the narrower record. Could this pull request also amend FR7 (or the family's Adoption and exceptions table) so a presentation without a combobox exposes busy semantics on the list its search updates?
| - The sheet's dialog, scrim, gestures, snap behavior, keyboard accommodation, | ||
| and dismissal paths — owned by `component:BottomSheet` and | ||
| `family:overlay-dismissal`. |
There was a problem hiding this comment.
On main, component:BottomSheet now says "BottomSheet's on-screen keyboard behavior is owned by spec:AST-099". Could this bullet, and the Content boundary at the end of the record, name spec:AST-099 for the sheet's on-screen keyboard so a reader lands on its owner?
f52d9d3 to
c4fc40b
Compare
There was a problem hiding this comment.
This spec change needs one record fix and an engineering owner's decision before it can be approved. FR20 shows and announces a fixed "No items available" whenever nothing is selected and the source's bootstrap set is empty, so a search-only source such as {search, bootstrap: () => []} tells people on every open that nothing is available, while the inline field shows nothing in that state; please settle what the sheet shows there. The decision covers component:Tokenizer DEC-3 and the new family:input-fields FR7 clause, which this head adds without an exact-head approval: any engineering owner, including the author, records it by commenting /approve-spec followed by the full head SHA.
[Automated review]
| | FR17 | In the sheet presentation the control surface is one button, named by the field label, with `aria-haspopup="dialog"` and `aria-expanded`. It shows the selected tokens without remove controls, or the placeholder when empty, wraps tokens onto further lines, and keeps the inline field's block size for its `size` step. Activating it opens one tall modal BottomSheet named by the field label; the suggestion popover, inline input, and end lane do not render. While disabled the trigger stays focusable with `aria-disabled="true"`, does not open, and shows `disabledMessage` as the inline field does (`spec:AST-085/FR1`); a field that becomes disabled while its sheet is open closes the sheet and returns focus to the trigger (`spec:AST-085/FR2`). | DEC-3; Lab touch-flow evidence; `spec:AST-085` | Current contract. Implementation gap: no trigger exists. | | ||
| | FR18 | The sheet is a single surface: a search row at the top using the panel search field the selection controls share, a scrolling list of rows, and, while the query is empty, a footer with Clear all and Done. There is no second sheet. On open, focus moves to the sheet's panel, not the search field, so the reading order starts at the top of the sheet and the on-screen keyboard rises only when the person reaches for the search (WCAG 2.2 SC 2.4.3 Focus Order and 3.2.1 On Focus through `spec:AST-020/FR1`). Done closes the sheet; swipe, scrim, and Escape dismissal follow BottomSheet `purpose="info"`; each of those closes clears the query and reports it through `onChangeQuery`, so the next open lists the bootstrap set, while a close the field forces (becoming disabled, FR17) keeps the query, since nothing the person did changed it; focus returns to the trigger after the sheet closes. | DEC-3; Lab touch-flow evidence; `component:BottomSheet` | Current contract. Implementation gap: no sheet exists. | | ||
| | FR19 | Each row is a `checkbox` named by the item label whose `aria-checked` mirrors membership in `value`. Activating an unchecked row commits an `add` change; a checked row commits a `remove` change; both apply immediately and are announced with the inline field's words through the sheet's own live region (AR6). Row order is stable while the sheet is open: the items selected when it opened, in `value` order, then selected items absent from the results, then the results in source order; a toggle never moves an existing row. The next open takes a fresh snapshot. The query filters the selected rows by case-insensitive label substring. A row renders an item without its `action` and warns once in development that the sheet does not render it, as the inline panel does (`spec:AST-058/FR9`). | DEC-3; Lab touch-flow evidence | Current contract. Implementation gap: no rows exist. | | ||
| | FR20 | An empty query lists the source's bootstrap set, requested at once on open and on clearing the query, so an instant source shows no busy feedback and announces no busy start — the outcome below still applies, so an instant source with no bootstrap items shows and announces the "No items available" empty state; a query shorter than `minQueryLength` is not searched: it lists only the selected rows it matches and, with `hasCreate`, the create row, with no busy feedback, no empty state, and no outcome announcement, as the inline field shows nothing for such a query; otherwise the source is searched `debounceMs` after the query change and the results are capped at `maxMenuItems`. `onChangeQuery` fires on every query change. From open, and from each query change, until that search settles, the list is pending: it shows named busy feedback above any rows still listed from the previous search, the list element carries the busy semantics (`aria-busy`), since the sheet has no combobox (`family:input-fields/FR7`, which places busy semantics on the list a presentation without a combobox updates), and no empty state shows; when the settled search removes the row that holds focus, focus moves to the search field (`spec:AST-085/FR4`). Once settled, the outcome is announced through the sheet's live region (AR6): the result count for a searched query, or the rendered text of the empty state when it shows (`spec:AST-056/FR3`). A source that rejects settles the search to no results, as the inline field does. With no rows and no create row, an empty query shows a "No items available" empty state and a searched query shows the `emptySearchText` empty state (its default message when omitted). | DEC-3; Lab touch-flow evidence; `family:input-fields` | Current contract. Implementation gap: no sheet search exists. | |
There was a problem hiding this comment.
With nothing selected and a source whose bootstrap() returns [] (a supported shape: the minQueryLength={3} "People" field in Tokenizer.test.tsx uses {search, bootstrap: () => []}), this row shows and announces "No items available" on every open, although typing finds items. The inline field shows and reports nothing in that state (BaseTypeahead.tsx treats a bootstrap that returns nothing as "not a search that failed"), and Tokenizer has no emptyText to change the words. Could an empty query with an empty bootstrap set show no empty state and announce nothing, as the inline field does, with the verification row's "no bootstrap items" case updated to match?
There was a problem hiding this comment.
This spec change is ready for an engineering owner's decision: component:Tokenizer DEC-3 adds the experimental presentation option and the sheet requirements FR16–FR24, AR6, PR6 and AV7, and this pull request adds a clause to family:input-fields FR7, none of which main states yet, while the only /approve-spec so far names an earlier commit. The FR20 fix is in: with nothing selected and an empty bootstrap set, the sheet now shows only the search field and announces nothing, as the inline field does, so no record fix is needed first. Any engineering owner, including the author, records the decision by commenting /approve-spec followed by the full head SHA. Optionally, update the description's "FR20 timing and focus" bullet, which still says an instant source with no bootstrap items shows and announces "No items available".
[Automated review]
There was a problem hiding this comment.
This spec change needs one record fix and an engineering owner's decision before it can be approved. Under adaptive, the sheet that replaces the popover on a phone always renders more elements than the popover, and architecture:component-style-authoring INV7 (with spec:AST-017 FR60) allows that only through an owner exception recorded with element counts, while AV7 allows the sheet "any internal structure" and DEC-3 records no exception: please record the exception in DEC-3, add INV7 to AV7's proviso, and list element counts at rest and open under a fine and a coarse pointer in the sheet verification row. The decision then covers component:Tokenizer DEC-3 and the family:input-fields FR7 clause, which no /approve-spec names at this head; any engineering owner, including the author, records it by commenting /approve-spec followed by the full head SHA.
[Automated review]
| - **AV7 — Sheet surface composition.** The sheet presentation may compose | ||
| BottomSheet, the shared panel search field, EmptyState, AlertDialog, and the | ||
| checkbox indicator in any internal structure, and may load that surface on | ||
| demand, provided FR16–FR24, AR6, and PR6 hold. |
There was a problem hiding this comment.
AV7 lets the sheet compose its parts "in any internal structure", but architecture:component-style-authoring INV7 (current on main) also binds that structure: an open layer may have no more elements under a coarse pointer than under a fine one, and spec:AST-017 FR60 allows more only "through an owner exception recorded with its element counts". Under adaptive this sheet always outnumbers the popover it replaces (the panel search field, every selected row, a list item and checkbox indicator per row, the footer, the sheet's panel and handle). Could AV7's proviso name INV7, DEC-3 record the exception with its reason, and the sheet verification row list element counts at rest and open under a fine and a coarse pointer?
There was a problem hiding this comment.
This spec change is ready for an engineering owner's decision: component:Tokenizer DEC-3 adds the experimental presentation option and the sheet requirements FR16–FR24, AR6, PR6 and AV7, and the pull request amends family:input-fields FR7, none of which main states yet. The architecture:component-style-authoring INV7 exception for the open sheet is now recorded in DEC-3 with its element counts, so no record fix is needed first; the decision should also settle whether that exception, which covers the open sheet only, needs to name the closed adaptive trigger that replaces the inline input under a coarse pointer. Any engineering owner, including the author, records the decision by commenting /approve-spec followed by the full head SHA.
[Automated review]
| forced `bottom-sheet` presentation renders the same elements under either | ||
| pointer (114 at rest, 165 open). At rest the coarse | ||
| pointer never has more elements than the fine one: nothing of the sheet is in | ||
| the DOM until a press. The exception covers the open sheet only; its counts |
There was a problem hiding this comment.
This scopes the exception to the open sheet, but under adaptive the closed field is also a coarse-pointer presentation built from different elements (FR16–FR17: a trigger button in place of the inline input and its token remove actions), and architecture:component-style-authoring INV7's at-rest budget for "text and other inputs" says "A pointer-specific presentation is a change of attributes and styles on the same elements". Worth settling in the decision whether the exception should also name the closed trigger (its counts, 154 fine / 120 coarse, are already here) or whether the swap meets the at-rest budget as it stands.
There was a problem hiding this comment.
This spec change needs one record fix and an engineering owner's decision before it can be approved. DEC-3's INV7 exception covers the closed adaptive trigger as well as the open sheet, but AV7 still ends "except as DEC-3 records for the open sheet" and paraphrases INV7 without its at-rest rule that a pointer-specific presentation changes only attributes and styles on the same elements, so please extend AV7 to the closed trigger and cite that rule directly. After that, an engineering owner needs to approve component:Tokenizer DEC-3 (the experimental presentation option, FR16–FR24, AR6, PR6, AV7 and the element-budget exception) together with this pull request's amendment of family:input-fields FR7, none of which main states yet; the only /approve-spec so far names an earlier commit. Any engineering owner, including the author, records that decision by commenting /approve-spec followed by the full head SHA.
[Automated review]
| demand, provided FR16–FR24, AR6, and PR6 hold and the structure stays within | ||
| `architecture:component-style-authoring` INV7 — minimal DOM, content | ||
| rendered once, at rest no more elements under a coarse pointer than under a | ||
| fine one — except as DEC-3 records for the open sheet. |
There was a problem hiding this comment.
DEC-3 now ends "The exception covers the closed adaptive trigger and the open sheet" (line 545), but this clause still limits the exception to the open sheet, so a reviewer applying AV7 to the trigger finds no exception for it. Could it read "except as DEC-3 records for the closed adaptive trigger and the open sheet"?
There was a problem hiding this comment.
This spec change needs two small record fixes and an engineering owner's decision before it can be approved. DEC-3's element breakdown counts a footer that FR18 does not render at the measured query "e" and does not add up to the open total it reports, and AV7 still paraphrases architecture:component-style-authoring INV7's at-rest rule as a count instead of citing INV7's own words. Once both are fixed, an engineering owner needs to approve component:Tokenizer DEC-3 (the experimental presentation option, FR16–FR24, AR6, PR6, AV7 and the element-budget exception) together with this pull request's amendment of family:input-fields FR7, since the only /approve-spec so far names an earlier commit. Any engineering owner, including the author, records that decision by commenting /approve-spec followed by the full head SHA.
[Automated review]
| `architecture:component-style-authoring` INV7 — minimal DOM, content | ||
| rendered once, at rest no more elements under a coarse pointer than under a | ||
| fine one — except as DEC-3 records for the closed `adaptive` trigger and |
There was a problem hiding this comment.
This gloss states INV7's at-rest rule as a count, but INV7 on main ("A pointer-specific presentation is a change of attributes and styles on the same elements") and DEC-3 below state it as the same elements; the coarse-not-more-than-fine count is INV7's open-layer budget. Read alone, AV7 would need no exception for the closed adaptive trigger it excepts (120 against 154). Could AV7 cite INV7 without the gloss, or use INV7's words: at rest, the same elements under any pointer; open, no more elements under a coarse pointer than under a fine one?
There was a problem hiding this comment.
Approval of this spec change waits on an engineering owner's decision on component:Tokenizer DEC-3 and the family:input-fields FR7 clause it relies on, and three record fixes are needed first:
- DEC-3's open-sheet breakdown, measured at query "e", counts a footer that FR18 and the searching state do not show and a header the record never names, and its totals do not reconcile (120 + 47 is 167, not the 165 reported open; 114 + 47 is 161, not 165).
- Public concepts says every prop outside the inert list keeps its meaning, but FR20 lists the whole bootstrap set in the sheet while
maxMenuItemscaps it in the inline menu, so that sentence should namemaxMenuItems(or FR20 should cap the bootstrap list too). - AV7 restates
architecture:component-style-authoringINV7's open-layer budget without thespec:AST-017FR60 / DEC-29 allowance INV7 cites; citing INV7 without the paraphrase keeps the two in step.
Once those are fixed, any engineering owner, the author included, records the decision by commenting /approve-spec followed by the full head SHA of that head.
[Automated review]
| `pointer: coarse`, open = query "e": at rest 154 fine / 120 coarse (the | ||
| trigger with its tokens is smaller than the inline field with its token | ||
| remove actions and input), open 157 fine (inline menu) / 165 coarse (sheet); | ||
| the open sheet's dialog is 47 elements: search row 17, list 15 (two rows of 7 | ||
| and the list), the sheet's live region 1, and the panel, handle, header and | ||
| footer 14; a pending Clear all confirmation adds 20 while it is open. The |
There was a problem hiding this comment.
At query "e" FR18 and the searching state render no footer, and no other part of the record names a header, yet the 47-element dialog counts "the panel, handle, header and footer 14". The totals also do not reconcile: 120 at rest plus 47 is 167, not the 165 reported open. Please re-measure at a state the record describes and name each part the sheet adds beyond BottomSheet's frame, so the parts add up to the open total.
| trigger departs from as DEC-3 records; open, the layer under a coarse | ||
| pointer has at most as many elements as under a fine one, which the open | ||
| sheet departs from as DEC-3 records. |
There was a problem hiding this comment.
INV7's open-layer budget on main continues "(beyond that, only by the open-layer allowance spec:AST-017 FR60 and DEC-29 set)"; this clause drops it, so the record states the rule more strictly than its owner does. Citing INV7 without the paraphrase, or carrying the allowance, keeps the two in step.
There was a problem hiding this comment.
This spec change waits on an engineering owner's decision on component:Tokenizer DEC-3 and the family:input-fields FR7 amendment it relies on, and three small record fixes belong on the head that decision approves:
- AV7 (L244–L245) and the sheet verification row (L446) call DEC-3's exception "that allowance", but
spec:AST-017FR60 allows an open layer only 5 extra elements and nothing for a closed trigger, so please word both as the owner exception DEC-3 records. - Public concepts (L171–L172) says every prop outside the inert list keeps its meaning, but FR20 lists the whole empty-query bootstrap set while the inline field caps it at
maxMenuItems, so please say that there or cap the bootstrap list in FR20. - FR16 and the ownership bullet (L135–L137, L204) cite
component:Selector/FR4for the rulespec:AST-094FR13 owns, and DEC-3 (L546–L547) citescomponent:Dialogfor the toast outletspec:AST-003FR18 states, so please cite those owners.
Once those are in, any engineering owner, the author included, records the decision by commenting /approve-spec followed by the full head SHA.
[Automated review]
| lists the bootstrap set), `menuWidth`, `tokenOverflowBehavior`, and | ||
| `hasAutoFocus`. Passing one of them with the sheet presentation emits one | ||
| development-mode warning naming the prop, so it is not silently dropped | ||
| (`architecture:public-component-api/INV3`); every other prop keeps its |
There was a problem hiding this comment.
This sentence covers maxMenuItems, but FR20 (L208) lists the whole bootstrap set on an empty query and caps only searched results. The inline field caps the bootstrap list too (BaseTypeahead.tsx L779 slices it to maxMenuItems, default 10). Could this name maxMenuItems as capping only searched results in the sheet, or could FR20 cap the bootstrap list as well?
| attributes and styles on the same elements; open, in INV7's words, "the | ||
| open layer under a coarse pointer has at most as many elements as under a | ||
| fine pointer (beyond that, only by the open-layer allowance `spec:AST-017` | ||
| FR60 and DEC-29 set)". DEC-3 records that allowance for the closed |
There was a problem hiding this comment.
spec:AST-017 FR60 and DEC-29 set an allowance of up to 5 more elements for an open layer, and none for a closed trigger. "Anything beyond this needs an owner exception with before and after counts." The open sheet's 165 elements against 157, and the trigger that replaces the input, are that owner exception, which is what DEC-3 (L520) calls them. Could this read "DEC-3 records the owner exception FR60 requires beyond that allowance, for the closed adaptive trigger and the open sheet", with the verification row (L446) saying "two-part exception"?
There was a problem hiding this comment.
This spec change waits on an engineering owner's decision on component:Tokenizer DEC-3 and the family:input-fields FR7 amendment it relies on, and one record fix belongs on the head that decision approves; the three fixes asked of the previous head are in. AR6 (L343–L347) defines the sheet trigger's accessible description as the selected count and token labels only, so please say that the field's helper text, status message and disabled reason, which the inline input carries in aria-describedby, stay in that description and in what order, since spec:AST-085 FR5 requires a disabled control to expose its description and disabled reason on itself, and list that case in the sheet verification row. Once that is in, any engineering owner, the author included, records the decision by commenting /approve-spec followed by the full head SHA.
[Automated review]
| and its accessible description is the selected count followed by the | ||
| selected token labels, so the selection the sighted person sees is exposed | ||
| to assistive technology as the trigger's description; the labels are the | ||
| rendered tokens themselves, each referenced by id from `aria-describedby` | ||
| after a hidden count, so no label renders twice |
There was a problem hiding this comment.
This makes the trigger's description the count and token labels only. The inline input's aria-describedby also carries the caller's ids, the field description, the status message and the disabledMessage reason (Tokenizer.tsx L916–L923 and L1046–L1047 on main), Public concepts says description and status keep their meaning, and spec:AST-085 FR5 requires a disabled control to expose "any description or disabled reason" on itself. Could AR6 say those stay in the trigger's description, and in what order relative to the count and labels, with the sheet verification row listing a trigger that has a description and an error status?
…on (DEC-3) component:Tokenizer gains DEC-3: the touch surface is the presentation option of the optional experimental prop object (popover | bottom-sheet | adaptive), resolved through the shared compact-touch policy, with the single searchable sheet's behavior recorded as FR16-FR24, AR6 and PR6. Omitting experimental keeps the inline combobox. Rows main does not meet yet carry implementation gap notes.
There was a problem hiding this comment.
This spec change is ready for an engineering owner's decision: the AR6 trigger-description fix asked of the previous head is in, and no record fix remains. Approval of component:Tokenizer DEC-3 and the family:input-fields FR7 amendment waits on an engineering owner, the author included, commenting /approve-spec followed by the full head SHA, because the earlier approval named an earlier commit. Approving as written also accepts that hasAutoFocus, listed as inert in the sheet presentation, still moves focus onto the trigger under adaptive but not under a forced bottom-sheet.
[Automated review]
|
/approve-spec 538789a |
Exact decision needed
Whether
Tokenizeroffers its touch surface — a trigger that opens one searchable modal bottom sheet in place of the inline combobox — and through which public boundary. The record iscomponent:Tokenizer(packages/core/src/Tokenizer/Tokenizer.spec.md,current), so a new public surface needs a record amendment before the implementation lands, following the shapecomponent:DrawerDEC-4 set forexperimental.isFullWidthOnMobile.Existing authority searched
component:Tokenizer(current): FR1–FR15, AR1–AR5, DEC-1, DEC-2. No row admits a second presentation; the Public concepts section says no public concept is introduced. Nothing settles how a touch surface is exposed.spec:AST-017(current) FR21/FR22: an experimental prop on a stable component lives inside one optionalexperimentalobject; omitting it preserves stable behavior; declarations carry@experimental, docs carrystability: 'experimental'; changes ship as[experimental]patch Changesets.component:DrawerDEC-4 is the precedent that applies it.component:SelectorFR4 and its Compatibility section,component:MultiSelector,component:DropdownMenu:presentationis'popover' | 'bottom-sheet' | 'adaptive',adaptiveresolves to the sheet on compact coarse-pointer screens. Their pending default change (DEP-0011/CLN-0020, popover → adaptive) is a stable-default decision for those components and does not reach Tokenizer.component:BottomSheet(current) andfamily:overlay-dismissal(current): the sheet's dialog, gestures, keyboard accommodation, and dismissal ordering stay with their owners; the amendment names them and claims nothing of theirs.spec:AST-085(current) FR1/FR2: disabled controls stay focusable witharia-disabledand refuse activation — the trigger and the at-limit rows follow it.spec:AST-002(current) DEC-1: no new geometry or peer-identity API; the amendment adds none.spec:AST-086(current) FR14 and Platform support: the shared announcement channel on the page body is not exposed while a native modal is open, so a message from an owner inside an Astryx modal is delivered through that modal's own persistent containers by the publicuseAnnounce; the sheet usesshowModal(), so AR6 cites FR14 for every announcement made from inside it.packages/core/src/Tokenizer/Tokenizer.tsx,TokenizerProps,@astryxdesign/core/Tokenizer. Open pull requests editingTokenizer.tsx(fix(Tokenizer): place the caret after the last token #7922, fix(Tokenizer): wrap tokens before the clear-all lane #7375, feat(Typeahead): show local matches at once and append server results (spec:AST-092) #7962, fix(core): announce loading from busy owners, not composed Spinners #7940, fix(Tokenizer): suppress Create option for already-selected labels (#6366) #6369, fix(Tokenizer): keep choices open after selection #6360, feat(Tokenizer): add isLoading and changeAction #6000, feat(core): make Tokenizer InputGroup-compatible #4405, feat(Tokenizer): split delimited text into separate tokens on type and paste (#4175) #4329) change the inline combobox; none adds a presentation. The earlier touch draft (feat(core): Tokenizer fits the pointer — a chip row and a sheet on a finger #5315) switches automatically onpointer: coarsewith no opt-in, which FR21 rules out for a stable component; this record supersedes that direction. The Lab prototype inpackages/lab/src/MobileTokenizer/is the touch-flow evidence; feat(core): BottomSheet stacks by open order, replaces on a same-update switch, and BottomSheetSwitcher is deprecated #7648 edits it and is not touched.Decision
component:TokenizerDEC-3 (deciderimdreamrunner, 2026-10-23). The record's frontmatter carriesauthority: current,approved_by: imdreamrunner,approved_at: 2026-10-23from the owner's decision; the current head still needs the engineering owner's/approve-spec <full head SHA>comment, which records the decision for this head.Two decision points settled by the owner and recorded in DEC-3:
Value set:
adaptive, notadaptive-bottom-sheet. Tokenizer picks several items from a searchable list, so its nearest siblings areMultiSelectorandSelector, which useAdaptivePresentation('popover' | 'bottom-sheet' | 'adaptive'). The date and time fields usePickerPresentation(utils/inputPresentation.ts) because they have a native-picker axis that Tokenizer lacks. DEC-3 carries this as a one-line rejected alternative.No new theming targets while experimental. The record says it twice, in Theming and in DEC-3: the sheet presentation exposes the field's existing
tokenizertarget on the trigger; the sheet's own parts carry no Tokenizer theming targets while the option is experimental (spec:AST-017FR27) and are themed through the targets of the components they compose (BottomSheet, Button, CheckboxIndicator, EmptyState, the panel search field); sheet targets follow the option's promotion.Surface:
experimental.presentation,'popover' | 'bottom-sheet' | 'adaptive', inlined on the prop; no experimental hook or type is exported.popover— the value whenexperimentalor the option is omitted — is today's inline combobox with its anchored suggestion menu, unchanged.adaptiveresolves through the same shared compact coarse-pointer policy the selection controls use, so a Tokenizer and a Selector on one page switch together; a resolution change while the sheet is open applies after it closes and focus has returned to the trigger; whenever the presentation changes while its control holds focus — then, or while the sheet is closed, as when a phone rotates across the breakpoint after Done — focus moves to the new presentation's control, the combobox input or the trigger, never to the page body (FR16,spec:AST-085FR4).Scope: FR1–FR15, AR1–AR5, and the first six design rows describe the
popoverpresentation; FR16–FR24, AR6, PR6, and the sheet rows describe the sheet presentation, which has no inline combobox, suggestion menu, or end lane — so FR17/FR19 and FR1/FR13 no longer read as contradicting one another.Trigger (FR17): one button named by the field label with
aria-haspopup="dialog"/aria-expanded, showing the selected tokens without remove controls or the placeholder, wrapping tokens, at the inline field's height for itssizestep (as Selector's bottom-sheet trigger); opens one tall modal BottomSheet named by the label. Disabled: focusable,aria-disabled, does not open, showsdisabledMessage; a field that becomes disabled while its sheet is open closes the sheet and returns focus to the trigger (spec:AST-085FR2), as FR15 closes the inline popup.Base props (Public concepts): in the sheet presentation the trigger is the control —
ref,id,data-*,aria-*, and the other base props reach it (family:input-fieldsFR10,architecture:public-component-apiINV8),handleRefacts on it,onFocus/onBlur/onKeyDownreport its events. Thereftype follows the opt-in, and the record now says how: the exportedTokenizerPropsremains an interface withref?: Ref<HTMLInputElement>as released (sointerface X extends TokenizerProps<T>andOmit<TokenizerProps<T>, …>keep compiling), and the component's own parameter type is where the widening lives — withexperimentalpassed,refalso acceptsRef<HTMLInputElement | HTMLButtonElement>. The summary table's Public contract row says the same. The props with no sheet surface (renderItem,renderToken,endContent,hasClear,hasEntriesOnFocus,menuWidth,tokenOverflowBehavior,hasAutoFocus) emit one development-mode warning naming the prop rather than being dropped silently (INV3), matching Selector's warning for an option that carriesaction.One sheet (FR18): the shared panel search field on top, a scrolling list of rows, and — while the query is empty — a footer with Clear all and Done. No second sheet. Done closes and clears the query; swipe/scrim/Escape follow
purpose="info"; focus returns to the trigger.Rows (FR19, FR23):
checkboxrows named by item label; toggling commitsadd/removeimmediately and announces with the inline field's words through the sheet's own live region; a row renders an item without itsactionand warns once in development, as the inline panel does (spec:AST-058FR9); order is fixed while open (selected-at-open invalueorder, then selected items absent from results, then results) and a toggle never moves a row; the next open re-snapshots. AtmaxEntries, unchecked rows arearia-disabledand inert, checked rows still toggle.Sheet surface (FR18): on open, focus moves to the sheet's panel, not the search field, so the reading order starts at the top of the sheet and the on-screen keyboard rises only when the person reaches for the search (WCAG 2.2 SC 2.4.3 Focus Order and 3.2.1 On Focus through
spec:AST-020FR1; Selector's AR2 is the precedent for naming the destination); the representative states carry both FR18 rules — "open" says focus is on the panel, a new "dismissed" state says the query is empty after a swipe, scrim, Escape or Done close with focus back on the trigger — and the sheet verification row lists them; every close — Done, swipe, scrim, Escape — clears the query, so the next open lists the bootstrap set (the Lab prototype kept the query; one rule for every close is simpler to state and to test, and "Done clears it" alone never applied, since Done shows only while the query is empty).adaptiveon the server and at hydration (FR16, aligned with PowerSearch's FR10):adaptiveserver-renders and hydrates as the inline field — the resolver answerspopoverwhere there is nomatchMediaand in the hydrating render — and on a compact coarse-pointer client swaps to the trigger in the first render after hydration; DEC-3 records thearchitecture:component-style-authoringINV7 /spec:AST-017FR60 exception in two parts — the closedadaptivefield, which renders a different element (the trigger button with the selected tokens) in place of the inline input, its token remove actions and overflow handling on a compact coarse-pointer client, removing elements rather than adding them (154 fine / 120 coarse at rest, under the fine count); and the open sheet — with measured counts (Chromium, Experimental: adaptive story: at rest 154 fine / 120 coarse; open at the query "e" with no footer: 157 fine (the inline field with its menu on the one matching item, plus the two page live regions) / 165 coarse, reconciled exactly as 120 at rest − 2 (the app-global surfaces host the open modal re-hosts) + the dialog of 47, broken down by the record's own parts — BottomSheet dialog/panel/handle/edge tint 7, sheet body 2, search row 19, list 16, live region 1, re-hosted surfaces host 2 — with a pending Clear all confirmation adding 20 and the empty-query footer 9; forcedbottom-sheet114/165 under either pointer) and the reason — a different surface replacing an anchored listbox, not the same one restyled; at rest the coarse pointer never has more elements. AV7's proviso states INV7's at-rest rule (same elements) and quotes its open-layer budget verbatim, allowance clause included; the closedadaptivetrigger (a different element, which no allowance covers) and the open sheet (more than FR60's five extra elements) are named as the owner exceptions DEC-3 records beyond that allowance, with before/after counts — the same wording in the verification row. The compact-touch resolution rule is cited to its ownerspec:AST-094/FR13(compact:spec:AST-012/FR9) in the ownership bullet and FR16, and the toast outlet the open modal re-hosts tospec:AST-003/FR18; those owners joinsystem_specs. FR20 states the empty-query bootstrap set is capped atmaxMenuItems, as the inline field caps its focus bootstrap (parity; the code already did). FR16 records anarchitecture:layer-runtimeINV14 gap: the presentation hold belongs to the layer runtime, which does not yet provide one, so the field keeps its own until it does. Earlier clause kept: AV7's proviso ("…beyond that, only by the open-layer allowancespec:AST-017FR60 and DEC-29 set"), so DEC-3's exception reads as that allowance for the closedadaptivetrigger and the open sheet; the sheet verification row lists the counts. The owner's/approve-speccovers the exception. FR20: with an empty query and nothing to list — nothing selected and an empty bootstrap set, as a search-only source has — the sheet shows no list, no message, and announces nothing, only the search field above the footer, as the inline field opens no menu and reports nothing for a bootstrap that returned nothing; "No items available" is gone from the record (the verification row lists the search-only case and the list once a query finds items). The trigger's accessible description is everything the inline input's is, in its order — the caller'saria-describedbyids, the fielddescription, the status message, thedisabledMessagereason when disabled (spec:AST-085FR5, now insystem_specs) — followed by the selected count and the selected labels; the sheet verification row lists the disabled-trigger case. The count and labels: the labels being the rendered tokens themselves referenced by id after a hidden count, so no label renders twice (AR6, witharchitecture:component-style-authoringINV7). The record namesspec:AST-099as the owner of the sheet's on-screen keyboard behavior in the ownership bullet and the Content boundary.family:input-fieldsFR7 is amended in this pull request (knowledge-contracts DEC-1: a conflict between current records is resolved at the canonical owner): busy semantics go on the combobox — or, in a presentation without a combobox, such as Tokenizer's sheet (DEC-3), on the result list the search updates; FR20 cites that clause. Also: nothing is focused before interaction, so focus is undisturbed (document.activeElementstays the body) — except withhasAutoFocus, whose server-renderedautofocusfocuses the inline input on load: the record says the swap then moves that focus to the trigger, the same element and visit for the caller, so noonBlur/onFocuspair is reported, and the verification row lists the case (the SSR test covers it). The exact layout effect is stated: the trigger keeps the field's box — same width, top, label andsizeblock size — except the inline input's own row: when the tokens leave the input no room on their row the inline field wraps the input onto a row of its own, and the trigger, which has no input, drops that row (onemdrow, 27px, measured on a 390 and a 414px phone with five tokens); with room for the input beside the tokens nothing shifts. The "adaptiveresolution" state row says the same; the verification row lists the server-render-then-hydrate test (Tokenizer.ssr.test.tsx, in the implementation pull request and itsverified_by) and the 390/414px box measurement. Evidence (Chromium 149, iPhone profile, same coarse CSS media in both runs, the resolver forced topopoverfor the hydrating render): 390px — hydrating field 358×59 (five tokens on two rows, the input on the second), trigger 358×32 (one row), same top and label; 414px — 382×59 → 382×32, same;activeElementbody in both.Batches reach assistive technology (FR21): the rows sit in a
listwhoselistitemwrappers carryaria-setsize(the total) andaria-posinset— thecheckboxrole admits neither, and axe 4.12.1 reports a criticalaria-allowed-attrwhen they sit on the checkbox — so a person moving through the rows can learn more exist before the list has scrolled; the record says that whether a screen reader speaks the position ("n of total") from the focused checkbox depends on the screen reader and names the accessibility tree as the proof of thelist/listitem/aria-setsize/aria-posinsetexposure (spec:AST-009FR8, listed undersystem_specs); focus reaching the last rendered row renders the next batch (WCAG 2.2 SC 1.3.1 and 4.1.2 throughspec:AST-020FR1). Clear all (FR24): confirming moves focus to the search field, the destination this record names (spec:AST-085FR4; SC 2.4.3): the clear is spent and the search field is where the next selection starts; a disabled Clear all stays focusable per AST-085 FR1, so the record no longer says otherwise; cancelling returns focus to Clear all. AR6: citesspec:AST-086/FR14exactly as it stands onmain; its description claim now says the selection is exposed as the trigger's description, that whether a screen reader reads it with the button depends on the screen reader, and that the DOM and accessibility tree prove thearia-describedbyexposure (spec:AST-009FR8). Closes (FR18): Done, swipe, scrim and Escape clear the query and report it throughonChangeQuery; a close the field forces by becoming disabled keeps the query, since nothing the person did changed it (the "dismissed" state says so).FR20 timing and focus: the bootstrap set is requested at once on open and on clearing the query, so an instant source shows no busy feedback and announces no busy start; an instant source with an empty bootstrap set and nothing selected shows only the search field and announces nothing (the search-only case above); only a typed query waits
debounceMs; when a settled search removes the row that holds focus, focus moves to the search field (spec:AST-085FR4), as FR16, FR18, FR22 and FR24 name their destinations; both are in the verification row.Search (FR20), batches (FR21), create (FR22), Clear all (FR24): a query shorter than
minQueryLengthis not searched — it lists only the selected rows it matches and, withhasCreate, the create row, with no busy feedback, empty state or announcement, as the inline field shows nothing for such a query; a rejecting source settles the search to no results, as the inline field does; activating the create row commits, clears the query, and moves focus to the sheet's search field, since the row it was on is gone; from open and from each query change until that search settles the list is pending — named busy feedback above any rows still listed from the previous search, with the list element carrying the busy semantics (aria-busy) since the sheet has no combobox (family:input-fieldsFR7), never "No items" or "No results" during the debounce, as the lab prototype held its empty state back; then bootstrap on an empty query,minQueryLength/debounceMs/maxMenuItems/onChangeQuery/emptySearchTextkeep their meaning; 50-row batches advanced within 200px of the list end, reset on open and on query change; withhasCreateaCreate "{text}"row with a trailingAdd {text}action commits acreatechange and clears the query, absent at the limit; Clear all is disabled when empty —aria-disabled, still in the focus order, a tooltip saying nothing is selected, a click opens nothing (spec:AST-085FR1) — otherwise confirms through an AlertDialog before committing[]; the confirmed clear is announced from inside the sheet ("All items removed", AR6).Inert props in the sheet presentation (Public concepts):
renderItem,renderToken,endContent,hasClear,hasEntriesOnFocus,menuWidth,tokenOverflowBehavior,hasAutoFocus, and the inputref;handleRef.focus()focuses the trigger.AR6: sheet semantics: the trigger is a button named by the field label with
aria-haspopup="dialog"andaria-expanded, and its accessible description lists the selected token labels — exposed as the trigger's description; whether a screen reader reads it with the button depends on the screen reader, and the DOM and accessibility tree prove thearia-describedbyexposure (spec:AST-009FR8; WCAG 2.2 SC 1.3.1 and 4.1.2 throughspec:AST-020FR1); the sheet is a dialog named by the label with aSearch {label}field whose clear is named; rows arecheckboxcontrols toggled by tap or click and by Space and Enter (spec:AST-020FR15; WCAG 2.2 SC 2.1.1 and 4.1.2); a disabled row and a disabled trigger usearia-disabledand keep their focus order; while the sheet is open, additions, removals, a confirmed Clear all ("All items removed", made once the confirmation has closed, since a message sent while the confirmation covers the sheet is not heard —spec:AST-086FR14; FR24 states the same moment and the verification row lists it), the search's busy start, and how each search ended (the result count for a non-empty query, or the empty state's text when it shows —spec:AST-056FR3) are announced once per occasion from inside the sheet throughuseAnnounce, whichspec:AST-086FR14 (current onmain) routes into the persistent polite container of the sheet's own dialog while it is open (WCAG 2.2 SC 4.1.3); after the sheet closes focus returns to the trigger and the field's announcements follow the same FR14 route from wherever the field renders. The Sheet trigger and Sheet footer design rows name Token and Button plainly (they are not record ids). PR6: the sheet surface is opt-in cost — not loaded for thepopoverpresentation, loaded on demand for the sheet presentation, nothing rendered on the server or in the hydrating render.Supporting rows: Compatibility bullet, Ownership bullets (owns the touch surface; does not own the adaptive rule or BottomSheet's mechanics), AV7, six representative states, four Design-relationship anatomy rows, a verification-map row, the Compatibility bullet reads "the stable public API … unchanged";
system_specsgainsspec:AST-017/FR21,spec:AST-020/FR1,spec:AST-056/FR3,spec:AST-058/FR9,spec:AST-085/FR1,spec:AST-085/FR4, andspec:AST-086/FR14(current onmain), and one open question (whether rows and trigger should honorrenderItem/renderToken).Every row main does not meet carries a present-tense "Implementation gap" note, as the BottomSheet promotion did (including the
reftype statement, which main does not meet); the implementation pull request deletes those notes and adds its test file toverified_by.Rejected alternatives
pointer: coarsewith no opt-in: changes stable behavior for every existing caller, whichspec:AST-017FR21 forbids outside an experimental boundary.presentationprop, matching the selectors today: the surface has no consumer evidence yet, and a stable prop cannot be changed or removed in a patch.BottomSheetSwitcher: there is only one sheet, and the switcher is headed for deprecation; a standaloneBottomSheetdriven byisOpenis the same dialog with less machinery.PickerPresentationvalue set (adaptive-bottom-sheet): its native-picker axis has no counterpart in a list picker; the selection controls'AdaptivePresentationis the right family.Each is one
Rejected:line inside DEC-3.Claim scope and non-goals
The record governs the experimental option, its resolution, and the observable behavior of the trigger and sheet listed above. It does not decide the stable default (whether
adaptiveever becomes Tokenizer's default follows the selection controls' own lifecycle, outside this surface), the sheet's visual tuning, BottomSheet's dialog and gesture contract, the shared compact-touch query, orrenderItem/renderTokensupport in the sheet (left open).Evidence and implementation relationship
adaptivetests stub the compact-touch query both ways; the implementation's presentation tests mirror them.feat/tokenizer-experimental-bottom-sheet) addsexperimental.presentation, the sheet surface, i18n keys, docs, a story, tests, and the[experimental]Changeset, and deletes the gap notes; it adds no theming targets.Writing rubric results
Rejected:entries.#+ digits,/pull/,/issues/). The summary table's former#6360reference is gone with the rewritten Compatibility/readiness row; the pre-existing#5804link in Intent and Family relationships is unchanged.Scope
component:BottomSheetandfamily:overlay-dismissaldepend on it; the shared panel search field is named so the sheet reads as part of the panel, as the selection controls' panels do.Validation
pnpm check:knowledge— "Knowledge templates and records are aligned."packages/core/src/Tokenizer/Tokenizer.spec.mdformatted.git diff --check— clean.