@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
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ScenarioConfig } from "../scenario.mjs";
|
|
2
|
+
import { QuintEvent, QuintModelValue } from "@quint-machine/core";
|
|
3
|
+
import { Schema } from "effect";
|
|
4
|
+
//#region src/apg/listbox-shared.d.ts
|
|
5
|
+
export declare const specEvent: (tag: string, params?: Record<string, bigint | string | boolean>) => QuintEvent;
|
|
6
|
+
export type ListboxSelectionMode = "single" | "multiple" | "none";
|
|
7
|
+
export type ListboxAutoFocus = boolean | number | "first";
|
|
8
|
+
/**
|
|
9
|
+
* What every listbox fixture is mounted with. `items` are the option labels
|
|
10
|
+
* (the DOM's visible/typeahead text); the spec models are index-based, so the
|
|
11
|
+
* differential walk mounts the spec's option names as labels.
|
|
12
|
+
*/
|
|
13
|
+
export interface ListboxMountConfig {
|
|
14
|
+
readonly items: ReadonlyArray<string>;
|
|
15
|
+
readonly selectionMode: ListboxSelectionMode;
|
|
16
|
+
readonly followsFocus: boolean;
|
|
17
|
+
/** Option indices selected at mount (uncontrolled default / controlled initial). */
|
|
18
|
+
readonly defaultSelected: ReadonlyArray<number>;
|
|
19
|
+
/** Option indices that cannot be selected or focused by navigation. */
|
|
20
|
+
readonly disabled: ReadonlyArray<number>;
|
|
21
|
+
readonly focusOnHover: boolean;
|
|
22
|
+
readonly autoFocus: ListboxAutoFocus | undefined;
|
|
23
|
+
readonly ariaLabel: string | undefined;
|
|
24
|
+
/** The host owns the value: the fixture passes it as `value`. */
|
|
25
|
+
readonly controlled: boolean;
|
|
26
|
+
/** Controlled only: the host writes every request's intent back as the value. */
|
|
27
|
+
readonly accepts: boolean;
|
|
28
|
+
/** Uncontrolled active option at mount (the type-ahead model's value). */
|
|
29
|
+
readonly defaultActive: number | undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Scenario `config` -> the fixture mount config, shared by every listbox
|
|
33
|
+
* bridge and every target. Unknown keys (triggers, popups, layouts, value
|
|
34
|
+
* plumbing, ...) throw `Unsupported`, which skips the scenario everywhere.
|
|
35
|
+
*/
|
|
36
|
+
export declare const listboxConfig: (config: ScenarioConfig, defaultMode?: ListboxSelectionMode) => ListboxMountConfig;
|
|
37
|
+
/** `option:3`/`option` -> index; anything else is unsupported. */
|
|
38
|
+
export declare const optionIndexOf: (target: string | undefined) => number;
|
|
39
|
+
export declare const partOf: (target: string | undefined) => string;
|
|
40
|
+
export declare const jsonRecord: (value: Schema.Json | undefined) => Readonly<Record<string, Schema.Json>>;
|
|
41
|
+
export interface KeyModifiers {
|
|
42
|
+
readonly shift: boolean;
|
|
43
|
+
readonly ctrl: boolean;
|
|
44
|
+
readonly meta: boolean;
|
|
45
|
+
readonly alt: boolean;
|
|
46
|
+
}
|
|
47
|
+
export declare const keyModifiers: (value: Schema.Json | undefined) => KeyModifiers;
|
|
48
|
+
/** bit i of a selection mask. */
|
|
49
|
+
export declare const bit: (mask: bigint, index: number) => boolean;
|
|
50
|
+
export declare const maskOf: (state: QuintModelValue) => bigint;
|
|
51
|
+
export declare const selectedIndices: (mask: bigint, count: number) => ReadonlyArray<number>;
|
|
52
|
+
/** Option indices -> a selection bitmask (bit i = option i). */
|
|
53
|
+
export declare const indicesMask: (indices: ReadonlyArray<number>) => number;
|
|
54
|
+
/** Parse a `prop setValue`/`value`/`selected` payload into option indices. */
|
|
55
|
+
export declare const indicesOf: (value: Schema.Json | undefined) => ReadonlyArray<number>;
|
|
56
|
+
/** The last request's intent, as option indices (reads the `requested` mask). */
|
|
57
|
+
export declare const requestedIndices: (state: QuintModelValue, count: number) => ReadonlyArray<number>;
|
|
58
|
+
//#endregion
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { Unsupported } from "../scenario.mjs";
|
|
2
|
+
//#region src/apg/listbox-shared.ts
|
|
3
|
+
const specEvent = (tag, params = {}) => ({
|
|
4
|
+
_tag: tag,
|
|
5
|
+
...params
|
|
6
|
+
});
|
|
7
|
+
const toItems = (value) => {
|
|
8
|
+
if (typeof value === "number") {
|
|
9
|
+
if (!Number.isInteger(value) || value < 0) throw new Unsupported(`items ${String(value)}`);
|
|
10
|
+
return Array.from({ length: value }, (_, i) => String(i));
|
|
11
|
+
}
|
|
12
|
+
if (Array.isArray(value) && value.every((v) => typeof v === "string")) return value;
|
|
13
|
+
throw new Unsupported(`items ${JSON.stringify(value)}`);
|
|
14
|
+
};
|
|
15
|
+
const indexOfLabel = (items, key) => {
|
|
16
|
+
const exact = items.indexOf(key);
|
|
17
|
+
if (exact >= 0) return exact;
|
|
18
|
+
const lower = key.toLowerCase();
|
|
19
|
+
const folded = items.findIndex((item) => item.toLowerCase() === lower);
|
|
20
|
+
if (folded < 0) throw new Unsupported(`key ${key}`);
|
|
21
|
+
return folded;
|
|
22
|
+
};
|
|
23
|
+
const numberArray = (value) => {
|
|
24
|
+
if (Array.isArray(value) && value.every((v) => typeof v === "number" && Number.isInteger(v))) return value;
|
|
25
|
+
throw new Unsupported(`indices ${JSON.stringify(value)}`);
|
|
26
|
+
};
|
|
27
|
+
const keyIndices = (items, value) => {
|
|
28
|
+
if (Array.isArray(value) && value.every((v) => typeof v === "string")) return value.map((key) => indexOfLabel(items, key));
|
|
29
|
+
throw new Unsupported(`keys ${JSON.stringify(value)}`);
|
|
30
|
+
};
|
|
31
|
+
const MODES = /* @__PURE__ */ new Set([
|
|
32
|
+
"single",
|
|
33
|
+
"multiple",
|
|
34
|
+
"none"
|
|
35
|
+
]);
|
|
36
|
+
/**
|
|
37
|
+
* Scenario `config` -> the fixture mount config, shared by every listbox
|
|
38
|
+
* bridge and every target. Unknown keys (triggers, popups, layouts, value
|
|
39
|
+
* plumbing, ...) throw `Unsupported`, which skips the scenario everywhere.
|
|
40
|
+
*/
|
|
41
|
+
const listboxConfig = (config, defaultMode = "single") => {
|
|
42
|
+
let items = [];
|
|
43
|
+
let selectionMode = defaultMode;
|
|
44
|
+
let followsFocus = false;
|
|
45
|
+
let defaultSelected = [];
|
|
46
|
+
let disabled = [];
|
|
47
|
+
let focusOnHover = false;
|
|
48
|
+
let autoFocus;
|
|
49
|
+
let ariaLabel;
|
|
50
|
+
let controlled = false;
|
|
51
|
+
let accepts = false;
|
|
52
|
+
let defaultActive;
|
|
53
|
+
for (const [key, value] of Object.entries(config)) switch (key) {
|
|
54
|
+
case "items":
|
|
55
|
+
items = toItems(value);
|
|
56
|
+
break;
|
|
57
|
+
case "selectionMode":
|
|
58
|
+
if (typeof value !== "string" || !MODES.has(value)) throw new Unsupported(`selectionMode ${JSON.stringify(value)}`);
|
|
59
|
+
selectionMode = value;
|
|
60
|
+
break;
|
|
61
|
+
case "selectionModel":
|
|
62
|
+
if (value !== "recommended") throw new Unsupported(`selectionModel ${JSON.stringify(value)}`);
|
|
63
|
+
break;
|
|
64
|
+
case "followsFocus":
|
|
65
|
+
followsFocus = value === true;
|
|
66
|
+
break;
|
|
67
|
+
case "defaultSelectedKeys":
|
|
68
|
+
defaultSelected = keyIndices(items, value);
|
|
69
|
+
break;
|
|
70
|
+
case "defaultSelected":
|
|
71
|
+
defaultSelected = numberArray(value);
|
|
72
|
+
break;
|
|
73
|
+
case "value":
|
|
74
|
+
case "selected":
|
|
75
|
+
defaultSelected = numberArray(value);
|
|
76
|
+
controlled = true;
|
|
77
|
+
break;
|
|
78
|
+
case "controlled":
|
|
79
|
+
if (typeof value !== "boolean") throw new Unsupported(`controlled ${JSON.stringify(value)}`);
|
|
80
|
+
controlled = value;
|
|
81
|
+
accepts = value;
|
|
82
|
+
break;
|
|
83
|
+
case "accepts":
|
|
84
|
+
if (typeof value !== "boolean") throw new Unsupported(`accepts ${JSON.stringify(value)}`);
|
|
85
|
+
controlled = true;
|
|
86
|
+
accepts = value;
|
|
87
|
+
break;
|
|
88
|
+
case "defaultActive":
|
|
89
|
+
if (typeof value !== "number" || !Number.isInteger(value)) throw new Unsupported(`defaultActive ${JSON.stringify(value)}`);
|
|
90
|
+
defaultActive = value;
|
|
91
|
+
break;
|
|
92
|
+
case "disabledKeys":
|
|
93
|
+
disabled = keyIndices(items, value);
|
|
94
|
+
break;
|
|
95
|
+
case "disabled":
|
|
96
|
+
disabled = numberArray(value);
|
|
97
|
+
break;
|
|
98
|
+
case "shouldFocusOnHover":
|
|
99
|
+
focusOnHover = value === true;
|
|
100
|
+
break;
|
|
101
|
+
case "autoFocus":
|
|
102
|
+
if (value === true || value === "first" || typeof value === "number" && Number.isInteger(value)) autoFocus = value;
|
|
103
|
+
else if (value === false) autoFocus = void 0;
|
|
104
|
+
else throw new Unsupported(`autoFocus ${JSON.stringify(value)}`);
|
|
105
|
+
break;
|
|
106
|
+
case "ariaLabel":
|
|
107
|
+
if (typeof value !== "string") throw new Unsupported(`ariaLabel ${JSON.stringify(value)}`);
|
|
108
|
+
ariaLabel = value;
|
|
109
|
+
break;
|
|
110
|
+
default: throw new Unsupported(`config ${key}=${JSON.stringify(value)}`);
|
|
111
|
+
}
|
|
112
|
+
if (items.length === 0) throw new Unsupported("config has no items");
|
|
113
|
+
return {
|
|
114
|
+
items,
|
|
115
|
+
selectionMode,
|
|
116
|
+
followsFocus,
|
|
117
|
+
defaultSelected,
|
|
118
|
+
disabled,
|
|
119
|
+
focusOnHover,
|
|
120
|
+
autoFocus,
|
|
121
|
+
ariaLabel,
|
|
122
|
+
controlled,
|
|
123
|
+
accepts,
|
|
124
|
+
defaultActive
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
/** `option:3`/`option` -> index; anything else is unsupported. */
|
|
128
|
+
const optionIndexOf = (target) => {
|
|
129
|
+
const part = (target ?? "option").replace(/:0$/, "");
|
|
130
|
+
if (part === "option") return 0;
|
|
131
|
+
const match = /^option:(\d+)$/.exec(part);
|
|
132
|
+
if (match?.[1] === void 0) throw new Unsupported(`target ${target ?? ""}`);
|
|
133
|
+
return Number(match[1]);
|
|
134
|
+
};
|
|
135
|
+
const partOf = (target) => (target ?? "").replace(/:0$/, "");
|
|
136
|
+
const isJsonObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
137
|
+
const jsonRecord = (value) => {
|
|
138
|
+
if (value === void 0 || !isJsonObject(value)) throw new Unsupported(`value ${JSON.stringify(value)}`);
|
|
139
|
+
return value;
|
|
140
|
+
};
|
|
141
|
+
const keyModifiers = (value) => {
|
|
142
|
+
const record = jsonRecord(value);
|
|
143
|
+
return {
|
|
144
|
+
shift: record["shift"] === true,
|
|
145
|
+
ctrl: record["ctrl"] === true,
|
|
146
|
+
meta: record["meta"] === true || record["ctrl"] === true,
|
|
147
|
+
alt: record["alt"] === true
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
/** bit i of a selection mask. */
|
|
151
|
+
const bit = (mask, index) => (mask >> BigInt(index)) % 2n === 1n;
|
|
152
|
+
const maskOf = (state) => {
|
|
153
|
+
const value = state["selected"];
|
|
154
|
+
return typeof value === "bigint" ? value : 0n;
|
|
155
|
+
};
|
|
156
|
+
const selectedIndices = (mask, count) => {
|
|
157
|
+
const out = [];
|
|
158
|
+
for (let i = 0; i < count; i += 1) if (bit(mask, i)) out.push(i);
|
|
159
|
+
return out;
|
|
160
|
+
};
|
|
161
|
+
/** Option indices -> a selection bitmask (bit i = option i). */
|
|
162
|
+
const indicesMask = (indices) => indices.reduce((mask, index) => mask + 2 ** index, 0);
|
|
163
|
+
/** Parse a `prop setValue`/`value`/`selected` payload into option indices. */
|
|
164
|
+
const indicesOf = (value) => numberArray(value);
|
|
165
|
+
/** The last request's intent, as option indices (reads the `requested` mask). */
|
|
166
|
+
const requestedIndices = (state, count) => {
|
|
167
|
+
const value = state["requested"];
|
|
168
|
+
return selectedIndices(typeof value === "bigint" ? value : 0n, count);
|
|
169
|
+
};
|
|
170
|
+
//#endregion
|
|
171
|
+
export { bit, indicesMask, indicesOf, jsonRecord, keyModifiers, listboxConfig, maskOf, optionIndexOf, partOf, requestedIndices, selectedIndices, specEvent };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ActionStep, Probe, ScenarioConfig, SpecBridge } from "../scenario.mjs";
|
|
2
|
+
import { ListboxMountConfig } from "./listbox-shared.mjs";
|
|
3
|
+
//#region src/apg/listbox-single.bridge.d.ts
|
|
4
|
+
/** Scenario `config` -> mount config; only the single-select model is modelled. */
|
|
5
|
+
export declare const listboxSingleConfig: (config: ScenarioConfig) => ListboxMountConfig;
|
|
6
|
+
export declare const listboxSingleBridge: SpecBridge;
|
|
7
|
+
/** The user-level steps a differential walk draws from. */
|
|
8
|
+
export declare const listboxSingleSteps: ReadonlyArray<ActionStep>;
|
|
9
|
+
export declare const listboxSingleProbe: Probe;
|
|
10
|
+
//#endregion
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { Unsupported } from "../scenario.mjs";
|
|
2
|
+
import { bit, indicesMask, indicesOf, jsonRecord, listboxConfig, maskOf, optionIndexOf, partOf, requestedIndices, specEvent } from "./listbox-shared.mjs";
|
|
3
|
+
//#region src/apg/listbox-single.bridge.ts
|
|
4
|
+
/** Scenario `config` -> mount config; only the single-select model is modelled. */
|
|
5
|
+
const listboxSingleConfig = (config) => {
|
|
6
|
+
const mount = listboxConfig(config, "single");
|
|
7
|
+
if (mount.selectionMode !== "single") throw new Unsupported(`selectionMode ${mount.selectionMode}`);
|
|
8
|
+
return mount;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Listbox (single-select) vocabulary <-> `specs/apg/listbox-single.qnt`.
|
|
12
|
+
* Parts: `listbox` (the focus holder / tab stop) and `option:N`. The primitive
|
|
13
|
+
* keeps DOM focus on the active option, so `focus` is the active option's
|
|
14
|
+
* target; `listbox[aria-activedescendant]` cross-checks it.
|
|
15
|
+
*
|
|
16
|
+
* Config the spec does not model (triggers/popups, `open`, `defaultValue`,
|
|
17
|
+
* layouts) throws `Unsupported`, skipping the scenario on every target.
|
|
18
|
+
*/
|
|
19
|
+
const keyEvents = (key) => {
|
|
20
|
+
switch (key) {
|
|
21
|
+
case "ArrowDown": return [specEvent("arrow_down")];
|
|
22
|
+
case "ArrowUp": return [specEvent("arrow_up")];
|
|
23
|
+
case "Home": return [specEvent("home")];
|
|
24
|
+
case "End": return [specEvent("end")];
|
|
25
|
+
case " ":
|
|
26
|
+
case "Space":
|
|
27
|
+
case "Enter": return [specEvent("space")];
|
|
28
|
+
default: throw new Unsupported(`key ${key}`);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const propEvents = (step) => {
|
|
32
|
+
switch (step.prop) {
|
|
33
|
+
case "focus": return [specEvent("focus_in")];
|
|
34
|
+
case "blur": return [specEvent("blur")];
|
|
35
|
+
case "followsFocus":
|
|
36
|
+
case "set_follows_focus": return [specEvent("set_follows_focus", { follow: step.value === true })];
|
|
37
|
+
case "setValue":
|
|
38
|
+
case "value":
|
|
39
|
+
case "selected": return [specEvent("setValue", { v: BigInt(indicesMask(indicesOf(step.value))) })];
|
|
40
|
+
case "hostToggle": return [specEvent("hostToggle", { panel: 0n })];
|
|
41
|
+
case "key": {
|
|
42
|
+
const key = jsonRecord(step.value)["key"];
|
|
43
|
+
if (typeof key !== "string") throw new Unsupported(`prop key ${JSON.stringify(step.value)}`);
|
|
44
|
+
return keyEvents(key);
|
|
45
|
+
}
|
|
46
|
+
default: throw new Unsupported(`prop ${step.prop ?? ""}`);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const listboxSingleBridge = {
|
|
50
|
+
setup: (config) => {
|
|
51
|
+
const mount = listboxSingleConfig(config);
|
|
52
|
+
const events = [];
|
|
53
|
+
if (mount.followsFocus) events.push(specEvent("set_follows_focus", { follow: true }));
|
|
54
|
+
if (mount.controlled) {
|
|
55
|
+
events.push(specEvent("control"));
|
|
56
|
+
if (mount.accepts) events.push(specEvent("acceptRequests"));
|
|
57
|
+
if (mount.defaultSelected.length > 0) events.push(specEvent("setValue", { v: BigInt(indicesMask(mount.defaultSelected)) }));
|
|
58
|
+
} else events.push(specEvent("seed", { index: BigInt(mount.defaultSelected[0] ?? 6) }));
|
|
59
|
+
return events;
|
|
60
|
+
},
|
|
61
|
+
event: (step) => {
|
|
62
|
+
switch (step.event) {
|
|
63
|
+
case "focus": return [specEvent("focus_in")];
|
|
64
|
+
case "blur": return [specEvent("blur")];
|
|
65
|
+
case "click": return [specEvent("click", { index: BigInt(optionIndexOf(step.target)) })];
|
|
66
|
+
case "key": return keyEvents(step.key ?? "");
|
|
67
|
+
case "hover":
|
|
68
|
+
case "unhover": return [];
|
|
69
|
+
case "prop": return propEvents(step);
|
|
70
|
+
default: throw new Unsupported(`event ${step.event}`);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
focus: (state) => state["hasFocus"] === true ? `option:${state["active"]}` : null,
|
|
74
|
+
attr: (state, target, name) => {
|
|
75
|
+
const part = partOf(target);
|
|
76
|
+
if (part === "listbox") {
|
|
77
|
+
if (name === "aria-multiselectable") return "false";
|
|
78
|
+
if (name === "aria-activedescendant") return state["hasFocus"] === true ? `option:${state["active"]}` : null;
|
|
79
|
+
} else if (part.startsWith("option")) {
|
|
80
|
+
if (name === "aria-selected") return bit(maskOf(state), optionIndexOf(target)) ? "true" : "false";
|
|
81
|
+
if (name === "aria-disabled") return null;
|
|
82
|
+
}
|
|
83
|
+
throw new Unsupported(`attribute ${name} on ${target}`);
|
|
84
|
+
},
|
|
85
|
+
visible: () => [],
|
|
86
|
+
events: (before, after) => before["requests"] === after["requests"] ? [] : [["onValueChange", requestedIndices(after, 6).map((index) => String(index))]]
|
|
87
|
+
};
|
|
88
|
+
/** The user-level steps a differential walk draws from. */
|
|
89
|
+
const listboxSingleSteps = [
|
|
90
|
+
{
|
|
91
|
+
event: "prop",
|
|
92
|
+
prop: "focus"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
event: "prop",
|
|
96
|
+
prop: "blur"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
event: "prop",
|
|
100
|
+
prop: "key",
|
|
101
|
+
value: { key: "ArrowDown" }
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
event: "prop",
|
|
105
|
+
prop: "key",
|
|
106
|
+
value: { key: "ArrowUp" }
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
event: "prop",
|
|
110
|
+
prop: "key",
|
|
111
|
+
value: { key: "Home" }
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
event: "prop",
|
|
115
|
+
prop: "key",
|
|
116
|
+
value: { key: "End" }
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
event: "prop",
|
|
120
|
+
prop: "key",
|
|
121
|
+
value: { key: " " }
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
event: "prop",
|
|
125
|
+
prop: "key",
|
|
126
|
+
value: { key: "Enter" }
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
event: "prop",
|
|
130
|
+
prop: "followsFocus",
|
|
131
|
+
value: true
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
event: "prop",
|
|
135
|
+
prop: "followsFocus",
|
|
136
|
+
value: false
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
event: "prop",
|
|
140
|
+
prop: "setValue",
|
|
141
|
+
value: [0]
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
event: "prop",
|
|
145
|
+
prop: "setValue",
|
|
146
|
+
value: [3]
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
event: "prop",
|
|
150
|
+
prop: "setValue",
|
|
151
|
+
value: []
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
event: "prop",
|
|
155
|
+
prop: "hostToggle"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
event: "click",
|
|
159
|
+
target: "option:0"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
event: "click",
|
|
163
|
+
target: "option:1"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
event: "click",
|
|
167
|
+
target: "option:2"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
event: "click",
|
|
171
|
+
target: "option:3"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
event: "click",
|
|
175
|
+
target: "option:4"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
event: "click",
|
|
179
|
+
target: "option:5"
|
|
180
|
+
}
|
|
181
|
+
];
|
|
182
|
+
const listboxSingleProbe = {
|
|
183
|
+
attrs: [
|
|
184
|
+
["listbox", "aria-multiselectable"],
|
|
185
|
+
["listbox", "aria-activedescendant"],
|
|
186
|
+
["option:0", "aria-selected"],
|
|
187
|
+
["option:1", "aria-selected"],
|
|
188
|
+
["option:2", "aria-selected"],
|
|
189
|
+
["option:3", "aria-selected"],
|
|
190
|
+
["option:4", "aria-selected"],
|
|
191
|
+
["option:5", "aria-selected"]
|
|
192
|
+
],
|
|
193
|
+
events: ["onValueChange"]
|
|
194
|
+
};
|
|
195
|
+
//#endregion
|
|
196
|
+
export { listboxSingleBridge, listboxSingleConfig, listboxSingleProbe, listboxSingleSteps };
|