@astryxdesign/core 0.7.2-canary.fdd384f → 0.7.2
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/CHANGELOG.md +156 -2
- package/README.md +4 -4
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +8 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +4 -1
- package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
- package/dist/Chat/ChatLayoutScrollButton.js +1 -0
- package/dist/CheckboxList/CheckboxList.d.ts +1 -1
- package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxList.js +6 -2
- package/dist/DropdownMenu/DropdownMenu.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +38 -110
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuGroup.d.ts +6 -4
- package/dist/DropdownMenu/DropdownMenuGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuGroup.js +4 -3
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +1 -1
- package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts +24 -0
- package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts.map +1 -0
- package/dist/DropdownMenu/lazyMenuBottomSheet.js +46 -0
- package/dist/DropdownMenu/menuRegionStops.d.ts +38 -0
- package/dist/DropdownMenu/menuRegionStops.d.ts.map +1 -0
- package/dist/DropdownMenu/menuRegionStops.js +131 -0
- package/dist/FormLayout/FormLayout.d.ts +5 -4
- package/dist/FormLayout/FormLayout.d.ts.map +1 -1
- package/dist/FormLayout/FormLayout.js +1 -1
- package/dist/FormLayout/FormLayoutContext.d.ts +3 -3
- package/dist/FormLayout/FormLayoutContext.js +3 -3
- package/dist/Grid/Grid.d.ts +8 -7
- package/dist/Grid/Grid.d.ts.map +1 -1
- package/dist/Grid/Grid.js +5 -4
- package/dist/Grid/GridSpan.d.ts +1 -1
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +8 -2
- package/dist/Markdown/plugins/headingLinks.d.ts.map +1 -1
- package/dist/Markdown/plugins/headingLinks.js +9 -1
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +5 -0
- package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchValueEditor.js +45 -11
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +6 -2
- package/dist/Token/Token.js +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +2 -0
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +2 -0
- package/dist/astryx.css +1 -1
- package/dist/reset.a11y.chromium.spec.js +50 -1
- package/docs.mjs +4 -4
- package/package.json +4 -4
- package/scripts/agent-doc-state.mjs +66 -6
- package/scripts/postinstall.mjs +44 -3
- package/src/Avatar/Avatar.tsx +11 -2
- package/src/AvatarGroup/AvatarGroupOverflow.test.tsx +24 -1
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +2 -0
- package/src/Chat/ChatLayoutScrollButton.test.tsx +13 -0
- package/src/Chat/ChatLayoutScrollButton.tsx +4 -2
- package/src/Chat/__tests__/ChatLayoutScrollButton.a11y.chromium.spec.ts +64 -19
- package/src/CheckboxList/CheckboxList.fr10.test.tsx +29 -2
- package/src/CheckboxList/CheckboxList.tsx +4 -1
- package/src/Code/Code.doc.mjs +1 -0
- package/src/CommandPalette/CommandPalette.doc.mjs +2 -0
- package/src/ContextMenu/ContextMenu.test.tsx +12 -2
- package/src/Drawer/Drawer.doc.mjs +1 -0
- package/src/DropdownMenu/DropdownMenu.ssr.test.tsx +57 -0
- package/src/DropdownMenu/DropdownMenu.test.tsx +17 -5
- package/src/DropdownMenu/DropdownMenu.tsx +43 -142
- package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +1 -1
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +1 -1
- package/src/DropdownMenu/DropdownMenuGroup.doc.mjs +169 -10
- package/src/DropdownMenu/DropdownMenuGroup.tsx +6 -4
- package/src/DropdownMenu/DropdownMenuLazySheet.test.tsx +126 -0
- package/src/DropdownMenu/DropdownMenuRadioItem.doc.mjs +1 -1
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +1 -1
- package/src/DropdownMenu/__tests__/DropdownMenuGroupAudit.a11y.chromium.spec.ts +334 -0
- package/src/DropdownMenu/__tests__/warmMenuBottomSheet.tsx +43 -0
- package/src/DropdownMenu/lazyMenuBottomSheet.ts +71 -0
- package/src/DropdownMenu/menuRegionStops.ts +167 -0
- package/src/Field/FieldLabel.doc.mjs +133 -26
- package/src/Field/FieldLabel.spec.md +172 -0
- package/src/Field/__tests__/FieldLabelAudit.a11y.chromium.spec.ts +296 -0
- package/src/FormLayout/FormLayout.doc.mjs +6 -3
- package/src/FormLayout/FormLayout.spec.md +192 -0
- package/src/FormLayout/FormLayout.test.tsx +15 -0
- package/src/FormLayout/FormLayout.tsx +7 -6
- package/src/FormLayout/FormLayoutContext.ts +3 -3
- package/src/Grid/Grid.doc.mjs +7 -1
- package/src/Grid/Grid.spec.md +93 -113
- package/src/Grid/Grid.test.tsx +22 -0
- package/src/Grid/Grid.tsx +10 -9
- package/src/Grid/GridSpan.doc.mjs +3 -3
- package/src/Grid/GridSpan.tsx +1 -1
- package/src/Icon/Icon.doc.mjs +36 -0
- package/src/Indicator/Indicator.doc.mjs +3 -3
- package/src/Layout/LayoutHeader.doc.mjs +5 -0
- package/src/Markdown/parser.ts +8 -2
- package/src/Markdown/plugins/headingLinks.ts +9 -1
- package/src/MetadataList/MetadataList.doc.mjs +1 -1
- package/src/MoreMenu/MoreMenu.test.tsx +5 -1
- package/src/PowerSearch/PowerSearch.test.tsx +62 -0
- package/src/PowerSearch/PowerSearch.tsx +9 -0
- package/src/PowerSearch/PowerSearchValueEditor.test.tsx +71 -0
- package/src/PowerSearch/PowerSearchValueEditor.tsx +45 -0
- package/src/ProgressBar/ProgressBar.doc.mjs +1 -0
- package/src/Resizable/useResizable.doc.mjs +2 -0
- package/src/ScrollableArea/ScrollableArea.doc.mjs +1 -1
- package/src/SideNav/SideNav.spec.md +227 -0
- package/src/SideNav/SideNav.test.tsx +41 -0
- package/src/SideNav/SideNavItem.tsx +5 -2
- package/src/Spinner/Spinner.doc.mjs +1 -1
- package/src/Table/Table.spec.md +6 -1
- package/src/Table/plugins/activation/useTableActivation.spec.md +254 -0
- package/src/Timestamp/Timestamp.doc.mjs +1 -1
- package/src/ToggleButton/ToggleButtonGroup.doc.mjs +4 -0
- package/src/Token/Token.tsx +1 -1
- package/src/Token/__tests__/TokenRemoveTarget.a11y.chromium.spec.ts +86 -0
- package/src/Toolbar/Toolbar.test.tsx +0 -8
- package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +163 -0
- package/src/Toolbar/__tests__/Toolbar.a11y.renders.tsx +63 -0
- package/src/Toolbar/__tests__/Toolbar.a11y.states.ts +142 -0
- package/src/Toolbar/__tests__/Toolbar.a11y.test.tsx +126 -0
- package/src/TopNav/TopNavMegaMenu.test.tsx +25 -0
- package/src/TopNav/TopNavMegaMenu.tsx +2 -0
- package/src/TopNav/TopNavMenu.test.tsx +27 -1
- package/src/TopNav/TopNavMenu.tsx +2 -0
- package/src/postinstall.test.mjs +79 -0
- package/src/reset.a11y.chromium.spec.ts +52 -1
- package/src/reset.css +9 -0
- package/src/reset.test.ts +11 -0
- package/src/unicodePropertyEscapes.source-build.test.mjs +69 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_version: 3
|
|
3
|
+
template_version: 4
|
|
4
|
+
kind: module
|
|
5
|
+
id: module:Table/useTableActivation
|
|
6
|
+
authority: draft
|
|
7
|
+
archive_reason: null
|
|
8
|
+
superseded_by: null
|
|
9
|
+
approved_by: null
|
|
10
|
+
approved_at: null
|
|
11
|
+
owners: [cixzhang]
|
|
12
|
+
review_triggers:
|
|
13
|
+
[public-api, behavior, layout, theming, accessibility, interaction]
|
|
14
|
+
verified_by:
|
|
15
|
+
[
|
|
16
|
+
packages/core/src/Table/plugins/activation/useTableActivation.test.tsx,
|
|
17
|
+
packages/core/src/Table/__tests__/TableActivation.a11y.chromium.spec.ts,
|
|
18
|
+
packages/core/src/Table/Table.perf.test.tsx,
|
|
19
|
+
scripts/check-knowledge.mjs,
|
|
20
|
+
]
|
|
21
|
+
parent_component: component:Table
|
|
22
|
+
references:
|
|
23
|
+
[
|
|
24
|
+
architecture:component-theming-surface,
|
|
25
|
+
architecture:interaction-modality,
|
|
26
|
+
architecture:public-component-api,
|
|
27
|
+
spec:AST-002,
|
|
28
|
+
spec:AST-009,
|
|
29
|
+
]
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
# useTableActivation module contract
|
|
33
|
+
|
|
34
|
+
## Contract at a glance
|
|
35
|
+
|
|
36
|
+
| Area | Contract |
|
|
37
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
38
|
+
| Public contract | Candidate `useTableActivation<T>` hook returning one `TablePlugin<T>` from controlled `activeKey`, `onActivate`, and optional row eligibility. V1 has one press interaction model and consumes Table's canonical resolved row key rather than introducing a second extractor. |
|
|
39
|
+
| Behavior | Eligible rows request activation from a primary pointer click or Enter/Space after earlier row handlers run. Cancellation, nested or portalled controls, repeated keys, and competing whole-row owners do not trigger duplicate activation; native row semantics remain intact. |
|
|
40
|
+
| End-user impact | People can keep one current row visually anchored while it drives adjacent detail or preview content, without losing cell controls or other Table behavior. |
|
|
41
|
+
| Builder impact | Builders own the active key and response to activation. The plugin owns row-state reflection and one whole-row activation path; it does not own routing, detail content, or selection. |
|
|
42
|
+
| Compatibility/readiness | Additive, unreleased module. This draft is not implementation authority. Canonical row identity, competing activation-owner enforcement, ineligible active-state treatment, and active-state paint require owner decisions before promotion. |
|
|
43
|
+
| Review checks | Reject selection coupling, a second row-key extractor, duplicate keys activating multiple rows, implicit navigation, button/link roles on `<tr>`, default `aria-current`, hover activation, non-primary or repeated activation, missing Enter/Space metadata, clobbered or uncancellable handlers, activation from nested/portalled controls or an in-row selection gesture, active paint that fails on pinned cells, and plugin identity churn for stable inputs. |
|
|
44
|
+
| Governing rules | `component:Table` FR9-FR12 and ORD1-ORD4 own plugin composition; `architecture:interaction-modality` owns shared input-modality rules; `architecture:public-component-api` and `spec:AST-002` own public caller burden; `spec:AST-009` owns accessibility-evidence escalation. |
|
|
45
|
+
|
|
46
|
+
This table is a review projection; the body below is authoritative.
|
|
47
|
+
|
|
48
|
+
## Intent
|
|
49
|
+
|
|
50
|
+
`useTableActivation` gives a data-driven Table one controlled current product-row state
|
|
51
|
+
for master-detail, preview, and inspector layouts. It consumes identity through the
|
|
52
|
+
parent dependency in OQ6, requests activation, reflects active state, composes with
|
|
53
|
+
existing row behavior, and keeps state visually coherent through pinned cells.
|
|
54
|
+
|
|
55
|
+
Activation means “this row currently drives adjacent product state.” It is not a
|
|
56
|
+
selected-set operation, a navigation promise, or focus ownership. Those distinctions
|
|
57
|
+
are part of the module contract rather than left to each product implementation.
|
|
58
|
+
|
|
59
|
+
## Compatibility and migration
|
|
60
|
+
|
|
61
|
+
- Released default preserved: `yes`; Table behavior remains unchanged unless the new plugin is supplied.
|
|
62
|
+
- Compatibility class: additive public hook, types, documentation, and examples.
|
|
63
|
+
- Migration decision: `module:Table/useTableActivation/DEC-1` after the remaining identity, ownership, eligibility, and active-presentation decisions are approved.
|
|
64
|
+
|
|
65
|
+
Consumer migration instructions belong in consumer docs and release notes.
|
|
66
|
+
|
|
67
|
+
## Ownership boundary
|
|
68
|
+
|
|
69
|
+
**Owns**
|
|
70
|
+
|
|
71
|
+
- The public `useTableActivation` hook, its configuration, and the controlled active-row contract.
|
|
72
|
+
- Consumption of Table's canonical resolved row key, row eligibility, activation request boundaries, and active-state reflection.
|
|
73
|
+
- Composition with existing row handlers and first-party Table plugins, including existing-handler-first cancellation through `preventDefault`.
|
|
74
|
+
- Suppression of accidental activation from nested or portalled interactive content, non-primary clicks, repeated key presses, and active text selection.
|
|
75
|
+
- Primary-pointer and Enter/Space parity while preserving native row semantics, prior focus ownership, and visible focus.
|
|
76
|
+
- One whole-row activation owner; selection, expansion, tree controls, and other non-owning row behavior may compose without duplicate activation.
|
|
77
|
+
- Active-row paint, pinned-cell coherence, accessibility, performance, and evidence.
|
|
78
|
+
|
|
79
|
+
**Does not own / non-goals**
|
|
80
|
+
|
|
81
|
+
- Aggregate `TablePlugin` protocol, transform order, failure isolation, or plugin-array identity — owned by `component:Table`.
|
|
82
|
+
- Selection state, checkbox semantics, or `aria-selected` — owned by `useTableSelection` and its caller.
|
|
83
|
+
- Routing, URLs, link semantics, or `aria-current="page"` — owned by a navigation surface, not activation.
|
|
84
|
+
- Adjacent detail content, preview loading, prefetching, or focus movement — owned by the product or another module.
|
|
85
|
+
- Cell controls. Buttons, links, inputs, editable content, and portalled controls remain independently operable and do not activate their row as a side effect.
|
|
86
|
+
- Hover-driven activation. Hover may remain a visual enhancement, but v1 never changes controlled active state from pointer movement alone.
|
|
87
|
+
- Children-mode rows. The plugin pipeline applies to data-driven rows only.
|
|
88
|
+
- A new public theme target. The active row remains part of the existing `table-row` anatomy unless a separate theming decision proves another owner is necessary.
|
|
89
|
+
|
|
90
|
+
## Public API and concepts
|
|
91
|
+
|
|
92
|
+
The candidate public shape is:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
export interface UseTableActivationConfig<T extends Record<string, unknown>> {
|
|
96
|
+
activeKey: string | number | null;
|
|
97
|
+
onActivate: (item: T, key: string | number) => void;
|
|
98
|
+
getIsItemActivatable?: (item: T) => boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function useTableActivation<T extends Record<string, unknown>>(
|
|
102
|
+
config: UseTableActivationConfig<T>,
|
|
103
|
+
): TablePlugin<T>;
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Consumer docs own final signatures, defaults, imports, and examples after promotion.
|
|
107
|
+
|
|
108
|
+
| Concept | Closed values or states | Meaning | Default | Owner | Stability |
|
|
109
|
+
| -------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------- | --------------------------------- | ------------------------------ |
|
|
110
|
+
| Active key | Stable unique `string`, `number`, or `null` | Matches Table's one validated canonical row key; `null` means no active row. | Controlled `null` | Caller through this module | Candidate |
|
|
111
|
+
| Row eligibility | Product row eligible or inert; synthetic group header inert | Distinguishes product activation from grouping controls before applying caller eligibility. | Every product row eligible; group headers excluded | `module:Table/useTableActivation` | Parent distinction pending OQ7 |
|
|
112
|
+
| Activation request | `(item, key)` callback | Asks the caller to update controlled state; the plugin never mutates `activeKey`. | No implicit state update | Caller through this module | Candidate |
|
|
113
|
+
| Activation inputs | Primary pointer click, Enter, or Space | One press interaction model requests activation; hover and auxiliary clicks never change active state. | All three paths enabled | `module:Table/useTableActivation` | Candidate |
|
|
114
|
+
| Active presentation | Inactive or active | Reflects controlled state on the existing row anatomy and through pinned cells. | Inactive | `module:Table/useTableActivation` | Unsettled |
|
|
115
|
+
| Selection/navigation | Independent | Activation neither changes selection nor claims a route, destination, or navigation role. | Unchanged | Their respective owners | Required |
|
|
116
|
+
|
|
117
|
+
## Behavioral contract
|
|
118
|
+
|
|
119
|
+
| ID | Candidate invariant | Basis | Draft review state |
|
|
120
|
+
| ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------ |
|
|
121
|
+
| FR1 | One mounted hook instance returns one stable `TablePlugin`, and each mounted row keeps a stable composed activation handler while controlled keys or current callbacks change. | `component:Table` FR12 and PR1; existing stable-identity plugin evidence | Settled candidate |
|
|
122
|
+
| FR2 | `activeKey` is fully controlled. Activation depends on the unapproved parent contract in OQ6: one explicit stable unique `idKey`, its resolved key and validity available to activation, and duplicate-key rows inert in all builds with one deduplicated development warning. Current Table derives keys but does not validate them or pass them to row transforms. The candidate compares `activeKey` with the resolved key using strict equality and emits that same key: a property-name `idKey` resolves `String(item[idKey])`, so `"42"` matches and `42` does not; a function preserves its returned string or number. Without `idKey`, activation is inert with one deduplicated development warning, not keyed by row index. No second extractor or activation-side coercion is added. | One row identity across parent and module | Parent API decision required |
|
|
123
|
+
| FR3 | An eligible row whose validated canonical key equals `activeKey` exposes a stable `data-active` state and component-owned active presentation; all other rows remove both. No default `aria-current` or `aria-selected` is inferred. | Activation/selection/navigation boundary | Settled candidate |
|
|
124
|
+
| FR4 | An ineligible product row contributes no activation handler, keyboard affordance, active paint, or active-state attribute even if its key equals the controlled key, subject to OQ5. Synthetic `useTableGroupedRows` headers are always ineligible for product activation, including when an eligibility predicate returns true; their existing group toggle and disclosure button remain independent. Safe product/header distinction is an OQ7 parent dependency; grouped composition is not admitted until that boundary is enforceable. | Disabled interaction boundary | Verify whether controlled active state should remain visible |
|
|
125
|
+
| FR5 | A primary pointer click, Enter, or Space calls `onActivate(item, key)` once. The module runs each earlier accumulated handler first, preserves its side effects, and stops before activation when that handler calls `preventDefault`; it never replaces a handler already present when its transform runs. | Parent sequential plugin contract; proven interaction composition | Settled candidate |
|
|
126
|
+
| FR6 | An event originating in a nested button, link, input, select, textarea, editable surface, equivalent interactive role, or portalled control outside the row does not activate the row. Text selection suppresses activation only when the non-collapsed selection associated with that gesture intersects the activated row. | Accidental-activation prevention | Settled candidate |
|
|
127
|
+
| FR7 | Each eligible row is a sequential focus stop by default and activates on Enter or Space while retaining native `<tr>` semantics and visible focus. An earlier plugin's explicit `tabIndex`, including `-1` for roving focus, remains authoritative; a programmatically focused row still activates. The row advertises `Enter Space` through composed `aria-keyshortcuts` metadata. Promotion evidence covers representative large unvirtualized tables because the default sequential path grows with mounted eligible rows. | Proven keyboard model; WCAG 2.2 SC 2.1.1, 2.4.7, and 2.4.11 | Settled candidate |
|
|
128
|
+
| FR8 | Only a primary-button click activates; auxiliary clicks remain available to earlier handlers. Keyboard auto-repeat never activates. Repeated Space remains default-prevented so holding it cannot scroll the page, while repeated Enter is ignored without claiming a browser default. Pointer hover never changes `activeKey` in v1. | Proven press boundary; shared modality rules | Settled candidate |
|
|
129
|
+
| FR9 | Activation is independent from selection, expansion, tree controls, context menus, sorting, and navigation. Non-owning plugins retain their handlers and controlled state. A product row has at most one whole-row activation owner; expansion configured with `hasRowClickExpansion` and future navigation are competing owners. Synthetic `useTableGroupedRows` headers remain grouping-owned and ineligible under FR4. OQ7 requires parent ownership enforcement and product/header distinction; plugin insertion order must not decide whether a group toggle also calls product `onActivate`. | Module boundary, single ownership, and plugin composition | Parent enforcement decision required |
|
|
130
|
+
| FR10 | Active presentation uses a component-owned fallback on existing row anatomy. OQ3 owns the unresolved paint precedence across striping, hover, focus, selection's row wash, `hasRowHighlight`, and active state; no selected-plus-active combination is implementation-ready without that decision. Each admitted combination defines one effective fill/overlay for both normal and pinned cells and a distinguishable active cue independent of colour. `hasRowHighlight=false` continues to disable selection-owned wash, not selection semantics; whether activation replaces the current detail-panel background-reclamation use case remains an OQ3 decision. No theme-authored `active` state is implied. | Current private row overlay and theming-surface boundaries | Human visual decision |
|
|
131
|
+
| FR11 | Children mode, empty state, and rows excluded by upstream transforms remain unchanged. Any module-owned resource usage is bounded by mounted rows, fully released when the owning row or Table detaches, and produces no activation or global side effect after release. | Observable resource limits; implementation freedom | Settled candidate |
|
|
132
|
+
| FR12 | For valid unique canonical keys, changing only `activeKey` does not replace the plugin array, row handlers, columns, or product data and does not React-rerender unrelated rows. At most the old active and new active mounted rows change their reflected state/presentation. | Bounded-update performance requirement and parent row memoisation | Settled candidate |
|
|
133
|
+
|
|
134
|
+
### Transformation and precedence order
|
|
135
|
+
|
|
136
|
+
- **ORD1 — Eligibility before state.** Distinguish product rows from synthetic grouped headers through the approved OQ7 parent contract, then resolve product eligibility. Group headers remain ineligible regardless of the caller predicate; ineligible rows return prior accumulated row props unchanged.
|
|
137
|
+
- **ORD2 — Controlled state.** The OQ6 dependency supplies one validated canonical key. Compare it with `activeKey` using FR2's strict, type-preserving equality and emit the same key; never derive a second identity or infer active state from focus or a prior event. Missing explicit identity or duplicate-key rows are inert.
|
|
138
|
+
- **ORD3 — Existing handler.** Run the accumulated handler for that event before the module's request. A handler that calls `preventDefault` cancels activation without discarding its own side effects.
|
|
139
|
+
- **ORD4 — Event boundary.** Nested interactive descendants, portalled events outside the row, and a non-collapsed selection intersecting the activated row suppress the module request without suppressing descendant behavior; an unrelated selection elsewhere does not.
|
|
140
|
+
- **ORD5 — Accepted press.** After the prior handler and boundary guards, accept only a primary click or non-repeated Enter/Space. Repeated Space prevents scrolling without activating; hover and auxiliary clicks never activate.
|
|
141
|
+
- **ORD6 — Presentation.** Apply inactive or active state after eligibility while preserving prior classes, styles, refs, attributes, and children.
|
|
142
|
+
- **ORD7 — Parent pipeline.** Relative order with non-owning plugins remains the order supplied by `component:Table`; this module creates no private second pipeline. Competing whole-row owners follow the OQ7 parent enforcement outcome.
|
|
143
|
+
|
|
144
|
+
### Performance and resources
|
|
145
|
+
|
|
146
|
+
- **PR1 — Constant module overhead.** Canonical-key comparison, state reflection, and handler guards add O(1) module-owned work per relevant row update. The module invokes `getIsItemActivatable` at most once when resolving that row update; caller callback complexity is outside the module's guarantee and is measured separately by call count.
|
|
147
|
+
- **PR2 — Stable public identities.** The returned plugin object, Table's resolved plugin array, and mounted row activation handlers stay referentially stable across `activeKey`, callback, and eligibility-function changes while still observing their current values.
|
|
148
|
+
- **PR3 — Bounded active update.** For valid unique keys, changing only `activeKey` changes reflected state/presentation on at most the old active and new active mounted rows and does not React-rerender unrelated rows.
|
|
149
|
+
- **PR4 — Bounded lifetime.** Module-owned retained resources scale no faster than mounted rows, are released when their owning row/Table detaches, and leave no post-release callback or global side effect. Equivalent implementations that meet those observable bounds remain valid.
|
|
150
|
+
|
|
151
|
+
## Accessibility contract
|
|
152
|
+
|
|
153
|
+
- **AR1 — Native table semantics.** Body rows retain their native row role; the module does not claim button, link, option, or gridcell semantics it cannot fulfil.
|
|
154
|
+
- **AR2 — Keyboard parity.** Eligible rows are focusable by default, preserve an earlier roving-focus `tabIndex`, activate on Enter/Space, compose `aria-keyshortcuts="Enter Space"`, and keep visible unobscured focus without replacing native row semantics (WCAG 2.2 SC 2.1.1, 2.4.7, and 2.4.11).
|
|
155
|
+
- **AR3 — Nested and portalled controls.** Interactive descendants and portalled controls remain reachable and operable without activating the row (WCAG 2.2 SC 2.1.1).
|
|
156
|
+
- **AR4 — State honesty.** The module does not emit `aria-selected` or default `aria-current`; active presentation alone does not claim selection or navigation semantics.
|
|
157
|
+
- **AR5 — Non-colour state.** If active state communicates current position required to use an adjacent view, its focus/current treatment cannot rely on colour alone (WCAG 2.2 SC 1.4.1). The exact representation is blocked on OQ3.
|
|
158
|
+
- **AR6 — No hover-only activation.** Hover may enhance row paint, but it never changes active state; pointer, touch, and keyboard users share the press-owned activation outcome (WCAG 2.2 SC 2.1.1 and `architecture:interaction-modality` INV4).
|
|
159
|
+
- **AR7 — Proportionate evidence.** Unit, DOM, accessibility-tree, axe, and real-browser keyboard evidence prove static semantics, nested-control isolation, focus, and reflected state. Real AT is added only if an approved outcome depends on announcement, browse/virtual-cursor behavior, or a recorded AT/browser divergence, as required by `spec:AST-009`.
|
|
160
|
+
|
|
161
|
+
## Design relationships
|
|
162
|
+
|
|
163
|
+
| Anatomy or state | Design requirement | Representation authority | Module contract |
|
|
164
|
+
| -------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------ |
|
|
165
|
+
| Activatable row | Communicates availability without replacing native table structure or cell controls. | This module plus `architecture:interaction-modality` | FR5-FR9, AR1-AR3 |
|
|
166
|
+
| Active row | Remains visually traceable through normal and pinned cells. | Component-owned fallback; public theme-state admission remains OQ3 | FR3, FR10, AR4-AR5 |
|
|
167
|
+
| Focused row | Shows keyboard focus independently from controlled active state. | Shared focus rules and existing row anatomy | FR2, FR7, AR2, AR5 |
|
|
168
|
+
| Nested cell control | Keeps its own interaction, focus, and semantic contract. | The nested component | FR6, FR9, AR3 |
|
|
169
|
+
| Selection/navigation | Never inferred from activation. | Selection or navigation owner | FR2, FR3, FR9, AR4 |
|
|
170
|
+
|
|
171
|
+
The default contract adds no public theme target or theme-authored `active` state. The module owns a component fallback and must propagate its effective overlay to pinned cells. OQ3 may admit an `active` state on the existing `table-row` target only if it defines the fallback, guaranteed paint properties, state precedence, and pinned-overlay projection required by `architecture:component-theming-surface`. A separate direct target still requires another theming decision.
|
|
172
|
+
|
|
173
|
+
## Parent and system relationships
|
|
174
|
+
|
|
175
|
+
- `component:Table` owns the aggregate `TablePlugin` protocol, canonical ordering, sequential transforms, failure isolation, and stable plugin-array identity. Its current contract does not promise validated canonical identity or key delivery to activation. OQ6 requires a separately approved parent addition for explicit stable unique `idKey` validation, resolved-key equality, and duplicate-key inertness; OQ7 requires a separately approved ownership and product/header distinction contract. This module neither supplies nor approves those missing parent contracts.
|
|
176
|
+
- `architecture:interaction-modality` owns cross-component keyboard/pointer modality rules; this module owns only their row-activation projection.
|
|
177
|
+
- `architecture:public-component-api` and `spec:AST-002` own additive API discipline and caller burden. The module exposes controlled product state rather than routing or data-fetching dependencies.
|
|
178
|
+
- `spec:AST-009` owns accessibility-evidence escalation. This module uses lower-layer semantic and browser evidence unless an approved outcome depends on assistive-technology behavior.
|
|
179
|
+
- `useTableSelection`, tree, expansion, context-menu, sticky-column, and future navigation modules retain their independent state and semantics.
|
|
180
|
+
|
|
181
|
+
## Verification map
|
|
182
|
+
|
|
183
|
+
| Contract | Verification | Representative states | Mutation or failure expectation |
|
|
184
|
+
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
185
|
+
| FR1-FR4, FR11-FR12, PR1-PR4 | Public plugin/handler identity, controlled render/mutation/callback counts, invalid-key fallback, and resource-lifetime tests | Active, inactive, `null`, eligible, ineligible, missing `idKey`, property-key `"42"` versus numeric `42`, function-returned numeric keys, duplicate resolved keys, grouped headers, callback/key changes | Plugin/handlers churn, duplicate-key rows activate, unrelated rows rerender, more than two valid-key rows mutate, callbacks overrun, or resources leak. |
|
|
186
|
+
| FR5-FR9, ORD1-ORD7 | Unit/browser interaction tests with earlier handlers and selection, grouped-row headers, control-only expansion, `hasRowClickExpansion`, tree, sticky, context-menu, navigation-owner, and custom plugins | Primary/auxiliary pointer, Enter/Space, auto-repeat, prior `tabIndex`, grouped headers and product rows in both plugin orders, eligibility override, nested/portalled controls, in-row/unrelated text selection, cancellation | Handlers are replaced, callbacks fire twice, auxiliary/repeated/hover input activates, Space repeat scrolls, earlier focus ownership is lost, unrelated selection blocks activation, nested/portalled controls activate rows, or two whole-row owners fire. |
|
|
187
|
+
| FR10, AR5-AR6 | Real-browser screenshots in light/dark, striped, hover, pinned-column, keyboard-focus, touch emulation, and forced-colors states | OQ3's complete selection/activation/`hasRowHighlight` matrix, selection-first and activation-first updates, focus/hover, light/dark, and normal/pinned cells | Component fallback or approved theme state disappears under pinned cells, relies on colour alone, loses focus indication, or hover changes active state. |
|
|
188
|
+
| AR1-AR7 | Unit/DOM/accessibility-tree tests, axe, and real-browser keyboard automation; real AT only for an adopted AT-dependent outcome | Native traversal in representative small and large unvirtualized tables, default/roving/programmatic focus, Enter/Space metadata and activation, nested link/button/input, portalled control, state update | Row semantics are replaced, Enter/Space is undiscoverable or unavailable, focus order becomes incoherent at representative scale, earlier focus ownership is lost, nested controls activate, or selection/navigation semantics are falsely exposed. |
|
|
189
|
+
| Plugin composition | Focused integration matrix plus existing Table selection/tree/expansion/sticky/context-menu suites | Every supported first-party combination and custom row handler order | Enabling activation breaks another plugin, changes column order, or loses accumulated row props. |
|
|
190
|
+
| Public API and documentation | Public-subpath compile fixture including required explicit `idKey` and parent-supplied validated canonical row key, generated inventory, `.doc.mjs`, Storybook, and one recipe | Generic row type, controlled update, eligibility, duplicate keys, primary click, Enter/Space | A second extractor or mode switch appears, duplicate behavior is undefined, the hook/types are unavailable, or docs imply hover activation. |
|
|
191
|
+
| Performance | Extend `Table.perf.test.tsx` with plugin/handler identity, render and DOM-mutation counts, eligibility-callback counts, stable no-op, and adjacent valid-key changes | Large representative data, same key, adjacent key, callbacks changed | One key change replaces the plugin path, rerenders unrelated rows, mutates more than old/new rows, over-invokes eligibility, or exceeds budgets. |
|
|
192
|
+
| Structural ownership | `pnpm check:knowledge` | Draft module and parent backlink | A missing, orphaned, misnamed, wrong-kind, or duplicate module record passes validation. |
|
|
193
|
+
|
|
194
|
+
## Decision log
|
|
195
|
+
|
|
196
|
+
### DEC-1 — Activation is controlled and independent
|
|
197
|
+
|
|
198
|
+
**Reference:** `module:Table/useTableActivation/DEC-1`
|
|
199
|
+
**Decider:** pending owner approval
|
|
200
|
+
|
|
201
|
+
Activation represents one caller-owned current product row. It depends on OQ6's parent validation and key projection, not a validation guarantee in current Table. The candidate requires explicit stable unique `idKey`, compares the resolved key with `activeKey` as specified in FR2, and emits the same key; missing identity and duplicate-key rows remain inert. Grouped headers do not become product rows under an eligibility override. The module never owns product state, selection, routing, focus, detail content, or prefetching.
|
|
202
|
+
|
|
203
|
+
Rejected: a second activation-specific key extractor, deriving activation from selection or focus, storing an internal fallback key, or combining row navigation and activation into one hook.
|
|
204
|
+
|
|
205
|
+
### DEC-2 — Preserve native row semantics
|
|
206
|
+
|
|
207
|
+
**Reference:** `module:Table/useTableActivation/DEC-2`
|
|
208
|
+
**Decider:** pending owner approval
|
|
209
|
+
|
|
210
|
+
Whole-row convenience keeps native semantic table-row structure. Eligible rows are focusable by default, preserve an earlier roving-focus `tabIndex`, advertise Enter/Space through `aria-keyshortcuts`, and activate from primary click, Enter, or Space without claiming button or link semantics.
|
|
211
|
+
|
|
212
|
+
Rejected: `role="button"` or `role="link"` on `<tr>`, pointer-only activation, and
|
|
213
|
+
implicit `aria-selected` or `aria-current`.
|
|
214
|
+
|
|
215
|
+
### DEC-3 — Active state uses existing row anatomy
|
|
216
|
+
|
|
217
|
+
**Reference:** `module:Table/useTableActivation/DEC-3`
|
|
218
|
+
**Decider:** pending owner approval
|
|
219
|
+
|
|
220
|
+
The module reflects active state through existing row anatomy, with one effective fill/overlay reaching normal and pinned cells. OQ3 owns the exact representation and precedence, including selection's wash and `hasRowHighlight`; neither selected-plus-active precedence nor a replacement for the detail-panel background-reclamation path is implied. A second target is not justified. Theme-authored `active` state is admitted only if OQ3 defines fallback, properties, precedence, and pinned projection.
|
|
221
|
+
|
|
222
|
+
Rejected: styling only transparent row backgrounds that disappear under pinned cells, implying that the current `table-row` target already accepts an undeclared `active` state, and adding a public `table-row-activation` target without an independent theme-author need.
|
|
223
|
+
|
|
224
|
+
### DEC-4 — Press owns activation and earlier handlers may cancel
|
|
225
|
+
|
|
226
|
+
**Reference:** `module:Table/useTableActivation/DEC-4`
|
|
227
|
+
**Decider:** pending owner approval
|
|
228
|
+
|
|
229
|
+
The module has one press interaction model. It runs the accumulated handler first, treats `preventDefault` as cancellation, and then accepts only a primary click or non-repeated Enter/Space outside nested or portalled controls and intersecting text selection. Repeated Space prevents page scrolling without activation. A product row has at most one whole-row activation owner; grouped headers remain grouping-owned and ineligible. OQ7 owns the missing parent distinction and enforcement contract.
|
|
230
|
+
|
|
231
|
+
Rejected: hover activation, replacing earlier handlers, activating after cancellation, and activating from auxiliary clicks or key auto-repeat.
|
|
232
|
+
|
|
233
|
+
## Open questions
|
|
234
|
+
|
|
235
|
+
- **OQ3 — How is required current state communicated?** (`human-design`) Decide the component-owned fallback, focus-versus-active precedence, non-colour cue, and any admitted `active` state on the existing `table-row` target. The decision must specify every state in the matrix below, including whether selected-plus-active uses selection-dominant fill with a distinct active cue or active-dominant fill with a persistent selection cue. `hasRowHighlight=false` removes selection wash while preserving selection semantics; it is not an activation disable flag. Decide whether an active row driving detail content replaces the current background-reclamation use case or needs another explicit caller choice. Each outcome must define the same effective paint on normal and pinned cells, including selection/active changes in either order. No active or selected-plus-active paint is admitted while this question remains open. An opt-in caller-supplied `aria-current` value, if warranted for genuine current-item sets, is a separate decision; default selection/navigation semantics remain forbidden.
|
|
236
|
+
|
|
237
|
+
| Selection | Activation | `hasRowHighlight` | Required outcome before admission |
|
|
238
|
+
| ---------- | ---------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
239
|
+
| Unselected | Inactive | Either | Existing striping, hover, and focus treatment. |
|
|
240
|
+
| Selected | Inactive | `true` | Existing selection wash and selection semantics on normal and pinned cells. |
|
|
241
|
+
| Selected | Inactive | `false` | No selection-owned wash; selection semantics remain. |
|
|
242
|
+
| Unselected | Active | Either | OQ3 defines active fill, non-colour cue, and focus/hover precedence independently of the selection flag. |
|
|
243
|
+
| Selected | Active | `true` | OQ3 chooses selection-versus-active fill precedence and preserves distinguishable selection, active state, and focus on normal and pinned cells. |
|
|
244
|
+
| Selected | Active | `false` | OQ3 defines active paint without reintroducing selection wash; selection semantics remain. |
|
|
245
|
+
|
|
246
|
+
- **OQ5 — Should an ineligible product row matching `activeKey` remain visibly active?** (`human-api`) Decide whether caller eligibility governs interaction only or both interaction and controlled presentation for real product rows. This decision does not relax FR4's exclusion of synthetic grouped headers.
|
|
247
|
+
- **OQ6 — How does Table expose validated canonical row identity?** (`human-api`) Approve the missing parent-owned validation and key-delivery contract. Current Table resolves an index when `idKey` is omitted, `String(item[idKey])` for a property name, or the function's returned key; it does not validate uniqueness or deliver the key to `transformBodyRow`. The activation candidate requires explicit stable unique identity, FR2's strict resolved-key equality and same-key callback, inert missing/duplicate identity in all builds, and deduplicated development diagnostics. The approved parent record must own these additions and their compatibility; this module adds no activation-specific extractor or coercion.
|
|
248
|
+
- **OQ7 — How does Table enforce whole-row ownership and product eligibility?** (`human-api`) Approve the missing parent contract for distinguishing caller product rows from synthetic `useTableGroupedRows` headers and enforcing FR4's default header ineligibility. Group headers keep their grouping-owned click and disclosure-button paths; `hasRowClickExpansion` and future navigation compete with activation on product rows. Define deterministic rejection or neutralisation, development diagnostics, and production fallback for every admitted combination without making plugin insertion order select an owner. Grouping integration remains unsupported until its distinction and ownership boundary are enforceable; key prefixes or private marker inspection are not caller obligations.
|
|
249
|
+
|
|
250
|
+
## Content boundary
|
|
251
|
+
|
|
252
|
+
This record does not duplicate consumer signatures/examples, the parent Table plugin
|
|
253
|
+
protocol or ordering, shared input-modality rules, current usage audits, implementation
|
|
254
|
+
steps, or navigation/selection/prefetch contracts. It links to their canonical owners.
|
|
@@ -6,7 +6,7 @@ export const docs = {
|
|
|
6
6
|
name: 'Timestamp',
|
|
7
7
|
displayName: 'Timestamp',
|
|
8
8
|
category: 'Content',
|
|
9
|
-
keywords: ['date', 'time', 'datetime', 'relative', 'ago', 'clock', 'format', 'duration'],
|
|
9
|
+
keywords: ['date', 'time', 'datetime', 'relative', 'ago', 'clock', 'format', 'duration', 'relativetime'],
|
|
10
10
|
playground: {
|
|
11
11
|
// `value` is required but has no semantic default, so the properties-tab
|
|
12
12
|
// preview fell back to the literal string "value" — which
|
|
@@ -6,6 +6,10 @@ export const docs = {
|
|
|
6
6
|
name: 'ToggleButtonGroup',
|
|
7
7
|
subComponentOf: 'ToggleButton',
|
|
8
8
|
displayName: 'Toggle Button Group',
|
|
9
|
+
keywords: [
|
|
10
|
+
'togglegroup',
|
|
11
|
+
'buttontogglegroup',
|
|
12
|
+
],
|
|
9
13
|
description: 'Groups toggle buttons for exclusive (single) or multi-select behavior. Uses discriminated union on type for type-safe value/onChange.',
|
|
10
14
|
props: [
|
|
11
15
|
{
|
package/src/Token/Token.tsx
CHANGED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file TokenRemoveTarget.a11y.chromium.spec.ts
|
|
5
|
+
* @input The `Core/PowerSearch` Pre-set Filters story in a built Storybook and
|
|
6
|
+
* real mouse input, in both text directions.
|
|
7
|
+
* @output Real-engine proof that a Token's enlarged remove target stays on its
|
|
8
|
+
* X: a press on the end of the value beside it runs the token's own action,
|
|
9
|
+
* and a press just outside the visible X still removes.
|
|
10
|
+
* @position Run with `pnpm test:a11y-contract`. jsdom has no hit testing, so
|
|
11
|
+
* the remove target's reach is only observable here.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {expect, test, type Page} from '@playwright/test';
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_STORYBOOK_DIR,
|
|
17
|
+
serveStorybook,
|
|
18
|
+
type StaticServer,
|
|
19
|
+
} from '@astryxdesign/a11y-spec/storybook';
|
|
20
|
+
|
|
21
|
+
let storybook: StaticServer;
|
|
22
|
+
|
|
23
|
+
test.beforeAll(async () => {
|
|
24
|
+
storybook = await serveStorybook(
|
|
25
|
+
process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test.afterAll(async () => {
|
|
30
|
+
await storybook?.close();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const STORY = 'core-powersearch--with-preset-filters';
|
|
34
|
+
const REMOVE_BUTTONS = '#storybook-root button[aria-label^="Remove "]';
|
|
35
|
+
|
|
36
|
+
async function open(page: Page, direction: 'ltr' | 'rtl') {
|
|
37
|
+
await page.setViewportSize({width: 900, height: 600});
|
|
38
|
+
await page.goto(
|
|
39
|
+
`${storybook.origin}/iframe.html?id=${STORY}&viewMode=story&globals=direction:${direction}`,
|
|
40
|
+
);
|
|
41
|
+
await expect(page.locator(REMOVE_BUTTONS)).toHaveCount(2);
|
|
42
|
+
const remove = page.locator(REMOVE_BUTTONS).first();
|
|
43
|
+
const value = page
|
|
44
|
+
.locator('#storybook-root')
|
|
45
|
+
.getByText('Open', {exact: true})
|
|
46
|
+
.first();
|
|
47
|
+
const x = await remove.boundingBox();
|
|
48
|
+
const v = await value.boundingBox();
|
|
49
|
+
if (x == null || v == null) {
|
|
50
|
+
throw new Error('the Status token has no visible value or remove button');
|
|
51
|
+
}
|
|
52
|
+
return {remove, x, v};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const direction of ['ltr', 'rtl'] as const) {
|
|
56
|
+
test(`a press on the end of the value opens its editor (${direction})`, async ({
|
|
57
|
+
page,
|
|
58
|
+
}) => {
|
|
59
|
+
const {x, v} = await open(page, direction);
|
|
60
|
+
// Two pixels inside the value's inline-end edge, level with the X.
|
|
61
|
+
const px = direction === 'ltr' ? v.x + v.width - 2 : v.x + 2;
|
|
62
|
+
const py = x.y + x.height / 2;
|
|
63
|
+
// Precondition: the point is on the value, not on the visible X.
|
|
64
|
+
expect(px < x.x || px > x.x + x.width).toBe(true);
|
|
65
|
+
|
|
66
|
+
await page.mouse.click(px, py);
|
|
67
|
+
|
|
68
|
+
await expect(page.getByRole('combobox', {name: 'Field'})).toBeVisible();
|
|
69
|
+
await expect(page.locator(REMOVE_BUTTONS)).toHaveCount(2);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test(`a press just outside the visible X still removes (${direction})`, async ({
|
|
73
|
+
page,
|
|
74
|
+
}) => {
|
|
75
|
+
const {x} = await open(page, direction);
|
|
76
|
+
// Two pixels past the X's inline-end edge: outside the 16px glyph, inside
|
|
77
|
+
// the enlarged target.
|
|
78
|
+
const px = direction === 'ltr' ? x.x + x.width + 2 : x.x - 2;
|
|
79
|
+
const py = x.y + x.height / 2;
|
|
80
|
+
|
|
81
|
+
await page.mouse.click(px, py);
|
|
82
|
+
|
|
83
|
+
await expect(page.locator(REMOVE_BUTTONS)).toHaveCount(1);
|
|
84
|
+
await expect(page.getByRole('combobox', {name: 'Field'})).toHaveCount(0);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -140,14 +140,6 @@ describe('Toolbar', () => {
|
|
|
140
140
|
);
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
-
it('sets aria-orientation to vertical', () => {
|
|
144
|
-
render(<Toolbar label="Actions" orientation="vertical" />);
|
|
145
|
-
expect(screen.getByRole('toolbar')).toHaveAttribute(
|
|
146
|
-
'aria-orientation',
|
|
147
|
-
'vertical',
|
|
148
|
-
);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
143
|
it('reflects the size', () => {
|
|
152
144
|
render(<Toolbar label="Actions" size="sm" />);
|
|
153
145
|
const toolbar = screen.getByRole('toolbar');
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Toolbar.a11y.chromium.spec.ts
|
|
5
|
+
* @input Built toolbar stories and TOOLBAR_PATTERN in real Chromium
|
|
6
|
+
* @output Browser evidence for every toolbar expectation across Toolbar's representative states, and a paint check that each state's controls sit on the axis and in the direction it declares
|
|
7
|
+
* @position Chromium lane of the Toolbar binding
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
expect,
|
|
12
|
+
test,
|
|
13
|
+
type CDPSession,
|
|
14
|
+
type Locator,
|
|
15
|
+
type Page,
|
|
16
|
+
} from '@playwright/test';
|
|
17
|
+
import {
|
|
18
|
+
TOOLBAR_PATTERN,
|
|
19
|
+
blockingResults,
|
|
20
|
+
checkAccessibilitySpec,
|
|
21
|
+
formatFailures,
|
|
22
|
+
neverExercised,
|
|
23
|
+
summarize,
|
|
24
|
+
type BindingResult,
|
|
25
|
+
} from '@astryxdesign/a11y-spec';
|
|
26
|
+
import {
|
|
27
|
+
createChromiumHarness,
|
|
28
|
+
holdMotionStill,
|
|
29
|
+
} from '@astryxdesign/a11y-spec/chromium';
|
|
30
|
+
import {
|
|
31
|
+
DEFAULT_STORYBOOK_DIR,
|
|
32
|
+
serveStorybook,
|
|
33
|
+
type StaticServer,
|
|
34
|
+
} from '@astryxdesign/a11y-spec/storybook';
|
|
35
|
+
import {
|
|
36
|
+
TOOLBAR_BINDING_STATES,
|
|
37
|
+
type ToolbarBindingState,
|
|
38
|
+
} from './Toolbar.a11y.states';
|
|
39
|
+
|
|
40
|
+
let storybook: StaticServer;
|
|
41
|
+
|
|
42
|
+
test.beforeAll(async () => {
|
|
43
|
+
storybook = await serveStorybook(
|
|
44
|
+
process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test.afterAll(async () => {
|
|
49
|
+
await storybook?.close();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
function storyUrl(storyId: string): string {
|
|
53
|
+
return `${storybook.origin}/iframe.html?id=${storyId}&viewMode=story`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function mountState(
|
|
57
|
+
page: Page,
|
|
58
|
+
state: ToolbarBindingState,
|
|
59
|
+
): Promise<{subject: Locator; related: Record<string, Locator>}> {
|
|
60
|
+
await page.goto(storyUrl(state.storyId), {waitUntil: 'load'});
|
|
61
|
+
await holdMotionStill(page);
|
|
62
|
+
const root = page.locator('#storybook-root');
|
|
63
|
+
const subject = root.getByRole('toolbar', {name: state.label, exact: true});
|
|
64
|
+
await expect(subject).toBeVisible();
|
|
65
|
+
const related: Record<string, Locator> = {
|
|
66
|
+
before: root.getByRole('button', {
|
|
67
|
+
name: 'Before the toolbar',
|
|
68
|
+
exact: true,
|
|
69
|
+
}),
|
|
70
|
+
after: root.getByRole('button', {name: 'After the toolbar', exact: true}),
|
|
71
|
+
};
|
|
72
|
+
for (const [relation, name] of Object.entries(state.controls)) {
|
|
73
|
+
related[relation] = subject.getByRole('button', {name, exact: true});
|
|
74
|
+
}
|
|
75
|
+
return {subject, related};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function runState(
|
|
79
|
+
page: Page,
|
|
80
|
+
cdp: CDPSession,
|
|
81
|
+
state: ToolbarBindingState,
|
|
82
|
+
): Promise<BindingResult> {
|
|
83
|
+
return checkAccessibilitySpec({
|
|
84
|
+
spec: TOOLBAR_PATTERN,
|
|
85
|
+
binding: 'Toolbar',
|
|
86
|
+
state: state.id,
|
|
87
|
+
facts: state.facts,
|
|
88
|
+
mount: async () => {
|
|
89
|
+
const {subject, related} = await mountState(page, state);
|
|
90
|
+
return createChromiumHarness({page, cdp, subject, related});
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
test('every toolbar expectation is exercised by a Toolbar state', async ({
|
|
96
|
+
page,
|
|
97
|
+
}) => {
|
|
98
|
+
const cdp = await page.context().newCDPSession(page);
|
|
99
|
+
const results: BindingResult[] = [];
|
|
100
|
+
for (const state of TOOLBAR_BINDING_STATES) {
|
|
101
|
+
results.push(await runState(page, cdp, state));
|
|
102
|
+
}
|
|
103
|
+
expect(neverExercised(results)).toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The contract reads `orientation` and `direction` as facts the binding
|
|
108
|
+
* declares. This checks the painted page agrees: the second control sits
|
|
109
|
+
* below the first in a vertical state, and after it in reading order — to
|
|
110
|
+
* its right, or its left under right-to-left — in a horizontal one.
|
|
111
|
+
*/
|
|
112
|
+
for (const state of TOOLBAR_BINDING_STATES) {
|
|
113
|
+
test(`Toolbar [${state.id}] paints its controls on the declared axis and direction`, async ({
|
|
114
|
+
page,
|
|
115
|
+
}) => {
|
|
116
|
+
const {related} = await mountState(page, state);
|
|
117
|
+
const first = await related['first-control'].boundingBox();
|
|
118
|
+
const second = await related['second-control'].boundingBox();
|
|
119
|
+
if (first == null || second == null) {
|
|
120
|
+
throw new Error(`state "${state.id}" has an unpainted control`);
|
|
121
|
+
}
|
|
122
|
+
const firstMiddle = {
|
|
123
|
+
x: first.x + first.width / 2,
|
|
124
|
+
y: first.y + first.height / 2,
|
|
125
|
+
};
|
|
126
|
+
const secondMiddle = {
|
|
127
|
+
x: second.x + second.width / 2,
|
|
128
|
+
y: second.y + second.height / 2,
|
|
129
|
+
};
|
|
130
|
+
if (state.facts.orientation === 'vertical') {
|
|
131
|
+
expect(second.y, 'second control below the first').toBeGreaterThanOrEqual(
|
|
132
|
+
first.y + first.height,
|
|
133
|
+
);
|
|
134
|
+
} else {
|
|
135
|
+
expect(
|
|
136
|
+
Math.abs(secondMiddle.y - firstMiddle.y),
|
|
137
|
+
'controls share a row',
|
|
138
|
+
).toBeLessThan(first.height / 2);
|
|
139
|
+
if (state.facts.direction === 'rtl') {
|
|
140
|
+
expect(secondMiddle.x, 'second control left of the first').toBeLessThan(
|
|
141
|
+
firstMiddle.x,
|
|
142
|
+
);
|
|
143
|
+
} else {
|
|
144
|
+
expect(
|
|
145
|
+
secondMiddle.x,
|
|
146
|
+
'second control right of the first',
|
|
147
|
+
).toBeGreaterThan(firstMiddle.x);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const state of TOOLBAR_BINDING_STATES) {
|
|
154
|
+
test(`Toolbar [${state.id}] — ${state.summary}`, async ({page}) => {
|
|
155
|
+
const cdp = await page.context().newCDPSession(page);
|
|
156
|
+
const result = await runState(page, cdp, state);
|
|
157
|
+
const report = summarize(TOOLBAR_PATTERN, [result]);
|
|
158
|
+
// Required rows block (AST-020 FR9).
|
|
159
|
+
expect(formatFailures(blockingResults([result]))).toBe('');
|
|
160
|
+
expect(report.unrunLayers).toEqual([]);
|
|
161
|
+
expect(report.counts.unexpectedPass).toBe(0);
|
|
162
|
+
});
|
|
163
|
+
}
|