From d88563a58b90381ab3d15cef4eb11910886a144f Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Mon, 26 Oct 2026 16:37:46 -0700 Subject: [PATCH 01/17] =?UTF-8?q?spec(family):=20action=20groups=20?= =?UTF-8?q?=E2=80=94=20press,=20slide,=20act=20on=20lift,=20one=20tracker?= =?UTF-8?q?=20(AST-094=20FR26=E2=80=93FR28,=20DEC-17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/families/action-groups.md | 176 ++++++++++++++++++ .../specs/AST-094-touch-presentations/spec.md | 16 ++ 2 files changed, 192 insertions(+) create mode 100644 docs/families/action-groups.md diff --git a/docs/families/action-groups.md b/docs/families/action-groups.md new file mode 100644 index 0000000000000..a92fe537ea6c3 --- /dev/null +++ b/docs/families/action-groups.md @@ -0,0 +1,176 @@ +--- +schema_version: 1 +template_version: 1 +kind: family +id: family:action-groups +authority: current +archive_reason: null +superseded_by: null +approved_by: cixzhang +approved_at: 2026-10-26 +owners: [cixzhang] +review_triggers: [behavior, interaction, accessibility] +verified_by: + [ + packages/core/src/hooks/useMenuPress.test.tsx, + packages/core/src/hooks/menuPressGesture.test.ts, + ] +members: + [ + component:DropdownMenu, + component:DropdownMenuSubMenu, + component:ContextMenu, + component:Selector, + component:MultiSelector, + component:DateInput, + component:SegmentedControl, + component:AlertDialog, + component:Dialog, + component:ButtonGroup, + component:ToggleButtonGroup, + component:Toolbar, + component:Banner, + component:Toast, + component:Card, + ] +architecture: [architecture:interaction-modality] +contributing: [] +deciding_specs: [spec:AST-094/DEC-17] +--- + +# Action groups contract + + + +```json +{ + "scope": "global", + "triggers": { + "interaction": ["FR1", "FR2", "FR3", "FR4", "FR5"] + } +} +``` + +## Intent + +A person can press one action in a group of sibling actions, slide across the +others while the highlight follows, and act on the one they lift on, the same +way in every group. One internal module, `actionGroupTracker`, gives every +member the same behavior, so no member builds its own. + +## Membership rule + +A component is a member when it renders a set of two or more sibling +momentary, toggle, or option actions that a person chooses one of. Members +today: + +- menus: DropdownMenu, ContextMenu, and their sub-menus; +- option lists: Selector's and MultiSelector's options, and the date wheels of + DateInput and DateTimeInput; +- SegmentedControl; +- AlertDialog's actions, and Dialog's actions and footer area, not arbitrary + dialog content; +- ButtonGroup and ToggleButtonGroup; +- Toolbar's buttons; a field inside a toolbar is skipped; +- the action rows of Banner, Toast, and Card when a row has two or more + sibling actions. + +A component built later that matches the rule joins by the rule, including an +action sheet, a tab bar or navigation bar, a filter-chip group, and a rating. + +These are not members: + +- text inputs and InputGroup's input, which keep the caret and selection; +- TabList, whose scrollable rows need a drag to scroll and whose tabs switch + views; +- Pagination and Stepper, whose items are navigation destinations; +- RadioList, CheckboxList, and SelectableCard, whose rows and cards sit in + scrolling lists; +- Token chips, which are removable and have no group control. + +## Shared owner + +`actionGroupTracker` is the one internal module that tracks a held pointer +across a group's actions. `module:DropdownMenu/useMenuPress` converges onto +it. It is internal and unexported (`spec:AST-017` FR21). + +## Canonical concepts + +- **Action group:** two or more sibling actions a person chooses one of. +- **Held pointer:** a press that has not lifted, tracked across the group. +- **Lifted action:** the enabled action under the pointer when it lifts. + +## Cross-component invariants + +- **FR1 — The lift decides.** In a member, a press on an action that slides + across its siblings acts on the action the pointer lifts on, through that + action's own activation; a press that never leaves its action acts on it. + One gesture acts at most once (`spec:AST-094` FR26). +- **FR2 — The highlight follows the pointer.** While the pointer is held, the + enabled action under it is highlighted, at most one at a time, and nothing + is highlighted over a disabled action, a gap, or outside the group + (`spec:AST-094` FR27). +- **FR3 — Sliding off cancels.** A press that lifts outside the group acts on + nothing; a cancelled pointer, a second finger, or the window losing focus + ends the gesture with nothing acting (`spec:AST-094` FR28). +- **FR4 — A toggle group toggles only the lifted action.** In a toggle group, + only the action lifted on toggles; sliding across never toggles the actions + passed over. +- **FR5 — Every pointer, one target rule.** The behavior applies to every web + pointer, mouse and pen as well as a finger. The 44 px target applies under a + coarse pointer only. Keyboard and assistive-technology behavior is + unchanged. + +## Allowed component variation + +- What an action does on activation, and how a member shows its highlight, + stay with the member. +- Menus keep their own opening and handoff (`spec:AST-094` FR29) and their + scrolling rules. + +## Representative matrix + +| Member and state | Shared invariant | Deliberate variation | +| -------------------------------------- | --------------------------------------------------- | ----------------------------------------------- | +| DropdownMenu / slide from row to row | the lifted row acts; one row lit at a time | opening and handoff stay the menu's (FR29) | +| SegmentedControl / slide across | the lifted segment is selected | the selected surface's look stays the control's | +| AlertDialog / slide from Cancel | the lifted action runs; a lift outside does nothing | the actions' order and stacking stay local | +| ToggleButtonGroup / slide across three | only the lifted toggle changes | single or multiple selection stays the group's | +| Toolbar with a field / slide across | buttons track; the field is skipped | which controls a toolbar holds stays local | + +## Adoption and exceptions + +| Components or surface | Required adoption | +| --------------------------------------------- | ------------------------------------------------------------------------------ | +| Menus, Selector, MultiSelector, date wheels | Track through `actionGroupTracker`; `useMenuPress` converges onto it. | +| SegmentedControl | Track through `actionGroupTracker`. | +| AlertDialog, Dialog's actions and footer area | Track the actions only; dialog content outside the action area is not a group. | +| ButtonGroup, ToggleButtonGroup, Toolbar | Track sibling buttons; a field inside is skipped; toggles follow FR4. | +| Banner, Toast, and Card action rows | Track only a row with two or more sibling actions. | + +These are required outcomes, not a claim of completed adoption. The excluded +components listed under the membership rule are not exceptions to approve; +they are outside the family. + +## Verification map + +| Contract | Verification | Representative members and states | +| -------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| FR1–FR3 | `actionGroupTracker` tests and each member's pointer-stream tests | a slide across a menu, a SegmentedControl, AlertDialog's actions, a ButtonGroup; a lift outside | +| FR4 | ToggleButtonGroup pointer-stream tests | a slide across three toggles; only the lifted one toggles | +| FR5 | Member tests with a mouse, a pen, a finger, the keyboard, and a screen reader | a 44 px target under a coarse pointer only; unchanged keyboard and screen-reader paths | + +## Decision links + +- `spec:AST-094/DEC-17` — every group of sibling actions tracks a held + pointer the same way. + +## Open questions + +None. + +## Content boundary + +This file owns action-group membership and the shared press, slide, and lift +behavior. It does not duplicate component prop tables, what each action does, +menu opening, or the tracker's thresholds. diff --git a/docs/specs/AST-094-touch-presentations/spec.md b/docs/specs/AST-094-touch-presentations/spec.md index 94fda268d7a00..752db932a98e7 100644 --- a/docs/specs/AST-094-touch-presentations/spec.md +++ b/docs/specs/AST-094-touch-presentations/spec.md @@ -315,6 +315,10 @@ condition; FR34 applies to menus; FR31 and FR35 apply to anchored menus and to Popover under a finger; FR32 and FR33 apply to menus only. A mouse keeps today's behavior. +FR26–FR28 are the shared rule of `family:action-groups`: every member, not +only menus, follows them under the family's scope, governed by the one +internal `actionGroupTracker` (DEC-17). + - **FR26 — The lift decides.** A menu row MUST act when the finger lifts over it, never because the press began on it; a press that never left its row is a tap and acts on that row. One gesture acts at most once: the browser's @@ -702,6 +706,18 @@ iOS 26.2 simulator measures it. Rejected: the 150 ms delay on every menu row. +### DEC-17 — Every group of sibling actions tracks a held pointer the same way + +**Reference:** `spec:AST-094/DEC-17` +**Decider:** `cixzhang`, `2026-10-26` + +A person who presses one action and slides to another expects the same result +in a menu, a segmented control, an alert's buttons, or a button group. One +internal tracker governs every group of sibling actions, so the behavior is +consistent and built once (`family:action-groups`). + +Rejected: a tracking rule for menus only. + ## Open questions - **OQ1 — Scroll view bounce-back curve.** What curve brings a scroll view From 99d61013805329e45e17a4e907c8de67f7f4b329 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Mon, 26 Oct 2026 16:40:06 -0700 Subject: [PATCH 02/17] spec(family): SegmentedControl is a named exception to action groups --- docs/families/action-groups.md | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/families/action-groups.md b/docs/families/action-groups.md index a92fe537ea6c3..898f29510ce6f 100644 --- a/docs/families/action-groups.md +++ b/docs/families/action-groups.md @@ -23,7 +23,6 @@ members: component:Selector, component:MultiSelector, component:DateInput, - component:SegmentedControl, component:AlertDialog, component:Dialog, component:ButtonGroup, @@ -55,7 +54,8 @@ deciding_specs: [spec:AST-094/DEC-17] A person can press one action in a group of sibling actions, slide across the others while the highlight follows, and act on the one they lift on, the same -way in every group. One internal module, `actionGroupTracker`, gives every +way in every group, with SegmentedControl's richer model as the one named +exception. One internal module, `actionGroupTracker`, gives every member the same behavior, so no member builds its own. ## Membership rule @@ -67,7 +67,6 @@ today: - menus: DropdownMenu, ContextMenu, and their sub-menus; - option lists: Selector's and MultiSelector's options, and the date wheels of DateInput and DateTimeInput; -- SegmentedControl; - AlertDialog's actions, and Dialog's actions and footer area, not arbitrary dialog content; - ButtonGroup and ToggleButtonGroup; @@ -78,6 +77,10 @@ today: A component built later that matches the rule joins by the rule, including an action sheet, a tab bar or navigation bar, a filter-chip group, and a rating. +SegmentedControl is a named exception: its thumb follows the finger and moves +the selection, a richer model that `component:SegmentedControl` governs. It may +use `actionGroupTracker` internally, but this family's rule does not bind it. + These are not members: - text inputs and InputGroup's input, which keep the caret and selection; @@ -130,20 +133,18 @@ it. It is internal and unexported (`spec:AST-017` FR21). ## Representative matrix -| Member and state | Shared invariant | Deliberate variation | -| -------------------------------------- | --------------------------------------------------- | ----------------------------------------------- | -| DropdownMenu / slide from row to row | the lifted row acts; one row lit at a time | opening and handoff stay the menu's (FR29) | -| SegmentedControl / slide across | the lifted segment is selected | the selected surface's look stays the control's | -| AlertDialog / slide from Cancel | the lifted action runs; a lift outside does nothing | the actions' order and stacking stay local | -| ToggleButtonGroup / slide across three | only the lifted toggle changes | single or multiple selection stays the group's | -| Toolbar with a field / slide across | buttons track; the field is skipped | which controls a toolbar holds stays local | +| Member and state | Shared invariant | Deliberate variation | +| -------------------------------------- | --------------------------------------------------- | ---------------------------------------------- | +| DropdownMenu / slide from row to row | the lifted row acts; one row lit at a time | opening and handoff stay the menu's (FR29) | +| AlertDialog / slide from Cancel | the lifted action runs; a lift outside does nothing | the actions' order and stacking stay local | +| ToggleButtonGroup / slide across three | only the lifted toggle changes | single or multiple selection stays the group's | +| Toolbar with a field / slide across | buttons track; the field is skipped | which controls a toolbar holds stays local | ## Adoption and exceptions | Components or surface | Required adoption | | --------------------------------------------- | ------------------------------------------------------------------------------ | | Menus, Selector, MultiSelector, date wheels | Track through `actionGroupTracker`; `useMenuPress` converges onto it. | -| SegmentedControl | Track through `actionGroupTracker`. | | AlertDialog, Dialog's actions and footer area | Track the actions only; dialog content outside the action area is not a group. | | ButtonGroup, ToggleButtonGroup, Toolbar | Track sibling buttons; a field inside is skipped; toggles follow FR4. | | Banner, Toast, and Card action rows | Track only a row with two or more sibling actions. | @@ -154,11 +155,11 @@ they are outside the family. ## Verification map -| Contract | Verification | Representative members and states | -| -------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -| FR1–FR3 | `actionGroupTracker` tests and each member's pointer-stream tests | a slide across a menu, a SegmentedControl, AlertDialog's actions, a ButtonGroup; a lift outside | -| FR4 | ToggleButtonGroup pointer-stream tests | a slide across three toggles; only the lifted one toggles | -| FR5 | Member tests with a mouse, a pen, a finger, the keyboard, and a screen reader | a 44 px target under a coarse pointer only; unchanged keyboard and screen-reader paths | +| Contract | Verification | Representative members and states | +| -------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | +| FR1–FR3 | `actionGroupTracker` tests and each member's pointer-stream tests | a slide across a menu, AlertDialog's actions, a ButtonGroup; a lift outside | +| FR4 | ToggleButtonGroup pointer-stream tests | a slide across three toggles; only the lifted one toggles | +| FR5 | Member tests with a mouse, a pen, a finger, the keyboard, and a screen reader | a 44 px target under a coarse pointer only; unchanged keyboard and screen-reader paths | ## Decision links From 3369b14289a88aecb865f758c3627e692b857799 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Mon, 26 Oct 2026 16:45:45 -0700 Subject: [PATCH 03/17] spec(family): composed footers (Dialog, Card) get the behavior through ButtonGroup --- docs/families/action-groups.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/families/action-groups.md b/docs/families/action-groups.md index 898f29510ce6f..aaea6ef0cb240 100644 --- a/docs/families/action-groups.md +++ b/docs/families/action-groups.md @@ -24,13 +24,11 @@ members: component:MultiSelector, component:DateInput, component:AlertDialog, - component:Dialog, component:ButtonGroup, component:ToggleButtonGroup, component:Toolbar, component:Banner, component:Toast, - component:Card, ] architecture: [architecture:interaction-modality] contributing: [] @@ -67,12 +65,14 @@ today: - menus: DropdownMenu, ContextMenu, and their sub-menus; - option lists: Selector's and MultiSelector's options, and the date wheels of DateInput and DateTimeInput; -- AlertDialog's actions, and Dialog's actions and footer area, not arbitrary - dialog content; +- AlertDialog's actions; - ButtonGroup and ToggleButtonGroup; - Toolbar's buttons; a field inside a toolbar is skipped; -- the action rows of Banner, Toast, and Card when a row has two or more - sibling actions. +- the action rows of Banner and Toast when a row has two or more sibling + actions. + +Action rows that products compose, such as a Dialog's or a Card's footer in a +`LayoutFooter`, get the behavior by using ButtonGroup. A component built later that matches the rule joins by the rule, including an action sheet, a tab bar or navigation bar, a filter-chip group, and a rating. @@ -142,12 +142,12 @@ it. It is internal and unexported (`spec:AST-017` FR21). ## Adoption and exceptions -| Components or surface | Required adoption | -| --------------------------------------------- | ------------------------------------------------------------------------------ | -| Menus, Selector, MultiSelector, date wheels | Track through `actionGroupTracker`; `useMenuPress` converges onto it. | -| AlertDialog, Dialog's actions and footer area | Track the actions only; dialog content outside the action area is not a group. | -| ButtonGroup, ToggleButtonGroup, Toolbar | Track sibling buttons; a field inside is skipped; toggles follow FR4. | -| Banner, Toast, and Card action rows | Track only a row with two or more sibling actions. | +| Components or surface | Required adoption | +| ------------------------------------------- | --------------------------------------------------------------------- | +| Menus, Selector, MultiSelector, date wheels | Track through `actionGroupTracker`; `useMenuPress` converges onto it. | +| AlertDialog's actions | Track the actions only. | +| ButtonGroup, ToggleButtonGroup, Toolbar | Track sibling buttons; a field inside is skipped; toggles follow FR4. | +| Banner and Toast action rows | Track only a row with two or more sibling actions. | These are required outcomes, not a claim of completed adoption. The excluded components listed under the membership rule are not exceptions to approve; From f4f87d28e5da8e83ef13110e52b52c7f40a2d03a Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Mon, 26 Oct 2026 16:46:11 -0700 Subject: [PATCH 04/17] spec(family): any LayoutFooter composes ButtonGroup --- docs/families/action-groups.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/families/action-groups.md b/docs/families/action-groups.md index aaea6ef0cb240..ea54093357c18 100644 --- a/docs/families/action-groups.md +++ b/docs/families/action-groups.md @@ -71,8 +71,8 @@ today: - the action rows of Banner and Toast when a row has two or more sibling actions. -Action rows that products compose, such as a Dialog's or a Card's footer in a -`LayoutFooter`, get the behavior by using ButtonGroup. +Action rows in a Dialog's footer, a Card's footer, or any `LayoutFooter` get +the behavior by composing ButtonGroup. A component built later that matches the rule joins by the rule, including an action sheet, a tab bar or navigation bar, a filter-chip group, and a rating. From 5d22a75b661a789f857fb2a89a75d04cfe3dcec3 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Mon, 26 Oct 2026 16:46:43 -0700 Subject: [PATCH 05/17] spec(AST-094, family): action groups stay on for a fine pointer with one passive listener (FR19/FR24 exception scoped to action groups) --- docs/families/action-groups.md | 9 ++++++--- docs/specs/AST-094-touch-presentations/spec.md | 12 +++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/families/action-groups.md b/docs/families/action-groups.md index ea54093357c18..421009b8d8a8a 100644 --- a/docs/families/action-groups.md +++ b/docs/families/action-groups.md @@ -120,9 +120,12 @@ it. It is internal and unexported (`spec:AST-017` FR21). only the action lifted on toggles; sliding across never toggles the actions passed over. - **FR5 — Every pointer, one target rule.** The behavior applies to every web - pointer, mouse and pen as well as a finger. The 44 px target applies under a - coarse pointer only. Keyboard and assistive-technology behavior is - unchanged. + pointer, mouse and pen as well as a finger. Under a fine pointer a group + keeps one passive `pointerdown` listener on its element and adds document + listeners only during a press, removing them at the release; nothing else + is idle (`spec:AST-094` FR19, FR24). The 44 px target is each member's hit + expansion under a coarse pointer only. Keyboard and assistive-technology + behavior is unchanged. ## Allowed component variation diff --git a/docs/specs/AST-094-touch-presentations/spec.md b/docs/specs/AST-094-touch-presentations/spec.md index 752db932a98e7..6bd3bf678786a 100644 --- a/docs/specs/AST-094-touch-presentations/spec.md +++ b/docs/specs/AST-094-touch-presentations/spec.md @@ -263,7 +263,10 @@ a measured, sourced difference exists. touchscreen whose primary pointer is fine gets the browser's defaults for this record's behaviors (`spec:AST-017` FR59, DEC-25). This rule governs this record's behaviors only; `spec:AST-057` keeps its per-event pointer - read for the row swipe, as its record states. A recognizer that begins MUST claim its touches by cancelling + read for the row swipe, as its record states. A `family:action-groups` member is + the one exception for its own group: under a fine pointer it keeps one + passive `pointerdown` listener on the group element and adds document + listeners only during a press, removed at the release (DEC-17). A recognizer that begins MUST claim its touches by cancelling `touchmove` through a non-passive listener on its element; a watcher, which never claims, is exempt (DEC-7). A `touchmove` of a touch no recognizer owns MUST NOT be cancelled, with two exceptions. While a scroll lock under a modal presentation holds the @@ -450,7 +453,8 @@ internal `actionGroupTracker` (DEC-17). primary pointer is fine (FR19), and while a touch presentation is disabled, no touch-only listener, observer or timer is active, except the released `module:DropdownMenu/useMenuPress` behavior a finger keeps there - (its FR9). The number of `document`- or `window`-level + (its FR9), and an action group's one passive `pointerdown` listener and its + press-time document listeners (FR19). The number of `document`- or `window`-level touch listeners MUST NOT grow with the number of component instances, a repeated row or cell adds no touch observer of its own, and nothing touch-related remains after the last instance unmounts @@ -714,7 +718,9 @@ Rejected: the 150 ms delay on every menu row. A person who presses one action and slides to another expects the same result in a menu, a segmented control, an alert's buttons, or a button group. One internal tracker governs every group of sibling actions, so the behavior is -consistent and built once (`family:action-groups`). +consistent and built once (`family:action-groups`). The behavior works with a mouse +as well, so it stays on for every pointer, while the 44 px target stays +touch-only. Rejected: a tracking rule for menus only. From 0fb1d04f6d4f0c4e98989f2bc244021b20f1493a Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Mon, 26 Oct 2026 19:32:07 -0700 Subject: [PATCH 06/17] spec(family, AST-094): FR11 lets action-group controls act at the lift; click swallower allowed; wheels scroll; DateTimeInput, TimeInput; ownership lines amended --- docs/families/action-groups.md | 19 +++++++++----- docs/families/buttons.md | 4 ++- .../specs/AST-094-touch-presentations/spec.md | 25 +++++++++++++------ packages/core/src/Banner/Banner.spec.md | 3 ++- .../core/src/ButtonGroup/ButtonGroup.spec.md | 3 ++- 5 files changed, 37 insertions(+), 17 deletions(-) diff --git a/docs/families/action-groups.md b/docs/families/action-groups.md index 421009b8d8a8a..5d872e4a19230 100644 --- a/docs/families/action-groups.md +++ b/docs/families/action-groups.md @@ -23,6 +23,8 @@ members: component:Selector, component:MultiSelector, component:DateInput, + component:DateTimeInput, + component:TimeInput, component:AlertDialog, component:ButtonGroup, component:ToggleButtonGroup, @@ -63,8 +65,8 @@ momentary, toggle, or option actions that a person chooses one of. Members today: - menus: DropdownMenu, ContextMenu, and their sub-menus; -- option lists: Selector's and MultiSelector's options, and the date wheels of - DateInput and DateTimeInput; +- option lists: Selector's and MultiSelector's options, and the wheels of + DateInput, DateTimeInput, and TimeInput; - AlertDialog's actions; - ButtonGroup and ToggleButtonGroup; - Toolbar's buttons; a field inside a toolbar is skipped; @@ -108,7 +110,8 @@ it. It is internal and unexported (`spec:AST-017` FR21). - **FR1 — The lift decides.** In a member, a press on an action that slides across its siblings acts on the action the pointer lifts on, through that action's own activation; a press that never leaves its action acts on it. - One gesture acts at most once (`spec:AST-094` FR26). + One gesture acts at most once (`spec:AST-094` FR26). After a slide, focus does what + a click on the lifted action does. - **FR2 — The highlight follows the pointer.** While the pointer is held, the enabled action under it is highlighted, at most one at a time, and nothing is highlighted over a disabled action, a gap, or outside the group @@ -121,9 +124,11 @@ it. It is internal and unexported (`spec:AST-017` FR21). passed over. - **FR5 — Every pointer, one target rule.** The behavior applies to every web pointer, mouse and pen as well as a finger. Under a fine pointer a group - keeps one passive `pointerdown` listener on its element and adds document - listeners only during a press, removing them at the release; nothing else - is idle (`spec:AST-094` FR19, FR24). The 44 px target is each member's hit + keeps one `pointerdown` listener on its element, cancelable where the member + needs it, adds document listeners only during a press and removes them at + the release, and may arm one per-document click swallower from the release + until that gesture's click; nothing else is idle (`spec:AST-094` FR19, + FR24). The 44 px target is each member's hit expansion under a coarse pointer only. Keyboard and assistive-technology behavior is unchanged. @@ -133,6 +138,8 @@ it. It is internal and unexported (`spec:AST-017` FR21). stay with the member. - Menus keep their own opening and handoff (`spec:AST-094` FR29) and their scrolling rules. +- A wheel is a scroll container: a drag scrolls it, as it does today, and a + tap acts on the option under it; only a press that does not scroll tracks. ## Representative matrix diff --git a/docs/families/buttons.md b/docs/families/buttons.md index 6ef0a725e986e..42af32e0a181b 100644 --- a/docs/families/buttons.md +++ b/docs/families/buttons.md @@ -161,7 +161,9 @@ Membership follows public responsibility, not an import of Button or a rendered - **FR11 — A button-specific group has one accessible owner.** ButtonGroup and ToggleButtonGroup MUST expose an accessible group label and propagate their documented size and disabled defaults without removing a member's accessible - name. The group owns only the behavior and surface declared by its group mode. + name. The group owns only the behavior and surface declared by its group + mode, and the press tracking across members that `family:action-groups` + gives it. A ToggleButtonGroup member identified by `value` MUST derive pressed state and selection requests from the group, not its standalone `isPressed`, `onPressedChange`, or `pressedChangeAction`. Member `onPressedChange` and diff --git a/docs/specs/AST-094-touch-presentations/spec.md b/docs/specs/AST-094-touch-presentations/spec.md index 6bd3bf678786a..12f639c4d1846 100644 --- a/docs/specs/AST-094-touch-presentations/spec.md +++ b/docs/specs/AST-094-touch-presentations/spec.md @@ -185,7 +185,9 @@ a measured, sourced difference exists. profiles. A control (UIControl) highlights on touch-down outside scroll views and activates by the browser's click on a lift inside. On the web a control's press ends when the browser takes the touch (FR20), and its - activation stays the browser's click. + activation stays the browser's click, except that a control inside a + `family:action-groups` member activates at the lift through the group's + tracker (FR26). A cell (a table or collection cell) highlights 150 ms after touch-down inside a scroll view, drops the highlight when the scroll wins (FR20), and activates on a lift inside it with no scroll taking the touch (FR20). A control is inside a scroll view when it sits in a scrollable container, @@ -265,8 +267,10 @@ a measured, sourced difference exists. this record's behaviors only; `spec:AST-057` keeps its per-event pointer read for the row swipe, as its record states. A `family:action-groups` member is the one exception for its own group: under a fine pointer it keeps one - passive `pointerdown` listener on the group element and adds document - listeners only during a press, removed at the release (DEC-17). A recognizer that begins MUST claim its touches by cancelling + `pointerdown` listener on the group element, cancelable where the member + needs it, adds document listeners only during a press, removed at the + release, and may arm one per-document click swallower from the release until + that gesture's click (DEC-17). A recognizer that begins MUST claim its touches by cancelling `touchmove` through a non-passive listener on its element; a watcher, which never claims, is exempt (DEC-7). A `touchmove` of a touch no recognizer owns MUST NOT be cancelled, with two exceptions. While a scroll lock under a modal presentation holds the @@ -453,8 +457,8 @@ internal `actionGroupTracker` (DEC-17). primary pointer is fine (FR19), and while a touch presentation is disabled, no touch-only listener, observer or timer is active, except the released `module:DropdownMenu/useMenuPress` behavior a finger keeps there - (its FR9), and an action group's one passive `pointerdown` listener and its - press-time document listeners (FR19). The number of `document`- or `window`-level + (its FR9), and an action group's one `pointerdown` listener, its press-time + document listeners, and its release-armed click swallower (FR19). The number of `document`- or `window`-level touch listeners MUST NOT grow with the number of component instances, a repeated row or cell adds no touch observer of its own, and nothing touch-related remains after the last instance unmounts @@ -521,6 +525,11 @@ internal `actionGroupTracker` (DEC-17). and FR9 under a finger) are stated here (FR26–FR30); FR6's last sentence gives way to the cell press profile (FR11); its mouse rules (FR8 and the mouse halves of FR1–FR3) and its non-gesture rules stay in the module. +- `family:action-groups` members (menus, Selector's and MultiSelector's + options, the date and time wheels, AlertDialog's actions, ButtonGroup, + ToggleButtonGroup, Toolbar's buttons, and Banner's and Toast's action rows): + a press that slides across sibling actions acts on the action it lifts on + (FR26–FR28), under every pointer. - `component:DropdownMenu`, `component:ContextMenu` and `component:Popover`: under a finger they follow FR26–FR35 as each applies. - BottomSheet, Toast, Lightbox, NavigationStack and keyboard dismissal: a @@ -545,7 +554,7 @@ internal `actionGroupTracker` (DEC-17). | FR6 | Unit and browser tests catching an animation mid-flight and retargeting it | catch at peak velocity; retarget with a new spring | a layout read, a jump, or a lost velocity fails | | FR7, FR8 | Component browser tests and storyboards with and without Reduce Motion | entrance, exit, move; a drag and its lift under Reduce Motion | a missing exit, any travel, scale or spring under Reduce Motion other than a coast after a throw and its settle at an edge (`design:reduced-motion` DR2), or an opacity fade over 200 ms fails | | FR9, FR10 | Recognizer unit tests on synthetic streams and browser tests with real touch input | drag, second finger, pinch, page zoom, main-thread block | a second finger that breaks a drag, or a page pinch blocked other than by the author's `touch-action`, fails | -| FR11, FR12 | Press and long-press tests on synthetic streams with explicit timestamps | inside and outside a scroll view; a drag the browser takes; a lift inside; a menu row under a finger; a press on a row of a menu whose rows fit and of one that scrolls | the wrong profile's timing, a press that survives the browser taking the touch, or a control activated other than by the browser's click, a row of a menu whose rows fit that waits 150 ms, or one of a scrolling menu that does not, fails | +| FR11, FR12 | Press and long-press tests on synthetic streams with explicit timestamps | inside and outside a scroll view; a drag the browser takes; a lift inside; a menu row under a finger; a press on a row of a menu whose rows fit and of one that scrolls | the wrong profile's timing, a press that survives the browser taking the touch, or a control outside an action group activated other than by the browser's click, a row of a menu whose rows fit that waits 150 ms, or one of a scrolling menu that does not, fails | | FR13 | Resolver unit tests and a server-render test | coarse and fine pointer; narrow, wide and short; server render; hydrating render | a component that answers the conditions differently, or a touch presentation in the server or hydrating render, fails | | FR14 | Browser tests of a sheet over a scrolled list | scrolled and at its top | a midway handoff fails | | FR15–FR17 | Recognizer unit tests on synthetic streams with explicit timestamps | exclusive, simultaneous, require-to-fail, nested; a watcher beside a claiming winner; a blocked main thread; a pan at 10 pt per axis | a wrong winner, a late deadline, or a translation that keeps the hysteresis, or a watcher failed or cancelled by a claiming winner fails | @@ -716,8 +725,8 @@ Rejected: the 150 ms delay on every menu row. **Decider:** `cixzhang`, `2026-10-26` A person who presses one action and slides to another expects the same result -in a menu, a segmented control, an alert's buttons, or a button group. One -internal tracker governs every group of sibling actions, so the behavior is +in a menu, an alert's buttons, a button group, or a toolbar. One +internal tracker governs every member group of sibling actions, so the behavior is consistent and built once (`family:action-groups`). The behavior works with a mouse as well, so it stays on for every pointer, while the 44 px target stays touch-only. diff --git a/packages/core/src/Banner/Banner.spec.md b/packages/core/src/Banner/Banner.spec.md index 57118b806d736..f7412bf9d5a29 100644 --- a/packages/core/src/Banner/Banner.spec.md +++ b/packages/core/src/Banner/Banner.spec.md @@ -92,7 +92,8 @@ Consumer migration instructions belong in consumer docs and release notes. **Does not own / non-goals** - Button geometry, interaction layers, tooltip painting, or consumer-supplied - `endContent` — delegated to `component:Button`. + `endContent` — delegated to `component:Button`. An action row of two or more + actions follows `family:action-groups`. - A new theme property, prop, variant, or DOM element. ## Public concepts diff --git a/packages/core/src/ButtonGroup/ButtonGroup.spec.md b/packages/core/src/ButtonGroup/ButtonGroup.spec.md index 7e7838f91ace6..bb27d4185e6b2 100644 --- a/packages/core/src/ButtonGroup/ButtonGroup.spec.md +++ b/packages/core/src/ButtonGroup/ButtonGroup.spec.md @@ -78,7 +78,8 @@ Consumer migration instructions belong in consumer docs and release notes. **Does not own / non-goals** - Each member's own surface, naming, activation, pending, and disabled rendering - — owned by `component:Button` and `component:IconButton`. + — owned by `component:Button` and `component:IconButton`. A press that slides across + members acts on the lifted member, as `family:action-groups` owns. - A member's menu or other layer, and keys pressed inside it — owned by that member (for example `component:DropdownMenu`). - Selection state across toggles — owned by `component:ToggleButtonGroup`. From 271b8b16837440e6eb89b441ac13f31417203be3 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Mon, 26 Oct 2026 20:17:13 -0700 Subject: [PATCH 07/17] spec(family, AST-094): rules stated as behavior; the tracker moves to interaction-modality INV13; SegmentedControl exception trimmed; FR24 row --- docs/architecture/interaction-modality.md | 7 +++++++ docs/families/action-groups.md | 21 ++++++++----------- .../specs/AST-094-touch-presentations/spec.md | 14 ++++++------- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/docs/architecture/interaction-modality.md b/docs/architecture/interaction-modality.md index 39be790cdb1f9..568a5b81f7ab1 100644 --- a/docs/architecture/interaction-modality.md +++ b/docs/architecture/interaction-modality.md @@ -165,6 +165,12 @@ work. threshold compares the tracker's release velocity. Lab keeps no copy of it: no Lab component has its own sample buffer, velocity math, or release filter, so Lab reports the same velocity as Core for the same motion. +- **INV13 — Press tracking across a group has one owner.** In + `@astryxdesign/core`, one internal module, `actionGroupTracker`, tracks a + held pointer across a group's actions for every `family:action-groups` + member, and `module:DropdownMenu/useMenuPress` converges onto it. No member + keeps its own copy of the tracking. It is not exported (`spec:AST-017` + FR21). ## Allowed variation @@ -254,6 +260,7 @@ reachability, newer-intent precedence, and the evidence/scope guard were approve | INV8 | Owner-defined intent order, pending, close, cancellation, disablement, and unmount mutations | Older work changes current content, selection, status or announcement, visibility, focus, or activation after owner-defined supersession | | INV9 | Concrete-harm reproduction, representative normal/legacy/no-new-prop paths, and coincident public-delta inventory | Unsupported or aesthetic preference is treated as harm, unaffected behavior changes, or an API, layout, sizing, default, sticky, or exception delta escapes direct authority | | INV10 | Source boundary check over Core and Lab, a Lab-to-Core comparison on one recorded stream, and release-filter unit tests over recorded iOS pan and scroll-view streams | A Core component with its own sample buffer, velocity math, or release filter; a Lab component with its own sample buffer, velocity math, or release filter; a Lab component reporting a different velocity than Core for the same stream; an export of the tracker; a non-zero release after a still pause | +| INV13 | Source boundary check over Core and each family member's pointer-stream tests | A member that tracks a held pointer across its actions with its own code, or a public export of the tracker, fails | Real-browser evidence is required when the behavior depends on `:focus-visible`, pointer capability, touch or pen synthesis, rendered focus diff --git a/docs/families/action-groups.md b/docs/families/action-groups.md index 5d872e4a19230..099f4bd71d31f 100644 --- a/docs/families/action-groups.md +++ b/docs/families/action-groups.md @@ -54,9 +54,9 @@ deciding_specs: [spec:AST-094/DEC-17] A person can press one action in a group of sibling actions, slide across the others while the highlight follows, and act on the one they lift on, the same -way in every group, with SegmentedControl's richer model as the one named -exception. One internal module, `actionGroupTracker`, gives every -member the same behavior, so no member builds its own. +way in every group, with SegmentedControl as the one named exception. Every +member gets the behavior from one shared implementation, so no member builds +its own. ## Membership rule @@ -79,9 +79,7 @@ the behavior by composing ButtonGroup. A component built later that matches the rule joins by the rule, including an action sheet, a tab bar or navigation bar, a filter-chip group, and a rating. -SegmentedControl is a named exception: its thumb follows the finger and moves -the selection, a richer model that `component:SegmentedControl` governs. It may -use `actionGroupTracker` internally, but this family's rule does not bind it. +SegmentedControl is a named exception: this family's rule does not bind it. These are not members: @@ -95,9 +93,8 @@ These are not members: ## Shared owner -`actionGroupTracker` is the one internal module that tracks a held pointer -across a group's actions. `module:DropdownMenu/useMenuPress` converges onto -it. It is internal and unexported (`spec:AST-017` FR21). +One shared, internal implementation gives every member FR1–FR5 +(`architecture:interaction-modality` INV13); no member keeps its own copy. ## Canonical concepts @@ -154,7 +151,7 @@ it. It is internal and unexported (`spec:AST-017` FR21). | Components or surface | Required adoption | | ------------------------------------------- | --------------------------------------------------------------------- | -| Menus, Selector, MultiSelector, date wheels | Track through `actionGroupTracker`; `useMenuPress` converges onto it. | +| Menus, Selector, MultiSelector, date wheels | Follow FR1–FR5 through the shared implementation. | | AlertDialog's actions | Track the actions only. | | ButtonGroup, ToggleButtonGroup, Toolbar | Track sibling buttons; a field inside is skipped; toggles follow FR4. | | Banner and Toast action rows | Track only a row with two or more sibling actions. | @@ -167,7 +164,7 @@ they are outside the family. | Contract | Verification | Representative members and states | | -------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| FR1–FR3 | `actionGroupTracker` tests and each member's pointer-stream tests | a slide across a menu, AlertDialog's actions, a ButtonGroup; a lift outside | +| FR1–FR3 | Shared-implementation tests and each member's pointer-stream tests | a slide across a menu, AlertDialog's actions, a ButtonGroup; a lift outside | | FR4 | ToggleButtonGroup pointer-stream tests | a slide across three toggles; only the lifted one toggles | | FR5 | Member tests with a mouse, a pen, a finger, the keyboard, and a screen reader | a 44 px target under a coarse pointer only; unchanged keyboard and screen-reader paths | @@ -184,4 +181,4 @@ None. This file owns action-group membership and the shared press, slide, and lift behavior. It does not duplicate component prop tables, what each action does, -menu opening, or the tracker's thresholds. +menu opening, or the shared implementation's thresholds. diff --git a/docs/specs/AST-094-touch-presentations/spec.md b/docs/specs/AST-094-touch-presentations/spec.md index 12f639c4d1846..73f163f2fce1c 100644 --- a/docs/specs/AST-094-touch-presentations/spec.md +++ b/docs/specs/AST-094-touch-presentations/spec.md @@ -186,8 +186,7 @@ a measured, sourced difference exists. views and activates by the browser's click on a lift inside. On the web a control's press ends when the browser takes the touch (FR20), and its activation stays the browser's click, except that a control inside a - `family:action-groups` member activates at the lift through the group's - tracker (FR26). + `family:action-groups` member activates at the lift (FR26). A cell (a table or collection cell) highlights 150 ms after touch-down inside a scroll view, drops the highlight when the scroll wins (FR20), and activates on a lift inside it with no scroll taking the touch (FR20). A control is inside a scroll view when it sits in a scrollable container, @@ -323,8 +322,7 @@ FR31 and FR35 apply to anchored menus and to Popover under a finger; FR32 and FR33 apply to menus only. A mouse keeps today's behavior. FR26–FR28 are the shared rule of `family:action-groups`: every member, not -only menus, follows them under the family's scope, governed by the one -internal `actionGroupTracker` (DEC-17). +only menus, follows them under the family's scope (DEC-17). - **FR26 — The lift decides.** A menu row MUST act when the finger lifts over it, never because the press began on it; a press that never left its row is @@ -560,7 +558,7 @@ internal `actionGroupTracker` (DEC-17). | FR15–FR17 | Recognizer unit tests on synthetic streams with explicit timestamps | exclusive, simultaneous, require-to-fail, nested; a watcher beside a claiming winner; a blocked main thread; a pan at 10 pt per axis | a wrong winner, a late deadline, or a translation that keeps the hysteresis, or a watcher failed or cancelled by a claiming winner fails | | FR18–FR22 | Browser tests with real touch input (`spec:AST-013` FR10) | per-axis pans beside native scrolling, page zoom, cancelled touches, clicks after taps, keyboard activation, a modal scroll lock, a control that keeps its press through a drag; a still finger with sub-pixel jitter over a scroll view; a finger moved 9 px and 10 px straight, and (7, 7) and (8, 8) diagonally; a recognizer on an element whose author `touch-action` is narrower, including one without `pinch-zoom`; a screen-edge pan landing at 40 px and at 41 px from the edge, at 14 px and 15 px of travel, and a horizontal scroll that starts outside the band | a ghost click, a scroll or zoom blocked other than by the author's `touch-action`, or a cancelled unowned touchmove outside FR19's two exceptions, a tap cancelled by sub-pixel movement or by under 10 px of travel, a scroll that does not take the touch at 10 px, or a derived `touch-action` wider than the author's, a screen-edge pan that claims outside its band or before 15 px, or has not claimed at 15 px, or an author scroll outside the band that is blocked fails | | FR23 | Haptics tests on the recorded moments | tap, drag, lift | a haptic outside a native moment fails | -| FR24 | Resource tests counting listeners, observers and timers | fine primary pointer; coarse pointer enabled and disabled; many instances; unmount | a touch listener on a fine pointer, a per-instance `document` or `window` listener, or a leak fails | +| FR24 | Resource tests counting listeners, observers and timers | fine primary pointer; coarse pointer enabled and disabled; many instances; unmount | a touch listener on a fine pointer, other than an action group's (FR19),, a per-instance `document` or `window` listener, or a leak fails | | FR25 | Component tests of each moved default and its `presentation` value | default; `presentation` set to the released surface | a moved default that `presentation` does not restore, or a public threshold, fails | | FR26–FR28, FR30 | Menu press tests on synthetic touch streams and a real-Chromium touch test | a slide from one row to another; a lift outside; a cancel; a second finger; edge autoscroll | an act on the press-start row, two acts for one gesture, a lift outside that closes, or a lit row after a cancel fails | | FR29 | Trigger tests with explicit timestamps | a press in a fixed bar; a press in scrolling content held 149 and 150 ms; a quick tap in scrolling content; a scroll taking the touch before 150 ms; a drift of 12 and 14 pt; moves before and after the handoff; `presentation="popover"` | a fixed-bar press that waits, a held finger's scroll-view open before 150 ms or after a scroll, a quick tap that does not open at its lift, an open past the drift allowance, a lift that acts before the handoff, or a `popover` trigger that opens on press fails | @@ -725,9 +723,9 @@ Rejected: the 150 ms delay on every menu row. **Decider:** `cixzhang`, `2026-10-26` A person who presses one action and slides to another expects the same result -in a menu, an alert's buttons, a button group, or a toolbar. One -internal tracker governs every member group of sibling actions, so the behavior is -consistent and built once (`family:action-groups`). The behavior works with a mouse +in a menu, an alert's buttons, a button group, or a toolbar. Every member +group of sibling actions behaves the same way, built once +(`family:action-groups`). The behavior works with a mouse as well, so it stays on for every pointer, while the 44 px target stays touch-only. From 357ba25a14ff24dd50580272bf2ec22e011b80db Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Mon, 26 Oct 2026 21:40:30 -0700 Subject: [PATCH 08/17] spec(family, AST-094, interaction-modality): menus keep useMenuPress and their own release-outside rule; INV13 and DEC-17 listed in the record's scope --- docs/architecture/interaction-modality.md | 13 ++++++---- docs/families/action-groups.md | 25 +++++++++++-------- .../specs/AST-094-touch-presentations/spec.md | 8 ++++-- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/docs/architecture/interaction-modality.md b/docs/architecture/interaction-modality.md index 568a5b81f7ab1..22f808bcbbffc 100644 --- a/docs/architecture/interaction-modality.md +++ b/docs/architecture/interaction-modality.md @@ -168,8 +168,9 @@ work. - **INV13 — Press tracking across a group has one owner.** In `@astryxdesign/core`, one internal module, `actionGroupTracker`, tracks a held pointer across a group's actions for every `family:action-groups` - member, and `module:DropdownMenu/useMenuPress` converges onto it. No member - keeps its own copy of the tracking. It is not exported (`spec:AST-017` + member other than menus, whose tracking and public `menuPressStep` stay + with `module:DropdownMenu/useMenuPress`. No other member keeps its own copy + of the tracking. It is not exported (`spec:AST-017` FR21). ## Allowed variation @@ -235,14 +236,16 @@ is asked to carry a new decision. lifetimes, and focus-paint ownership. - `architecture:react-component-runtime` owns shared lifecycle and resource mechanisms. This record owns only the cross-cutting modality-path, - no-stale-interaction-outcome, and gesture-velocity-source requirements. + no-stale-interaction-outcome, gesture-velocity-source, and group + press-tracking (INV13) requirements. - The Core velocity tracker and its release filter (INV10) — the one source of gesture velocity for Core components; not exported. ## Deciding specs -`spec:AST-100/DEC-1` decides one release velocity per kind of surface (INV10). -Otherwise no separate specification changes this record. The last-input rule was approved as +`spec:AST-100/DEC-1` decides one release velocity per kind of surface (INV10), +and `spec:AST-094/DEC-17` decides one press tracking for every action group +(INV13). Otherwise no separate specification changes this record. The last-input rule was approved as a system architecture decision by `cixzhang` on 2026-08-30. Essential-action reachability, newer-intent precedence, and the evidence/scope guard were approved by `cixzhang` on 2026-09-10. diff --git a/docs/families/action-groups.md b/docs/families/action-groups.md index 099f4bd71d31f..924fb0cc43829 100644 --- a/docs/families/action-groups.md +++ b/docs/families/action-groups.md @@ -55,8 +55,8 @@ deciding_specs: [spec:AST-094/DEC-17] A person can press one action in a group of sibling actions, slide across the others while the highlight follows, and act on the one they lift on, the same way in every group, with SegmentedControl as the one named exception. Every -member gets the behavior from one shared implementation, so no member builds -its own. +member gets the behavior from one shared implementation, or menus from their +own press model, so no member builds its own. ## Membership rule @@ -93,8 +93,9 @@ These are not members: ## Shared owner -One shared, internal implementation gives every member FR1–FR5 -(`architecture:interaction-modality` INV13); no member keeps its own copy. +One shared, internal implementation gives every member other than menus +FR1–FR5 (`architecture:interaction-modality` INV13); menus get them from +`module:DropdownMenu/useMenuPress`. No member keeps another copy. ## Canonical concepts @@ -115,7 +116,8 @@ One shared, internal implementation gives every member FR1–FR5 (`spec:AST-094` FR27). - **FR3 — Sliding off cancels.** A press that lifts outside the group acts on nothing; a cancelled pointer, a second finger, or the window losing focus - ends the gesture with nothing acting (`spec:AST-094` FR28). + ends the gesture with nothing acting (`spec:AST-094` FR28). Whether a + member that opened for the gesture stays open stays with the member. - **FR4 — A toggle group toggles only the lifted action.** In a toggle group, only the action lifted on toggles; sliding across never toggles the actions passed over. @@ -149,12 +151,13 @@ One shared, internal implementation gives every member FR1–FR5 ## Adoption and exceptions -| Components or surface | Required adoption | -| ------------------------------------------- | --------------------------------------------------------------------- | -| Menus, Selector, MultiSelector, date wheels | Follow FR1–FR5 through the shared implementation. | -| AlertDialog's actions | Track the actions only. | -| ButtonGroup, ToggleButtonGroup, Toolbar | Track sibling buttons; a field inside is skipped; toggles follow FR4. | -| Banner and Toast action rows | Track only a row with two or more sibling actions. | +| Components or surface | Required adoption | +| ------------------------------------------------ | --------------------------------------------------------------------- | +| Menus and Selector's and MultiSelector's options | Follow FR1–FR5 through `module:DropdownMenu/useMenuPress`. | +| Date and time wheels | Follow FR1–FR5 through the shared implementation. | +| AlertDialog's actions | Track the actions only. | +| ButtonGroup, ToggleButtonGroup, Toolbar | Track sibling buttons; a field inside is skipped; toggles follow FR4. | +| Banner and Toast action rows | Track only a row with two or more sibling actions. | These are required outcomes, not a claim of completed adoption. The excluded components listed under the membership rule are not exceptions to approve; diff --git a/docs/specs/AST-094-touch-presentations/spec.md b/docs/specs/AST-094-touch-presentations/spec.md index 73f163f2fce1c..68e4fa4caa52d 100644 --- a/docs/specs/AST-094-touch-presentations/spec.md +++ b/docs/specs/AST-094-touch-presentations/spec.md @@ -322,7 +322,10 @@ FR31 and FR35 apply to anchored menus and to Popover under a finger; FR32 and FR33 apply to menus only. A mouse keeps today's behavior. FR26–FR28 are the shared rule of `family:action-groups`: every member, not -only menus, follows them under the family's scope (DEC-17). +only menus, follows them under the family's scope (DEC-17). Whether a menu +stays open after a release outside it stays with the menu: FR28 keeps it open +under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and +`component:Selector` FR7. - **FR26 — The lift decides.** A menu row MUST act when the finger lifts over it, never because the press began on it; a press that never left its row is @@ -498,7 +501,8 @@ only menus, follows them under the family's scope (DEC-17). unchanged until the public motion-token set is decided; then its roles join the motion configuration (FR4, DEC-11). - `architecture:interaction-modality`: the touch-presentation conditions - (FR13) sit beside the shared modality state; its invariants are unchanged. + (FR13) sit beside the shared modality state, and INV13 gives every action + group one press tracking (DEC-17). - `architecture:react-component-runtime`: FR24 states the caller-visible outcome of INV5 and INV7 for touch work; both are unchanged. - Under FR41: ContextMenu's trigger, DropdownMenu's built-in trigger and its menu, and From 9dfa4849fce9be606bd7fddbc58c27ca1c239b5d Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Mon, 26 Oct 2026 21:42:28 -0700 Subject: [PATCH 09/17] spec(interaction-modality): menus' move onto the shared tracker is planned (OQ1) --- docs/architecture/interaction-modality.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/architecture/interaction-modality.md b/docs/architecture/interaction-modality.md index 22f808bcbbffc..2d5af33668efa 100644 --- a/docs/architecture/interaction-modality.md +++ b/docs/architecture/interaction-modality.md @@ -170,7 +170,7 @@ work. held pointer across a group's actions for every `family:action-groups` member other than menus, whose tracking and public `menuPressStep` stay with `module:DropdownMenu/useMenuPress`. No other member keeps its own copy - of the tracking. It is not exported (`spec:AST-017` + of the tracking. Moving menus onto the shared tracker is planned (OQ1). It is not exported (`spec:AST-017` FR21). ## Allowed variation @@ -277,3 +277,9 @@ indicator on pointer press, and keeping internal action focus owned by the action. The benchmark also verifies programmatic editor focus after both keyboard and pointer input before the behavior is treated as complete. A benchmark demonstrates the invariants; it does not carry authority for them. + +## Open questions + +- **OQ1 — When menus move onto the shared tracker.** (`engineering`) Open. + Planned: `module:DropdownMenu/useMenuPress` converges onto the shared + tracker as a follow-up, on the timing the owner set. From 5c363609566b0bdbb8848f80dbb8d2a65cfc56ca Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Tue, 27 Oct 2026 00:23:24 -0700 Subject: [PATCH 10/17] Merge main into spec/ast094-action-groups --- docs/architecture/interaction-modality.md | 7 ++++--- docs/families/action-groups.md | 9 +++++---- docs/specs/AST-094-touch-presentations/spec.md | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/architecture/interaction-modality.md b/docs/architecture/interaction-modality.md index 282ae8011d128..9d0a31599d1a1 100644 --- a/docs/architecture/interaction-modality.md +++ b/docs/architecture/interaction-modality.md @@ -25,7 +25,7 @@ verified_by: packages/core/src/Selector/Selector.test.tsx, packages/core/src/Slider/Slider.test.tsx, ] -deciding_specs: [spec:AST-100/DEC-1] +deciding_specs: [spec:AST-100/DEC-1, spec:AST-094/DEC-17] --- # Interaction modality architecture @@ -178,8 +178,9 @@ work. - **INV13 — Press tracking across a group has one owner.** In `@astryxdesign/core`, one internal module, `actionGroupTracker`, tracks a held pointer across a group's actions for every `family:action-groups` - member other than menus, whose tracking and public `menuPressStep` stay - with `module:DropdownMenu/useMenuPress`. No other member keeps its own copy + member other than menus and Selector's and MultiSelector's options, whose + tracking and public `menuPressStep` stay with + `module:DropdownMenu/useMenuPress`. No other member keeps its own copy of the tracking. Moving menus onto the shared tracker is planned (OQ1). It is not exported (`spec:AST-017` FR21). diff --git a/docs/families/action-groups.md b/docs/families/action-groups.md index 924fb0cc43829..dd1277c626b34 100644 --- a/docs/families/action-groups.md +++ b/docs/families/action-groups.md @@ -55,8 +55,8 @@ deciding_specs: [spec:AST-094/DEC-17] A person can press one action in a group of sibling actions, slide across the others while the highlight follows, and act on the one they lift on, the same way in every group, with SegmentedControl as the one named exception. Every -member gets the behavior from one shared implementation, or menus from their -own press model, so no member builds its own. +member gets the behavior from one shared implementation, or menus and +Selector's and MultiSelector's options from the menu press model, so no member builds its own. ## Membership rule @@ -93,8 +93,9 @@ These are not members: ## Shared owner -One shared, internal implementation gives every member other than menus -FR1–FR5 (`architecture:interaction-modality` INV13); menus get them from +One shared, internal implementation gives every member other than menus and +Selector's and MultiSelector's options FR1–FR5 +(`architecture:interaction-modality` INV13); those get them from `module:DropdownMenu/useMenuPress`. No member keeps another copy. ## Canonical concepts diff --git a/docs/specs/AST-094-touch-presentations/spec.md b/docs/specs/AST-094-touch-presentations/spec.md index 3c0acdb47d682..5f60834be43de 100644 --- a/docs/specs/AST-094-touch-presentations/spec.md +++ b/docs/specs/AST-094-touch-presentations/spec.md @@ -569,7 +569,7 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and | FR15–FR17 | Recognizer unit tests on synthetic streams with explicit timestamps | exclusive, simultaneous, require-to-fail, nested; a watcher beside a claiming winner; a blocked main thread; a pan at 10 pt per axis | a wrong winner, a late deadline, or a translation that keeps the hysteresis, or a watcher failed or cancelled by a claiming winner fails | | FR18–FR22 | Browser tests with real touch input (`spec:AST-013` FR10) | per-axis pans beside native scrolling, page zoom, cancelled touches, clicks after taps, keyboard activation, a modal scroll lock, a control that keeps its press through a drag; a still finger with sub-pixel jitter over a scroll view; a finger moved 9 px and 10 px straight, and (7, 7) and (8, 8) diagonally; a recognizer on an element whose author `touch-action` is narrower, including one without `pinch-zoom`; a screen-edge pan landing at 40 px and at 41 px from the edge, at 14 px and 15 px of travel, and a horizontal scroll that starts outside the band; a sheet's pull from a list at its top, and from a list scrolled away from its top | a ghost click, a scroll or zoom blocked other than by the author's `touch-action`, or a cancelled unowned touchmove outside FR19's three exceptions, a tap cancelled by sub-pixel movement or by under 10 px of travel, a scroll that does not take the touch at 10 px, or a derived `touch-action` wider than the author's, a screen-edge pan that claims outside its band or before 15 px, or has not claimed at 15 px, or an author scroll outside the band that is blocked, or a scroll-edge pan that cancels a move a scroller could scroll, or begins before 10 pt, fails | | FR23 | Haptics tests on the recorded moments | tap, drag, lift | a haptic outside a native moment fails | -| FR24 | Resource tests counting listeners, observers and timers | fine primary pointer; coarse pointer enabled and disabled; many instances; unmount | a touch listener on a fine pointer, other than an action group's (FR19),, a per-instance `document` or `window` listener, or a leak fails | +| FR24 | Resource tests counting listeners, observers and timers | fine primary pointer; coarse pointer enabled and disabled; many instances; unmount | a touch listener on a fine pointer outside an action group (FR19), other than an action group's (FR19), a per-instance `document` or `window` listener, or a leak fails | | FR25 | Component tests of each moved default and its `presentation` value | default; `presentation` set to the released surface | a moved default that `presentation` does not restore, or a public threshold, fails | | FR26–FR28, FR30 | Menu press tests on synthetic touch streams and a real-Chromium touch test | a slide from one row to another; a lift outside; a cancel; a second finger; edge autoscroll | an act on the press-start row, two acts for one gesture, a lift outside that closes, or a lit row after a cancel fails | | FR29 | Trigger tests with explicit timestamps | a press in a fixed bar; a press in scrolling content held 149 and 150 ms; a quick tap in scrolling content; a scroll taking the touch before 150 ms; a drift of 12 and 14 pt; moves before and after the handoff; `presentation="popover"` | a fixed-bar press that waits, a held finger's scroll-view open before 150 ms or after a scroll, a quick tap that does not open at its lift, an open past the drift allowance, a lift that acts before the handoff, or a `popover` trigger that opens on press fails | From 9e3a9109cbe8035c516cef7c647aa89c23a9b052 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Tue, 27 Oct 2026 03:43:59 -0700 Subject: [PATCH 11/17] Merge main into spec/ast094-action-groups --- docs/architecture/interaction-modality.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/architecture/interaction-modality.md b/docs/architecture/interaction-modality.md index db3328031d4bc..9760cb7224333 100644 --- a/docs/architecture/interaction-modality.md +++ b/docs/architecture/interaction-modality.md @@ -179,9 +179,9 @@ work. `@astryxdesign/core`, one internal module, `actionGroupTracker`, tracks a held pointer across a group's actions for every `family:action-groups` member other than menus and Selector's and MultiSelector's options, whose - tracking and public `menuPressStep` stay with - `module:DropdownMenu/useMenuPress`. No other member keeps its own copy - of the tracking. Moving menus onto the shared tracker is planned (OQ1). It is not exported (`spec:AST-017` + tracking and public `menuPressStep` belong to + `module:DropdownMenu/useMenuPress` (OQ1; implementation gap: MultiSelector + does not mount it yet). No other member keeps its own copy of the tracking. It is not exported (`spec:AST-017` FR21). ## Allowed variation @@ -255,7 +255,7 @@ is asked to carry a new decision. - The native-scroll module's progress and its subscription (INV12) — the one source of scroll progress for Core components; not exported. - The action-group tracker (INV13) — the one press tracker for action groups - outside menus; not exported. + outside menus and Selector's and MultiSelector's options; not exported. ## Deciding specs @@ -280,7 +280,7 @@ reachability, newer-intent precedence, and the evidence/scope guard were approve | INV9 | Concrete-harm reproduction, representative normal/legacy/no-new-prop paths, and coincident public-delta inventory | Unsupported or aesthetic preference is treated as harm, unaffected behavior changes, or an API, layout, sizing, default, sticky, or exception delta escapes direct authority | | INV10 | Source boundary check over Core and Lab, a Lab-to-Core comparison on one recorded stream, and release-filter unit tests over recorded iOS pan and scroll-view streams | A Core component with its own sample buffer, velocity math, or release filter; a Lab component with its own sample buffer, velocity math, or release filter; a Lab component reporting a different velocity than Core for the same stream; an export of the tracker; a non-zero release after a still pause | | INV12 | Unit tests of native scroll's progress and real-browser proofs of its adopters | Progress routed through React state, a missing edge, travel under Reduce Motion, use where native scroll-driven animation is supported, or a public export fails | -| INV13 | Source boundary check over Core and each family member's pointer-stream tests | A member that tracks a held pointer across its actions with its own code, or a public export of the tracker, fails | +| INV13 | Source boundary check over Core and each family member's pointer-stream tests | A member outside menus and Selector's and MultiSelector's options that tracks a held pointer across its actions with its own code, or a public export of the tracker, fails | Real-browser evidence is required when the behavior depends on `:focus-visible`, pointer capability, touch or pen synthesis, rendered focus @@ -297,6 +297,5 @@ benchmark demonstrates the invariants; it does not carry authority for them. ## Open questions -- **OQ1 — When menus move onto the shared tracker.** (`engineering`) Open. - Planned: `module:DropdownMenu/useMenuPress` converges onto the shared - tracker as a follow-up, on the timing the owner set. +- **OQ1 — When do menus and Selector's and MultiSelector's options move onto + the shared tracker?** (`engineering`) Open. From 108ec602cc2bd9951b869ed61cb2ceae716e0eb0 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Tue, 27 Oct 2026 10:11:02 -0700 Subject: [PATCH 12/17] Merge main into spec/ast094-action-groups --- docs/specs/AST-094-touch-presentations/spec.md | 4 ++-- packages/core/src/ButtonGroup/ButtonGroup.spec.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/specs/AST-094-touch-presentations/spec.md b/docs/specs/AST-094-touch-presentations/spec.md index 3c438f281b1b5..758d227e2f0ac 100644 --- a/docs/specs/AST-094-touch-presentations/spec.md +++ b/docs/specs/AST-094-touch-presentations/spec.md @@ -271,8 +271,8 @@ a measured, sourced difference exists. - **FR19 — Claims are made in time and only by an owner.** Recognizers attach for every pointer, with FR43's exceptions. This rule governs this record's behaviors only; `spec:AST-057` keeps its per-event pointer - read for the row swipe, as its record states. A `family:action-groups` member is - the one exception for its own group: under a fine pointer it keeps one + read for the row swipe, as its record states. A `family:action-groups` member + tracks its own group as FR24 allows: under a fine pointer it keeps one `pointerdown` listener on the group element, cancelable where the member needs it, adds document listeners only during a press, removed at the release, and may arm one per-document click swallower from the release until diff --git a/packages/core/src/ButtonGroup/ButtonGroup.spec.md b/packages/core/src/ButtonGroup/ButtonGroup.spec.md index 527418e9dba81..5d4023558975b 100644 --- a/packages/core/src/ButtonGroup/ButtonGroup.spec.md +++ b/packages/core/src/ButtonGroup/ButtonGroup.spec.md @@ -142,7 +142,7 @@ Consumer migration instructions belong in consumer docs and release notes. - **PR1 — Tab-stop repair.** The group repairs the Tab stop after each of its own renders and when focus enters a member; it adds no global listener - beyond the press-time listeners `family:action-groups` FR5 gives it. + beyond the press tracking `family:action-groups` FR5 gives it. ## Accessibility contract From 9be54ddefae517ed24efd126a7f4615ab8fdeb14 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Tue, 27 Oct 2026 11:04:54 -0700 Subject: [PATCH 13/17] Merge main into spec/ast094-action-groups --- docs/families/action-groups.md | 25 +++++--- .../specs/AST-094-touch-presentations/spec.md | 57 ++++++++++--------- 2 files changed, 46 insertions(+), 36 deletions(-) diff --git a/docs/families/action-groups.md b/docs/families/action-groups.md index dd1277c626b34..c205407aa907f 100644 --- a/docs/families/action-groups.md +++ b/docs/families/action-groups.md @@ -64,7 +64,8 @@ A component is a member when it renders a set of two or more sibling momentary, toggle, or option actions that a person chooses one of. Members today: -- menus: DropdownMenu, ContextMenu, and their sub-menus; +- menus: DropdownMenu, ContextMenu, and their sub-menus, and the navigation + menus of TopNav (TopNavMenu), SideNav's collapsed rail, and NavHeadingMenu; - option lists: Selector's and MultiSelector's options, and the wheels of DateInput, DateTimeInput, and TimeInput; - AlertDialog's actions; @@ -90,6 +91,9 @@ These are not members: - RadioList, CheckboxList, and SelectableCard, whose rows and cards sit in scrolling lists; - Token chips, which are removable and have no group control. +- the suggestion lists of Typeahead (also Tokenizer's), CommandPalette, and the + chat composer's trigger menu, whose focus stays in the text input, as a text + input's does. ## Shared owner @@ -152,13 +156,18 @@ Selector's and MultiSelector's options FR1–FR5 ## Adoption and exceptions -| Components or surface | Required adoption | -| ------------------------------------------------ | --------------------------------------------------------------------- | -| Menus and Selector's and MultiSelector's options | Follow FR1–FR5 through `module:DropdownMenu/useMenuPress`. | -| Date and time wheels | Follow FR1–FR5 through the shared implementation. | -| AlertDialog's actions | Track the actions only. | -| ButtonGroup, ToggleButtonGroup, Toolbar | Track sibling buttons; a field inside is skipped; toggles follow FR4. | -| Banner and Toast action rows | Track only a row with two or more sibling actions. | +| Components or surface | Required adoption | +| -------------------------------------------------------------- | --------------------------------------------------------------------- | +| Menus and Selector's and MultiSelector's options | Follow FR1–FR5 through `module:DropdownMenu/useMenuPress`. | +| TopNav's, SideNav's collapsed-rail, and NavHeadingMenu's menus | Follow FR1–FR5 through `module:DropdownMenu/useMenuPress`, as menus. | +| Date and time wheels | Follow FR1–FR5 through the shared implementation. | +| AlertDialog's actions | Track the actions only. | +| ButtonGroup, ToggleButtonGroup, Toolbar | Track sibling buttons; a field inside is skipped; toggles follow FR4. | +| Banner and Toast action rows | Track only a row with two or more sibling actions. | + +TopNav's, SideNav's collapsed-rail, and NavHeadingMenu's menus track presses +with their own code today; their adoption of the menu press model is an +implementation gap. These are required outcomes, not a claim of completed adoption. The excluded components listed under the membership rule are not exceptions to approve; diff --git a/docs/specs/AST-094-touch-presentations/spec.md b/docs/specs/AST-094-touch-presentations/spec.md index 758d227e2f0ac..6b664e66bc53c 100644 --- a/docs/specs/AST-094-touch-presentations/spec.md +++ b/docs/specs/AST-094-touch-presentations/spec.md @@ -628,32 +628,32 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and ## Verification -| Contract | Verification | Representative states | Mutation or failure expectation | -| --------------- | ------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| FR1, FR2 | Value check over every measured value and its uses | every value; a guess and its measurement task; a platform-specific value; two components playing the same motion | an unsourced value, an unknown source kind, a guess without a listed measurement, an unsourced platform split, or two values for one use fails | -| FR3 | Unit tests of the spring, decay, rubber band and projection math against an independent integration and values read from UIKit | under-, critically and overdamped springs; release velocities; both rates; overscroll depths | changing a formula or a constant breaks the agreement with UIKit's values | -| FR4 | Role unit tests over every touch motion | each role at its table value; a theme that writes `motion` | a literal played in place of a role, a value that differs from the table, or a theme that changes a role before the public set is decided fails | -| FR5 | Browser proof: an animation keeps advancing frame by frame through a 1 s main-thread block, with no layout or paint per frame | Chromium phone emulation; WebKit | a script-driven or layout-animating implementation freezes or lays out | -| FR6 | Unit and browser tests catching an animation mid-flight and retargeting it | catch at peak velocity; retarget with a new spring | a layout read, a jump, or a lost velocity fails | -| FR7, FR8 | Component browser tests and storyboards with and without Reduce Motion | entrance, exit, move; a drag and its lift under Reduce Motion | a missing exit, any travel, scale or spring under Reduce Motion other than a coast after a throw and its settle at an edge (`design:reduced-motion` DR2), or an opacity fade over 200 ms fails | -| FR9, FR10 | Recognizer unit tests on synthetic streams and browser tests with real touch input | drag, second finger, pinch, page zoom, main-thread block | a second finger that breaks a drag, or a page pinch blocked other than by the author's `touch-action`, fails | -| FR11, FR12 | Press and long-press tests on synthetic streams with explicit timestamps | inside and outside a scroll view; a drag the browser takes; a lift inside; a menu row under a finger; a press on a row of a menu whose rows fit and of one that scrolls; a slide-off lift on a Switch and on a Radio | the wrong profile's timing, a press that survives the browser taking the touch, or a control outside an action group activated other than by the browser's click, outside `spec:AST-020` FR9's slide-off exception, a row of a menu whose rows fit that waits 150 ms, or one of a scrolling menu that does not, or a slide-off lift that completes a control outside `spec:AST-020` FR9's exception, fails | -| FR13 | Resolver unit tests and a server-render test | coarse and fine pointer; narrow, wide and short; server render; hydrating render | a component that answers the conditions differently, or a touch presentation in the server or hydrating render, fails | -| FR14 | Browser tests of a sheet over a scrolled list | scrolled and at its top | a midway handoff fails | -| FR15–FR17 | Recognizer unit tests on synthetic streams with explicit timestamps | exclusive, simultaneous, require-to-fail, nested; a watcher beside a claiming winner; a blocked main thread; a pan at 10 pt per axis | a wrong winner, a late deadline, or a translation that keeps the hysteresis, or a watcher failed or cancelled by a claiming winner fails | -| FR18–FR22 | Browser tests with real touch input (`spec:AST-013` FR10) | per-axis pans beside native scrolling, page zoom, cancelled touches, clicks after taps, keyboard activation, a modal scroll lock, a control that keeps its press through a drag; a still finger with sub-pixel jitter over a scroll view; a finger moved 9 px and 10 px straight, and (7, 7) and (8, 8) diagonally; a recognizer on an element whose author `touch-action` is narrower, including one without `pinch-zoom`; a screen-edge pan landing at 40 px and at 41 px from the edge, at 14 px and 15 px of travel, and a horizontal scroll that starts outside the band; a screen-edge pan on an element within the band, and on one that reaches beyond it; a sheet's pull from a list at its top, and from a list scrolled away from its top | a ghost click, a scroll or zoom blocked other than by the author's `touch-action`, or a cancelled unowned touchmove outside FR19's three exceptions, a tap cancelled by sub-pixel movement or by under 10 px of travel, a scroll that does not take the touch at 10 px, or a derived `touch-action` wider than the author's, a screen-edge pan that claims outside its band or before 15 px on an element within the band, or has not claimed at 15 px, or an author scroll outside the band that is blocked, a band claim of `pan-x` on an element that reaches beyond the band, or a 15 px start there, or a scroll-edge pan that cancels a move a scroller could scroll, or begins before 10 pt, fails | -| FR23 | Haptics tests on the recorded moments | tap, drag, lift | a haptic outside a native moment fails | -| FR24 | Resource tests counting listeners, observers and timers | fine primary pointer; coarse pointer enabled and disabled; many instances; unmount | a gesture listener in a component that uses no gesture, an idle document listener outside a press other than the one shared press controller and the one stray-click swallower, a second press controller, a per-instance `document` or `window` listener, or a leak fails | -| FR25 | Component tests of each moved default and its `presentation` value | default; `presentation` set to the released surface | a moved default that `presentation` does not restore, or a public threshold, fails | -| FR26–FR28, FR30 | Menu press tests on synthetic touch streams and a real-Chromium touch test | a slide from one row to another; a lift outside; a cancel; a second finger; edge autoscroll | an act on the press-start row, two acts for one gesture, a lift outside that closes, or a lit row after a cancel fails | -| FR29 | Trigger tests with explicit timestamps | a press in a fixed bar; a press in scrolling content held 149 and 150 ms; a quick tap in scrolling content; a scroll taking the touch before 150 ms; a drift of 12 and 14 pt; moves before and after the handoff; `presentation="popover"` | a fixed-bar press that waits, a held finger's scroll-view open before 150 ms or after a scroll, a quick tap that does not open at its lift, an open past the drift allowance, a lift that acts before the handoff, or a `popover` trigger that opens on press fails | -| FR31–FR33 | Browser tests and storyboards under a finger | a menu, a Popover and a ContextMenu lift: open, close, a pan outside, a tap outside, a pinch over the surface; a mouse | a surface that does not grow from and shrink into its trigger, a pan outside that leaves a menu open, a Popover whose dismissal changed, a pinch blocked other than by the author's `touch-action`, a tap outside that reaches the page, or a changed mouse path fails | -| FR34, FR35 | Browser tests and storyboards under a finger | a sub-menu drilling in and its Back row returning; a tap and a drag during a popover's and a menu's growth; a pinch during growth; a mouse; a tap and a drag during an alert's, a sheet's, and a dialog's entrance; a key and a screen reader during an entrance; a pinch during an alert's and a sheet's entrance; a tap and a scroll on the page beside an entering non-modal sheet, which pass | a drill-in that leaves the parent's rows in place, a change between levels that does not play on its roles, a tap during growth that closes, scrolls or moves focus, a pinch blocked other than by the author's `touch-action`, or a changed mouse path, a held key or assistive-technology activation, a hidden content tree, or a tap or drag during an entrance that closes the presentation, scrolls a held page, moves focus or reaches anything beneath, fails | -| FR36–FR40 | Release tests on synthetic touch streams with explicit timestamps, per use | below and past each commit point; a flick that projects past it; a slow drag past it; a release moving back; Reduce Motion; keyboard and mouse paths | a decision that changes mid-settle, a release moving back that commits, a settle that does not start at the release velocity, or an action with no keyboard or mouse path fails | -| FR41 | Browser tests with real touch input (`spec:AST-013` FR10) | a tap; a long press on a ContextMenu trigger, a DropdownMenu trigger the caller renders, a row with a context menu, and a slider thumb; a long press on a checkbox row's label and on plain text; a cancelled press; mouse and keyboard selection; a pen long press; a mouse long press, which makes no context request | a tap that selects, a hold with an action that starts a selection or callout, a label or plain text whose long press does not select natively, a suppression that outlives the press, or a change to native selection elsewhere or to mouse, keyboard, or assistive-technology selection fails | -| FR42 | Default-theme geometry tests under a coarse and a fine pointer, and hit-testing between adjacent targets | every component at phone width with a finger and with a mouse; a dense toolbar; a chip and its remove button; a Switch beside a label link | a default-theme box that grows under touch or at phone width other than with its text, or, where no component or module record sets the hit area, a touch target under 44 px where neighbors allow it or a hit area that overlaps a neighbor's target, fails | -| FR43 | Browser tests with a mouse, a trackpad, a pen, a finger, and the keyboard, and resource counts | a pan by drag and by wheel; a ctrl-wheel pinch; a long press, a right click, and Shift+F10; a mouse press; a handle drag; a row swipe with a mouse and a trackpad; a component with no gesture | a gesture missing under a mouse or pen, a mouse-drag swipe action, a pen pinch, an edge pan under a mouse, a trackpad, or a pen, a press delay under a fine pointer, an idle listener in a component with no gesture, or added stylesheet bytes, fails | -| all | iOS simulator: the UIKit reference capture and the same gesture in Safari; real iOS Safari and Android Chrome devices | per component | an unmatched curve or timing is a parity defect | +| Contract | Verification | Representative states | Mutation or failure expectation | +| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| FR1, FR2 | Value check over every measured value and its uses | every value; a guess and its measurement task; a platform-specific value; two components playing the same motion | an unsourced value, an unknown source kind, a guess without a listed measurement, an unsourced platform split, or two values for one use fails | +| FR3 | Unit tests of the spring, decay, rubber band and projection math against an independent integration and values read from UIKit | under-, critically and overdamped springs; release velocities; both rates; overscroll depths | changing a formula or a constant breaks the agreement with UIKit's values | +| FR4 | Role unit tests over every touch motion | each role at its table value; a theme that writes `motion` | a literal played in place of a role, a value that differs from the table, or a theme that changes a role before the public set is decided fails | +| FR5 | Browser proof: an animation keeps advancing frame by frame through a 1 s main-thread block, with no layout or paint per frame | Chromium phone emulation; WebKit | a script-driven or layout-animating implementation freezes or lays out | +| FR6 | Unit and browser tests catching an animation mid-flight and retargeting it | catch at peak velocity; retarget with a new spring | a layout read, a jump, or a lost velocity fails | +| FR7, FR8 | Component browser tests and storyboards with and without Reduce Motion | entrance, exit, move; a drag and its lift under Reduce Motion | a missing exit, any travel, scale or spring under Reduce Motion other than a coast after a throw and its settle at an edge (`design:reduced-motion` DR2), or an opacity fade over 200 ms fails | +| FR9, FR10 | Recognizer unit tests on synthetic streams and browser tests with real touch input | drag, second finger, pinch, page zoom, main-thread block | a second finger that breaks a drag, or a page pinch blocked other than by the author's `touch-action`, fails | +| FR11, FR12 | Press and long-press tests on synthetic streams with explicit timestamps | inside and outside a scroll view; a drag the browser takes; a lift inside; a menu row under a finger; a press on a row of a menu whose rows fit and of one that scrolls; a slide-off lift on a Switch and on a Radio | the wrong profile's timing, a press that survives the browser taking the touch, or a control outside an action group activated other than by the browser's click, outside `spec:AST-020` FR9's slide-off exception, a row of a menu whose rows fit that waits 150 ms, or one of a scrolling menu that does not, or a slide-off lift that completes a control outside `spec:AST-020` FR9's exception, fails | +| FR13 | Resolver unit tests and a server-render test | coarse and fine pointer; narrow, wide and short; server render; hydrating render | a component that answers the conditions differently, or a touch presentation in the server or hydrating render, fails | +| FR14 | Browser tests of a sheet over a scrolled list | scrolled and at its top | a midway handoff fails | +| FR15–FR17 | Recognizer unit tests on synthetic streams with explicit timestamps | exclusive, simultaneous, require-to-fail, nested; a watcher beside a claiming winner; a blocked main thread; a pan at 10 pt per axis | a wrong winner, a late deadline, or a translation that keeps the hysteresis, or a watcher failed or cancelled by a claiming winner fails | +| FR18–FR22 | Browser tests with real touch input (`spec:AST-013` FR10) | per-axis pans beside native scrolling, page zoom, cancelled touches, clicks after taps, keyboard activation, a modal scroll lock, a control that keeps its press through a drag; a still finger with sub-pixel jitter over a scroll view; a finger moved 9 px and 10 px straight, and (7, 7) and (8, 8) diagonally; a recognizer on an element whose author `touch-action` is narrower, including one without `pinch-zoom`; a screen-edge pan landing at 40 px and at 41 px from the edge, at 14 px and 15 px of travel, and a horizontal scroll that starts outside the band; a screen-edge pan on an element within the band, and on one that reaches beyond it; a sheet's pull from a list at its top, and from a list scrolled away from its top | a ghost click, a scroll or zoom blocked other than by the author's `touch-action`, or a cancelled unowned touchmove outside FR19's three exceptions, a tap cancelled by sub-pixel movement or by under 10 px of travel, a scroll that does not take the touch at 10 px, or a derived `touch-action` wider than the author's, a screen-edge pan that claims outside its band or before 15 px on an element within the band, or has not claimed at 15 px, or an author scroll outside the band that is blocked, a band claim of `pan-x` on an element that reaches beyond the band, or a 15 px start there, or a scroll-edge pan that cancels a move a scroller could scroll, or begins before 10 pt, fails | +| FR23 | Haptics tests on the recorded moments | tap, drag, lift | a haptic outside a native moment fails | +| FR24 | Resource tests counting listeners, observers and timers | fine primary pointer; coarse pointer enabled and disabled; many instances; unmount | a gesture listener in a component that uses no gesture, an idle document listener outside a press other than the one shared press controller and the one stray-click swallower, a second press controller, a per-instance `document` or `window` listener, or a leak fails | +| FR25 | Component tests of each moved default and its `presentation` value | default; `presentation` set to the released surface | a moved default that `presentation` does not restore, or a public threshold, fails | +| FR26–FR28, FR30 | Menu press tests and each member's pointer-stream tests, as `family:action-groups`' verification map lists them and a real-Chromium touch test | a slide from one row to another; a lift outside; a cancel; a second finger; edge autoscroll | an act on the press-start row, two acts for one gesture, a lift outside that closes, or a lit row after a cancel fails | +| FR29 | Trigger tests with explicit timestamps | a press in a fixed bar; a press in scrolling content held 149 and 150 ms; a quick tap in scrolling content; a scroll taking the touch before 150 ms; a drift of 12 and 14 pt; moves before and after the handoff; `presentation="popover"` | a fixed-bar press that waits, a held finger's scroll-view open before 150 ms or after a scroll, a quick tap that does not open at its lift, an open past the drift allowance, a lift that acts before the handoff, or a `popover` trigger that opens on press fails | +| FR31–FR33 | Browser tests and storyboards under a finger | a menu, a Popover and a ContextMenu lift: open, close, a pan outside, a tap outside, a pinch over the surface; a mouse | a surface that does not grow from and shrink into its trigger, a pan outside that leaves a menu open, a Popover whose dismissal changed, a pinch blocked other than by the author's `touch-action`, a tap outside that reaches the page, or a changed mouse path fails | +| FR34, FR35 | Browser tests and storyboards under a finger | a sub-menu drilling in and its Back row returning; a tap and a drag during a popover's and a menu's growth; a pinch during growth; a mouse; a tap and a drag during an alert's, a sheet's, and a dialog's entrance; a key and a screen reader during an entrance; a pinch during an alert's and a sheet's entrance; a tap and a scroll on the page beside an entering non-modal sheet, which pass | a drill-in that leaves the parent's rows in place, a change between levels that does not play on its roles, a tap during growth that closes, scrolls or moves focus, a pinch blocked other than by the author's `touch-action`, or a changed mouse path, a held key or assistive-technology activation, a hidden content tree, or a tap or drag during an entrance that closes the presentation, scrolls a held page, moves focus or reaches anything beneath, fails | +| FR36–FR40 | Release tests on synthetic touch streams with explicit timestamps, per use | below and past each commit point; a flick that projects past it; a slow drag past it; a release moving back; Reduce Motion; keyboard and mouse paths | a decision that changes mid-settle, a release moving back that commits, a settle that does not start at the release velocity, or an action with no keyboard or mouse path fails | +| FR41 | Browser tests with real touch input (`spec:AST-013` FR10) | a tap; a long press on a ContextMenu trigger, a DropdownMenu trigger the caller renders, a row with a context menu, and a slider thumb; a long press on a checkbox row's label and on plain text; a cancelled press; mouse and keyboard selection; a pen long press; a mouse long press, which makes no context request | a tap that selects, a hold with an action that starts a selection or callout, a label or plain text whose long press does not select natively, a suppression that outlives the press, or a change to native selection elsewhere or to mouse, keyboard, or assistive-technology selection fails | +| FR42 | Default-theme geometry tests under a coarse and a fine pointer, and hit-testing between adjacent targets | every component at phone width with a finger and with a mouse; a dense toolbar; a chip and its remove button; a Switch beside a label link | a default-theme box that grows under touch or at phone width other than with its text, or, where no component or module record sets the hit area, a touch target under 44 px where neighbors allow it or a hit area that overlaps a neighbor's target, fails | +| FR43 | Browser tests with a mouse, a trackpad, a pen, a finger, and the keyboard, and resource counts | a pan by drag and by wheel; a ctrl-wheel pinch; a long press, a right click, and Shift+F10; a mouse press; a handle drag; a row swipe with a mouse and a trackpad; a component with no gesture | a gesture missing under a mouse or pen, a mouse-drag swipe action, a pen pinch, an edge pan under a mouse, a trackpad, or a pen, a press delay under a fine pointer, an idle listener in a component with no gesture, or added stylesheet bytes, fails | +| all | iOS simulator: the UIKit reference capture and the same gesture in Safari; real iOS Safari and Android Chrome devices | per component | an unmatched curve or timing is a parity defect | ## Decision log @@ -813,8 +813,9 @@ Rejected: the 150 ms delay on every menu row. A person who presses one action and slides to another expects the same result in a menu, an alert's buttons, a button group, or a toolbar. Every member -group of sibling actions behaves the same way, built once -(`family:action-groups`). The behavior works with a mouse +group of sibling actions behaves the same way, decided once and built in +two owners: the menu press model and the action-group tracker +(`family:action-groups`; `architecture:interaction-modality` INV13). The behavior works with a mouse as well, so it stays on for every pointer, while the 44 px target stays touch-only. From f5c492bb69734dba004823e62bec06ca0d04c18c Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Tue, 27 Oct 2026 11:47:27 -0700 Subject: [PATCH 14/17] Merge main into spec/ast094-action-groups --- docs/families/action-groups.md | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/docs/families/action-groups.md b/docs/families/action-groups.md index c205407aa907f..21127f1742f9c 100644 --- a/docs/families/action-groups.md +++ b/docs/families/action-groups.md @@ -31,6 +31,10 @@ members: component:Toolbar, component:Banner, component:Toast, + component:TopNavMenu, + component:SideNavHeading, + component:NavHeadingMenu, + component:BreadcrumbItem, ] architecture: [architecture:interaction-modality] contributing: [] @@ -64,8 +68,9 @@ A component is a member when it renders a set of two or more sibling momentary, toggle, or option actions that a person chooses one of. Members today: -- menus: DropdownMenu, ContextMenu, and their sub-menus, and the navigation - menus of TopNav (TopNavMenu), SideNav's collapsed rail, and NavHeadingMenu; +- menus: DropdownMenu, ContextMenu, and their sub-menus, the navigation + menus of TopNav (TopNavMenu), SideNavHeading, and NavHeadingMenu; and + BreadcrumbItem's `menu`, which renders DropdownMenu items; - option lists: Selector's and MultiSelector's options, and the wheels of DateInput, DateTimeInput, and TimeInput; - AlertDialog's actions; @@ -87,7 +92,10 @@ These are not members: - text inputs and InputGroup's input, which keep the caret and selection; - TabList, whose scrollable rows need a drag to scroll and whose tabs switch views; -- Pagination and Stepper, whose items are navigation destinations; +- Pagination and Stepper, inline page controls in the page flow rather than a + menu; +- Calendar's day grid, a two-dimensional date grid with its own keyboard and + range-selection model, which a slide to commit would break; - RadioList, CheckboxList, and SelectableCard, whose rows and cards sit in scrolling lists; - Token chips, which are removable and have no group control. @@ -156,16 +164,16 @@ Selector's and MultiSelector's options FR1–FR5 ## Adoption and exceptions -| Components or surface | Required adoption | -| -------------------------------------------------------------- | --------------------------------------------------------------------- | -| Menus and Selector's and MultiSelector's options | Follow FR1–FR5 through `module:DropdownMenu/useMenuPress`. | -| TopNav's, SideNav's collapsed-rail, and NavHeadingMenu's menus | Follow FR1–FR5 through `module:DropdownMenu/useMenuPress`, as menus. | -| Date and time wheels | Follow FR1–FR5 through the shared implementation. | -| AlertDialog's actions | Track the actions only. | -| ButtonGroup, ToggleButtonGroup, Toolbar | Track sibling buttons; a field inside is skipped; toggles follow FR4. | -| Banner and Toast action rows | Track only a row with two or more sibling actions. | +| Components or surface | Required adoption | +| ------------------------------------------------------ | --------------------------------------------------------------------- | +| Menus and Selector's and MultiSelector's options | Follow FR1–FR5 through `module:DropdownMenu/useMenuPress`. | +| TopNav's, SideNavHeading's, and NavHeadingMenu's menus | Follow FR1–FR5 through `module:DropdownMenu/useMenuPress`, as menus. | +| Date and time wheels | Follow FR1–FR5 through the shared implementation. | +| AlertDialog's actions | Track the actions only. | +| ButtonGroup, ToggleButtonGroup, Toolbar | Track sibling buttons; a field inside is skipped; toggles follow FR4. | +| Banner and Toast action rows | Track only a row with two or more sibling actions. | -TopNav's, SideNav's collapsed-rail, and NavHeadingMenu's menus track presses +TopNav's, SideNavHeading's, and NavHeadingMenu's menus track presses with their own code today; their adoption of the menu press model is an implementation gap. From 1e17099dd8050c3a3f8a2945791fd38ea08508bd Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Tue, 27 Oct 2026 14:32:15 -0700 Subject: [PATCH 15/17] Merge main into spec/ast094-action-groups --- docs/architecture/interaction-modality.md | 5 +++-- docs/families/action-groups.md | 27 ++++++++++++----------- docs/families/layout-regions.md | 7 ++++-- packages/core/src/Toolbar/Toolbar.spec.md | 3 ++- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/docs/architecture/interaction-modality.md b/docs/architecture/interaction-modality.md index 9760cb7224333..68cd0c6898fec 100644 --- a/docs/architecture/interaction-modality.md +++ b/docs/architecture/interaction-modality.md @@ -180,8 +180,9 @@ work. held pointer across a group's actions for every `family:action-groups` member other than menus and Selector's and MultiSelector's options, whose tracking and public `menuPressStep` belong to - `module:DropdownMenu/useMenuPress` (OQ1; implementation gap: MultiSelector - does not mount it yet). No other member keeps its own copy of the tracking. It is not exported (`spec:AST-017` + `module:DropdownMenu/useMenuPress` (OQ1; implementation gaps: MultiSelector, + and TopNav's, SideNavHeading's, NavHeadingMenu's and BreadcrumbItem's menus, + do not mount it yet). No other member keeps its own copy of the tracking. It is not exported (`spec:AST-017` FR21). ## Allowed variation diff --git a/docs/families/action-groups.md b/docs/families/action-groups.md index 21127f1742f9c..f4ea2acba3306 100644 --- a/docs/families/action-groups.md +++ b/docs/families/action-groups.md @@ -89,7 +89,8 @@ SegmentedControl is a named exception: this family's rule does not bind it. These are not members: -- text inputs and InputGroup's input, which keep the caret and selection; +- text inputs and InputGroup as a whole, whose input keeps the caret and + selection and whose addons belong to that input rather than forming a group; - TabList, whose scrollable rows need a drag to scroll and whose tabs switch views; - Pagination and Stepper, inline page controls in the page flow rather than a @@ -164,18 +165,18 @@ Selector's and MultiSelector's options FR1–FR5 ## Adoption and exceptions -| Components or surface | Required adoption | -| ------------------------------------------------------ | --------------------------------------------------------------------- | -| Menus and Selector's and MultiSelector's options | Follow FR1–FR5 through `module:DropdownMenu/useMenuPress`. | -| TopNav's, SideNavHeading's, and NavHeadingMenu's menus | Follow FR1–FR5 through `module:DropdownMenu/useMenuPress`, as menus. | -| Date and time wheels | Follow FR1–FR5 through the shared implementation. | -| AlertDialog's actions | Track the actions only. | -| ButtonGroup, ToggleButtonGroup, Toolbar | Track sibling buttons; a field inside is skipped; toggles follow FR4. | -| Banner and Toast action rows | Track only a row with two or more sibling actions. | - -TopNav's, SideNavHeading's, and NavHeadingMenu's menus track presses -with their own code today; their adoption of the menu press model is an -implementation gap. +| Components or surface | Required adoption | +| ------------------------------------------------------------------------ | --------------------------------------------------------------------- | +| Menus and Selector's and MultiSelector's options | Follow FR1–FR5 through `module:DropdownMenu/useMenuPress`. | +| TopNav's, SideNavHeading's, NavHeadingMenu's, and BreadcrumbItem's menus | Follow FR1–FR5 through `module:DropdownMenu/useMenuPress`, as menus. | +| Date and time wheels | Follow FR1–FR5 through the shared implementation. | +| AlertDialog's actions | Track the actions only. | +| ButtonGroup, ToggleButtonGroup, Toolbar | Track sibling buttons; a field inside is skipped; toggles follow FR4. | +| Banner and Toast action rows | Track only a row with two or more sibling actions. | + +TopNav's, SideNavHeading's, NavHeadingMenu's, and BreadcrumbItem's menus act on +the browser's click today and track no held pointer across their rows; their +adoption of the menu press model is an implementation gap. These are required outcomes, not a claim of completed adoption. The excluded components listed under the membership rule are not exceptions to approve; diff --git a/docs/families/layout-regions.md b/docs/families/layout-regions.md index 19d09fee8b72a..c1b011e41c1b6 100644 --- a/docs/families/layout-regions.md +++ b/docs/families/layout-regions.md @@ -107,7 +107,9 @@ wrapper, or a header-like visual treatment. - **FR1 — A region owns structure, not product meaning.** A member establishes a spatial boundary and renders caller content without adopting the content's - product semantics, state, or component contracts. + product semantics, state, or component contracts. Toolbar's press + tracking across its buttons (`family:action-groups` FR5) is the one + exception. - **FR2 — Region direction is logical.** Start and end follow writing direction. Divider placement and panel adjacency use the same logical region position. - **FR3 — Layout selects geometry from slot presence.** Header, Content, Footer, @@ -176,7 +178,8 @@ wrapper, or a header-like visual treatment. - **AV4 — Size model.** Layout owns fill/auto container height and content-width propagation; individual regions own applicable height, width, padding, and scroll props; Section owns its box-size props. -- **AV5 — Interaction.** Toolbar owns roving focus and keyboard hints. Other +- **AV5 — Interaction.** Toolbar owns roving focus, keyboard hints, and the + press tracking `family:action-groups` FR5 gives it across its buttons. Other members add no toolbar behavior. Resizable owns resize interaction. - **AV6 — Composition mechanism.** Members may use Stack utilities, direct CSS layout, contexts, or component composition. The observable region contract is diff --git a/packages/core/src/Toolbar/Toolbar.spec.md b/packages/core/src/Toolbar/Toolbar.spec.md index 7f8ee78b14b53..3b59d0788689b 100644 --- a/packages/core/src/Toolbar/Toolbar.spec.md +++ b/packages/core/src/Toolbar/Toolbar.spec.md @@ -97,7 +97,8 @@ Consumer migration instructions belong in consumer docs and release notes. - The painted outer chrome, variant, block padding, and selected divider edges — owned by Section and uncontracted here. - Content supplied through the three layout slots — owned by the caller or its - rendered components. + rendered components, except the press tracking `family:action-groups` FR5 + gives Toolbar across its buttons. - A general arbitrary-child layout primitive contract. - Arrow-key handling inside composed menus or popovers — owned by those components. From 2ebd1521643b1e91e634508ddc7cd91e2402a3a0 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Tue, 27 Oct 2026 15:17:34 -0700 Subject: [PATCH 16/17] Merge main into spec/ast094-action-groups --- docs/architecture/interaction-modality.md | 3 ++- docs/families/action-groups.md | 24 +++++++++++-------- .../ToggleButton/ToggleButtonGroup.spec.md | 22 ++++++++--------- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/docs/architecture/interaction-modality.md b/docs/architecture/interaction-modality.md index 68cd0c6898fec..73286fa34bc95 100644 --- a/docs/architecture/interaction-modality.md +++ b/docs/architecture/interaction-modality.md @@ -181,7 +181,8 @@ work. member other than menus and Selector's and MultiSelector's options, whose tracking and public `menuPressStep` belong to `module:DropdownMenu/useMenuPress` (OQ1; implementation gaps: MultiSelector, - and TopNav's, SideNavHeading's, NavHeadingMenu's and BreadcrumbItem's menus, + and TopNav's, SideNavHeading's, TopNavHeading's, NavHeadingMenu's and + BreadcrumbItem's menus, do not mount it yet). No other member keeps its own copy of the tracking. It is not exported (`spec:AST-017` FR21). diff --git a/docs/families/action-groups.md b/docs/families/action-groups.md index f4ea2acba3306..95a55c5a2e320 100644 --- a/docs/families/action-groups.md +++ b/docs/families/action-groups.md @@ -33,6 +33,7 @@ members: component:Toast, component:TopNavMenu, component:SideNavHeading, + component:TopNavHeading, component:NavHeadingMenu, component:BreadcrumbItem, ] @@ -69,7 +70,8 @@ momentary, toggle, or option actions that a person chooses one of. Members today: - menus: DropdownMenu, ContextMenu, and their sub-menus, the navigation - menus of TopNav (TopNavMenu), SideNavHeading, and NavHeadingMenu; and + menus of TopNav (TopNavMenu), SideNavHeading, TopNavHeading, and + NavHeadingMenu; and BreadcrumbItem's `menu`, which renders DropdownMenu items; - option lists: Selector's and MultiSelector's options, and the wheels of DateInput, DateTimeInput, and TimeInput; @@ -95,6 +97,8 @@ These are not members: views; - Pagination and Stepper, inline page controls in the page flow rather than a menu; +- TopNavMegaMenu's panel, a browsing grid of links in a `role="group"`, not a + menu (`component:TopNavMegaMenu` FR5); - Calendar's day grid, a two-dimensional date grid with its own keyboard and range-selection model, which a slide to commit would break; - RadioList, CheckboxList, and SelectableCard, whose rows and cards sit in @@ -165,16 +169,16 @@ Selector's and MultiSelector's options FR1–FR5 ## Adoption and exceptions -| Components or surface | Required adoption | -| ------------------------------------------------------------------------ | --------------------------------------------------------------------- | -| Menus and Selector's and MultiSelector's options | Follow FR1–FR5 through `module:DropdownMenu/useMenuPress`. | -| TopNav's, SideNavHeading's, NavHeadingMenu's, and BreadcrumbItem's menus | Follow FR1–FR5 through `module:DropdownMenu/useMenuPress`, as menus. | -| Date and time wheels | Follow FR1–FR5 through the shared implementation. | -| AlertDialog's actions | Track the actions only. | -| ButtonGroup, ToggleButtonGroup, Toolbar | Track sibling buttons; a field inside is skipped; toggles follow FR4. | -| Banner and Toast action rows | Track only a row with two or more sibling actions. | +| Components or surface | Required adoption | +| ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | +| Menus and Selector's and MultiSelector's options | Follow FR1–FR5 through `module:DropdownMenu/useMenuPress`. | +| TopNav's, SideNavHeading's, TopNavHeading's, NavHeadingMenu's, and BreadcrumbItem's menus | Follow FR1–FR5 through `module:DropdownMenu/useMenuPress`, as menus. | +| Date and time wheels | Follow FR1–FR5 through the shared implementation. | +| AlertDialog's actions | Track the actions only. | +| ButtonGroup, ToggleButtonGroup, Toolbar | Track sibling buttons; a field inside is skipped; toggles follow FR4. | +| Banner and Toast action rows | Track only a row with two or more sibling actions. | -TopNav's, SideNavHeading's, NavHeadingMenu's, and BreadcrumbItem's menus act on +TopNav's, SideNavHeading's, TopNavHeading's, NavHeadingMenu's, and BreadcrumbItem's menus act on the browser's click today and track no held pointer across their rows; their adoption of the menu press model is an implementation gap. diff --git a/packages/core/src/ToggleButton/ToggleButtonGroup.spec.md b/packages/core/src/ToggleButton/ToggleButtonGroup.spec.md index 2410738062148..c67188e3c810a 100644 --- a/packages/core/src/ToggleButton/ToggleButtonGroup.spec.md +++ b/packages/core/src/ToggleButton/ToggleButtonGroup.spec.md @@ -163,17 +163,17 @@ No design record governs ToggleButtonGroup. Its root carries the ## Verification map -| Contract | Verification | Representative states | Mutation or failure expectation | Audit section | -| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | -| FR1, AR1 | `ToggleButton.test.tsx` group role and label cases | Single and multiple | A missing name or role fails. Evidence gap: no test asserts the root target, `data-testid`, or `xstyle`. | `audit:ToggleButtonGroup/accessibility` | -| FR3–FR5, ORD1 | `ToggleButton.test.tsx` single and multiple suites | Select, switch, deselect, add, remove | A wrong pressed member or a wrong `onChange` payload fails. | `audit:ToggleButtonGroup/behavior` | -| FR6 | `ToggleButton.test.tsx` group-ownership cases | Members with standalone handlers and `onClick` | A member Action runs, or pressed state moves before `value`, fails. | `audit:ToggleButtonGroup/behavior` | -| FR7, ORD3, AR3 | `ToggleButton.test.tsx` disabled suite; `ToggleButton.a11y.test.tsx` | Member disabled; group disabled; tooltip-bearing member | A disabled member that toggles, or a re-enabled member, fails. The shared disabled states expect native `disabled`, matching the FR7 gap. | `audit:ToggleButtonGroup/accessibility` | -| FR2 | `ToggleButton.test.tsx` grouped elevation case | Member with its own elevation | A member losing its elevation inside the group fails. Evidence gap: no test asserts the spacing or the unpainted wrapper. Evidence gap: no test reads a member's painted shadow. | `audit:ToggleButtonGroup/layout` | -| FR9 (selection) | `layerScopedContext.test.tsx` "ends ancestor selection and callbacks while honoring explicit toggle props" | Member inside a fixed Layer and an inline Dialog | A member inside a layer that follows the outer group's selection or callbacks fails. | `audit:ToggleButtonGroup/behavior` | -| FR8, FR9 (size and disabled) | Evidence gap: no test of member Tab order, or of size and disabled scoping at a layer | — | — | `audit:ToggleButtonGroup/accessibility` | -| FR7 (size), ORD2, PR1 | Evidence gap: no test asserts the group size default, member size precedence, or the absence of group listeners | — | — | `audit:ToggleButtonGroup/behavior` | -| AR2 | `ToggleButton.a11y.test.tsx` and `ToggleButton.a11y.chromium.spec.ts` operable and focusable facts for the single- and multiple-group states | Group members | A group member that is not operable or that Tab does not reach fails. | `audit:ToggleButtonGroup/accessibility` | +| Contract | Verification | Representative states | Mutation or failure expectation | Audit section | +| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | +| FR1, AR1 | `ToggleButton.test.tsx` group role and label cases | Single and multiple | A missing name or role fails. Evidence gap: no test asserts the root target, `data-testid`, or `xstyle`. | `audit:ToggleButtonGroup/accessibility` | +| FR3–FR5, ORD1 | `ToggleButton.test.tsx` single and multiple suites | Select, switch, deselect, add, remove | A wrong pressed member or a wrong `onChange` payload fails. | `audit:ToggleButtonGroup/behavior` | +| FR6 | `ToggleButton.test.tsx` group-ownership cases | Members with standalone handlers and `onClick` | A member Action runs, or pressed state moves before `value`, fails. | `audit:ToggleButtonGroup/behavior` | +| FR7, ORD3, AR3 | `ToggleButton.test.tsx` disabled suite; `ToggleButton.a11y.test.tsx` | Member disabled; group disabled; tooltip-bearing member | A disabled member that toggles, or a re-enabled member, fails. The shared disabled states expect native `disabled`, matching the FR7 gap. | `audit:ToggleButtonGroup/accessibility` | +| FR2 | `ToggleButton.test.tsx` grouped elevation case | Member with its own elevation | A member losing its elevation inside the group fails. Evidence gap: no test asserts the spacing or the unpainted wrapper. Evidence gap: no test reads a member's painted shadow. | `audit:ToggleButtonGroup/layout` | +| FR9 (selection) | `layerScopedContext.test.tsx` "ends ancestor selection and callbacks while honoring explicit toggle props" | Member inside a fixed Layer and an inline Dialog | A member inside a layer that follows the outer group's selection or callbacks fails. | `audit:ToggleButtonGroup/behavior` | +| FR8, FR9 (size and disabled) | Evidence gap: no test of member Tab order, or of size and disabled scoping at a layer | — | — | `audit:ToggleButtonGroup/accessibility` | +| FR7 (size), ORD2, PR1 | Evidence gap: no test asserts the group size default, member size precedence, or that the group adds no listeners beyond the press tracking `family:action-groups` FR5 gives it | — | — | `audit:ToggleButtonGroup/behavior` | +| AR2 | `ToggleButton.a11y.test.tsx` and `ToggleButton.a11y.chromium.spec.ts` operable and focusable facts for the single- and multiple-group states | Group members | A group member that is not operable or that Tab does not reach fails. | `audit:ToggleButtonGroup/accessibility` | ## Current-state impact From fe7bf0171af26a5e3310c87eeba617b5990dee19 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Tue, 27 Oct 2026 15:56:41 -0700 Subject: [PATCH 17/17] Merge main into spec/ast094-action-groups --- docs/architecture/interaction-modality.md | 4 ++-- docs/families/action-groups.md | 29 +++++++++++++++-------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/docs/architecture/interaction-modality.md b/docs/architecture/interaction-modality.md index 73286fa34bc95..4fb2c663bdedb 100644 --- a/docs/architecture/interaction-modality.md +++ b/docs/architecture/interaction-modality.md @@ -181,8 +181,8 @@ work. member other than menus and Selector's and MultiSelector's options, whose tracking and public `menuPressStep` belong to `module:DropdownMenu/useMenuPress` (OQ1; implementation gaps: MultiSelector, - and TopNav's, SideNavHeading's, TopNavHeading's, NavHeadingMenu's and - BreadcrumbItem's menus, + and TopNav's, SideNavHeading's, TopNavHeading's, NavHeadingMenu's, + BreadcrumbItem's and TabMenu's menus, do not mount it yet). No other member keeps its own copy of the tracking. It is not exported (`spec:AST-017` FR21). diff --git a/docs/families/action-groups.md b/docs/families/action-groups.md index 95a55c5a2e320..39074b0c9ebf1 100644 --- a/docs/families/action-groups.md +++ b/docs/families/action-groups.md @@ -34,6 +34,8 @@ members: component:TopNavMenu, component:SideNavHeading, component:TopNavHeading, + component:TabMenu, + component:DateRangeInput, component:NavHeadingMenu, component:BreadcrumbItem, ] @@ -73,10 +75,12 @@ today: menus of TopNav (TopNavMenu), SideNavHeading, TopNavHeading, and NavHeadingMenu; and BreadcrumbItem's `menu`, which renders DropdownMenu items; +- TabMenu's menu of tab options; - option lists: Selector's and MultiSelector's options, and the wheels of DateInput, DateTimeInput, and TimeInput; - AlertDialog's actions; - ButtonGroup and ToggleButtonGroup; +- DateRangeInput's presets, one labeled group of preset buttons; - Toolbar's buttons; a field inside a toolbar is skipped; - the action rows of Banner and Toast when a row has two or more sibling actions. @@ -99,6 +103,10 @@ These are not members: menu; - TopNavMegaMenu's panel, a browsing grid of links in a `role="group"`, not a menu (`component:TopNavMegaMenu` FR5); +- SideNavItem's collapsed-rail flyout, a popover of the item's navigation rows, + not a menu; +- Popover itself, a surface whose content owns its role; a menu inside it + follows its own component; - Calendar's day grid, a two-dimensional date grid with its own keyboard and range-selection model, which a slide to commit would break; - RadioList, CheckboxList, and SelectableCard, whose rows and cards sit in @@ -169,16 +177,17 @@ Selector's and MultiSelector's options FR1–FR5 ## Adoption and exceptions -| Components or surface | Required adoption | -| ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | -| Menus and Selector's and MultiSelector's options | Follow FR1–FR5 through `module:DropdownMenu/useMenuPress`. | -| TopNav's, SideNavHeading's, TopNavHeading's, NavHeadingMenu's, and BreadcrumbItem's menus | Follow FR1–FR5 through `module:DropdownMenu/useMenuPress`, as menus. | -| Date and time wheels | Follow FR1–FR5 through the shared implementation. | -| AlertDialog's actions | Track the actions only. | -| ButtonGroup, ToggleButtonGroup, Toolbar | Track sibling buttons; a field inside is skipped; toggles follow FR4. | -| Banner and Toast action rows | Track only a row with two or more sibling actions. | - -TopNav's, SideNavHeading's, TopNavHeading's, NavHeadingMenu's, and BreadcrumbItem's menus act on +| Components or surface | Required adoption | +| ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | +| Menus and Selector's and MultiSelector's options | Follow FR1–FR5 through `module:DropdownMenu/useMenuPress`. | +| TopNav's, SideNavHeading's, TopNavHeading's, NavHeadingMenu's, BreadcrumbItem's, and TabMenu's menus | Follow FR1–FR5 through `module:DropdownMenu/useMenuPress`, as menus. | +| Date and time wheels | Follow FR1–FR5 through the shared implementation. | +| AlertDialog's actions | Track the actions only. | +| ButtonGroup, ToggleButtonGroup, Toolbar | Track sibling buttons; a field inside is skipped; toggles follow FR4. | +| DateRangeInput's presets | Track the preset buttons only. | +| Banner and Toast action rows | Track only a row with two or more sibling actions. | + +TopNav's, SideNavHeading's, TopNavHeading's, NavHeadingMenu's, BreadcrumbItem's, and TabMenu's menus act on the browser's click today and track no held pointer across their rows; their adoption of the menu press model is an implementation gap.