@astryxdesign/core 0.7.5-canary.85e48a0 → 0.7.5-canary.8be99c4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/Badge/Badge.spec.md +10 -10
- package/src/Button/Button.spec.md +1 -1
- package/src/ButtonGroup/ButtonGroup.spec.md +218 -0
- package/src/ContextMenu/ContextMenu.spec.md +19 -15
- package/src/DropdownMenu/DropdownMenu.spec.md +12 -6
- package/src/DropdownMenu/modules/useMenuPress.spec.md +62 -57
- package/src/Popover/Popover.spec.md +19 -0
- package/src/Selector/Selector.spec.md +3 -2
- package/src/Token/Token.spec.md +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.7.5-canary.
|
|
3
|
+
"version": "0.7.5-canary.8be99c4",
|
|
4
4
|
"displayName": "Astryx Core",
|
|
5
5
|
"description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -792,8 +792,8 @@
|
|
|
792
792
|
"react-dom": ">=19.0.0"
|
|
793
793
|
},
|
|
794
794
|
"devDependencies": {
|
|
795
|
-
"@astryxdesign/a11y-spec": "0.7.5-canary.
|
|
796
|
-
"@astryxdesign/cli": "0.7.5-canary.
|
|
795
|
+
"@astryxdesign/a11y-spec": "0.7.5-canary.8be99c4",
|
|
796
|
+
"@astryxdesign/cli": "0.7.5-canary.8be99c4",
|
|
797
797
|
"@babel/cli": "^7.29.7",
|
|
798
798
|
"@babel/core": "^7.29.7",
|
|
799
799
|
"@babel/preset-react": "^7.29.7",
|
package/src/Badge/Badge.spec.md
CHANGED
|
@@ -98,16 +98,16 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
98
98
|
|
|
99
99
|
## Behavioral and layout contract
|
|
100
100
|
|
|
101
|
-
| ID | Invariant
|
|
102
|
-
| --- |
|
|
103
|
-
| FR1 | Badge renders one inline-flex `span` root, centered on both axes, with block size `--spacing-5
|
|
104
|
-
| FR2 | When `icon` is supplied it renders directly inside the root, before the Label container, separated by `--spacing-1`; the label always renders inside its own Label container.
|
|
105
|
-
| FR3 | The badge is one line and never wraps. The root is capped at the inline size of its container and may shrink below its content size; a label that does not fit is clipped with a trailing ellipsis inside the Label container and does not escape the badge.
|
|
106
|
-
| FR4 | A non-empty string label or any number label sets the root `title` to the label text, whether or not the label is clipped. A rich-node or empty-string label sets no `title`.
|
|
107
|
-
| FR5 | `variant` defaults to `neutral`. `neutral` paints `--color-neutral` with primary text; `info`, `success`, `warning`, and `error` paint the solid accent, success, warning, and error colors with their matching on-color text; each color variant paints its tinted background with its matching colored text.
|
|
108
|
-
| FR6 | The `badge` target carries the resolved `variant` for every value, including a value a theme adds through `BadgeVariantMap`. A theme-added value receives no component fill or text color, so it renders the FR1 geometry over a transparent background in the inherited text color until a theme paints it.
|
|
109
|
-
| FR7 | `ref` reaches the root `span`. Supported DOM, ARIA, data, and event props reach the root; `xstyle`, `className`, and `style` merge in that order after the component styles.
|
|
110
|
-
| FR8 | Badge uses no hooks, measurement, or client-only API, so it renders identically on the server and the client.
|
|
101
|
+
| ID | Invariant | Basis |
|
|
102
|
+
| --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
|
|
103
|
+
| FR1 | Badge renders one inline-flex `span` root, centered on both axes, with block size `--spacing-5` (at least that size once it grows with a set text size under `spec:AST-102` FR4; today's fixed size is a gap), no block padding, inline padding `--spacing-2`, `--radius-full` corners, inherited font family, `--text-supporting-size` and `--text-supporting-leading` type, and `--font-weight-medium` weight. | Current source |
|
|
104
|
+
| FR2 | When `icon` is supplied it renders directly inside the root, before the Label container, separated by `--spacing-1`; the label always renders inside its own Label container. | Current source and tests |
|
|
105
|
+
| FR3 | The badge is one line and never wraps. The root is capped at the inline size of its container and may shrink below its content size; a label that does not fit is clipped with a trailing ellipsis inside the Label container and does not escape the badge. | Current source and consumer docs |
|
|
106
|
+
| FR4 | A non-empty string label or any number label sets the root `title` to the label text, whether or not the label is clipped. A rich-node or empty-string label sets no `title`. | Current source and tests |
|
|
107
|
+
| FR5 | `variant` defaults to `neutral`. `neutral` paints `--color-neutral` with primary text; `info`, `success`, `warning`, and `error` paint the solid accent, success, warning, and error colors with their matching on-color text; each color variant paints its tinted background with its matching colored text. | Current source and consumer docs |
|
|
108
|
+
| FR6 | The `badge` target carries the resolved `variant` for every value, including a value a theme adds through `BadgeVariantMap`. A theme-added value receives no component fill or text color, so it renders the FR1 geometry over a transparent background in the inherited text color until a theme paints it. | Current source and extensible-axis test |
|
|
109
|
+
| FR7 | `ref` reaches the root `span`. Supported DOM, ARIA, data, and event props reach the root; `xstyle`, `className`, and `style` merge in that order after the component styles. | Current source and tests |
|
|
110
|
+
| FR8 | Badge uses no hooks, measurement, or client-only API, so it renders identically on the server and the client. | Current source and server-safety test |
|
|
111
111
|
|
|
112
112
|
### Allowed variation
|
|
113
113
|
|
|
@@ -120,7 +120,7 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
120
120
|
| FR1 | Without link mode Button renders one native `<button>` with `type="button"` unless `type` says otherwise; `name`, `value`, and `form` reach that element. | `family:buttons` FR2; current source and tests |
|
|
121
121
|
| FR2 | Content renders in this order: the leading `icon` slot, the label text (`children` when supplied, else `label`), then `endContent`. Icon-only renders the icon alone and omits the label text and `endContent`. `endContent` inherits the Button's text color. | `family:buttons` AV2; current source and tests |
|
|
122
122
|
| FR3 | An Astryx Icon in the `icon` slot without an explicit size renders `sm` in `sm` and `md` Buttons and `md` in `lg` Buttons; an explicit Icon size wins. | `family:buttons` FR7; current source |
|
|
123
|
-
| FR4 | Height comes from the shared element size for the resolved size: an explicit `size` wins, then the nearest size provider, then `md`. Icon-only is square (`--button-icon-only-aspect`, default `1 / 1`) with no padding.
|
|
123
|
+
| FR4 | Height comes from the shared element size for the resolved size, as a minimum that grows with a set text size under `spec:AST-102` FR4 (today's fixed height is a gap): an explicit `size` wins, then the nearest size provider, then `md`. Icon-only is square (`--button-icon-only-aspect`, default `1 / 1`) with no padding. | `family:buttons` FR7; `architecture:component-size-cascade` INV3–INV5 |
|
|
124
124
|
| FR5 | The label stays on one line. A labelled Button may shrink in a row, never exceeds its container's inline size, and truncates the label with an ellipsis. An explicit `width` applies in button and link modes and is still capped at the container. Icon-only does not shrink and is not capped. | Current source and tests |
|
|
125
125
|
| FR6 | On activation `onClick` runs first. If it calls `preventDefault()`, `clickAction` does not run. Otherwise `clickAction` runs, and the Button is pending until its promise settles. | `family:buttons` FR5; current source and tests |
|
|
126
126
|
| FR7 | A fire-once `clickAction` runs once per in-flight action: a second activation while it is pending, including a same-tick double-click, does nothing. With `isInterruptible`, a pending Button stays operable and each activation starts a new `clickAction`. | `family:buttons` FR5; current source and tests |
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_version: 3
|
|
3
|
+
template_version: 7
|
|
4
|
+
kind: component
|
|
5
|
+
id: component:ButtonGroup
|
|
6
|
+
authority: current
|
|
7
|
+
archive_reason: null
|
|
8
|
+
superseded_by: null
|
|
9
|
+
approved_by: cixzhang
|
|
10
|
+
approved_at: 2026-10-25
|
|
11
|
+
owners: [cixzhang]
|
|
12
|
+
review_triggers: [public-api, behavior, layout, theming, accessibility]
|
|
13
|
+
verified_by:
|
|
14
|
+
[
|
|
15
|
+
packages/core/src/ButtonGroup/ButtonGroup.test.tsx,
|
|
16
|
+
packages/core/src/Layer/layerScopedContext.test.tsx,
|
|
17
|
+
packages/core/src/hooks/useListFocus.test.tsx,
|
|
18
|
+
]
|
|
19
|
+
modules: []
|
|
20
|
+
families: [family:buttons]
|
|
21
|
+
design_specs: []
|
|
22
|
+
architecture:
|
|
23
|
+
[
|
|
24
|
+
architecture:component-size-cascade,
|
|
25
|
+
architecture:component-theming-surface,
|
|
26
|
+
architecture:public-component-api,
|
|
27
|
+
]
|
|
28
|
+
contributing: []
|
|
29
|
+
system_specs: [spec:AST-038, spec:AST-085]
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
# ButtonGroup component contract
|
|
33
|
+
|
|
34
|
+
## Contract at a glance
|
|
35
|
+
|
|
36
|
+
| Area | Contract |
|
|
37
|
+
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
38
|
+
| Public contract | None changes. ButtonGroup takes `label`, `orientation`, `size`, `elevation`, `isDisabled`, and Button or IconButton children. |
|
|
39
|
+
| Behavior | A labelled `role="group"` that joins its Button members into one connected surface, gives them a default size and its disabled state, owns one shared elevation, and is one Tab stop with arrow-key roving. |
|
|
40
|
+
| End-user impact | Keyboard users reach the whole group with one Tab and move through it with arrows, which skip disabled members. A disabled group without tooltips has no Tab stop, so keyboard users cannot find it or hear that it is disabled. |
|
|
41
|
+
| Builder impact | None. |
|
|
42
|
+
| Compatibility/readiness | Records shipped behavior; no API, default, or DOM change. Disabled members use native `disabled`, the Tab stop leaves disabled and busy-only members, and wrapped members lose connected geometry; all are listed under Current-state impact. |
|
|
43
|
+
| Review checks | Reject a group without a name, gaps between members, rounded inner corners, a shadow on a member, more than one Tab stop, arrows that land on a disabled member, or arrows that steal keys from a member's open menu. |
|
|
44
|
+
| Governing rules | `family:buttons` FR3, FR8, FR10–FR12, AV5; `architecture:component-size-cascade` INV3–INV8; `spec:AST-085` FR1, FR7; `spec:AST-038` FR7, FR8. |
|
|
45
|
+
|
|
46
|
+
This table is a review projection; the body below is authoritative.
|
|
47
|
+
|
|
48
|
+
## Intent
|
|
49
|
+
|
|
50
|
+
ButtonGroup presents a few related actions as one connected control. It owns
|
|
51
|
+
the group's accessible name, connected geometry, shared elevation, inherited
|
|
52
|
+
size and disabled state, and the group's single-Tab-stop keyboard model, as
|
|
53
|
+
`family:buttons` assigns. Consumer usage lives in `ButtonGroup.doc.mjs`.
|
|
54
|
+
|
|
55
|
+
## Compatibility and migration
|
|
56
|
+
|
|
57
|
+
- Released default preserved: `yes`; `orientation="horizontal"`, `size="md"`,
|
|
58
|
+
`elevation="none"`, `isDisabled={false}`
|
|
59
|
+
- Compatibility class: records shipped behavior; no public prop, default,
|
|
60
|
+
export, or DOM change
|
|
61
|
+
- Controlled/uncontrolled behavior: not applicable
|
|
62
|
+
- Migration decision: none
|
|
63
|
+
|
|
64
|
+
Consumer migration instructions belong in consumer docs and release notes.
|
|
65
|
+
|
|
66
|
+
## Ownership boundary
|
|
67
|
+
|
|
68
|
+
**Owns**
|
|
69
|
+
|
|
70
|
+
- The group root: role, accessible name, orientation and disabled exposure, and
|
|
71
|
+
the `button-group` target.
|
|
72
|
+
- Connected geometry its Button members paint: outer-only corners, leading-edge
|
|
73
|
+
dividers, and no press scale.
|
|
74
|
+
- One shared resting elevation, and flat members.
|
|
75
|
+
- The default size and disabled state it gives its members.
|
|
76
|
+
- Roving focus: one Tab stop, arrow keys, Home and End.
|
|
77
|
+
|
|
78
|
+
**Does not own / non-goals**
|
|
79
|
+
|
|
80
|
+
- Each member's own surface, naming, activation, pending, and disabled rendering
|
|
81
|
+
— owned by `component:Button` and `component:IconButton`.
|
|
82
|
+
- A member's menu or other layer, and keys pressed inside it — owned by that
|
|
83
|
+
member (for example `component:DropdownMenu`).
|
|
84
|
+
- Selection state across toggles — owned by `component:ToggleButtonGroup`.
|
|
85
|
+
- Horizontal overflow — uncontracted; the group does not wrap or collapse.
|
|
86
|
+
- Nested ButtonGroups — unsupported; behavior is uncontracted.
|
|
87
|
+
|
|
88
|
+
## Public concepts
|
|
89
|
+
|
|
90
|
+
| Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
|
|
91
|
+
| ----------- | ---------------------------- | ----------------------------------------- | ----------------------------------------- | ------------------------- | ----------------------- | --------- | -------------------------------- |
|
|
92
|
+
| Orientation | `horizontal`, `vertical` | Direction of the row and of arrow keys. | Always. | `horizontal` | `component:ButtonGroup` | Stable | TypeScript rejects other values. |
|
|
93
|
+
| Size | `sm`, `md`, `lg` | Default size for members. | Always; a member's own `size` wins. | Inherited size, else `md` | `family:buttons` | Stable | TypeScript rejects other values. |
|
|
94
|
+
| Elevation | `none`, `low`, `med`, `high` | Shadow under the whole connected surface. | Always; members paint none. | `none` | `family:buttons` | Stable | TypeScript rejects other values. |
|
|
95
|
+
| Disabled | off, on | Every Button member is disabled. | Always. | off | `component:ButtonGroup` | Stable | Not applicable. |
|
|
96
|
+
|
|
97
|
+
## Behavioral and layout contract
|
|
98
|
+
|
|
99
|
+
| ID | Invariant | Basis |
|
|
100
|
+
| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
101
|
+
| FR1 | The root is one element with `role="group"` and `aria-label` set to `label`. It reflects orientation as `data-orientation`, never `aria-orientation`, and sets `aria-disabled="true"` only while disabled. `ref`, `data-testid`, `className`, `style`, `xstyle`, and other DOM props reach it. | `family:buttons` FR11; current source and tests |
|
|
102
|
+
| FR2 | Members sit edge to edge with no gap, stretched across the cross axis: a row when horizontal, a column when vertical. | `family:buttons` FR12; current source |
|
|
103
|
+
| FR3 | Group geometry is painted by Astryx Button members; other children keep their own paint. Only the group's outer corners are rounded: the first member's leading corners and the last member's trailing corners. A trailing layer element a member renders after itself (popover, template, or dialog) does not count as a member, so the real last member keeps its rounded corners. Any other following element, including a display-contents wrapper or a raw button, counts as a member. A Button member wrapped in a Tooltip or HoverCard MUST join the connected surface like a direct member (`family:buttons` FR12). Implementation gap: inside that display-contents wrapper the Button is both the first and the last child, so in a group of two or more it keeps all four corners rounded wherever it sits, and draws no divider when it is not first. | `family:buttons` FR12; current tests |
|
|
104
|
+
| FR4 | Every Button member after the first draws a divider on its leading edge (inline-start when horizontal, block-start when vertical) in the border color, or in the on-accent color on a primary or destructive member; a wrapped member does not (FR3 gap). Members do not take Button's press scale. | `family:buttons` FR12; current source |
|
|
105
|
+
| FR5 | The group paints the shadow for its `elevation`, following the element radius. Every Button member paints no shadow and reflects `data-elevation="none"`, whatever its own `elevation` says. | `family:buttons` FR8, FR10; current tests |
|
|
106
|
+
| FR6 | The group provides its resolved size to descendants up to the next layer boundary (`spec:AST-038` FR7, FR8): an explicit `size`, then the nearest outer provider, then `md`. A member's own `size` wins. | `architecture:component-size-cascade` INV3–INV8; current source; `spec:AST-038` FR7 |
|
|
107
|
+
| FR7 | While disabled, every Button member MUST be disabled under `component:Button` FR10: focusable with `aria-disabled`, inert, and never running its action, so the group keeps its Tab stop (`spec:AST-085` FR1, FR7). Implementation gap: members without a tooltip render native `disabled`, so a disabled group contributes no Tab stop. | `family:buttons` FR3; `spec:AST-085` FR7; current tests |
|
|
108
|
+
| FR8 | The group is one Tab stop. The stop starts on the first enabled member and moves with arrow navigation. A link member counts as a member. The stop MUST always sit on exactly one member, and a member that becomes busy or disabled MUST keep it (`component:Button` FR9, FR10; `spec:AST-085` FR1, FR7). Implementation gap: the group repairs the stop only after its own renders and when focus enters a member, and each repair keeps an enabled holder or gives the stop to the first enabled member. Between repairs the stop can sit on a member that has become busy-only, disabled, or unmounted, so the group can have a second stop or none; focus that arrives by pointer or by code does not take the stop, so the next Tab or Shift+Tab can stop inside the group; and a fully disabled group repairs nothing, so a group disabled from its first render gives each tooltip-bearing member its own stop and native-disabled members none. | `family:buttons` AV5; WAI-ARIA APG roving tabindex; current tests |
|
|
109
|
+
| FR9 | Horizontal groups move with Left and Right in visual order, reversed under RTL; vertical groups move with Up and Down. Home and End jump to the first and last enabled member. Movement wraps at the ends and skips natively disabled and `aria-disabled` members, busy-only members (`component:Button` FR9) included; a member busy with an interruptible action stays in the order. | `family:buttons` AV5; current tests |
|
|
110
|
+
| FR10 | Keys pressed inside a member's open layer (such as a DropdownMenu) belong to that layer, and the group keeps its Tab stop on its own members. A group inside a popover or other layer still handles its own arrow keys. The group's context does not reach Buttons rendered inside a member's layer (`spec:AST-038` FR7). | Current source and tests; `spec:AST-038` FR7 |
|
|
111
|
+
| FR11 | The root carries `astryx-button-group` and reflects `data-size`, `data-orientation`, and `data-elevation`. | `family:buttons` FR10; `architecture:component-theming-surface`; tests |
|
|
112
|
+
|
|
113
|
+
### Allowed variation
|
|
114
|
+
|
|
115
|
+
- **AV1 — Members.** Button, IconButton, and a Button-triggered DropdownMenu are
|
|
116
|
+
supported members; variants may differ between members.
|
|
117
|
+
- **AV2 — Theme paint.** Themes may restyle the group and member surfaces
|
|
118
|
+
through `button-group` and `button` without changing connected geometry.
|
|
119
|
+
|
|
120
|
+
### Representative states
|
|
121
|
+
|
|
122
|
+
| State | Required invariant | Allowed variation |
|
|
123
|
+
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
|
|
124
|
+
| Horizontal, at rest | Named group; one Tab stop on the first member; outer corners only. | Member variants. |
|
|
125
|
+
| Vertical | Column; Up and Down move focus; block-start dividers. | — |
|
|
126
|
+
| Elevated | Shadow on the group; members flat. | Tier. |
|
|
127
|
+
| Trailing DropdownMenu | The menu trigger keeps rounded trailing corners; arrows inside the open menu stay with it. | Menu content. |
|
|
128
|
+
| One member disabled | Arrows skip it. Implementation gap: the Tab stop leaves it when the group repairs (FR8). | Tooltip present or not. |
|
|
129
|
+
| Group disabled | `aria-disabled` on the group; every member focusable and inert; one Tab stop. Implementation gap: native-disabled members give no Tab stop; disabled from the first render, tooltip-bearing members are one stop each (FR7, FR8). | — |
|
|
130
|
+
|
|
131
|
+
### Transformation and precedence order
|
|
132
|
+
|
|
133
|
+
- **ORD1 — Member size.** Member `size`, then the group's resolved size.
|
|
134
|
+
- **ORD2 — Member elevation.** Inside a group, always `none`; the group's
|
|
135
|
+
`elevation` paints.
|
|
136
|
+
- **ORD3 — Member availability.** A member is disabled when the group or the
|
|
137
|
+
member is disabled; neither can re-enable the other. Otherwise Button's own
|
|
138
|
+
availability rules apply.
|
|
139
|
+
|
|
140
|
+
### Performance and resources
|
|
141
|
+
|
|
142
|
+
- **PR1 — Tab-stop repair.** The group repairs the Tab stop after each of its
|
|
143
|
+
own renders and when focus enters a member; it adds no global listener.
|
|
144
|
+
|
|
145
|
+
## Accessibility contract
|
|
146
|
+
|
|
147
|
+
- **AR1 — Group name.** The group exposes `role="group"` and its `label` as
|
|
148
|
+
its accessible name; members keep their own names (`family:buttons` FR11).
|
|
149
|
+
- **AR2 — Keyboard.** FR8–FR10 are the keyboard contract.
|
|
150
|
+
- **AR3 — Disabled exposure.** A disabled group exposes `aria-disabled` on the
|
|
151
|
+
group; members expose disabled per `component:Button` AR5 (FR7 gap).
|
|
152
|
+
- **AR4 — Focus indicator.** Each member paints Button's focus indicator.
|
|
153
|
+
|
|
154
|
+
## Design relationships
|
|
155
|
+
|
|
156
|
+
| Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
|
|
157
|
+
| ---------------- | --------------------------------------------------------------------- | ------------------------ | -------------- | ------------------ |
|
|
158
|
+
| Button | Members joined into one surface with outer-only corners. | Current source and tests | Prominent | FR2, FR3 |
|
|
159
|
+
| Divider | Thin leading-edge border between members; on-accent on solid members. | Current source | Supporting | FR4 |
|
|
160
|
+
| Group shadow | One shadow under the connected surface. | Current source and tests | Supporting | FR5 |
|
|
161
|
+
|
|
162
|
+
No design record governs ButtonGroup. The root carries the `button-group`
|
|
163
|
+
target; the Button and Divider anatomy parts are painted by each member's
|
|
164
|
+
`button` target. The consumer anatomy names no root part, so this record
|
|
165
|
+
carries no anatomy-theming map.
|
|
166
|
+
|
|
167
|
+
## Family and system relationships
|
|
168
|
+
|
|
169
|
+
- `family:buttons` assigns ButtonGroup connected geometry, roving focus,
|
|
170
|
+
inherited size and disabled state, and one shared elevation (FR8, FR11,
|
|
171
|
+
FR12); ButtonGroup adopts them.
|
|
172
|
+
- `component:Button` paints the connected geometry from the group's context
|
|
173
|
+
and owns each member's behavior.
|
|
174
|
+
- `architecture:component-size-cascade` owns size inheritance (FR6).
|
|
175
|
+
- `spec:AST-085` FR7 applies to group disablement; the gap is FR7 here.
|
|
176
|
+
|
|
177
|
+
## Verification map
|
|
178
|
+
|
|
179
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
180
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
|
|
181
|
+
| FR1, AR1 | `ButtonGroup.test.tsx` role, label, ref, `data-testid`, orientation | Horizontal and vertical | A missing name, `aria-orientation`, a dropped ref, or a dropped `data-testid` fails. Evidence gap: no test passes `className`, `style`, or `xstyle`. | `audit:ButtonGroup/accessibility` |
|
|
182
|
+
| FR3 | `ButtonGroup.test.tsx` trailing-radius suite | Tooltip, link, DropdownMenu, HoverCard, and raw-button members | A square trailing member, or a rounded inner member, fails. Evidence gap: the suite renders wrapped members only last and asserts the member before them; no test checks a wrapped member's own corners or divider in any position. | `audit:ButtonGroup/layout` |
|
|
183
|
+
| FR5, FR11 | `ButtonGroup.test.tsx` elevation suite | Every tier; member with its own elevation | Identical classes for two tiers, a mismatched `data-elevation`, or a member not reporting `none` fails. | `audit:ButtonGroup/theming` |
|
|
184
|
+
| FR11 (target, size) | Evidence gap: no test asserts `astryx-button-group` or `data-size` on the root | Each size | — | `audit:ButtonGroup/theming` |
|
|
185
|
+
| FR7, AR3 | `ButtonGroup.test.tsx` disabled cases | Group disabled | An operable member fails. Native-disabled and no-Tab-stop assertions match the FR7 gap. | `audit:ButtonGroup/accessibility` |
|
|
186
|
+
| FR8, AR2 | `ButtonGroup.test.tsx` roving-tabindex suite | Enter and leave; disabled member; unmount; becomes disabled; link | Two Tab stops or a lost stop fails. The suite asserts that a disabled member never holds the stop and that the stop is repaired away from it, matching the FR8 gap. Evidence gap: no test of a busy or self-unmounted holder, pointer focus, or a group disabled after mount. | `audit:ButtonGroup/accessibility` |
|
|
187
|
+
| FR9, FR10, AR2 | `ButtonGroup.test.tsx` keyboard-navigation suite; `useListFocus.test.tsx` "useListFocus disabled-item skipping" for `aria-disabled` items and Home and End; `layerScopedContext.test.tsx` "ends group-owned disabled state while preserving explicit and inner-group state" for the layer boundary | Arrows, Home, End, vertical, RTL, open menu, group inside a popover | Wrong direction, no wrap, landing on a disabled member, stealing a menu's keys, or group state leaking into a member's layer fails. Evidence gap: no test skips a busy or tooltip-disabled Button member. | `audit:ButtonGroup/accessibility` |
|
|
188
|
+
| FR2, FR3, FR4, FR6 | Evidence gap: no assertion of gap-free layout, divider paint, the absent press scale, non-Button children keeping their own paint, or member size inheritance beyond rendering | — | — | `audit:ButtonGroup/layout` |
|
|
189
|
+
| ORD2 | `ButtonGroup.test.tsx` elevation suite | Member with its own elevation | A member not reporting `none` fails. | `audit:ButtonGroup/theming` |
|
|
190
|
+
| ORD1, ORD3, PR1, AR4 | Evidence gap: no test asserts member size precedence, a member `isDisabled` inside an enabled group, when the Tab stop is repaired, or the member focus indicator | — | — | `audit:ButtonGroup/behavior` |
|
|
191
|
+
|
|
192
|
+
## Current-state impact
|
|
193
|
+
|
|
194
|
+
- **Disabled focusability (FR7, AR3).** A disabled group renders its members
|
|
195
|
+
with native `disabled`, so the group has no Tab stop, against `spec:AST-085`
|
|
196
|
+
FR1 and FR7 and `family:buttons` FR3. The group's tests assert that state.
|
|
197
|
+
- **Tab stop (FR8).** The Tab stop leaves a disabled or busy-only member when the
|
|
198
|
+
group repairs it; between repairs the group can have a second stop or none,
|
|
199
|
+
including after a pointer click on a member or when the holder unmounts or
|
|
200
|
+
disables itself; and a group disabled from its first render gives each
|
|
201
|
+
tooltip-bearing member its own stop, against `spec:AST-085` FR1 and FR7.
|
|
202
|
+
- **Wrapped members (FR3, FR4).** A Button wrapped in a Tooltip or HoverCard
|
|
203
|
+
keeps four rounded corners in any position, and no divider when it is not
|
|
204
|
+
first, against
|
|
205
|
+
`family:buttons` FR12.
|
|
206
|
+
|
|
207
|
+
## Decision log
|
|
208
|
+
|
|
209
|
+
No component-local decision. ButtonGroup applies `family:buttons`.
|
|
210
|
+
|
|
211
|
+
## Open questions
|
|
212
|
+
|
|
213
|
+
None.
|
|
214
|
+
|
|
215
|
+
## Content boundary
|
|
216
|
+
|
|
217
|
+
This file does not duplicate consumer prop tables/examples, current audit
|
|
218
|
+
results, implementation steps, or family/system rules. It links to their owners.
|
|
@@ -43,11 +43,15 @@ theming reachability.
|
|
|
43
43
|
|
|
44
44
|
## Compatibility and migration
|
|
45
45
|
|
|
46
|
-
- Released default: `popover`
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
- Released default: `popover`. Under a finger, `spec:AST-094` FR33's lift
|
|
47
|
+
becomes the default, and `presentation="popover"` keeps the released menu
|
|
48
|
+
(`spec:AST-094` FR25).
|
|
49
|
+
- Compatibility class: compatible behavior correction; public API, DOM shape,
|
|
50
|
+
styling, and targets are preserved; the default under a finger changes as
|
|
51
|
+
above
|
|
49
52
|
- Controlled/uncontrolled behavior: internally managed open state
|
|
50
|
-
- Migration decision:
|
|
53
|
+
- Migration decision: `spec:AST-094` FR25 (a caller that needs the released
|
|
54
|
+
menu under a finger passes `presentation="popover"`)
|
|
51
55
|
|
|
52
56
|
Consumer migration instructions belong in consumer docs and release notes.
|
|
53
57
|
|
|
@@ -81,17 +85,17 @@ Consumer props, content modes, and presentation policy remain documented in
|
|
|
81
85
|
|
|
82
86
|
## Behavioral and layout contract
|
|
83
87
|
|
|
84
|
-
| ID | Candidate invariant
|
|
85
|
-
| --- |
|
|
86
|
-
| FR1 | Every render contains a caller-provided Trigger area. Pointer presentation opens a Pointer menu surface at the current local cursor anchor; touch presentation opens a Touch sheet frame containing a Touch menu surface and, for data mode, a Touch action list and Touch action rows.
|
|
87
|
-
| FR2 | The current `context-menu` target remains on the painted Pointer menu surface and alternative Touch menu surface, not on the Trigger area or cursor anchor.
|
|
88
|
-
| FR3 | Pointer action rows retain DropdownMenu ownership. The touch frame retains BottomSheet ownership, while data-driven touch lists and rows retain List ownership.
|
|
89
|
-
| FR4 | Compound `menuContent` remains a caller-supplied pointer-menu interior and may also render inside the touch frame; the data-driven touch path instead converts the same item data to List and ListItem presentation.
|
|
90
|
-
| FR5 | The Pointer menu surface carries `data-astryx-menu-press` and follows `module:DropdownMenu/useMenuPress` FR1–FR7: the row under the release acts, the highlight follows a held pointer, a mouse released outside closes and a finger leaves the menu open, the stray click never acts, and `touch-action` follows overflow. Right-click, Shift+F10, long-press invocation and keyboard navigation keep their existing behavior. | `module:DropdownMenu/useMenuPress`; `ContextMenu.test.tsx` press model suite | Current module projection; verified in jsdom |
|
|
91
|
-
| FR6 | With `triggerAs="span"` the Trigger area MUST be an inline element that owns a box, so the cursor anchor measures from the trigger's own rect on every invocation path. Right-click, Shift+F10, long-press, and `context-menu` target placement all use that box.
|
|
92
|
-
| FR7 | Sub-menu rows follow `component:DropdownMenu` FR12 inside the pointer menu: ContextMenu mounts the same drill-in view stack, named after its `label`, so a sub-menu drills in on a phone here too.
|
|
93
|
-
| FR8 | In the pointer menu ArrowDown on the last enabled row wraps to the first and ArrowUp on the first to the last; PageDown and PageUp page as in DropdownMenu FR6; a held key's auto-repeat does not activate; typeahead matches the row's label element alone.
|
|
94
|
-
| FR9 | While the Pointer menu surface is open, it participates in `family:overlay-dismissal` through the shared stack and provides descendant depth. One Escape press affects only the top-most present nested surface; ContextMenu closes only when it is that surface.
|
|
88
|
+
| ID | Candidate invariant | Basis | Draft review state |
|
|
89
|
+
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------- |
|
|
90
|
+
| FR1 | Every render contains a caller-provided Trigger area. Pointer presentation opens a Pointer menu surface at the current local cursor anchor; touch presentation opens a Touch sheet frame containing a Touch menu surface and, for data mode, a Touch action list and Touch action rows. | Current source, docs, and tests | Current behavior; verified |
|
|
91
|
+
| FR2 | The current `context-menu` target remains on the painted Pointer menu surface and alternative Touch menu surface, not on the Trigger area or cursor anchor. | Current source, docs, and tests | Current inventory; verified |
|
|
92
|
+
| FR3 | Pointer action rows retain DropdownMenu ownership. The touch frame retains BottomSheet ownership, while data-driven touch lists and rows retain List ownership. | Current source and owner docs | Current delegation; verified |
|
|
93
|
+
| FR4 | Compound `menuContent` remains a caller-supplied pointer-menu interior and may also render inside the touch frame; the data-driven touch path instead converts the same item data to List and ListItem presentation. | Current source and tests | Current branches; verified |
|
|
94
|
+
| FR5 | The Pointer menu surface carries `data-astryx-menu-press` and follows `module:DropdownMenu/useMenuPress` FR1–FR7: the row under the release acts, the highlight follows a held pointer, a mouse released outside closes and a finger leaves the menu open, the stray click never acts, and `touch-action` follows overflow. Under a finger the press follows `spec:AST-094` FR26–FR28 and FR30, and the menu's grow, dismissal, long-press lift and outside press follow FR31–FR33 and FR35 (pending implementation); a mouse press outside is unchanged. Right-click, Shift+F10, long-press invocation and keyboard navigation keep their existing behavior. | `module:DropdownMenu/useMenuPress`; `ContextMenu.test.tsx` press model suite | Current module projection; verified in jsdom |
|
|
95
|
+
| FR6 | With `triggerAs="span"` the Trigger area MUST be an inline element that owns a box, so the cursor anchor measures from the trigger's own rect on every invocation path. Right-click, Shift+F10, long-press, and `context-menu` target placement all use that box. | `component:ContextMenu/DEC-1` | Accepted; verified in jsdom |
|
|
96
|
+
| FR7 | Sub-menu rows follow `component:DropdownMenu` FR12 inside the pointer menu: ContextMenu mounts the same drill-in view stack, named after its `label`, so a sub-menu drills in on a phone here too. | `component:DropdownMenu/DEC-2` | Proposed; verified in jsdom; owner decision pending |
|
|
97
|
+
| FR8 | In the pointer menu ArrowDown on the last enabled row wraps to the first and ArrowUp on the first to the last; PageDown and PageUp page as in DropdownMenu FR6; a held key's auto-repeat does not activate; typeahead matches the row's label element alone. | Current source and tests; shared `useListFocus` and `useTypeahead` suites | Proposed; verified in jsdom; owner decision pending |
|
|
98
|
+
| FR9 | While the Pointer menu surface is open, it participates in `family:overlay-dismissal` through the shared stack and provides descendant depth. One Escape press affects only the top-most present nested surface; ContextMenu closes only when it is that surface. | `family:overlay-dismissal` FR1–FR6; `ContextMenu.test.tsx` nested-layer case | Current family projection; verified in jsdom |
|
|
95
99
|
|
|
96
100
|
### Allowed variation
|
|
97
101
|
|
|
@@ -50,12 +50,18 @@ highlight follows the pointer — is contracted by
|
|
|
50
50
|
|
|
51
51
|
## Compatibility and migration
|
|
52
52
|
|
|
53
|
-
- Released default preserved: `
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
- Released default preserved: `no` under a finger. The omitted
|
|
54
|
+
`presentation`, released as `'popover'`, follows `spec:AST-094` FR29 (open
|
|
55
|
+
on press) under a finger; `presentation="popover"` keeps the released
|
|
56
|
+
behavior (`spec:AST-094` FR25). Mouse and keyboard are unchanged.
|
|
57
|
+
- Compatibility class: compatible behavior correction; public API, DOM
|
|
58
|
+
shape, styling, and targets are preserved; the default under a finger
|
|
59
|
+
changes as above, and the press-model behavior delta is recorded in
|
|
60
|
+
`module:DropdownMenu/useMenuPress`
|
|
56
61
|
- Controlled/uncontrolled behavior: unchanged
|
|
57
62
|
- Migration decision: `module:DropdownMenu/useMenuPress/DEC-3`; proposed
|
|
58
|
-
`module:DropdownMenu/useMenuPress/DEC-4`
|
|
63
|
+
`module:DropdownMenu/useMenuPress/DEC-4`; `spec:AST-094/DEC-4` (a caller
|
|
64
|
+
that needs the released tap-open passes `presentation="popover"`)
|
|
59
65
|
|
|
60
66
|
Consumer migration instructions belong in consumer docs and release notes.
|
|
61
67
|
|
|
@@ -104,10 +110,10 @@ keeps its released default.
|
|
|
104
110
|
| FR2 | The six current local targets are `dropdown-menu`, `dropdown-menu-item`, `dropdown-menu-radio`, `dropdown-menu-section-heading`, `dropdown-menu-divider`, and `dropdown-menu-indicator-icon`; every target remains on its current painted element. | Current source, docs, and tests | Verified current inventory; no target change |
|
|
105
111
|
| FR3 | Button owns the Trigger button, BottomSheet owns the Touch sheet frame, List owns the Touch action list and Touch action rows, Indicator owns checkbox chrome, and Icon owns ordinary rendered icons. | Current source and owner docs | Verified current delegation; no ownership change |
|
|
106
112
|
| FR4 | The same `dropdown-menu` target reaches the alternative Pointer menu surface and Touch menu surface. Pointer action rows retain `dropdown-menu-item`; touch action rows instead use List's `list-item` target. | Current source and tests | Verified modality split; no target change |
|
|
107
|
-
| FR5 | Both menu surfaces carry `data-astryx-menu-press` and follow `module:DropdownMenu/useMenuPress` FR1–FR7: the row under the release acts, the highlight follows a held pointer, a mouse released outside closes and a finger leaves the menu open, the browser's stray click never acts, and the Pointer menu surface declares `touch-action` by overflow. Trigger opening and keyboard navigation are unchanged.
|
|
113
|
+
| FR5 | Both menu surfaces carry `data-astryx-menu-press` and follow `module:DropdownMenu/useMenuPress` FR1–FR7: the row under the release acts, the highlight follows a held pointer, a mouse released outside closes and a finger leaves the menu open, the browser's stray click never acts, and the Pointer menu surface declares `touch-action` by overflow. Under a finger the press, the menu's grow and collapse, and its dismissal follow `spec:AST-094` FR26–FR35 (pending implementation). Trigger opening and keyboard navigation are unchanged. | `module:DropdownMenu/useMenuPress`; `DropdownMenu.test.tsx` press model suite | Proposed; verified in jsdom and real Chromium |
|
|
108
114
|
| FR6 | When the pointer menu closes, focus returns to the Trigger button with a visible ring only when the menu was driven by keyboard; a press outside that landed on a focusable control keeps focus there. | `architecture:interaction-modality` INV1; proposed in this change; `DropdownMenu.test.tsx` closing suite | Proposed; verified in jsdom, pending owner review |
|
|
109
115
|
| FR7 | `menuMaxHeight` MUST replace the 300px term of the menu's and its popover viewport's block-size cap while the viewport gutters still bound both; the touch sheet ignores it. | `component:DropdownMenu/DEC-3` | Proposed; verified in jsdom; owner to confirm DEC-3 |
|
|
110
|
-
| FR8 | The Trigger button opens the pointer menu on a mouse press-down
|
|
116
|
+
| FR8 | The Trigger button opens the pointer menu on a mouse press-down, with the module's settle rule on the opening release, and under a finger as `spec:AST-094` FR29 states; pressing the trigger of an open menu closes it without reopening in the same gesture; a mouse click and the keyboard open as before (`module:DropdownMenu/useMenuPress` proposed FR8, FR9). | Proposed `module:DropdownMenu/useMenuPress/DEC-4`; `DropdownMenu.test.tsx` press model suite | Proposed; verified in jsdom, pending owner review |
|
|
111
117
|
| Trigger source | `button` (Button props) or `renderTrigger` (render prop) | Which control the menu hangs off. `trigger` receives `DropdownMenuTriggerProps` — the press model, keyboard opens, toggle click and ARIA wiring — and names the menu. | Both presentations | `button` | `component:DropdownMenu` | stable | Both given: `trigger` wins and a dev warning fires. |
|
|
112
118
|
| FR9 | With `renderTrigger`, the rendered control MUST carry `aria-haspopup`, `aria-expanded`, `aria-controls` and the `id` the menu's `aria-labelledby` points at, and MUST receive the same press-model, keyboard-open and toggle-click handlers as the Button path; the menu is named by that control. `button` and `trigger` are mutually exclusive: both given, `trigger` wins and a dev warning fires, in both presentations. A drilled-in Back row (FR11) returns to the menu by that same name: the caller's `aria-label` when given, else the control's accessible name, read as the menu opens. | `component:DropdownMenu/DEC-2` | Proposed; verified in jsdom; owner to confirm DEC-2 |
|
|
113
119
|
| Row destination | `href` (+ `target`, `rel`) on an action item | The row's act is navigation: the row itself is the link (`family:navigation-destinations`). | Pointer action row and Touch action row, compound and data mode | none (an action row) | `component:DropdownMenu` | stable | Blocked schemes follow the family rule; a disabled row has none. |
|
|
@@ -35,15 +35,15 @@ references:
|
|
|
35
35
|
|
|
36
36
|
## Contract at a glance
|
|
37
37
|
|
|
38
|
-
| Area | Contract
|
|
39
|
-
| ----------------------- |
|
|
40
|
-
| Public contract | New public hook `useMenuPress` and the pure `menuPressStep` machine it drives, exported from `@astryxdesign/core/hooks`; the marker `data-astryx-menu-press` on every menu or listbox root that mounts it;
|
|
41
|
-
| Behavior | The row under the pointer when it is RELEASED is the row that acts; the highlight follows a held mouse
|
|
42
|
-
| End-user impact | A finger that lands on one menu row and lifts on another acts on the second, once, instead of on the first; a drag inside an open menu picks where it lets go; a mouse that lets go outside dismisses, a finger does not. PROPOSED: a mouse can press the trigger, drag into the menu and let go on a row in one gesture
|
|
43
|
-
| Builder impact | None for consumers of the five surfaces: they mount the model themselves. A component CAN adopt this model only when it meets the eligibility criteria below, and its component contract MUST explicitly name this module and the governed surfaces before mounting `useMenuPress`. The builder supplies an enabled-row selector and a `touch-action` declaration.
|
|
44
|
-
| Compatibility/readiness | Additive public hook; existing consumer props unchanged; shipped keyboard behavior unchanged. Shipped-behavior changes: DEC-3 (current; a mouse released outside a menu dismisses it) and PROPOSED DEC-4 (a mouse opens the menu on press rather than click; a
|
|
45
|
-
| Review checks | Reject a row that acts on `pointerdown` or from the browser's click after a tracked release; a second activation reaching a row for one gesture; a highlight that lights two rows; a finger release outside that closes the menu, or a mouse release outside that leaves it open; a menu root without a `touch-action` declaration; a picker whose listbox takes DOM focus for the highlight; an opening release that acts before the pointer entered the menu or the press settled; a trigger whose press and click both toggle in one gesture.
|
|
46
|
-
| Governing rules | `architecture:interaction-modality` INV2–INV4 (one operable path per modality, hover as enhancement, menus may move focus with the pointer); `architecture:public-component-api` for the new public hook; `architecture:layer-runtime` for the native light dismiss the model leaves in place; `family:overlay-dismissal` for Escape ordering, which this module does not change.
|
|
38
|
+
| Area | Contract |
|
|
39
|
+
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
40
|
+
| Public contract | New public hook `useMenuPress` and the pure `menuPressStep` machine it drives, exported from `@astryxdesign/core/hooks`; the marker `data-astryx-menu-press` on every menu or listbox root that mounts it; the hook's trigger half (`onTriggerPress`, `triggerProps`, `isTriggerClickFromPress`, `longPressDelayMs`), released in 0.6.6. No consumer prop of DropdownMenu, ContextMenu, DropdownMenuSubMenu or Selector changes. |
|
|
41
|
+
| Behavior | The row under the pointer when it is RELEASED is the row that acts; the highlight follows a held mouse; a mouse released outside closes the menu; the browser's stray click for a tracked gesture never acts; the menu root declares `touch-action` by overflow. Where the primary pointer is coarse, a finger or pen's press follows `spec:AST-094` FR26–FR30; where it is fine, FR1–FR7 and FR9 here apply to it. PROPOSED (FR8, DEC-4): a mouse opens a DropdownMenu on press-down, with a settle rule on the opening release. |
|
|
42
|
+
| End-user impact | A finger that lands on one menu row and lifts on another acts on the second, once, instead of on the first; a drag inside an open menu picks where it lets go; a mouse that lets go outside dismisses, a finger does not. PROPOSED: a mouse can press the trigger, drag into the menu and let go on a row in one gesture. Opening under a finger is `spec:AST-094` FR29's. Keyboard behavior and sub-menu hover are unchanged. |
|
|
43
|
+
| Builder impact | None for consumers of the five surfaces: they mount the model themselves. A component CAN adopt this model only when it meets the eligibility criteria below, and its component contract MUST explicitly name this module and the governed surfaces before mounting `useMenuPress`. The builder supplies an enabled-row selector and a `touch-action` declaration. |
|
|
44
|
+
| Compatibility/readiness | Additive public hook; existing consumer props unchanged; shipped keyboard behavior unchanged. Shipped-behavior changes: DEC-3 (current; a mouse released outside a menu dismisses it) and PROPOSED DEC-4 (a mouse opens the menu on press rather than click; under a finger, `spec:AST-094` FR29). jsdom evidence is complete and real-Chromium evidence covers the touch pan, the finger slide and its stray click; iOS WebKit evidence is not required for this module. Authority: current for FR1–FR7, DEC-3, DEC-6, DEC-7 (`cixzhang`, 2026-10-02); FR8 and DEC-4 are proposed in this change and await the same owner (OQ1). |
|
|
45
|
+
| Review checks | Reject a row that acts on `pointerdown` or from the browser's click after a tracked release; a second activation reaching a row for one gesture; a highlight that lights two rows; a finger release outside that closes the menu, or a mouse release outside that leaves it open; a menu root without a `touch-action` declaration; a picker whose listbox takes DOM focus for the highlight; an opening release that acts before the pointer entered the menu or the press settled; a trigger whose press and click both toggle in one gesture. |
|
|
46
|
+
| Governing rules | `architecture:interaction-modality` INV2–INV4 (one operable path per modality, hover as enhancement, menus may move focus with the pointer); `architecture:public-component-api` for the new public hook; `architecture:layer-runtime` for the native light dismiss the model leaves in place; `family:overlay-dismissal` for Escape ordering, which this module does not change. |
|
|
47
47
|
|
|
48
48
|
This table is a review projection; the body below is authoritative.
|
|
49
49
|
|
|
@@ -60,8 +60,9 @@ carries a separate press controller for an eligible surface.
|
|
|
60
60
|
|
|
61
61
|
The module contracts the press model only. Menu anatomy, theming,
|
|
62
62
|
presentation policy, keyboard navigation and item semantics stay with the
|
|
63
|
-
owning component records; trigger opening by keyboard
|
|
64
|
-
|
|
63
|
+
owning component records; trigger opening by keyboard stays with the component
|
|
64
|
+
that renders the trigger, and opening under a finger follows `spec:AST-094`
|
|
65
|
+
FR29.
|
|
65
66
|
|
|
66
67
|
### Adoption eligibility
|
|
67
68
|
|
|
@@ -80,13 +81,18 @@ adopter.
|
|
|
80
81
|
|
|
81
82
|
## Compatibility and migration
|
|
82
83
|
|
|
83
|
-
- Released default preserved: `no` for
|
|
84
|
-
outside a menu now dismisses it)
|
|
85
|
-
|
|
84
|
+
- Released default preserved: `no` for two behaviors: DEC-3 (a mouse
|
|
85
|
+
released outside a menu now dismisses it), and an unset `longPressDelayMs`,
|
|
86
|
+
whose released 500 ms wait gives way to `spec:AST-094` FR29's timing
|
|
87
|
+
where the primary pointer is coarse;
|
|
88
|
+
`yes` for every consumer prop and every item, divider, section and
|
|
89
|
+
presentation API
|
|
86
90
|
- Compatibility class: additive public hook and types; behavior change inside
|
|
87
91
|
the menu family
|
|
88
92
|
- Migration decision: `module:DropdownMenu/useMenuPress/DEC-3`; proposed
|
|
89
|
-
`module:DropdownMenu/useMenuPress/DEC-4` (pending owner review)
|
|
93
|
+
`module:DropdownMenu/useMenuPress/DEC-4` (pending owner review);
|
|
94
|
+
`spec:AST-094` FR25 (a caller that needs the released 500 ms wait passes
|
|
95
|
+
`longPressDelayMs: 500`)
|
|
90
96
|
|
|
91
97
|
Consumer migration instructions belong in consumer docs and release notes.
|
|
92
98
|
|
|
@@ -121,8 +127,8 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
121
127
|
scroll and never listens for `scroll`.
|
|
122
128
|
- Native light dismiss, Escape ordering and focus trapping — owned by
|
|
123
129
|
`architecture:layer-runtime` and `family:overlay-dismissal`.
|
|
124
|
-
- Opening a menu from its trigger by keyboard
|
|
125
|
-
|
|
130
|
+
- Opening a menu from its trigger by keyboard — owned by the component that
|
|
131
|
+
renders the trigger; under a finger, `spec:AST-094` FR29 governs it.
|
|
126
132
|
- Keyboard navigation, typeahead and Enter/Space activation — owned by
|
|
127
133
|
`useListFocus`, `useTypeahead` and the component records.
|
|
128
134
|
- Hover-to-open of sub-menu flyouts — owned by `useMenuHover`.
|
|
@@ -130,32 +136,32 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
130
136
|
|
|
131
137
|
## Public API and concepts
|
|
132
138
|
|
|
133
|
-
| Concept | Closed values or states
|
|
134
|
-
| ------------------------ |
|
|
135
|
-
| gesture phase | `idle`, `tracking`; proposed `triggerPress`, `open`
|
|
136
|
-
| effect | `none`, `highlight`, `clear`, `act`, `settle`; proposed `open`
|
|
137
|
-
| pointer type | `mouse`, `touch`, `pen`
|
|
138
|
-
| enabled-row selector | caller string
|
|
139
|
-
| highlight | DOM focus, or a caller callback
|
|
140
|
-
| stray-click window | 400 ms, or the next `pointerdown`
|
|
141
|
-
| settle time | 300 ms
|
|
142
|
-
| long-press delay | 500 ms
|
|
143
|
-
| `data-astryx-menu-press` | present or absent
|
|
144
|
-
| activation click | `click` with `detail: 0`, the release's button and modifiers
|
|
139
|
+
| Concept | Closed values or states | Meaning | Default | Owner | Stability |
|
|
140
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | ---------------------------------- | --------- |
|
|
141
|
+
| gesture phase | `idle`, `tracking`; proposed `triggerPress`, `open` | where the press in flight stands | `idle` | `module:DropdownMenu/useMenuPress` | stable |
|
|
142
|
+
| effect | `none`, `highlight`, `clear`, `act`, `settle`; proposed `open` | what one event makes the host do; `open` asks the trigger to open the menu under the held pointer; `settle` carries `stray` (swallow the browser's click) and `dismiss` (close the menu) | — | `module:DropdownMenu/useMenuPress` | stable |
|
|
143
|
+
| pointer type | `mouse`, `touch`, `pen` | the gesture's own pointer; `touch` and `pen` are "a finger" | from the event | `module:DropdownMenu/useMenuPress` | stable |
|
|
144
|
+
| enabled-row selector | caller string | the rows a release may act on; anything else inside the root highlights nothing | required | `module:DropdownMenu/useMenuPress` | stable |
|
|
145
|
+
| highlight | DOM focus, or a caller callback | one lit row; focus with `preventScroll` in a menu, `aria-activedescendant` in a picker | focus | `module:DropdownMenu/useMenuPress` | stable |
|
|
146
|
+
| stray-click window | 400 ms, or the next `pointerdown` | how long after a tracked release the browser's click is still swallowed | `MENU_PRESS_STRAY_CLICK_MS` | `module:DropdownMenu/useMenuPress` | stable |
|
|
147
|
+
| settle time | 300 ms | proposed: how long an opening press must last before its release may act | `MENU_PRESS_SETTLE_MS` | `module:DropdownMenu/useMenuPress` | proposed |
|
|
148
|
+
| long-press delay | released 500 ms when set; unset, as `spec:AST-094` FR29 states where the primary pointer is coarse (none in a fixed bar; 150 ms in a scroll view), and 500 ms where it is fine | how long a held finger waits on a trigger before the menu opens. A caller that sets `longPressDelayMs` gets that wait in a fixed bar and in a scroll view alike, which keeps the released behavior (`spec:AST-094` FR25). | `longPressDelayMs` | `module:DropdownMenu/useMenuPress` | stable |
|
|
149
|
+
| `data-astryx-menu-press` | present or absent | the root carries this model; a host controller of its own stands aside | present on every mounted root | `module:DropdownMenu/useMenuPress` | stable |
|
|
150
|
+
| activation click | `click` with `detail: 0`, the release's button and modifiers | how the model acts on a row, marked for the synchronous extent by `isMenuPressActivation()`; `detail === 0` alone therefore no longer means "keyboard" on a menu row | — | `module:DropdownMenu/useMenuPress` | stable |
|
|
145
151
|
|
|
146
152
|
## Behavioral contract
|
|
147
153
|
|
|
148
|
-
| ID | Invariant
|
|
149
|
-
| --- |
|
|
150
|
-
| FR1 | A row MUST act when a press is released over it, never because the press began on it
|
|
151
|
-
| FR2 | While a press is held inside a menu, the highlighted row MUST be the enabled row under the pointer, and nothing MUST be highlighted over a divider, a heading, a disabled row, or outside the menu. A mouse moving with no button down moves the highlight the same way (existing hover-to-focus).
|
|
152
|
-
| FR3 | A press that began inside a menu and is released outside it MUST act on nothing. Under a mouse the menu MUST then close; under a finger or pen it MUST stay open.
|
|
153
|
-
| FR4 | A menu whose rows fit MUST declare `touch-action: pinch-zoom`; one that scrolls MUST declare `touch-action: pan-y pinch-zoom` and `overscroll-behavior: contain`. Neither admits a one-finger pan the menu does not scroll, and both leave a two-finger pinch to the browser, so the page stays zoomable over an open menu. The model MUST treat `pointercancel` and a second pointer as the end of the gesture with nothing acting, and MUST NOT re-implement scrolling.
|
|
154
|
-
| FR5 | The click the browser reports for a gesture this model tracked MUST NOT act on any row; only the release acts. A click with no tracked gesture behind it, or one whose `detail` is 0, MUST act on its target as before. The swallower arms at a tracked release and disarms at the next `pointerdown` or after the stray-click window.
|
|
155
|
-
| FR6 | At most one row of a menu is lit. Under a finger the row a press began on MUST NOT keep a pressed or hovered look once the pointer left it, so menu rows paint `:active` only under `(hover: hover)`.
|
|
156
|
-
| FR7 | While a press is tracked in a menu that scrolls, a pointer resting within the edge zone of the menu's top or bottom MUST scroll the menu toward that edge and re-read the row under the pointer as the rows move.
|
|
157
|
-
| FR8 | On a mouse a DropdownMenu trigger MUST open its menu on press-down (button 0, no Control), and a drag from the trigger into the menu that releases over an enabled row MUST act on it. The opening gesture's release MUST act only if the pointer entered the menu or the press lasted the settle time; otherwise nothing acts and the menu stays open. Pressing the trigger of an open menu MUST close it and MUST NOT reopen it in the same gesture.
|
|
158
|
-
| FR9 |
|
|
154
|
+
| ID | Invariant | Basis | Verification state |
|
|
155
|
+
| --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
|
|
156
|
+
| FR1 | A row MUST act when a press is released over it, never because the press began on it; a press that never left its row is a tap or click on that row. Where the primary pointer is coarse, a finger or pen follows `spec:AST-094` FR26 instead; where it is fine, a finger or pen keeps this rule (`spec:AST-094` FR19). | macOS and iOS menu behavior; `menuPressGesture.test.ts`, `useMenuPress.test.tsx` | verify |
|
|
157
|
+
| FR2 | While a press is held inside a menu, the highlighted row MUST be the enabled row under the pointer, and nothing MUST be highlighted over a divider, a heading, a disabled row, or outside the menu. A mouse moving with no button down moves the highlight the same way (existing hover-to-focus). Where the primary pointer is coarse, a finger or pen follows `spec:AST-094` FR27 and FR11's cell profile instead, and a second finger ends the gesture (FR4); where it is fine, a finger or pen keeps this rule (`spec:AST-094` FR19). | `architecture:interaction-modality` allowed variation for menus; `useMenuPress.test.tsx` | verify |
|
|
158
|
+
| FR3 | A press that began inside a menu and is released outside it MUST act on nothing. Under a mouse the menu MUST then close; under a finger or pen it MUST stay open (where the primary pointer is coarse, as `spec:AST-094` FR28 states). | DEC-3; `useMenuPress.test.tsx`, `DropdownMenu.test.tsx`, `Selector.test.tsx` | verify |
|
|
159
|
+
| FR4 | A menu whose rows fit MUST declare `touch-action: pinch-zoom`; one that scrolls MUST declare `touch-action: pan-y pinch-zoom` and `overscroll-behavior: contain`. Neither admits a one-finger pan the menu does not scroll, and both leave a two-finger pinch to the browser, so the page stays zoomable over an open menu. The model MUST treat `pointercancel` and a second pointer as the end of the gesture with nothing acting, and MUST NOT re-implement scrolling. | Browser scroll ownership; WCAG 2.2 SC 1.4.4 under `spec:AST-020` FR1; `DropdownMenu.test.tsx` touch-action case, `useMenuPress.test.tsx`, `MenuPinchZoom.a11y.chromium.spec.ts` | verify |
|
|
160
|
+
| FR5 | The click the browser reports for a gesture this model tracked MUST NOT act on any row; only the release acts. A click with no tracked gesture behind it, or one whose `detail` is 0, MUST act on its target as before. The swallower arms at a tracked release and disarms at the next `pointerdown` or after the stray-click window. | One gesture, one activation; `useMenuPress.test.tsx` FR5 suite | verify |
|
|
161
|
+
| FR6 | At most one row of a menu is lit. Under a finger the row a press began on MUST NOT keep a pressed or hovered look once the pointer left it, so menu rows paint `:active` only under `(hover: hover)`. Where the primary pointer is coarse, `spec:AST-094` FR27 and FR11 govern a finger's lit row and its press look instead. | `DropdownMenuItem.tsx` styles | verify |
|
|
162
|
+
| FR7 | While a press is tracked in a menu that scrolls, a pointer resting within the edge zone of the menu's top or bottom MUST scroll the menu toward that edge and re-read the row under the pointer as the rows move. Where the primary pointer is coarse, a finger or pen follows `spec:AST-094` FR30 instead; where it is fine, a finger or pen keeps this rule (`spec:AST-094` FR19). | `useMenuPress.test.tsx` autoscroll case | verify |
|
|
163
|
+
| FR8 | On a mouse a DropdownMenu trigger MUST open its menu on press-down (button 0, no Control), and a drag from the trigger into the menu that releases over an enabled row MUST act on it. The opening gesture's release MUST act only if the pointer entered the menu or the press lasted the settle time; otherwise nothing acts and the menu stays open. Pressing the trigger of an open menu MUST close it and MUST NOT reopen it in the same gesture. | Proposed DEC-4; `DropdownMenu.test.tsx` press model suite, `menuPressGesture.test.ts` trigger states | proposed; pending owner review |
|
|
164
|
+
| FR9 | Where the primary pointer is coarse, opening a menu from its trigger under a finger and the opening finger's handoff are governed by `spec:AST-094` FR29. Where it is fine, a finger keeps the released behavior (`spec:AST-094` FR19): a tap on the trigger opens the menu through its click, and a press held on the trigger for the long-press delay opens the menu with the finger down, prevents the default of the finger's later `touchmove`s until the gesture ends, and continues under FR1–FR4. | `spec:AST-094` FR29 | as `spec:AST-094` states |
|
|
159
165
|
|
|
160
166
|
### Transformation and precedence order
|
|
161
167
|
|
|
@@ -237,16 +243,16 @@ owned by the item components.
|
|
|
237
243
|
|
|
238
244
|
## Verification map
|
|
239
245
|
|
|
240
|
-
| Contract | Verification | Representative states
|
|
241
|
-
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
242
|
-
| FR1–FR3, FR5 | `menuPressGesture.test.ts` (one case per transition of the diagram) | idle, tracking; mouse and touch; releases over a row, over chrome, on the trigger, outside; cancel
|
|
243
|
-
| FR1–FR5, FR7, AR1, AR3 | `useMenuPress.test.tsx` with an injected hit test | release over another row, tap, stray click, `detail: 0`, mouse/finger outside, pointercancel, second pointer, autoscroll
|
|
244
|
-
| FR1, FR3–FR5 | `DropdownMenu.test.tsx`, `DropdownMenuSubMenu.test.tsx`, `ContextMenu.test.tsx` press suites | finger slide across rows, outside release by mouse and finger, `touch-action`, flyout release, root marker
|
|
245
|
-
| FR1, FR3, AR2 | `Selector.test.tsx` press model suite | finger release over another option, finger/mouse release outside, listbox marker
|
|
246
|
-
| FR4 (scroll) | `MenuPress.a11y.chromium.spec.ts` (real Chromium) | Chromium touch pan in an overflowing menu; touch-action by overflow; finger slide and its stray click
|
|
247
|
-
| FR4 (pinch) | `MenuPinchZoom.a11y.chromium.spec.ts` (real Chromium, phone emulation) | two-finger pinch over a menu whose rows fit, over one that scrolls, and over a picker listbox
|
|
248
|
-
| FR8
|
|
249
|
-
| ORD1a (proposed) | `DropdownMenuSubMenu.test.tsx` press cases; `SubMenuHeaderFooter.a11y.chromium.spec.ts` press-drag case | a mouse press on a menu row released on a row of a sub-menu rendered beside the menu; the same press released on that sub-menu's header field
|
|
246
|
+
| Contract | Verification | Representative states | Mutation or failure expectation |
|
|
247
|
+
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
248
|
+
| FR1–FR3, FR5 | `menuPressGesture.test.ts` (one case per transition of the diagram) | idle, tracking; mouse and touch; releases over a row, over chrome, on the trigger, outside; cancel | Acting on the press row, on the stray click, or dismissing under a finger fails a transition case |
|
|
249
|
+
| FR1–FR5, FR7, AR1, AR3 | `useMenuPress.test.tsx` with an injected hit test | release over another row, tap, stray click, `detail: 0`, mouse/finger outside, pointercancel, second pointer, autoscroll | A second activation, a swallowed assistive click, a wrong dismissal, or a dead autoscroll fails |
|
|
250
|
+
| FR1, FR3–FR5 | `DropdownMenu.test.tsx`, `DropdownMenuSubMenu.test.tsx`, `ContextMenu.test.tsx` press suites | finger slide across rows, outside release by mouse and finger, `touch-action`, flyout release, root marker | Each removed behavior fails its named case |
|
|
251
|
+
| FR1, FR3, AR2 | `Selector.test.tsx` press model suite | finger release over another option, finger/mouse release outside, listbox marker | A listbox that takes focus, acts on the press option, or closes under a finger fails |
|
|
252
|
+
| FR4 (scroll) | `MenuPress.a11y.chromium.spec.ts` (real Chromium) | Chromium touch pan in an overflowing menu; touch-action by overflow; finger slide and its stray click | Not provable in jsdom; a pan that acts on a row, or a slide that acts on the press row, fails |
|
|
253
|
+
| FR4 (pinch) | `MenuPinchZoom.a11y.chromium.spec.ts` (real Chromium, phone emulation) | two-finger pinch over a menu whose rows fit, over one that scrolls, and over a picker listbox | Not provable in jsdom; a pinch that leaves the page at scale 1, or acts on a row, fails |
|
|
254
|
+
| FR8 (proposed), FR9 | `menuPressGesture.test.ts` trigger states; `useMenuPress.test.tsx` trigger suite; `DropdownMenu.test.tsx` press-open cases | mouse opens at once, settle before/after, cancel; finger opening per `spec:AST-094` FR29; press-open + drag-release, unsettled release, trigger toggle, held touch, tap | A menu that does not open on a mouse press, an unsettled release that acts, or a trigger that reopens in the same gesture fails |
|
|
255
|
+
| ORD1a (proposed) | `DropdownMenuSubMenu.test.tsx` press cases; `SubMenuHeaderFooter.a11y.chromium.spec.ts` press-drag case | a mouse press on a menu row released on a row of a sub-menu rendered beside the menu; the same press released on that sub-menu's header field | A release on such a row that acts on nothing or closes the menu, or a release on its header field that acts, fails. |
|
|
250
256
|
|
|
251
257
|
## Decision log
|
|
252
258
|
|
|
@@ -261,14 +267,13 @@ outside dismisses. Each pointer keeps its platform's answer. Closing for both
|
|
|
261
267
|
would make a finger that slid off need a second gesture to reopen; staying
|
|
262
268
|
open for both would surprise a mouse user.
|
|
263
269
|
|
|
264
|
-
### DEC-4 (proposed) — A mouse opens on press-down,
|
|
270
|
+
### DEC-4 (proposed) — A mouse opens on press-down, with a settle rule
|
|
265
271
|
|
|
266
272
|
**Reference:** `module:DropdownMenu/useMenuPress/DEC-4`
|
|
267
273
|
**Decider:** proposed by `vjeux`, `2026-09-27`; pending owner review
|
|
268
274
|
|
|
269
|
-
Press-to-open with drag-release is the macOS menu.
|
|
270
|
-
|
|
271
|
-
the finger down so the fast path exists too. The release of the opening
|
|
275
|
+
Press-to-open with drag-release is the macOS menu. Opening under a finger
|
|
276
|
+
follows `spec:AST-094` FR29 and DEC-4. The release of the opening
|
|
272
277
|
gesture acts only after the pointer has entered the menu or the press has
|
|
273
278
|
settled (300 ms), as a native pop-up button's does; otherwise a menu that
|
|
274
279
|
opens under the pointer would pick a row nobody chose.
|
|
@@ -301,9 +306,9 @@ availability alone does not make a component an adopter.
|
|
|
301
306
|
## Open questions
|
|
302
307
|
|
|
303
308
|
- **OQ1 — Is press-to-open the menu family's trigger model?** (`human-design`)
|
|
304
|
-
The one owner decision this change needs: proposed FR8
|
|
305
|
-
mouse opens a DropdownMenu on press-down with the settle rule
|
|
306
|
-
a
|
|
309
|
+
The one owner decision this change needs: proposed FR8 and DEC-4 — a
|
|
310
|
+
mouse opens a DropdownMenu on press-down with the settle rule. Opening under
|
|
311
|
+
a finger is `spec:AST-094` FR29's. Until the owner rules, the current record's FR1–FR7
|
|
307
312
|
stand on their own.
|
|
308
313
|
|
|
309
314
|
The former browser-evidence question is resolved: the touch pan, the finger
|
|
@@ -58,6 +58,10 @@ rung, so a caller whose content owns its own edges can ask for a flush surface.
|
|
|
58
58
|
## Compatibility and migration
|
|
59
59
|
|
|
60
60
|
- Released default preserved: `yes`
|
|
61
|
+
- `presentation` (DEC-6) defaults to `popover`, the released anchored surface;
|
|
62
|
+
`presentation="popover"` is also the value that keeps it wherever a touch
|
|
63
|
+
default would otherwise apply (`architecture:public-component-api` INV16,
|
|
64
|
+
`spec:AST-017` FR3, DEC-23). Popover does not implement `presentation` yet.
|
|
61
65
|
- Canonical target: `popover` owns the broad painted-surface contract for
|
|
62
66
|
`<Popover>` and public `usePopover` compositions
|
|
63
67
|
- Removed alias: 0.7.0 removes `popover-surface`; themes and CSS selectors migrate
|
|
@@ -120,6 +124,7 @@ surface, not the Layer entry point. There is no installable
|
|
|
120
124
|
| Popup semantics | Dialog or neutral wrapper | Exposes dialog semantics or lets child menu/listbox semantics own the popup | Popover and hook | Dialog | `component:Popover` | Current public behavior | A dialog without a label warns in development. Neutral mode omits dialog role and modal semantics. |
|
|
121
125
|
| Focus entry | Automatic or caller-preserved | Focuses genuine caller content, falls back to the labeled dialog surface when none exists, or preserves current focus | Popover and hook | Automatic | `component:Popover` | Current public behavior | The injected fallback close control is excluded from initial-focus candidates and reveals only when reached sequentially. |
|
|
122
126
|
| Dismissal | Outside/Escape enabled or disabled within native constraints | Controls light dismiss and explicit Escape participation | Popover and hook | Both enabled | `component:Popover`; `family:overlay-dismissal` owns Escape/platform-close ordering | Current public behavior | Disabling Escape alone cannot override the native Escape behavior of an auto popover; explicit-dismiss behavior requires light dismiss to be disabled too. |
|
|
127
|
+
| Presentation | `popover`, `bottom-sheet`, or `adaptive` | Chooses the surface that holds the content: anchored, a BottomSheet, or a BottomSheet on a compact touch screen | Popover | `popover` | `component:Popover`; `component:BottomSheet` owns the sheet | Decided (DEC-6) | — |
|
|
123
128
|
| Surface treatment | Default surface or caller-owned treatment | Applies the shared painted surface and optional consumer styling | Popover and hook | Default surface | `component:Popover` | Current public behavior | Custom styling does not change lifecycle, focus, or dismissal semantics. |
|
|
124
129
|
| Surface padding | A spacing-scale step, `0` through `10` | Sets the inset the painted surface keeps around caller content; `0` is a flush surface for content that owns its own edges | Popover and hook | Popover: step 3; hook: none | `component:Popover` | Pending owner review (DEC-4) | The rung lands on the painted surface, so a theme's `padding` on the `popover` target replaces it rather than nesting; consumer `xstyle` merges after it. |
|
|
125
130
|
| Surface target ownership | Shared baseline or component-specific refinement | Assigns `popover` as the broad surface owner and lets a composed component add its own target on that same element | Popover and hook | `popover` baseline; no component-specific refinement | `component:Popover`; the composed component owns its refinement target | Current compatibility contract | `popover-surface` is compatibility output, not a new target for consumers or a second anatomy owner. |
|
|
@@ -181,6 +186,7 @@ table owns their semantic effect.
|
|
|
181
186
|
| FR6 | Component anatomy contains the caller trigger and content, one painted Popover surface, and the optional fallback close control. Popover owns no Header, Body, or separate shared-hook surface part. | Current source, docs target inventory, and tests | Accepted anatomy; stale consumer anatomy corrected by this contract |
|
|
182
187
|
| FR7 | The painted surface has one broad canonical target, `popover`; composed components may add one authoritative component-specific refinement target. | Owner direction on 2026-08-31 plus current target inventory | Accepted canonical contract; covered by focused target tests |
|
|
183
188
|
| FR8 | The surface padding is one spacing-scale rung applied on the painted surface. Popover defaults to step 3 (the released inset); `0` paints a flush surface; a direct `usePopover` composition paints none unless it asks for a rung. The rung sits where a theme's `padding` on the `popover` target replaces it. | PR #6683 and its Popover tests; `architecture:container-padding/INV8` (padding alone does not enroll) | Pending owner review (DEC-4); implemented and covered by the Popover surface-padding suite |
|
|
189
|
+
| FR9 | Under a finger, Popover grows out of its trigger and shrinks back into it, and a touch during its growth is ignored, as `spec:AST-094` FR31 and FR35 state. Its dismissal is unchanged, and a mouse keeps today's entrance. | `spec:AST-094` FR31, FR35 | Pending implementation; tests land with it |
|
|
184
190
|
|
|
185
191
|
### Allowed variation
|
|
186
192
|
|
|
@@ -328,6 +334,7 @@ consumers that need distinct theme reachability provide and document an owned
|
|
|
328
334
|
| FR2, AR2–AR4 | `Popover.test.tsx` focus/role suites and `useFocusTrap.test.tsx` container-entry suites | Pointer, keyboard/AT-style, read-only, neutral role, no autofocus, controlled, Tab/Shift+Tab, Escape, focus return | Changing semantic destination by modality, selecting fallback close initially, losing shared indication, or allowing focus escape fails assertions. | `audit:Popover/accessibility` |
|
|
329
335
|
| ORD2, FR4 | `Popover.test.tsx` sizing/overflow suites; `Popover.stories.tsx` match-trigger and overflow scenarios; geometry under `Core/Layer` viewport-inset stories | Explicit width clamped to the viewport, trigger matching, fitting and overflowing content | Reversing width precedence, capping to the span beside the trigger, or always enabling scroll fails emitted-style or overflow assertions. | `audit:Popover/layout` |
|
|
330
336
|
| FR8 | `Popover.test.tsx` surface-padding suite | Default rung, `0`, an explicit rung, a direct hook composition with and without a rung | A default other than step 3, a rung landing on a box inside the surface, or the hook painting a rung it was not given fails the suite. | `audit:Popover/theming` |
|
|
337
|
+
| FR9 | `spec:AST-094` verification for FR31 and FR35 | Under a finger: open, close, a touch during growth; a mouse | A Popover that does not grow from and shrink into its trigger, a changed dismissal, or a changed mouse path fails. | `audit:Popover/behavior` |
|
|
331
338
|
| FR5 | `Popover.test.tsx` scheduling and observer-lifecycle suites | Closed, opened, repeated signals, cleanup | Constructing observers while closed or measuring more than once per pending frame fails lifecycle assertions. | `audit:Popover/resources` |
|
|
332
339
|
| FR6, FR7 and theming anatomy | `Popover.test.tsx`, `Popover.doc.mjs`, `usePopover.doc.mjs`, `astryx theme targets Popover --json`, and `scripts/check-knowledge.mjs` | One real surface with canonical `popover` plus composed-component refinements | A fake anatomy part, missing canonical output, or an active target without a real owner fails review or validation. | `audit:Popover/theming` |
|
|
333
340
|
| AR5 | `Popover.stories.tsx` manual-AT fixture and PR #5373 test record | Read-only dialog manual-AT fixture; no recorded NVDA/VoiceOver announcement result | A fixture without recorded AT/browser observations cannot be cited as announcement proof. | `audit:Popover/at-evidence` |
|
|
@@ -391,6 +398,18 @@ Rejected: moving focus to the dialog surface on every pointer opening solely to
|
|
|
391
398
|
avoid a keyboard-looking outline. That mixes visual modality with semantic focus
|
|
392
399
|
placement and bypasses the shared focus-indication owner.
|
|
393
400
|
|
|
401
|
+
### DEC-6 — Presentation: the anchored popover, a sheet, or adaptive
|
|
402
|
+
|
|
403
|
+
**Reference:** `component:Popover/DEC-6`
|
|
404
|
+
**Decider:** `cixzhang`, `2026-10-25`
|
|
405
|
+
|
|
406
|
+
Popover takes `presentation` with the vocabulary the menus already use:
|
|
407
|
+
`popover`, `bottom-sheet`, or `adaptive`. `popover` is the default and the
|
|
408
|
+
released anchored surface. `bottom-sheet` presents the content in a
|
|
409
|
+
BottomSheet. `adaptive` chooses exactly as the menus do: the sheet under
|
|
410
|
+
`spec:AST-094` FR13's touch rule, with compact governed by `spec:AST-012` FR9,
|
|
411
|
+
and the anchored popover elsewhere.
|
|
412
|
+
|
|
394
413
|
## Verification gaps
|
|
395
414
|
|
|
396
415
|
These implementation and evidence items remain required follow-up, but they do
|
|
@@ -362,8 +362,9 @@ FR3 implements the system decision owned by `spec:AST-004/DEC-1`.
|
|
|
362
362
|
|
|
363
363
|
An omitted `presentation` resolves `adaptive` (FR4): the modal bottom sheet on
|
|
364
364
|
compact coarse-pointer screens and the anchored popover elsewhere, so
|
|
365
|
-
fine-pointer and large-screen output matches `popover`. `adaptive`
|
|
366
|
-
|
|
365
|
+
fine-pointer and large-screen output matches `popover`. `adaptive` resolves
|
|
366
|
+
through `spec:AST-094` FR13: a coarse primary pointer and the `spec:AST-012` FR9
|
|
367
|
+
compact definition.
|
|
367
368
|
Rejected: keeping `popover` as the default, because a phone then gets a
|
|
368
369
|
pointer-sized anchored list unless the caller opts in.
|
|
369
370
|
|
package/src/Token/Token.spec.md
CHANGED
|
@@ -129,7 +129,7 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
129
129
|
|
|
130
130
|
| ID | Invariant | Basis |
|
|
131
131
|
| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
|
|
132
|
-
| FR1 | Content renders in logical inline order: Icon, Label, End content, Remove button. The root is inline-flex with `--spacing-1` gaps, inline padding `--spacing-2`, `--radius-inner` corners, `--text-supporting-size` type at `--font-weight-medium`, and block size `--size-element-sm`, `-md`, or `-lg` minus 8px for `sm`, `md`, and `lg`.
|
|
132
|
+
| FR1 | Content renders in logical inline order: Icon, Label, End content, Remove button. The root is inline-flex with `--spacing-1` gaps, inline padding `--spacing-2`, `--radius-inner` corners, `--text-supporting-size` type at `--font-weight-medium`, and a minimum block size of `--size-element-sm`, `-md`, or `-lg` minus 8px for `sm`, `md`, and `lg`. That size grows with a set text size under `spec:AST-102` FR4; today's fixed size is a gap. | Current source and tests |
|
|
133
133
|
| FR2 | With no `href`, `onClick`, or enclosing trigger context, the token is a static `span` holding its content directly. | Current source and tests |
|
|
134
134
|
| FR3 | With an enabled `href` and no `onRemove`, the token root is the shared link component carrying the destination; it takes the root ref, passthrough props, and its own focus-visible outline. A supplied `onClick`, as an accepted event input, reaches the element that owns the contract (`architecture:public-component-api` INV5). Implementation gap: in both link structures `onClick` is not attached or called (Current-state impact). | Current source and tests |
|
|
135
135
|
| FR4 | With `onClick`, or an enclosing trigger context that resolves the token to a button, the root is a `span` container holding an inner `button` that wraps only the Label and calls `onClick`; a click on the container outside any nested button or link also calls `onClick`. With an enabled `href` and `onRemove`, the container instead holds an inner link wrapping only the Label; a plain click on the surface outside the Remove button follows that link, and a Cmd/Ctrl-click or middle click on the surface opens it in a new tab. A caller `onMouseUp` on the link-and-remove container runs before its delegation. | Current source and tests |
|