@cssints/apg-specs 0.0.0-stage → 0.5.1
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/LICENSE +21 -0
- package/README.md +65 -2
- package/dist/apg/accordion.bridge.d.mts +35 -0
- package/dist/apg/accordion.bridge.mjs +326 -0
- package/dist/apg/accordion.model.json +8397 -0
- package/dist/apg/alert.bridge.d.mts +22 -0
- package/dist/apg/alert.bridge.mjs +86 -0
- package/dist/apg/alert.model.json +871 -0
- package/dist/apg/alertdialog.bridge.d.mts +21 -0
- package/dist/apg/alertdialog.bridge.mjs +221 -0
- package/dist/apg/alertdialog.model.json +5232 -0
- package/dist/apg/breadcrumb.bridge.d.mts +26 -0
- package/dist/apg/breadcrumb.bridge.mjs +120 -0
- package/dist/apg/breadcrumb.model.json +574 -0
- package/dist/apg/button.bridge.d.mts +35 -0
- package/dist/apg/button.bridge.mjs +208 -0
- package/dist/apg/button.model.json +5814 -0
- package/dist/apg/carousel.bridge.d.mts +44 -0
- package/dist/apg/carousel.bridge.mjs +262 -0
- package/dist/apg/carousel.model.json +8963 -0
- package/dist/apg/checkbox-group.bridge.d.mts +25 -0
- package/dist/apg/checkbox-group.bridge.mjs +276 -0
- package/dist/apg/checkbox-group.model.json +3863 -0
- package/dist/apg/checkbox.bridge.d.mts +34 -0
- package/dist/apg/checkbox.bridge.mjs +253 -0
- package/dist/apg/checkbox.model.json +3158 -0
- package/dist/apg/combobox-autocomplete.bridge.d.mts +14 -0
- package/dist/apg/combobox-autocomplete.bridge.mjs +326 -0
- package/dist/apg/combobox-autocomplete.model.json +11586 -0
- package/dist/apg/combobox-dialog.bridge.d.mts +17 -0
- package/dist/apg/combobox-dialog.bridge.mjs +250 -0
- package/dist/apg/combobox-dialog.model.json +8981 -0
- package/dist/apg/combobox-grid.bridge.d.mts +17 -0
- package/dist/apg/combobox-grid.bridge.mjs +227 -0
- package/dist/apg/combobox-grid.model.json +8554 -0
- package/dist/apg/combobox-select-only.bridge.d.mts +24 -0
- package/dist/apg/combobox-select-only.bridge.mjs +304 -0
- package/dist/apg/combobox-select-only.model.json +7846 -0
- package/dist/apg/combobox-tree.bridge.d.mts +17 -0
- package/dist/apg/combobox-tree.bridge.mjs +211 -0
- package/dist/apg/combobox-tree.model.json +9345 -0
- package/dist/apg/dialog-modal.bridge.d.mts +35 -0
- package/dist/apg/dialog-modal.bridge.mjs +186 -0
- package/dist/apg/dialog-modal.model.json +7580 -0
- package/dist/apg/disclosure.bridge.d.mts +25 -0
- package/dist/apg/disclosure.bridge.mjs +202 -0
- package/dist/apg/disclosure.model.json +4703 -0
- package/dist/apg/feed.bridge.d.mts +31 -0
- package/dist/apg/feed.bridge.mjs +246 -0
- package/dist/apg/feed.model.json +4523 -0
- package/dist/apg/grid-selection.bridge.d.mts +13 -0
- package/dist/apg/grid-selection.bridge.mjs +160 -0
- package/dist/apg/grid-selection.model.json +6974 -0
- package/dist/apg/grid-sort.bridge.d.mts +12 -0
- package/dist/apg/grid-sort.bridge.mjs +138 -0
- package/dist/apg/grid-sort.model.json +3799 -0
- package/dist/apg/grid.bridge.d.mts +54 -0
- package/dist/apg/grid.bridge.mjs +441 -0
- package/dist/apg/grid.model.json +3802 -0
- package/dist/apg/link.bridge.d.mts +24 -0
- package/dist/apg/link.bridge.mjs +126 -0
- package/dist/apg/link.model.json +694 -0
- package/dist/apg/listbox-multi.bridge.d.mts +10 -0
- package/dist/apg/listbox-multi.bridge.mjs +261 -0
- package/dist/apg/listbox-multi.model.json +10893 -0
- package/dist/apg/listbox-rearrangeable.bridge.d.mts +20 -0
- package/dist/apg/listbox-rearrangeable.bridge.mjs +237 -0
- package/dist/apg/listbox-rearrangeable.model.json +17592 -0
- package/dist/apg/listbox-shared.d.mts +58 -0
- package/dist/apg/listbox-shared.mjs +171 -0
- package/dist/apg/listbox-single.bridge.d.mts +10 -0
- package/dist/apg/listbox-single.bridge.mjs +196 -0
- package/dist/apg/listbox-single.model.json +8024 -0
- package/dist/apg/listbox-typeahead.bridge.d.mts +10 -0
- package/dist/apg/listbox-typeahead.bridge.mjs +153 -0
- package/dist/apg/listbox-typeahead.model.json +5615 -0
- package/dist/apg/menu-button.bridge.d.mts +29 -0
- package/dist/apg/menu-button.bridge.mjs +259 -0
- package/dist/apg/menu-button.model.json +5706 -0
- package/dist/apg/menu-checkable.bridge.d.mts +25 -0
- package/dist/apg/menu-checkable.bridge.mjs +226 -0
- package/dist/apg/menu-checkable.model.json +4277 -0
- package/dist/apg/menu-menubar.bridge.d.mts +27 -0
- package/dist/apg/menu-menubar.bridge.mjs +273 -0
- package/dist/apg/menu-menubar.model.json +9540 -0
- package/dist/apg/meter.bridge.d.mts +28 -0
- package/dist/apg/meter.bridge.mjs +159 -0
- package/dist/apg/meter.model.json +1185 -0
- package/dist/apg/player.bridge.d.mts +24 -0
- package/dist/apg/player.bridge.mjs +342 -0
- package/dist/apg/player.model.json +11013 -0
- package/dist/apg/radio.bridge.d.mts +35 -0
- package/dist/apg/radio.bridge.mjs +371 -0
- package/dist/apg/radio.model.json +5011 -0
- package/dist/apg/slider-multithumb.bridge.d.mts +34 -0
- package/dist/apg/slider-multithumb.bridge.mjs +365 -0
- package/dist/apg/slider-multithumb.model.json +5774 -0
- package/dist/apg/slider.bridge.d.mts +54 -0
- package/dist/apg/slider.bridge.mjs +359 -0
- package/dist/apg/slider.model.json +4881 -0
- package/dist/apg/spinbutton.bridge.d.mts +41 -0
- package/dist/apg/spinbutton.bridge.mjs +327 -0
- package/dist/apg/spinbutton.model.json +7571 -0
- package/dist/apg/switch.bridge.d.mts +30 -0
- package/dist/apg/switch.bridge.mjs +243 -0
- package/dist/apg/switch.model.json +3159 -0
- package/dist/apg/tabs.bridge.d.mts +41 -0
- package/dist/apg/tabs.bridge.mjs +294 -0
- package/dist/apg/tabs.model.json +7659 -0
- package/dist/apg/timeline.bridge.d.mts +36 -0
- package/dist/apg/timeline.bridge.mjs +265 -0
- package/dist/apg/timeline.model.json +6189 -0
- package/dist/apg/toolbar.bridge.d.mts +25 -0
- package/dist/apg/toolbar.bridge.mjs +307 -0
- package/dist/apg/toolbar.model.json +6616 -0
- package/dist/apg/tooltip.bridge.d.mts +36 -0
- package/dist/apg/tooltip.bridge.mjs +208 -0
- package/dist/apg/tooltip.model.json +8295 -0
- package/dist/apg/treegrid.bridge.d.mts +28 -0
- package/dist/apg/treegrid.bridge.mjs +311 -0
- package/dist/apg/treegrid.model.json +17358 -0
- package/dist/apg/treeview.bridge.d.mts +26 -0
- package/dist/apg/treeview.bridge.mjs +396 -0
- package/dist/apg/treeview.model.json +13745 -0
- package/dist/apg/windowsplitter.bridge.d.mts +36 -0
- package/dist/apg/windowsplitter.bridge.mjs +242 -0
- package/dist/apg/windowsplitter.model.json +6253 -0
- package/dist/canvas/camera.model.json +6568 -0
- package/dist/canvas/canvas.bridge.d.mts +190 -0
- package/dist/canvas/canvas.bridge.mjs +729 -0
- package/dist/canvas/pointer.model.json +17095 -0
- package/dist/scenario.d.mts +233 -0
- package/dist/scenario.mjs +313 -0
- package/package.json +57 -4
- package/scenarios/apg/README.md +221 -0
- package/scenarios/apg/accordion.base-ui.json +1005 -0
- package/scenarios/apg/accordion.react-aria.json +929 -0
- package/scenarios/apg/alert.base-ui.json +81 -0
- package/scenarios/apg/alert.react-aria.json +98 -0
- package/scenarios/apg/alertdialog.base-ui.json +331 -0
- package/scenarios/apg/alertdialog.react-aria.json +225 -0
- package/scenarios/apg/breadcrumb.react-aria.json +312 -0
- package/scenarios/apg/button.ariakit.json +136 -0
- package/scenarios/apg/button.base-ui.json +747 -0
- package/scenarios/apg/button.react-aria.json +443 -0
- package/scenarios/apg/carousel.apg.json +909 -0
- package/scenarios/apg/checkbox.ariakit.json +299 -0
- package/scenarios/apg/checkbox.base-ui.json +1126 -0
- package/scenarios/apg/checkbox.react-aria.json +518 -0
- package/scenarios/apg/combobox.ariakit.json +945 -0
- package/scenarios/apg/combobox.base-ui.json +2444 -0
- package/scenarios/apg/combobox.react-aria.json +845 -0
- package/scenarios/apg/dialog-modal.ariakit.json +1102 -0
- package/scenarios/apg/dialog-modal.base-ui.json +775 -0
- package/scenarios/apg/dialog-modal.react-aria.json +638 -0
- package/scenarios/apg/disclosure.apg.json +349 -0
- package/scenarios/apg/disclosure.ariakit.json +163 -0
- package/scenarios/apg/disclosure.base-ui.json +451 -0
- package/scenarios/apg/disclosure.react-aria.json +643 -0
- package/scenarios/apg/feed.apg.json +442 -0
- package/scenarios/apg/grid.ariakit.json +197 -0
- package/scenarios/apg/grid.base-ui.json +720 -0
- package/scenarios/apg/grid.react-aria.json +969 -0
- package/scenarios/apg/landmarks.ariakit.json +93 -0
- package/scenarios/apg/landmarks.react-aria.json +330 -0
- package/scenarios/apg/link.ariakit.json +83 -0
- package/scenarios/apg/link.base-ui.json +88 -0
- package/scenarios/apg/link.react-aria.json +186 -0
- package/scenarios/apg/listbox.ariakit.json +78 -0
- package/scenarios/apg/listbox.base-ui.json +163 -0
- package/scenarios/apg/listbox.react-aria.json +1118 -0
- package/scenarios/apg/menu-button.ariakit.json +924 -0
- package/scenarios/apg/menu-button.base-ui.json +1155 -0
- package/scenarios/apg/menu-button.react-aria.json +1080 -0
- package/scenarios/apg/menubar.ariakit.json +758 -0
- package/scenarios/apg/menubar.base-ui.json +673 -0
- package/scenarios/apg/meter.base-ui.json +243 -0
- package/scenarios/apg/meter.react-aria.json +201 -0
- package/scenarios/apg/radio.ariakit.json +459 -0
- package/scenarios/apg/radio.base-ui.json +581 -0
- package/scenarios/apg/radio.react-aria.json +576 -0
- package/scenarios/apg/slider-multithumb.base-ui.json +316 -0
- package/scenarios/apg/slider-multithumb.react-aria.json +363 -0
- package/scenarios/apg/slider.base-ui.json +544 -0
- package/scenarios/apg/slider.react-aria.json +456 -0
- package/scenarios/apg/spinbutton.base-ui.json +1688 -0
- package/scenarios/apg/spinbutton.react-aria.json +426 -0
- package/scenarios/apg/switch.base-ui.json +456 -0
- package/scenarios/apg/switch.react-aria.json +303 -0
- package/scenarios/apg/table.ariakit.json +65 -0
- package/scenarios/apg/table.react-aria.json +221 -0
- package/scenarios/apg/tabs.ariakit.json +329 -0
- package/scenarios/apg/tabs.base-ui.json +875 -0
- package/scenarios/apg/tabs.react-aria.json +644 -0
- package/scenarios/apg/toolbar.ariakit.json +595 -0
- package/scenarios/apg/toolbar.base-ui.json +1189 -0
- package/scenarios/apg/toolbar.react-aria.json +383 -0
- package/scenarios/apg/tooltip.ariakit.json +441 -0
- package/scenarios/apg/tooltip.base-ui.json +411 -0
- package/scenarios/apg/tooltip.react-aria.json +454 -0
- package/scenarios/apg/treegrid.apg.json +2558 -0
- package/scenarios/apg/treegrid.react-aria.json +5094 -0
- package/scenarios/apg/treeview.apg.json +928 -0
- package/scenarios/apg/windowsplitter.apg.json +557 -0
- package/scenarios/canvas/README.md +69 -0
- package/scenarios/canvas/camera.contract.json +237 -0
- package/scenarios/canvas/camera.excalidraw.json +57 -0
- package/scenarios/canvas/marquee.excalidraw.json +122 -0
- package/scenarios/canvas/pan.excalidraw.json +46 -0
- package/scenarios/canvas/pointer.contract.json +528 -0
- package/scenarios/parts.json +291 -0
- package/shared/aria.qnt +186 -0
- package/shared/details-group.qnt +50 -0
- package/shared/focusgroup.qnt +152 -0
- package/shared/invokers.qnt +81 -0
- package/shared/popover.qnt +71 -0
- package/specs/apg/STATIC.md +103 -0
- package/specs/apg/accordion.qnt +578 -0
- package/specs/apg/alert.qnt +131 -0
- package/specs/apg/alertdialog.qnt +455 -0
- package/specs/apg/breadcrumb.qnt +88 -0
- package/specs/apg/button.qnt +445 -0
- package/specs/apg/carousel.qnt +732 -0
- package/specs/apg/checkbox-group.qnt +388 -0
- package/specs/apg/checkbox.qnt +394 -0
- package/specs/apg/combobox-autocomplete.qnt +764 -0
- package/specs/apg/combobox-dialog.qnt +627 -0
- package/specs/apg/combobox-grid.qnt +632 -0
- package/specs/apg/combobox-select-only.qnt +626 -0
- package/specs/apg/combobox-tree.qnt +633 -0
- package/specs/apg/dialog-modal.qnt +499 -0
- package/specs/apg/disclosure.qnt +309 -0
- package/specs/apg/feed.qnt +485 -0
- package/specs/apg/grid-selection.qnt +512 -0
- package/specs/apg/grid-sort.qnt +330 -0
- package/specs/apg/grid.qnt +360 -0
- package/specs/apg/link.qnt +119 -0
- package/specs/apg/listbox-multi.qnt +799 -0
- package/specs/apg/listbox-rearrangeable.qnt +801 -0
- package/specs/apg/listbox-single.qnt +511 -0
- package/specs/apg/listbox-typeahead.qnt +396 -0
- package/specs/apg/menu-button.qnt +363 -0
- package/specs/apg/menu-checkable.qnt +304 -0
- package/specs/apg/menu-menubar.qnt +531 -0
- package/specs/apg/meter.qnt +129 -0
- package/specs/apg/player.qnt +1081 -0
- package/specs/apg/radio.qnt +394 -0
- package/specs/apg/slider-multithumb.qnt +441 -0
- package/specs/apg/slider.qnt +396 -0
- package/specs/apg/spinbutton.qnt +494 -0
- package/specs/apg/switch.qnt +389 -0
- package/specs/apg/tabs.qnt +412 -0
- package/specs/apg/timeline.qnt +612 -0
- package/specs/apg/toolbar.qnt +364 -0
- package/specs/apg/tooltip.qnt +553 -0
- package/specs/apg/treegrid.qnt +983 -0
- package/specs/apg/treeview.qnt +1004 -0
- package/specs/apg/windowsplitter.qnt +487 -0
- package/specs/canvas/camera.qnt +407 -0
- package/specs/canvas/pointer.qnt +841 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexei Accio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,66 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @cssints/apg-specs — APG widget specs
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Quint specs behind [`@cssints/headless`](../headless/README.md): one spec per WAI-ARIA APG pattern (plus the canvas camera/pointer contract), the generated models and typed scaffolding, the `SpecBridge` for each pattern, the ported upstream scenario sets, and the spec-only verification. It is the test oracle of the widgets and of `native/headless`, and it is published so that widget libraries outside this repository can replay the same specs and scenarios against their own code.
|
|
4
|
+
|
|
5
|
+
The generated files (`*.model.generated.json`, `*.scaffolding.generated.ts`) are written by the code generation of [`@quint-machine/core`](https://www.npmjs.com/package/@quint-machine/core) and are never edited by hand. Ticket ids like `effects-of-atmosphere-dx00` in the comments of the specs and tests are tickets of the repository these files came from.
|
|
6
|
+
|
|
7
|
+
## Layout
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
specs/apg/<pattern>.qnt # APG pattern specs (one behavior-machine each)
|
|
11
|
+
specs/canvas/<machine>.qnt # canvas camera + pointer specs
|
|
12
|
+
shared/<module>.qnt # vocabulary + pure defs: aria, focusgroup, popover, invokers, details-group
|
|
13
|
+
src/<service>/<machine>.model.generated.json # generated model document
|
|
14
|
+
src/<service>/<machine>.scaffolding.generated.ts # generated typed scaffolding
|
|
15
|
+
src/apg/<pattern>.bridge.ts # scenario vocabulary <-> spec events/state
|
|
16
|
+
src/scenario.ts # scenario decoder + replay/differential-walk runtime
|
|
17
|
+
scenarios/apg/*.json, scenarios/canvas/*.json # ported upstream scenario sets
|
|
18
|
+
scenarios/parts.json # part names per pattern
|
|
19
|
+
test/<service>/<machine>.test.ts # spec-only verification
|
|
20
|
+
scripts/generate-all.ts # regenerates every spec under specs/
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Platform-contract modules (`shared/focusgroup.qnt` roving tabindex, `shared/popover.qnt` light dismiss, `shared/invokers.qnt` `commandfor`/`command`, `shared/details-group.qnt` `<details name>`) are one per native web feature; a pattern imports one and models only its delta. `shared/` holds vocabulary and pure defs only.
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
Run in `packages/apg-specs`:
|
|
28
|
+
|
|
29
|
+
- `bun run generate:all [specs/apg/tabs.qnt ...]` — regenerate artifacts (`node scripts/generate-all.ts`). Each generated file carries the sha256 of its spec; edit a spec, then regenerate. The formatter skips the generated files (`.prettierignore`).
|
|
30
|
+
- `bun run typecheck:quint` — Quint's own typechecker over every spec (~90 s).
|
|
31
|
+
- `bun run test` — spec-only verification (`vp test run`). The exhaustive explorations (player, camera) take a minute each and can time out on a loaded machine; run them alone before calling a failure real.
|
|
32
|
+
- `bun run typecheck` — `tsc-rs` over `src`, `test` and `scripts`.
|
|
33
|
+
- `bun run check` — all of the above, in order.
|
|
34
|
+
|
|
35
|
+
- `bun run build` — `vp pack`: `dist/` (the runner and the bridges as ESM with `.d.mts`, the generated models as `.json`).
|
|
36
|
+
|
|
37
|
+
## Install and use
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npm install @cssints/apg-specs effect @quint-machine/core
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`effect` (`^4.0.0`) is a peer, `@quint-machine/core` a dependency. The package has subpath exports only:
|
|
44
|
+
|
|
45
|
+
| Specifier | What |
|
|
46
|
+
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
47
|
+
| `@cssints/apg-specs/scenario` | the scenario decoder, `replayScenario`, `differentialWalk`, `specDriver`, `Unsupported`, driver and bridge types |
|
|
48
|
+
| `@cssints/apg-specs/apg/<pattern>.bridge` | the `SpecBridge`, `<pattern>Config`, `<pattern>Steps` and `<pattern>Probe` of a pattern |
|
|
49
|
+
| `@cssints/apg-specs/apg/<pattern>.model.json` | the generated model document (`src/apg/<pattern>.model.generated.json` in the repository) |
|
|
50
|
+
| `@cssints/apg-specs/canvas/{canvas.bridge,camera.model.json,pointer.model.json}` | the canvas bridge and models |
|
|
51
|
+
| `@cssints/apg-specs/scenarios/apg/<pattern>.<set>.json` | a ported scenario set (also `scenarios/canvas/*.json`, `scenarios/parts.json`) |
|
|
52
|
+
| `@cssints/apg-specs/specs/apg/<pattern>.qnt` | the raw Quint spec (also `specs/canvas/*.qnt`, `shared/*.qnt`, which the specs import) |
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import {
|
|
56
|
+
Unsupported,
|
|
57
|
+
differentialWalk,
|
|
58
|
+
replayScenario,
|
|
59
|
+
specDriver,
|
|
60
|
+
} from "@cssints/apg-specs/scenario";
|
|
61
|
+
import { buttonBridge, buttonConfig } from "@cssints/apg-specs/apg/button.bridge";
|
|
62
|
+
import buttonModel from "@cssints/apg-specs/apg/button.model.json" with { type: "json" };
|
|
63
|
+
import baseUi from "@cssints/apg-specs/scenarios/apg/button.base-ui.json" with { type: "json" };
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
In this repository the `development` (vite, tsc) and `bun` export conditions point at `src/`. The scenario format is specified in `scenarios/apg/README.md`.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ActionStep, Probe, ScenarioConfig, SpecBridge } from "../scenario.mjs";
|
|
2
|
+
//#region src/apg/accordion.bridge.d.ts
|
|
3
|
+
export interface AccordionConfig {
|
|
4
|
+
readonly items: number;
|
|
5
|
+
/** `defaultValue` when uncontrolled, the initial `value` prop when controlled. */
|
|
6
|
+
readonly expanded: ReadonlyArray<boolean>;
|
|
7
|
+
readonly multiple: boolean;
|
|
8
|
+
readonly collapsible: boolean;
|
|
9
|
+
readonly controlled: boolean;
|
|
10
|
+
/** Controlled only: the host writes every request's intent back as the value. */
|
|
11
|
+
readonly accepts: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Scenario `config` -> what both targets are mounted with. `expanded` is the
|
|
15
|
+
* controlled value (a host that rejects every request, like react-aria's
|
|
16
|
+
* `isExpanded`); `controlled` alone is a host that accepts every request;
|
|
17
|
+
* `accepts` overrides. `defaultExpanded` is the initial value either way. The
|
|
18
|
+
* spec models a fixed three-panel accordion, per-panel disabled, item values,
|
|
19
|
+
* custom ids and the keep-mounted / hidden-until-found flags are not modelled
|
|
20
|
+
* and make the scenario `Unsupported` (skipped, never silently replayed).
|
|
21
|
+
*/
|
|
22
|
+
export declare const accordionConfig: (config: ScenarioConfig) => AccordionConfig;
|
|
23
|
+
/**
|
|
24
|
+
* Accordion vocabulary <-> `specs/apg/accordion.qnt`, shared by the spec driver
|
|
25
|
+
* and the DOM driver so both gate scenarios identically.
|
|
26
|
+
*
|
|
27
|
+
* The spec has no "focus this header" event (`focus` is a Tab-driven slot), so
|
|
28
|
+
* a `focus` step is a no-op here and activation is driven by the step's target
|
|
29
|
+
* (`click_header`), which sets the same slot a real click would.
|
|
30
|
+
*/
|
|
31
|
+
export declare const accordionBridge: SpecBridge;
|
|
32
|
+
/** The user-level steps a differential walk draws from. */
|
|
33
|
+
export declare const accordionSteps: ReadonlyArray<ActionStep>;
|
|
34
|
+
export declare const accordionProbe: Probe;
|
|
35
|
+
//#endregion
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { Unsupported } from "../scenario.mjs";
|
|
2
|
+
//#region src/apg/accordion.bridge.ts
|
|
3
|
+
const specEvent = (tag, params = {}) => ({
|
|
4
|
+
_tag: tag,
|
|
5
|
+
...params
|
|
6
|
+
});
|
|
7
|
+
/** The generator models a fixed three-panel accordion (no collection state). */
|
|
8
|
+
const PANELS = 3;
|
|
9
|
+
/**
|
|
10
|
+
* Scenario parts -> the spec's names: `trigger` <- `header`, `panel` <- `content`;
|
|
11
|
+
* `heading` is the static wrapper. Anything else (groups, menus, ...) is
|
|
12
|
+
* `Unsupported`, which skips the scenario on every target.
|
|
13
|
+
*/
|
|
14
|
+
const partOf = (target) => {
|
|
15
|
+
const [name = "", indexText] = (target ?? "trigger").split(":");
|
|
16
|
+
const index = Number(indexText ?? "0");
|
|
17
|
+
if (name === "trigger" || name === "header" || name === "button") return {
|
|
18
|
+
kind: "trigger",
|
|
19
|
+
index
|
|
20
|
+
};
|
|
21
|
+
if (name === "panel" || name === "content") return {
|
|
22
|
+
kind: "panel",
|
|
23
|
+
index
|
|
24
|
+
};
|
|
25
|
+
if (name === "heading") return {
|
|
26
|
+
kind: "heading",
|
|
27
|
+
index
|
|
28
|
+
};
|
|
29
|
+
throw new Unsupported(`part ${name}`);
|
|
30
|
+
};
|
|
31
|
+
const b2i = (value) => value ? 1 : 0;
|
|
32
|
+
/** Bit 0 = panel 0, bit 1 = panel 1, bit 2 = panel 2. */
|
|
33
|
+
const packed = (expanded) => b2i(expanded[0] === true) + 2 * b2i(expanded[1] === true) + 4 * b2i(expanded[2] === true);
|
|
34
|
+
const expandedIndicesOfPacked = (packedValue) => [
|
|
35
|
+
0,
|
|
36
|
+
1,
|
|
37
|
+
2
|
|
38
|
+
].filter((index) => Math.floor(packedValue / 2 ** index) % 2 === 1);
|
|
39
|
+
/** Resolve one `expanded` entry (an index or a boolean) to a panel index. */
|
|
40
|
+
const indexOf = (entry, items) => {
|
|
41
|
+
if (typeof entry === "number" && Number.isInteger(entry)) {
|
|
42
|
+
if (entry < 0 || entry >= items) throw new Unsupported(`expanded index ${entry}`);
|
|
43
|
+
return entry;
|
|
44
|
+
}
|
|
45
|
+
throw new Unsupported(`expanded entry ${JSON.stringify(entry)}`);
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Scenario `config` -> what both targets are mounted with. `expanded` is the
|
|
49
|
+
* controlled value (a host that rejects every request, like react-aria's
|
|
50
|
+
* `isExpanded`); `controlled` alone is a host that accepts every request;
|
|
51
|
+
* `accepts` overrides. `defaultExpanded` is the initial value either way. The
|
|
52
|
+
* spec models a fixed three-panel accordion, per-panel disabled, item values,
|
|
53
|
+
* custom ids and the keep-mounted / hidden-until-found flags are not modelled
|
|
54
|
+
* and make the scenario `Unsupported` (skipped, never silently replayed).
|
|
55
|
+
*/
|
|
56
|
+
const accordionConfig = (config) => {
|
|
57
|
+
let items = PANELS;
|
|
58
|
+
let multiple = false;
|
|
59
|
+
let collapsible = true;
|
|
60
|
+
let controlled = false;
|
|
61
|
+
let accepts = false;
|
|
62
|
+
const expanded = [
|
|
63
|
+
false,
|
|
64
|
+
false,
|
|
65
|
+
false
|
|
66
|
+
];
|
|
67
|
+
for (const [key, value] of Object.entries(config)) if (key === "items") {
|
|
68
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > PANELS) throw new Unsupported(`config items=${JSON.stringify(value)}`);
|
|
69
|
+
items = value;
|
|
70
|
+
} else if (key === "defaultExpanded") {
|
|
71
|
+
if (!Array.isArray(value)) throw new Unsupported(`config defaultExpanded=${JSON.stringify(value)}`);
|
|
72
|
+
for (const entry of value) expanded[indexOf(entry, items)] = true;
|
|
73
|
+
} else if (key === "expanded") {
|
|
74
|
+
controlled = true;
|
|
75
|
+
if (typeof value === "boolean") expanded[0] = value;
|
|
76
|
+
else if (Array.isArray(value)) for (const entry of value) expanded[indexOf(entry, items)] = true;
|
|
77
|
+
else throw new Unsupported(`config expanded=${JSON.stringify(value)}`);
|
|
78
|
+
} else if (key === "controlled") {
|
|
79
|
+
if (typeof value !== "boolean") throw new Unsupported(`config controlled=${JSON.stringify(value)}`);
|
|
80
|
+
controlled = value;
|
|
81
|
+
accepts = value;
|
|
82
|
+
} else if (key === "accepts") {
|
|
83
|
+
if (typeof value !== "boolean") throw new Unsupported(`config accepts=${JSON.stringify(value)}`);
|
|
84
|
+
controlled = true;
|
|
85
|
+
accepts = value;
|
|
86
|
+
} else if (key === "multiple") {
|
|
87
|
+
if (typeof value !== "boolean") throw new Unsupported(`config multiple=${JSON.stringify(value)}`);
|
|
88
|
+
multiple = value;
|
|
89
|
+
} else if (key === "collapsible") {
|
|
90
|
+
if (typeof value !== "boolean") throw new Unsupported(`config collapsible=${JSON.stringify(value)}`);
|
|
91
|
+
collapsible = value;
|
|
92
|
+
} else throw new Unsupported(`config ${key}=${JSON.stringify(value)}`);
|
|
93
|
+
return {
|
|
94
|
+
items,
|
|
95
|
+
expanded,
|
|
96
|
+
multiple,
|
|
97
|
+
collapsible,
|
|
98
|
+
controlled,
|
|
99
|
+
accepts
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
const expandedCount = (state) => (state["expanded0"] === true ? 1 : 0) + (state["expanded1"] === true ? 1 : 0) + (state["expanded2"] === true ? 1 : 0);
|
|
103
|
+
const expandedAt = (state, index) => state[`expanded${index}`] === true;
|
|
104
|
+
const canCollapse = (state) => state["collapsible"] === true || expandedCount(state) >= 2;
|
|
105
|
+
const expandedIndices = (state) => [
|
|
106
|
+
0,
|
|
107
|
+
1,
|
|
108
|
+
2
|
|
109
|
+
].filter((index) => expandedAt(state, index));
|
|
110
|
+
const indicesFrom = (value) => {
|
|
111
|
+
if (typeof value === "boolean") return [
|
|
112
|
+
value,
|
|
113
|
+
false,
|
|
114
|
+
false
|
|
115
|
+
];
|
|
116
|
+
if (!Array.isArray(value)) throw new Unsupported(`expanded value=${JSON.stringify(value)}`);
|
|
117
|
+
const expanded = [
|
|
118
|
+
false,
|
|
119
|
+
false,
|
|
120
|
+
false
|
|
121
|
+
];
|
|
122
|
+
for (const entry of value) expanded[indexOf(entry, PANELS)] = true;
|
|
123
|
+
return expanded;
|
|
124
|
+
};
|
|
125
|
+
const programmatic = (step) => {
|
|
126
|
+
switch (step.prop) {
|
|
127
|
+
case "multiple": return [specEvent("set_multiple", { allow: step.value === true })];
|
|
128
|
+
case "collapsible": return [specEvent("set_collapsible", { allow: step.value === true })];
|
|
129
|
+
case "headingLevel": {
|
|
130
|
+
const level = typeof step.value === "number" ? step.value : 0;
|
|
131
|
+
if (!Number.isInteger(level) || level < 1 || level > 6) throw new Unsupported(`headingLevel ${JSON.stringify(step.value)}`);
|
|
132
|
+
return [specEvent("set_heading_level", { level: BigInt(level) })];
|
|
133
|
+
}
|
|
134
|
+
case "expanded":
|
|
135
|
+
case "setValue": return [specEvent("setValue", { v: BigInt(packed(indicesFrom(step.value))) })];
|
|
136
|
+
case "hostToggle": return [specEvent("hostToggle", { panel: BigInt(0) })];
|
|
137
|
+
default: throw new Unsupported(`prop ${step.prop ?? ""}`);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Accordion vocabulary <-> `specs/apg/accordion.qnt`, shared by the spec driver
|
|
142
|
+
* and the DOM driver so both gate scenarios identically.
|
|
143
|
+
*
|
|
144
|
+
* The spec has no "focus this header" event (`focus` is a Tab-driven slot), so
|
|
145
|
+
* a `focus` step is a no-op here and activation is driven by the step's target
|
|
146
|
+
* (`click_header`), which sets the same slot a real click would.
|
|
147
|
+
*/
|
|
148
|
+
const accordionBridge = {
|
|
149
|
+
setup: (config) => {
|
|
150
|
+
const { expanded, multiple, collapsible, controlled, accepts } = accordionConfig(config);
|
|
151
|
+
const events = [];
|
|
152
|
+
if (multiple) events.push(specEvent("set_multiple", { allow: true }));
|
|
153
|
+
if (!collapsible) events.push(specEvent("set_collapsible", { allow: false }));
|
|
154
|
+
if (controlled) {
|
|
155
|
+
events.push(specEvent("control"));
|
|
156
|
+
if (accepts) events.push(specEvent("acceptRequests"));
|
|
157
|
+
if (expanded.some(Boolean)) events.push(specEvent("setValue", { v: BigInt(packed(expanded)) }));
|
|
158
|
+
} else for (const index of [
|
|
159
|
+
2,
|
|
160
|
+
1,
|
|
161
|
+
0
|
|
162
|
+
]) if (expanded[index] === true) events.push(specEvent("click_header", { panel: BigInt(index) }));
|
|
163
|
+
if (events.length > 0) events.push(specEvent("blur"));
|
|
164
|
+
return events;
|
|
165
|
+
},
|
|
166
|
+
event: (step) => {
|
|
167
|
+
switch (step.event) {
|
|
168
|
+
case "focus": return [];
|
|
169
|
+
case "blur":
|
|
170
|
+
partOf(step.target);
|
|
171
|
+
return [specEvent("blur")];
|
|
172
|
+
case "click": {
|
|
173
|
+
const { kind, index } = partOf(step.target);
|
|
174
|
+
if (kind === "trigger") return [specEvent("click_header", { panel: BigInt(index) })];
|
|
175
|
+
if (kind === "panel") return [specEvent("click_content", { panel: BigInt(index) })];
|
|
176
|
+
throw new Unsupported(`click on ${step.target ?? ""}`);
|
|
177
|
+
}
|
|
178
|
+
case "key": {
|
|
179
|
+
const { kind, index } = partOf(step.target);
|
|
180
|
+
if (kind === "trigger" && (step.key === "Enter" || step.key === " " || step.key === "Space")) return [specEvent("click_header", { panel: BigInt(index) })];
|
|
181
|
+
throw new Unsupported(`key ${step.key ?? ""} on ${step.target ?? ""}`);
|
|
182
|
+
}
|
|
183
|
+
case "hover":
|
|
184
|
+
case "unhover": return [];
|
|
185
|
+
case "prop": return programmatic(step);
|
|
186
|
+
default: throw new Unsupported(`event ${step.event}`);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
value: (state) => ({ expanded: expandedIndices(state) }),
|
|
190
|
+
focus: (state) => {
|
|
191
|
+
const slot = Number(state["focus"]);
|
|
192
|
+
if (slot < 0) return null;
|
|
193
|
+
return slot % 2 === 0 ? `trigger:${slot / 2}` : `panel:${(slot - 1) / 2}`;
|
|
194
|
+
},
|
|
195
|
+
attr: (state, target, name) => {
|
|
196
|
+
const { kind, index } = partOf(target);
|
|
197
|
+
if (kind === "trigger") {
|
|
198
|
+
if (name === "aria-expanded") return expandedAt(state, index) ? "true" : "false";
|
|
199
|
+
if (name === "aria-controls") return `panel:${index}`;
|
|
200
|
+
if (name === "aria-disabled") return expandedAt(state, index) && !canCollapse(state) ? "true" : null;
|
|
201
|
+
} else if (kind === "panel") {
|
|
202
|
+
if (name === "hidden") return expandedAt(state, index) ? null : "true";
|
|
203
|
+
if (name === "role") return "region";
|
|
204
|
+
if (name === "aria-labelledby") return `trigger:${index}`;
|
|
205
|
+
} else if (kind === "heading") {
|
|
206
|
+
if (name === "role") return "heading";
|
|
207
|
+
if (name === "aria-level") return String(state["headingLevel"]);
|
|
208
|
+
}
|
|
209
|
+
throw new Unsupported(`attribute ${name} on ${target}`);
|
|
210
|
+
},
|
|
211
|
+
visible: (state) => [
|
|
212
|
+
0,
|
|
213
|
+
1,
|
|
214
|
+
2
|
|
215
|
+
].flatMap((index) => expandedAt(state, index) ? [`panel:${index}`] : []),
|
|
216
|
+
events: (before, after) => before["requests"] === after["requests"] ? [] : [["onExpandedChange", expandedIndicesOfPacked(Number(after["requested"]))]]
|
|
217
|
+
};
|
|
218
|
+
/** The user-level steps a differential walk draws from. */
|
|
219
|
+
const accordionSteps = [
|
|
220
|
+
{
|
|
221
|
+
event: "click",
|
|
222
|
+
target: "trigger:0"
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
event: "click",
|
|
226
|
+
target: "trigger:1"
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
event: "click",
|
|
230
|
+
target: "trigger:2"
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
event: "key",
|
|
234
|
+
key: "Enter",
|
|
235
|
+
target: "trigger:0"
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
event: "key",
|
|
239
|
+
key: "Enter",
|
|
240
|
+
target: "trigger:1"
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
event: "key",
|
|
244
|
+
key: "Enter",
|
|
245
|
+
target: "trigger:2"
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
event: "key",
|
|
249
|
+
key: " ",
|
|
250
|
+
target: "trigger:0"
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
event: "key",
|
|
254
|
+
key: " ",
|
|
255
|
+
target: "trigger:1"
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
event: "key",
|
|
259
|
+
key: " ",
|
|
260
|
+
target: "trigger:2"
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
event: "prop",
|
|
264
|
+
prop: "multiple",
|
|
265
|
+
value: true
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
event: "prop",
|
|
269
|
+
prop: "multiple",
|
|
270
|
+
value: false
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
event: "prop",
|
|
274
|
+
prop: "collapsible",
|
|
275
|
+
value: false
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
event: "prop",
|
|
279
|
+
prop: "collapsible",
|
|
280
|
+
value: true
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
event: "prop",
|
|
284
|
+
prop: "headingLevel",
|
|
285
|
+
value: 2
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
event: "prop",
|
|
289
|
+
prop: "headingLevel",
|
|
290
|
+
value: 4
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
event: "prop",
|
|
294
|
+
prop: "expanded",
|
|
295
|
+
value: [0]
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
event: "prop",
|
|
299
|
+
prop: "expanded",
|
|
300
|
+
value: [0, 1]
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
event: "prop",
|
|
304
|
+
prop: "expanded",
|
|
305
|
+
value: []
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
event: "prop",
|
|
309
|
+
prop: "hostToggle"
|
|
310
|
+
}
|
|
311
|
+
];
|
|
312
|
+
const accordionProbe = {
|
|
313
|
+
attrs: [
|
|
314
|
+
["trigger:0", "aria-expanded"],
|
|
315
|
+
["trigger:1", "aria-expanded"],
|
|
316
|
+
["trigger:2", "aria-expanded"],
|
|
317
|
+
["trigger:0", "aria-disabled"],
|
|
318
|
+
["trigger:1", "aria-disabled"],
|
|
319
|
+
["panel:0", "hidden"],
|
|
320
|
+
["panel:1", "hidden"],
|
|
321
|
+
["panel:2", "hidden"]
|
|
322
|
+
],
|
|
323
|
+
events: ["onExpandedChange"]
|
|
324
|
+
};
|
|
325
|
+
//#endregion
|
|
326
|
+
export { accordionBridge, accordionConfig, accordionProbe, accordionSteps };
|