Repository navigation
spec(family): action groups — press, slide across, act on lift (AST-094 FR26–FR28) #8622
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d88563a
99d6101
3369b14
f4f87d2
5d22a75
fda4afb
0fb1d04
271b8b1
d4e8584
357ba25
9dfa484
79348c6
e97a190
e6a49f8
b411834
5c36360
ed9c179
deadc3f
e53d883
0fa4547
453f1d5
9e3a910
4e0bc32
bf52725
108ec60
f45888a
9be54dd
8548bcc
f5c492b
a4a07c3
af4a2a7
1e17099
4bb380d
2ebd152
2b9a08a
fe7bf01
514dd56
63b7b09
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -188,6 +188,17 @@ work. | |
| path, through the animator or a CSS custom property alike. Scroll snapping and momentum stay native. It adds no | ||
| module and is internal (`spec:AST-017` FR21). | ||
|
|
||
| - **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 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, | ||
| 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). | ||
|
|
||
| ## Allowed variation | ||
|
|
||
| Component families keep their shipped interaction models: | ||
|
|
@@ -252,19 +263,23 @@ is asked to carry a new decision. | |
| - `architecture:react-component-runtime` owns shared lifecycle and resource | ||
| mechanisms. This record owns only the cross-cutting modality-path, | ||
| no-stale-interaction-outcome, gesture-velocity-source, hover-intent | ||
| (INV11), and scroll-progress-source (INV12) requirements. | ||
| (INV11), scroll-progress-source (INV12), 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. | ||
| - The hover recognizer, `HoverGestureRecognizer`, and its `intent` setting | ||
| (INV11) — the one source of hover-show timing for Core components; not | ||
| exported. | ||
| - 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 and Selector's and MultiSelector's options; 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The frontmatter still reads |
||
| (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. | ||
|
|
@@ -284,6 +299,7 @@ reachability, newer-intent precedence, and the evidence/scope guard were approve | |
| | 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 | | ||
| | INV11 | Evidence gap: not implemented yet; the shared hover recognizer and its adopters' move onto it are pending. Then: unit tests of the hover recognizer's intent and browser tests of its adopters with a mouse, a pen, a finger, and the keyboard | A show without the owner's entry delay, a missed handoff within a group, a delayed keyboard show, a hover-shown surface that closes before the pointer reaches it, a finger served by it, or a second hover-timing implementation fails | | ||
| | 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 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 | | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: this row lost its padding in the merge from |
||
|
|
||
| Real-browser evidence is required when the behavior depends on | ||
| `:focus-visible`, pointer capability, touch or pen synthesis, rendered focus | ||
|
|
@@ -297,3 +313,8 @@ 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 do menus and Selector's and MultiSelector's options move onto | ||
| the shared tracker?** (`engineering`) Open. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,219 @@ | ||
| --- | ||
| 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, | ||
| ] | ||
|
Comment on lines
+13
to
+17
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: |
||
| members: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: none of these member records lists
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| [ | ||
| component:DropdownMenu, | ||
| component:DropdownMenuSubMenu, | ||
| component:ContextMenu, | ||
| component:Selector, | ||
| component:MultiSelector, | ||
| component:DateInput, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The membership rule names the date wheels of DateInput and DateTimeInput (lines 66–67), but only |
||
| component:DateTimeInput, | ||
| component:TimeInput, | ||
| component:AlertDialog, | ||
| component:ButtonGroup, | ||
| component:ToggleButtonGroup, | ||
| component:Toolbar, | ||
| component:Banner, | ||
| component:Toast, | ||
| component:TopNavMenu, | ||
| component:SideNavHeading, | ||
| component:TopNavHeading, | ||
| component:TabMenu, | ||
| component:DateRangeInput, | ||
| component:NavHeadingMenu, | ||
| component:BreadcrumbItem, | ||
| ] | ||
| architecture: [architecture:interaction-modality] | ||
| contributing: [] | ||
| deciding_specs: [spec:AST-094/DEC-17] | ||
| --- | ||
|
|
||
| # Action groups contract | ||
|
|
||
| <!-- review-applicability:v1 --> | ||
|
|
||
| ```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, with SegmentedControl as the one named exception. Every | ||
| 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 | ||
|
|
||
| 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: | ||
|
Comment on lines
+70
to
+72
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This rule also covers existing option lists and menus that neither the members list below nor the exclusions name, and none of them mounts
Comment on lines
+70
to
+72
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Three current components meet this rule and appear neither in the list below nor under "These are not members": TabMenu's |
||
|
|
||
| - menus: DropdownMenu, ContextMenu, and their sub-menus, the navigation | ||
| 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. | ||
|
|
||
| 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. | ||
|
|
||
| SegmentedControl is a named exception: this family's rule does not bind it. | ||
|
|
||
| These are not members: | ||
|
|
||
| - 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 | ||
| 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 | ||
| 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 | ||
|
|
||
| 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 | ||
|
|
||
| - **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). 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 | ||
| (`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). 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. | ||
| - **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 `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. | ||
|
|
||
| ## 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. | ||
| - 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 | ||
|
|
||
| | 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 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. | ||
|
|
||
| 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 | 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 | | ||
|
|
||
| ## 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 shared implementation's thresholds. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This record still says it "owns only the cross-cutting modality-path, no-stale-interaction-outcome, and gesture-velocity-source requirements" (lines 237–238) and "Otherwise no separate specification changes this record" (line 245),
deciding_specslists onlyspec:AST-100/DEC-1, and AST-094's Current-state impact says this record's "invariants are unchanged" (lines 500–501). Suggest adding INV13 to the Owning code sentence (with a line like INV10's),spec:AST-094/DEC-17todeciding_specsand Deciding specs, and naming INV13 in AST-094's impact line.