@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,799 @@
|
|
|
1
|
+
// APG Listbox, multi-select — https://www.w3.org/WAI/ARIA/apg/patterns/listbox/
|
|
2
|
+
//
|
|
3
|
+
// Sections modelled:
|
|
4
|
+
// - "Keyboard Interaction" -> "Multiple Selection": BOTH interaction models.
|
|
5
|
+
// recommended no modifier needed: Space toggles; Shift+Down/Up move focus
|
|
6
|
+
// and toggle the entered option; Shift+Space selects the
|
|
7
|
+
// contiguous run from the most recently selected option to the
|
|
8
|
+
// focused one; Ctrl+Shift+Home/End select to the first/last
|
|
9
|
+
// option and move focus there; Ctrl+A selects all (and, as in
|
|
10
|
+
// the rearrangeable example, unselects all when all are
|
|
11
|
+
// selected).
|
|
12
|
+
// alternative plain Up/Down/Home/End move focus AND collapse the selection
|
|
13
|
+
// to the focused option; Ctrl+Down/Up move focus only;
|
|
14
|
+
// Ctrl+Space toggles; Shift+Down/Up, Shift+Space,
|
|
15
|
+
// Ctrl+Shift+Home/End and Ctrl+A as above.
|
|
16
|
+
// and the multi-select focus rule: on entry focus the first selected option,
|
|
17
|
+
// else the first option, with no selection change.
|
|
18
|
+
// - "WAI-ARIA Roles, States, and Properties": aria-multiselectable=true,
|
|
19
|
+
// aria-selected on each option, aria-activedescendant.
|
|
20
|
+
// - Note 1 of the keyboard section: focus is distinct from selection.
|
|
21
|
+
// - Example listbox-rearrangeable, example 2 (the multi-select side).
|
|
22
|
+
//
|
|
23
|
+
// State: focus, the active option, the selection as a bitmask over five options,
|
|
24
|
+
// the selection anchor (the most recently selected option, the start of a
|
|
25
|
+
// Shift+Space run), the selection model (a prop), and lastOp — the kind of the
|
|
26
|
+
// last handled operation, which lets invariants state per-operation guarantees
|
|
27
|
+
// ("after a range select the run is selected") without a history.
|
|
28
|
+
//
|
|
29
|
+
// Request vs commit (map ticket rt1p). Every event a user would see as a
|
|
30
|
+
// selection change — the plain moves in the alternative model, Space/Ctrl+Space,
|
|
31
|
+
// the Shift chords, Ctrl+Shift+Home/End, Ctrl+A and click — is a *request*
|
|
32
|
+
// carrying the intended selection mask. It fires onValueChange (`requests`
|
|
33
|
+
// counts the calls) when the intent differs from the current selection, and the
|
|
34
|
+
// selection commits through `landed(holds, selected, intent)`:
|
|
35
|
+
// uncontrolled accepted: the selection commits the intent
|
|
36
|
+
// controlled, holds rejected: the selection is untouched
|
|
37
|
+
// controlled, accepts accepted through the controlled value (hostWrites)
|
|
38
|
+
// The controlled value is `valueProp0..valueProp4` (one bool per option), the
|
|
39
|
+
// only thing that changes the selection while `controlled`; `setValue` /
|
|
40
|
+
// `hostToggle` are the host writing it. `control` / `acceptRequests` are
|
|
41
|
+
// configuration events (init is literal-only). active focus stays
|
|
42
|
+
// widget-internal, so a rejected request still moves it.
|
|
43
|
+
//
|
|
44
|
+
// The per-operation invariants (AnchorSelectedOrNone, the alternative collapse
|
|
45
|
+
// rules, the range rules) are scoped to the uncontrolled path: a controlled host
|
|
46
|
+
// owns the value and may hand the widget any set, so its anchor/lastOp bookkeeping
|
|
47
|
+
// is not constrained.
|
|
48
|
+
//
|
|
49
|
+
// Choices where the pattern leaves a gap (ponytail):
|
|
50
|
+
// - in the recommended model Ctrl+Arrow and Ctrl+Space are not handled; in the
|
|
51
|
+
// alternative model plain Space is not handled (the pattern lists only
|
|
52
|
+
// Ctrl+Space there)
|
|
53
|
+
// - Shift+Arrow at the first/last option does nothing (no move, no toggle)
|
|
54
|
+
// - a pointer click toggles in the recommended model and selects only the
|
|
55
|
+
// clicked option in the alternative model (the example's click handler
|
|
56
|
+
// toggles; the alternative model's "selection collapses" rule is applied)
|
|
57
|
+
// - the anchor is the last option toggled ON; deselecting the anchor clears it,
|
|
58
|
+
// so the anchor is always a selected option or "none"
|
|
59
|
+
// - Ctrl+Shift+Home/End also move focus to the first/last option (the
|
|
60
|
+
// pattern's optional part)
|
|
61
|
+
//
|
|
62
|
+
// Deliberate omissions: typeahead (listbox-typeahead.qnt), rearranging
|
|
63
|
+
// (listbox-rearrangeable.qnt), scrolling, horizontal orientation, aria-checked,
|
|
64
|
+
// Shift+click and Ctrl+click ranges, labels, disabled options, wrap-around.
|
|
65
|
+
//
|
|
66
|
+
// Generator notes: no parameterized actions and no `let`, so a request spells
|
|
67
|
+
// `landed(...)` out per assignment and every action assigns every var.
|
|
68
|
+
module apg_listbox_multi {
|
|
69
|
+
var hasFocus: bool // the listbox is the focused element
|
|
70
|
+
var active: int // the active option (aria-activedescendant target)
|
|
71
|
+
var selected: int // selection bitmask over the options (bit i = option i)
|
|
72
|
+
var anchor: int // most recently selected option (noOption = none)
|
|
73
|
+
var selectionModel: str // prop: "recommended" | "alternative"
|
|
74
|
+
var lastOp: str // kind of the last handled operation
|
|
75
|
+
var controlled: bool // the host owns the value (`value` prop present)
|
|
76
|
+
var accepting: bool // controlled host that answers every request with its intent
|
|
77
|
+
var valueProp0: bool // the controlled value prop, one bool per option
|
|
78
|
+
var valueProp1: bool
|
|
79
|
+
var valueProp2: bool
|
|
80
|
+
var valueProp3: bool
|
|
81
|
+
var valueProp4: bool
|
|
82
|
+
var requested: int // intent of the last request (selection mask)
|
|
83
|
+
var outcome: str // "none" | "accepted" | "rejected"
|
|
84
|
+
var base: int // selection when the last request was received
|
|
85
|
+
var requests: int // number of requests that fired onValueChange
|
|
86
|
+
|
|
87
|
+
// five options
|
|
88
|
+
def optionCount: int = 5
|
|
89
|
+
def lastIndex: int = 4
|
|
90
|
+
def noOption: int = 5
|
|
91
|
+
|
|
92
|
+
def pow2(i: int): int =
|
|
93
|
+
if (i == 0) 1
|
|
94
|
+
else if (i == 1) 2
|
|
95
|
+
else if (i == 2) 4
|
|
96
|
+
else if (i == 3) 8
|
|
97
|
+
else if (i == 4) 16
|
|
98
|
+
else 32
|
|
99
|
+
// bit i of a selection mask: option i is selected
|
|
100
|
+
def bit(mask: int, i: int): bool = (mask / pow2(i)) % 2 == 1
|
|
101
|
+
def popcount(mask: int): int =
|
|
102
|
+
(if (bit(mask, 0)) 1 else 0)
|
|
103
|
+
+ (if (bit(mask, 1)) 1 else 0)
|
|
104
|
+
+ (if (bit(mask, 2)) 1 else 0)
|
|
105
|
+
+ (if (bit(mask, 3)) 1 else 0)
|
|
106
|
+
+ (if (bit(mask, 4)) 1 else 0)
|
|
107
|
+
def firstSelected(mask: int): int =
|
|
108
|
+
if (bit(mask, 0)) 0
|
|
109
|
+
else if (bit(mask, 1)) 1
|
|
110
|
+
else if (bit(mask, 2)) 2
|
|
111
|
+
else if (bit(mask, 3)) 3
|
|
112
|
+
else if (bit(mask, 4)) 4
|
|
113
|
+
else noOption
|
|
114
|
+
// Tab entry: the first selected option, else the first option
|
|
115
|
+
def entryIndex(mask: int): int = if (mask == 0) 0 else firstSelected(mask)
|
|
116
|
+
def nextIndex(i: int): int = if (i < lastIndex) i + 1 else i
|
|
117
|
+
def prevIndex(i: int): int = if (i > 0) i - 1 else i
|
|
118
|
+
def allMask: int = pow2(optionCount) - 1
|
|
119
|
+
def isAlt(model: str): bool = model == "alternative"
|
|
120
|
+
|
|
121
|
+
// --- mask arithmetic (the subset has no bitwise operators) ---------------
|
|
122
|
+
def bitVal(mask: int, i: int): int = if (bit(mask, i)) pow2(i) else 0
|
|
123
|
+
def orMask(a: int, b: int): int =
|
|
124
|
+
(if (or(bit(a, 0), bit(b, 0))) 1 else 0)
|
|
125
|
+
+ (if (or(bit(a, 1), bit(b, 1))) 2 else 0)
|
|
126
|
+
+ (if (or(bit(a, 2), bit(b, 2))) 4 else 0)
|
|
127
|
+
+ (if (or(bit(a, 3), bit(b, 3))) 8 else 0)
|
|
128
|
+
+ (if (or(bit(a, 4), bit(b, 4))) 16 else 0)
|
|
129
|
+
def toggle(mask: int, i: int): int =
|
|
130
|
+
if (bit(mask, i)) mask - pow2(i) else mask + pow2(i)
|
|
131
|
+
def loOf(a: int, b: int): int = if (a < b) a else b
|
|
132
|
+
def hiOf(a: int, b: int): int = if (a < b) b else a
|
|
133
|
+
// options lo..hi inclusive
|
|
134
|
+
def rangeMask(lo: int, hi: int): int =
|
|
135
|
+
if (lo > hi) 0 else pow2(hi + 1) - pow2(lo)
|
|
136
|
+
|
|
137
|
+
// --- selection rules -----------------------------------------------------
|
|
138
|
+
// the anchor after toggling option i (mask is the mask BEFORE the toggle)
|
|
139
|
+
def anchorAfterToggle(mask: int, anch: int, i: int): int =
|
|
140
|
+
if (bit(mask, i)) (if (anch == i) noOption else anch) else i
|
|
141
|
+
// alternative model: a plain move collapses the selection to the target
|
|
142
|
+
def selectionAfterPlainMove(model: str, mask: int, target: int): int =
|
|
143
|
+
if (isAlt(model)) pow2(target) else mask
|
|
144
|
+
def anchorAfterPlainMove(model: str, anch: int, target: int): int =
|
|
145
|
+
if (isAlt(model)) target else anch
|
|
146
|
+
def runStart(anch: int, act: int): int = if (anch == noOption) act else anch
|
|
147
|
+
|
|
148
|
+
// --- request / commit (map ticket rt1p) ---------------------------------
|
|
149
|
+
def b2i(b: bool): int = if (b) 1 else 0
|
|
150
|
+
// the controlled value prop, packed into a mask
|
|
151
|
+
def propsMask: int =
|
|
152
|
+
b2i(valueProp0)
|
|
153
|
+
+ 2 * b2i(valueProp1)
|
|
154
|
+
+ 4 * b2i(valueProp2)
|
|
155
|
+
+ 8 * b2i(valueProp3)
|
|
156
|
+
+ 16 * b2i(valueProp4)
|
|
157
|
+
// the host flips one selected option of its own copy of the value
|
|
158
|
+
def flipAt(n: int, i: int): int = if (bit(n, i)) n - pow2(i) else n + pow2(i)
|
|
159
|
+
// controlled host that keeps the old value: the request is rejected
|
|
160
|
+
def holds: bool = and(controlled, not(accepting))
|
|
161
|
+
// controlled host that writes the intent back through the value prop
|
|
162
|
+
def hostWrites: bool = and(controlled, accepting)
|
|
163
|
+
def verdict: str = if (holds) "rejected" else "accepted"
|
|
164
|
+
// the value after a request for `want`
|
|
165
|
+
def landed(h: bool, current: int, want: int): int = if (h) current else want
|
|
166
|
+
// one value-prop member after a request for `want`
|
|
167
|
+
def propBit(writes: bool, current: bool, want: int, i: int): bool =
|
|
168
|
+
if (writes) bit(want, i) else current
|
|
169
|
+
// the intended selection of each request
|
|
170
|
+
def downIntent: int =
|
|
171
|
+
if (hasFocus) selectionAfterPlainMove(selectionModel, selected, nextIndex(active)) else selected
|
|
172
|
+
def upIntent: int =
|
|
173
|
+
if (hasFocus) selectionAfterPlainMove(selectionModel, selected, prevIndex(active)) else selected
|
|
174
|
+
def homeIntent: int =
|
|
175
|
+
if (hasFocus) selectionAfterPlainMove(selectionModel, selected, 0) else selected
|
|
176
|
+
def endIntent: int =
|
|
177
|
+
if (hasFocus) selectionAfterPlainMove(selectionModel, selected, lastIndex) else selected
|
|
178
|
+
def spaceIntent: int =
|
|
179
|
+
if (and(hasFocus, not(isAlt(selectionModel)))) toggle(selected, active) else selected
|
|
180
|
+
def ctrlSpaceIntent: int =
|
|
181
|
+
if (and(hasFocus, isAlt(selectionModel))) toggle(selected, active) else selected
|
|
182
|
+
def shiftDownIntent: int =
|
|
183
|
+
if (and(hasFocus, nextIndex(active) != active)) toggle(selected, nextIndex(active)) else selected
|
|
184
|
+
def shiftUpIntent: int =
|
|
185
|
+
if (and(hasFocus, prevIndex(active) != active)) toggle(selected, prevIndex(active)) else selected
|
|
186
|
+
def shiftSpaceIntent: int =
|
|
187
|
+
if (hasFocus) orMask(selected, rangeMask(loOf(runStart(anchor, active), active), hiOf(runStart(anchor, active), active))) else selected
|
|
188
|
+
def ctrlShiftHomeIntent: int =
|
|
189
|
+
if (hasFocus) orMask(selected, rangeMask(0, active)) else selected
|
|
190
|
+
def ctrlShiftEndIntent: int =
|
|
191
|
+
if (hasFocus) orMask(selected, rangeMask(active, lastIndex)) else selected
|
|
192
|
+
def ctrlAIntent: int =
|
|
193
|
+
if (hasFocus) (if (selected == allMask) 0 else allMask) else selected
|
|
194
|
+
def clickIntent(index: int): int =
|
|
195
|
+
if (isAlt(selectionModel)) pow2(index) else toggle(selected, index)
|
|
196
|
+
|
|
197
|
+
// --- ARIA contract, derived from the state ------------------------------
|
|
198
|
+
def ariaMultiselectable: bool = true
|
|
199
|
+
def ariaActiveDescendant(focused: bool, idx: int): int =
|
|
200
|
+
if (focused) idx else noOption
|
|
201
|
+
def ariaSelected(mask: int, i: int): bool = bit(mask, i)
|
|
202
|
+
def listboxTabIndex: int = 0
|
|
203
|
+
|
|
204
|
+
action init = all {
|
|
205
|
+
hasFocus' = false,
|
|
206
|
+
active' = 0,
|
|
207
|
+
selected' = 0,
|
|
208
|
+
anchor' = noOption,
|
|
209
|
+
selectionModel' = "recommended",
|
|
210
|
+
lastOp' = "none",
|
|
211
|
+
controlled' = false,
|
|
212
|
+
accepting' = false,
|
|
213
|
+
valueProp0' = false,
|
|
214
|
+
valueProp1' = false,
|
|
215
|
+
valueProp2' = false,
|
|
216
|
+
valueProp3' = false,
|
|
217
|
+
valueProp4' = false,
|
|
218
|
+
requested' = 0,
|
|
219
|
+
outcome' = "none",
|
|
220
|
+
base' = 0,
|
|
221
|
+
requests' = 0
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// mount: the uncontrolled initial selection (the host's `defaultValue`),
|
|
225
|
+
// active at the first option and no anchor, exactly as the primitive mounts
|
|
226
|
+
action seed = {
|
|
227
|
+
nondet v = Set(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31).oneOf()
|
|
228
|
+
all {
|
|
229
|
+
hasFocus' = false,
|
|
230
|
+
active' = 0,
|
|
231
|
+
selected' = v,
|
|
232
|
+
anchor' = noOption,
|
|
233
|
+
selectionModel' = selectionModel,
|
|
234
|
+
lastOp' = "none",
|
|
235
|
+
controlled' = false,
|
|
236
|
+
accepting' = false,
|
|
237
|
+
valueProp0' = false,
|
|
238
|
+
valueProp1' = false,
|
|
239
|
+
valueProp2' = false,
|
|
240
|
+
valueProp3' = false,
|
|
241
|
+
valueProp4' = false,
|
|
242
|
+
requested' = 0,
|
|
243
|
+
outcome' = "none",
|
|
244
|
+
base' = 0,
|
|
245
|
+
requests' = 0
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Tab into the listbox: focus the first selected option, else the first.
|
|
250
|
+
// No selection change; already focused is a no-op.
|
|
251
|
+
action focus_in = all {
|
|
252
|
+
hasFocus' = true,
|
|
253
|
+
active' = if (hasFocus) active else entryIndex(selected),
|
|
254
|
+
selected' = selected,
|
|
255
|
+
anchor' = anchor,
|
|
256
|
+
selectionModel' = selectionModel,
|
|
257
|
+
lastOp' = if (hasFocus) lastOp else "focus",
|
|
258
|
+
controlled' = controlled,
|
|
259
|
+
accepting' = accepting,
|
|
260
|
+
valueProp0' = valueProp0,
|
|
261
|
+
valueProp1' = valueProp1,
|
|
262
|
+
valueProp2' = valueProp2,
|
|
263
|
+
valueProp3' = valueProp3,
|
|
264
|
+
valueProp4' = valueProp4,
|
|
265
|
+
requested' = requested,
|
|
266
|
+
outcome' = outcome,
|
|
267
|
+
base' = base,
|
|
268
|
+
requests' = requests
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Tab out: the selection is kept.
|
|
272
|
+
action blur = all {
|
|
273
|
+
hasFocus' = false,
|
|
274
|
+
active' = active,
|
|
275
|
+
selected' = selected,
|
|
276
|
+
anchor' = anchor,
|
|
277
|
+
selectionModel' = selectionModel,
|
|
278
|
+
lastOp' = "blur",
|
|
279
|
+
controlled' = controlled,
|
|
280
|
+
accepting' = accepting,
|
|
281
|
+
valueProp0' = valueProp0,
|
|
282
|
+
valueProp1' = valueProp1,
|
|
283
|
+
valueProp2' = valueProp2,
|
|
284
|
+
valueProp3' = valueProp3,
|
|
285
|
+
valueProp4' = valueProp4,
|
|
286
|
+
requested' = requested,
|
|
287
|
+
outcome' = outcome,
|
|
288
|
+
base' = base,
|
|
289
|
+
requests' = requests
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Plain moves: focus only (recommended) / focus + collapse the selection
|
|
293
|
+
// to the target (alternative).
|
|
294
|
+
action arrow_down = all {
|
|
295
|
+
hasFocus' = hasFocus,
|
|
296
|
+
active' = if (hasFocus) nextIndex(active) else active,
|
|
297
|
+
selected' = landed(holds, selected, downIntent),
|
|
298
|
+
anchor' = if (hasFocus) anchorAfterPlainMove(selectionModel, anchor, nextIndex(active)) else anchor,
|
|
299
|
+
selectionModel' = selectionModel,
|
|
300
|
+
lastOp' = if (hasFocus) "move" else lastOp,
|
|
301
|
+
controlled' = controlled,
|
|
302
|
+
accepting' = accepting,
|
|
303
|
+
valueProp0' = propBit(hostWrites, valueProp0, downIntent, 0),
|
|
304
|
+
valueProp1' = propBit(hostWrites, valueProp1, downIntent, 1),
|
|
305
|
+
valueProp2' = propBit(hostWrites, valueProp2, downIntent, 2),
|
|
306
|
+
valueProp3' = propBit(hostWrites, valueProp3, downIntent, 3),
|
|
307
|
+
valueProp4' = propBit(hostWrites, valueProp4, downIntent, 4),
|
|
308
|
+
requested' = if (downIntent != selected) downIntent else requested,
|
|
309
|
+
outcome' = if (downIntent != selected) verdict else outcome,
|
|
310
|
+
base' = if (downIntent != selected) selected else base,
|
|
311
|
+
requests' = if (downIntent != selected) requests + 1 else requests
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
action arrow_up = all {
|
|
315
|
+
hasFocus' = hasFocus,
|
|
316
|
+
active' = if (hasFocus) prevIndex(active) else active,
|
|
317
|
+
selected' = landed(holds, selected, upIntent),
|
|
318
|
+
anchor' = if (hasFocus) anchorAfterPlainMove(selectionModel, anchor, prevIndex(active)) else anchor,
|
|
319
|
+
selectionModel' = selectionModel,
|
|
320
|
+
lastOp' = if (hasFocus) "move" else lastOp,
|
|
321
|
+
controlled' = controlled,
|
|
322
|
+
accepting' = accepting,
|
|
323
|
+
valueProp0' = propBit(hostWrites, valueProp0, upIntent, 0),
|
|
324
|
+
valueProp1' = propBit(hostWrites, valueProp1, upIntent, 1),
|
|
325
|
+
valueProp2' = propBit(hostWrites, valueProp2, upIntent, 2),
|
|
326
|
+
valueProp3' = propBit(hostWrites, valueProp3, upIntent, 3),
|
|
327
|
+
valueProp4' = propBit(hostWrites, valueProp4, upIntent, 4),
|
|
328
|
+
requested' = if (upIntent != selected) upIntent else requested,
|
|
329
|
+
outcome' = if (upIntent != selected) verdict else outcome,
|
|
330
|
+
base' = if (upIntent != selected) selected else base,
|
|
331
|
+
requests' = if (upIntent != selected) requests + 1 else requests
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
action home = all {
|
|
335
|
+
hasFocus' = hasFocus,
|
|
336
|
+
active' = if (hasFocus) 0 else active,
|
|
337
|
+
selected' = landed(holds, selected, homeIntent),
|
|
338
|
+
anchor' = if (hasFocus) anchorAfterPlainMove(selectionModel, anchor, 0) else anchor,
|
|
339
|
+
selectionModel' = selectionModel,
|
|
340
|
+
lastOp' = if (hasFocus) "move" else lastOp,
|
|
341
|
+
controlled' = controlled,
|
|
342
|
+
accepting' = accepting,
|
|
343
|
+
valueProp0' = propBit(hostWrites, valueProp0, homeIntent, 0),
|
|
344
|
+
valueProp1' = propBit(hostWrites, valueProp1, homeIntent, 1),
|
|
345
|
+
valueProp2' = propBit(hostWrites, valueProp2, homeIntent, 2),
|
|
346
|
+
valueProp3' = propBit(hostWrites, valueProp3, homeIntent, 3),
|
|
347
|
+
valueProp4' = propBit(hostWrites, valueProp4, homeIntent, 4),
|
|
348
|
+
requested' = if (homeIntent != selected) homeIntent else requested,
|
|
349
|
+
outcome' = if (homeIntent != selected) verdict else outcome,
|
|
350
|
+
base' = if (homeIntent != selected) selected else base,
|
|
351
|
+
requests' = if (homeIntent != selected) requests + 1 else requests
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
action end = all {
|
|
355
|
+
hasFocus' = hasFocus,
|
|
356
|
+
active' = if (hasFocus) lastIndex else active,
|
|
357
|
+
selected' = landed(holds, selected, endIntent),
|
|
358
|
+
anchor' = if (hasFocus) anchorAfterPlainMove(selectionModel, anchor, lastIndex) else anchor,
|
|
359
|
+
selectionModel' = selectionModel,
|
|
360
|
+
lastOp' = if (hasFocus) "move" else lastOp,
|
|
361
|
+
controlled' = controlled,
|
|
362
|
+
accepting' = accepting,
|
|
363
|
+
valueProp0' = propBit(hostWrites, valueProp0, endIntent, 0),
|
|
364
|
+
valueProp1' = propBit(hostWrites, valueProp1, endIntent, 1),
|
|
365
|
+
valueProp2' = propBit(hostWrites, valueProp2, endIntent, 2),
|
|
366
|
+
valueProp3' = propBit(hostWrites, valueProp3, endIntent, 3),
|
|
367
|
+
valueProp4' = propBit(hostWrites, valueProp4, endIntent, 4),
|
|
368
|
+
requested' = if (endIntent != selected) endIntent else requested,
|
|
369
|
+
outcome' = if (endIntent != selected) verdict else outcome,
|
|
370
|
+
base' = if (endIntent != selected) selected else base,
|
|
371
|
+
requests' = if (endIntent != selected) requests + 1 else requests
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Recommended model: Space toggles the active option.
|
|
375
|
+
action space = all {
|
|
376
|
+
hasFocus' = hasFocus,
|
|
377
|
+
active' = active,
|
|
378
|
+
selected' = landed(holds, selected, spaceIntent),
|
|
379
|
+
anchor' = if (and(hasFocus, not(isAlt(selectionModel)))) anchorAfterToggle(selected, anchor, active) else anchor,
|
|
380
|
+
selectionModel' = selectionModel,
|
|
381
|
+
lastOp' = if (and(hasFocus, not(isAlt(selectionModel)))) "toggle" else lastOp,
|
|
382
|
+
controlled' = controlled,
|
|
383
|
+
accepting' = accepting,
|
|
384
|
+
valueProp0' = propBit(hostWrites, valueProp0, spaceIntent, 0),
|
|
385
|
+
valueProp1' = propBit(hostWrites, valueProp1, spaceIntent, 1),
|
|
386
|
+
valueProp2' = propBit(hostWrites, valueProp2, spaceIntent, 2),
|
|
387
|
+
valueProp3' = propBit(hostWrites, valueProp3, spaceIntent, 3),
|
|
388
|
+
valueProp4' = propBit(hostWrites, valueProp4, spaceIntent, 4),
|
|
389
|
+
requested' = if (spaceIntent != selected) spaceIntent else requested,
|
|
390
|
+
outcome' = if (spaceIntent != selected) verdict else outcome,
|
|
391
|
+
base' = if (spaceIntent != selected) selected else base,
|
|
392
|
+
requests' = if (spaceIntent != selected) requests + 1 else requests
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Alternative model: Ctrl+Space toggles the active option.
|
|
396
|
+
action ctrl_space = all {
|
|
397
|
+
hasFocus' = hasFocus,
|
|
398
|
+
active' = active,
|
|
399
|
+
selected' = landed(holds, selected, ctrlSpaceIntent),
|
|
400
|
+
anchor' = if (and(hasFocus, isAlt(selectionModel))) anchorAfterToggle(selected, anchor, active) else anchor,
|
|
401
|
+
selectionModel' = selectionModel,
|
|
402
|
+
lastOp' = if (and(hasFocus, isAlt(selectionModel))) "toggle" else lastOp,
|
|
403
|
+
controlled' = controlled,
|
|
404
|
+
accepting' = accepting,
|
|
405
|
+
valueProp0' = propBit(hostWrites, valueProp0, ctrlSpaceIntent, 0),
|
|
406
|
+
valueProp1' = propBit(hostWrites, valueProp1, ctrlSpaceIntent, 1),
|
|
407
|
+
valueProp2' = propBit(hostWrites, valueProp2, ctrlSpaceIntent, 2),
|
|
408
|
+
valueProp3' = propBit(hostWrites, valueProp3, ctrlSpaceIntent, 3),
|
|
409
|
+
valueProp4' = propBit(hostWrites, valueProp4, ctrlSpaceIntent, 4),
|
|
410
|
+
requested' = if (ctrlSpaceIntent != selected) ctrlSpaceIntent else requested,
|
|
411
|
+
outcome' = if (ctrlSpaceIntent != selected) verdict else outcome,
|
|
412
|
+
base' = if (ctrlSpaceIntent != selected) selected else base,
|
|
413
|
+
requests' = if (ctrlSpaceIntent != selected) requests + 1 else requests
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Shift+Arrow (both models): move focus to and toggle the entered option.
|
|
417
|
+
action shift_arrow_down = all {
|
|
418
|
+
hasFocus' = hasFocus,
|
|
419
|
+
active' = if (and(hasFocus, nextIndex(active) != active)) nextIndex(active) else active,
|
|
420
|
+
selected' = landed(holds, selected, shiftDownIntent),
|
|
421
|
+
anchor' = if (and(hasFocus, nextIndex(active) != active)) anchorAfterToggle(selected, anchor, nextIndex(active)) else anchor,
|
|
422
|
+
selectionModel' = selectionModel,
|
|
423
|
+
lastOp' = if (and(hasFocus, nextIndex(active) != active)) "toggle" else lastOp,
|
|
424
|
+
controlled' = controlled,
|
|
425
|
+
accepting' = accepting,
|
|
426
|
+
valueProp0' = propBit(hostWrites, valueProp0, shiftDownIntent, 0),
|
|
427
|
+
valueProp1' = propBit(hostWrites, valueProp1, shiftDownIntent, 1),
|
|
428
|
+
valueProp2' = propBit(hostWrites, valueProp2, shiftDownIntent, 2),
|
|
429
|
+
valueProp3' = propBit(hostWrites, valueProp3, shiftDownIntent, 3),
|
|
430
|
+
valueProp4' = propBit(hostWrites, valueProp4, shiftDownIntent, 4),
|
|
431
|
+
requested' = if (shiftDownIntent != selected) shiftDownIntent else requested,
|
|
432
|
+
outcome' = if (shiftDownIntent != selected) verdict else outcome,
|
|
433
|
+
base' = if (shiftDownIntent != selected) selected else base,
|
|
434
|
+
requests' = if (shiftDownIntent != selected) requests + 1 else requests
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
action shift_arrow_up = all {
|
|
438
|
+
hasFocus' = hasFocus,
|
|
439
|
+
active' = if (and(hasFocus, prevIndex(active) != active)) prevIndex(active) else active,
|
|
440
|
+
selected' = landed(holds, selected, shiftUpIntent),
|
|
441
|
+
anchor' = if (and(hasFocus, prevIndex(active) != active)) anchorAfterToggle(selected, anchor, prevIndex(active)) else anchor,
|
|
442
|
+
selectionModel' = selectionModel,
|
|
443
|
+
lastOp' = if (and(hasFocus, prevIndex(active) != active)) "toggle" else lastOp,
|
|
444
|
+
controlled' = controlled,
|
|
445
|
+
accepting' = accepting,
|
|
446
|
+
valueProp0' = propBit(hostWrites, valueProp0, shiftUpIntent, 0),
|
|
447
|
+
valueProp1' = propBit(hostWrites, valueProp1, shiftUpIntent, 1),
|
|
448
|
+
valueProp2' = propBit(hostWrites, valueProp2, shiftUpIntent, 2),
|
|
449
|
+
valueProp3' = propBit(hostWrites, valueProp3, shiftUpIntent, 3),
|
|
450
|
+
valueProp4' = propBit(hostWrites, valueProp4, shiftUpIntent, 4),
|
|
451
|
+
requested' = if (shiftUpIntent != selected) shiftUpIntent else requested,
|
|
452
|
+
outcome' = if (shiftUpIntent != selected) verdict else outcome,
|
|
453
|
+
base' = if (shiftUpIntent != selected) selected else base,
|
|
454
|
+
requests' = if (shiftUpIntent != selected) requests + 1 else requests
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Alternative model: Ctrl+Arrow moves focus without touching the selection.
|
|
458
|
+
action ctrl_arrow_down = all {
|
|
459
|
+
hasFocus' = hasFocus,
|
|
460
|
+
active' = if (and(hasFocus, isAlt(selectionModel))) nextIndex(active) else active,
|
|
461
|
+
selected' = selected,
|
|
462
|
+
anchor' = anchor,
|
|
463
|
+
selectionModel' = selectionModel,
|
|
464
|
+
lastOp' = if (and(hasFocus, isAlt(selectionModel))) "ctrl_move" else lastOp,
|
|
465
|
+
controlled' = controlled,
|
|
466
|
+
accepting' = accepting,
|
|
467
|
+
valueProp0' = valueProp0,
|
|
468
|
+
valueProp1' = valueProp1,
|
|
469
|
+
valueProp2' = valueProp2,
|
|
470
|
+
valueProp3' = valueProp3,
|
|
471
|
+
valueProp4' = valueProp4,
|
|
472
|
+
requested' = requested,
|
|
473
|
+
outcome' = outcome,
|
|
474
|
+
base' = base,
|
|
475
|
+
requests' = requests
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
action ctrl_arrow_up = all {
|
|
479
|
+
hasFocus' = hasFocus,
|
|
480
|
+
active' = if (and(hasFocus, isAlt(selectionModel))) prevIndex(active) else active,
|
|
481
|
+
selected' = selected,
|
|
482
|
+
anchor' = anchor,
|
|
483
|
+
selectionModel' = selectionModel,
|
|
484
|
+
lastOp' = if (and(hasFocus, isAlt(selectionModel))) "ctrl_move" else lastOp,
|
|
485
|
+
controlled' = controlled,
|
|
486
|
+
accepting' = accepting,
|
|
487
|
+
valueProp0' = valueProp0,
|
|
488
|
+
valueProp1' = valueProp1,
|
|
489
|
+
valueProp2' = valueProp2,
|
|
490
|
+
valueProp3' = valueProp3,
|
|
491
|
+
valueProp4' = valueProp4,
|
|
492
|
+
requested' = requested,
|
|
493
|
+
outcome' = outcome,
|
|
494
|
+
base' = base,
|
|
495
|
+
requests' = requests
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Select the contiguous run from the anchor to the active option (the
|
|
499
|
+
// anchor starts as the active option when there is none).
|
|
500
|
+
action shift_space = all {
|
|
501
|
+
hasFocus' = hasFocus,
|
|
502
|
+
active' = active,
|
|
503
|
+
selected' = landed(holds, selected, shiftSpaceIntent),
|
|
504
|
+
anchor' = if (hasFocus) runStart(anchor, active) else anchor,
|
|
505
|
+
selectionModel' = selectionModel,
|
|
506
|
+
lastOp' = if (hasFocus) "range" else lastOp,
|
|
507
|
+
controlled' = controlled,
|
|
508
|
+
accepting' = accepting,
|
|
509
|
+
valueProp0' = propBit(hostWrites, valueProp0, shiftSpaceIntent, 0),
|
|
510
|
+
valueProp1' = propBit(hostWrites, valueProp1, shiftSpaceIntent, 1),
|
|
511
|
+
valueProp2' = propBit(hostWrites, valueProp2, shiftSpaceIntent, 2),
|
|
512
|
+
valueProp3' = propBit(hostWrites, valueProp3, shiftSpaceIntent, 3),
|
|
513
|
+
valueProp4' = propBit(hostWrites, valueProp4, shiftSpaceIntent, 4),
|
|
514
|
+
requested' = if (shiftSpaceIntent != selected) shiftSpaceIntent else requested,
|
|
515
|
+
outcome' = if (shiftSpaceIntent != selected) verdict else outcome,
|
|
516
|
+
base' = if (shiftSpaceIntent != selected) selected else base,
|
|
517
|
+
requests' = if (shiftSpaceIntent != selected) requests + 1 else requests
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Select the active option and everything above it; focus moves to the first.
|
|
521
|
+
action ctrl_shift_home = all {
|
|
522
|
+
hasFocus' = hasFocus,
|
|
523
|
+
active' = if (hasFocus) 0 else active,
|
|
524
|
+
selected' = landed(holds, selected, ctrlShiftHomeIntent),
|
|
525
|
+
anchor' = anchor,
|
|
526
|
+
selectionModel' = selectionModel,
|
|
527
|
+
lastOp' = if (hasFocus) "range_home" else lastOp,
|
|
528
|
+
controlled' = controlled,
|
|
529
|
+
accepting' = accepting,
|
|
530
|
+
valueProp0' = propBit(hostWrites, valueProp0, ctrlShiftHomeIntent, 0),
|
|
531
|
+
valueProp1' = propBit(hostWrites, valueProp1, ctrlShiftHomeIntent, 1),
|
|
532
|
+
valueProp2' = propBit(hostWrites, valueProp2, ctrlShiftHomeIntent, 2),
|
|
533
|
+
valueProp3' = propBit(hostWrites, valueProp3, ctrlShiftHomeIntent, 3),
|
|
534
|
+
valueProp4' = propBit(hostWrites, valueProp4, ctrlShiftHomeIntent, 4),
|
|
535
|
+
requested' = if (ctrlShiftHomeIntent != selected) ctrlShiftHomeIntent else requested,
|
|
536
|
+
outcome' = if (ctrlShiftHomeIntent != selected) verdict else outcome,
|
|
537
|
+
base' = if (ctrlShiftHomeIntent != selected) selected else base,
|
|
538
|
+
requests' = if (ctrlShiftHomeIntent != selected) requests + 1 else requests
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Select the active option and everything below it; focus moves to the last.
|
|
542
|
+
action ctrl_shift_end = all {
|
|
543
|
+
hasFocus' = hasFocus,
|
|
544
|
+
active' = if (hasFocus) lastIndex else active,
|
|
545
|
+
selected' = landed(holds, selected, ctrlShiftEndIntent),
|
|
546
|
+
anchor' = anchor,
|
|
547
|
+
selectionModel' = selectionModel,
|
|
548
|
+
lastOp' = if (hasFocus) "range_end" else lastOp,
|
|
549
|
+
controlled' = controlled,
|
|
550
|
+
accepting' = accepting,
|
|
551
|
+
valueProp0' = propBit(hostWrites, valueProp0, ctrlShiftEndIntent, 0),
|
|
552
|
+
valueProp1' = propBit(hostWrites, valueProp1, ctrlShiftEndIntent, 1),
|
|
553
|
+
valueProp2' = propBit(hostWrites, valueProp2, ctrlShiftEndIntent, 2),
|
|
554
|
+
valueProp3' = propBit(hostWrites, valueProp3, ctrlShiftEndIntent, 3),
|
|
555
|
+
valueProp4' = propBit(hostWrites, valueProp4, ctrlShiftEndIntent, 4),
|
|
556
|
+
requested' = if (ctrlShiftEndIntent != selected) ctrlShiftEndIntent else requested,
|
|
557
|
+
outcome' = if (ctrlShiftEndIntent != selected) verdict else outcome,
|
|
558
|
+
base' = if (ctrlShiftEndIntent != selected) selected else base,
|
|
559
|
+
requests' = if (ctrlShiftEndIntent != selected) requests + 1 else requests
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Select all; when everything is already selected, unselect all.
|
|
563
|
+
action ctrl_a = all {
|
|
564
|
+
hasFocus' = hasFocus,
|
|
565
|
+
active' = active,
|
|
566
|
+
selected' = landed(holds, selected, ctrlAIntent),
|
|
567
|
+
anchor' = if (and(hasFocus, selected == allMask)) noOption else anchor,
|
|
568
|
+
selectionModel' = selectionModel,
|
|
569
|
+
lastOp' = if (hasFocus) (if (selected == allMask) "clear_all" else "select_all") else lastOp,
|
|
570
|
+
controlled' = controlled,
|
|
571
|
+
accepting' = accepting,
|
|
572
|
+
valueProp0' = propBit(hostWrites, valueProp0, ctrlAIntent, 0),
|
|
573
|
+
valueProp1' = propBit(hostWrites, valueProp1, ctrlAIntent, 1),
|
|
574
|
+
valueProp2' = propBit(hostWrites, valueProp2, ctrlAIntent, 2),
|
|
575
|
+
valueProp3' = propBit(hostWrites, valueProp3, ctrlAIntent, 3),
|
|
576
|
+
valueProp4' = propBit(hostWrites, valueProp4, ctrlAIntent, 4),
|
|
577
|
+
requested' = if (ctrlAIntent != selected) ctrlAIntent else requested,
|
|
578
|
+
outcome' = if (ctrlAIntent != selected) verdict else outcome,
|
|
579
|
+
base' = if (ctrlAIntent != selected) selected else base,
|
|
580
|
+
requests' = if (ctrlAIntent != selected) requests + 1 else requests
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Pointer: focus the option; toggle it (recommended) or select only it
|
|
584
|
+
// (alternative).
|
|
585
|
+
action click = {
|
|
586
|
+
nondet index = Set(0, 1, 2, 3, 4).oneOf()
|
|
587
|
+
all {
|
|
588
|
+
hasFocus' = true,
|
|
589
|
+
active' = index,
|
|
590
|
+
selected' = landed(holds, selected, clickIntent(index)),
|
|
591
|
+
anchor' = if (isAlt(selectionModel)) index else anchorAfterToggle(selected, anchor, index),
|
|
592
|
+
selectionModel' = selectionModel,
|
|
593
|
+
lastOp' = "click",
|
|
594
|
+
controlled' = controlled,
|
|
595
|
+
accepting' = accepting,
|
|
596
|
+
valueProp0' = propBit(hostWrites, valueProp0, clickIntent(index), 0),
|
|
597
|
+
valueProp1' = propBit(hostWrites, valueProp1, clickIntent(index), 1),
|
|
598
|
+
valueProp2' = propBit(hostWrites, valueProp2, clickIntent(index), 2),
|
|
599
|
+
valueProp3' = propBit(hostWrites, valueProp3, clickIntent(index), 3),
|
|
600
|
+
valueProp4' = propBit(hostWrites, valueProp4, clickIntent(index), 4),
|
|
601
|
+
requested' = if (clickIntent(index) != selected) clickIntent(index) else requested,
|
|
602
|
+
outcome' = if (clickIntent(index) != selected) verdict else outcome,
|
|
603
|
+
base' = if (clickIntent(index) != selected) selected else base,
|
|
604
|
+
requests' = if (clickIntent(index) != selected) requests + 1 else requests
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Prop change between the two selection models; the selection is kept.
|
|
609
|
+
action set_selection_model = {
|
|
610
|
+
nondet model = Set("recommended", "alternative").oneOf()
|
|
611
|
+
all {
|
|
612
|
+
hasFocus' = hasFocus,
|
|
613
|
+
active' = active,
|
|
614
|
+
selected' = selected,
|
|
615
|
+
anchor' = anchor,
|
|
616
|
+
selectionModel' = model,
|
|
617
|
+
lastOp' = "config",
|
|
618
|
+
controlled' = controlled,
|
|
619
|
+
accepting' = accepting,
|
|
620
|
+
valueProp0' = valueProp0,
|
|
621
|
+
valueProp1' = valueProp1,
|
|
622
|
+
valueProp2' = valueProp2,
|
|
623
|
+
valueProp3' = valueProp3,
|
|
624
|
+
valueProp4' = valueProp4,
|
|
625
|
+
requested' = requested,
|
|
626
|
+
outcome' = outcome,
|
|
627
|
+
base' = base,
|
|
628
|
+
requests' = requests
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// commit: the host writes the controlled value (a no-op while uncontrolled)
|
|
633
|
+
action setValue = {
|
|
634
|
+
nondet v = Set(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31).oneOf()
|
|
635
|
+
all {
|
|
636
|
+
hasFocus' = hasFocus,
|
|
637
|
+
active' = active,
|
|
638
|
+
selected' = if (controlled) v else selected,
|
|
639
|
+
anchor' = anchor,
|
|
640
|
+
selectionModel' = selectionModel,
|
|
641
|
+
lastOp' = lastOp,
|
|
642
|
+
controlled' = controlled,
|
|
643
|
+
accepting' = accepting,
|
|
644
|
+
valueProp0' = if (controlled) bit(v, 0) else valueProp0,
|
|
645
|
+
valueProp1' = if (controlled) bit(v, 1) else valueProp1,
|
|
646
|
+
valueProp2' = if (controlled) bit(v, 2) else valueProp2,
|
|
647
|
+
valueProp3' = if (controlled) bit(v, 3) else valueProp3,
|
|
648
|
+
valueProp4' = if (controlled) bit(v, 4) else valueProp4,
|
|
649
|
+
requested' = requested,
|
|
650
|
+
outcome' = if (controlled) "none" else outcome,
|
|
651
|
+
base' = base,
|
|
652
|
+
requests' = requests
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// commit: the host flips one option of its own copy of the value
|
|
657
|
+
action hostToggle = {
|
|
658
|
+
nondet panel = Set(0, 1, 2, 3, 4).oneOf()
|
|
659
|
+
all {
|
|
660
|
+
hasFocus' = hasFocus,
|
|
661
|
+
active' = active,
|
|
662
|
+
selected' = if (controlled) flipAt(propsMask, panel) else selected,
|
|
663
|
+
anchor' = anchor,
|
|
664
|
+
selectionModel' = selectionModel,
|
|
665
|
+
lastOp' = lastOp,
|
|
666
|
+
controlled' = controlled,
|
|
667
|
+
accepting' = accepting,
|
|
668
|
+
valueProp0' = if (controlled) bit(flipAt(propsMask, panel), 0) else valueProp0,
|
|
669
|
+
valueProp1' = if (controlled) bit(flipAt(propsMask, panel), 1) else valueProp1,
|
|
670
|
+
valueProp2' = if (controlled) bit(flipAt(propsMask, panel), 2) else valueProp2,
|
|
671
|
+
valueProp3' = if (controlled) bit(flipAt(propsMask, panel), 3) else valueProp3,
|
|
672
|
+
valueProp4' = if (controlled) bit(flipAt(propsMask, panel), 4) else valueProp4,
|
|
673
|
+
requested' = requested,
|
|
674
|
+
outcome' = if (controlled) "none" else outcome,
|
|
675
|
+
base' = base,
|
|
676
|
+
requests' = requests
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// configuration: the host starts owning the value, adopting the current one
|
|
681
|
+
action control = all {
|
|
682
|
+
hasFocus' = hasFocus,
|
|
683
|
+
active' = active,
|
|
684
|
+
selected' = selected,
|
|
685
|
+
anchor' = anchor,
|
|
686
|
+
selectionModel' = selectionModel,
|
|
687
|
+
lastOp' = lastOp,
|
|
688
|
+
controlled' = true,
|
|
689
|
+
accepting' = accepting,
|
|
690
|
+
valueProp0' = bit(selected, 0),
|
|
691
|
+
valueProp1' = bit(selected, 1),
|
|
692
|
+
valueProp2' = bit(selected, 2),
|
|
693
|
+
valueProp3' = bit(selected, 3),
|
|
694
|
+
valueProp4' = bit(selected, 4),
|
|
695
|
+
requested' = requested,
|
|
696
|
+
outcome' = outcome,
|
|
697
|
+
base' = base,
|
|
698
|
+
requests' = requests
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// configuration: the controlled host answers every request with its intent
|
|
702
|
+
action acceptRequests = all {
|
|
703
|
+
hasFocus' = hasFocus,
|
|
704
|
+
active' = active,
|
|
705
|
+
selected' = selected,
|
|
706
|
+
anchor' = anchor,
|
|
707
|
+
selectionModel' = selectionModel,
|
|
708
|
+
lastOp' = lastOp,
|
|
709
|
+
controlled' = controlled,
|
|
710
|
+
accepting' = true,
|
|
711
|
+
valueProp0' = valueProp0,
|
|
712
|
+
valueProp1' = valueProp1,
|
|
713
|
+
valueProp2' = valueProp2,
|
|
714
|
+
valueProp3' = valueProp3,
|
|
715
|
+
valueProp4' = valueProp4,
|
|
716
|
+
requested' = requested,
|
|
717
|
+
outcome' = outcome,
|
|
718
|
+
base' = base,
|
|
719
|
+
requests' = requests
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
action step = any {
|
|
723
|
+
init, seed, focus_in, blur, arrow_down, arrow_up, home, end, space, ctrl_space,
|
|
724
|
+
shift_arrow_down, shift_arrow_up, ctrl_arrow_down, ctrl_arrow_up, shift_space,
|
|
725
|
+
ctrl_shift_home, ctrl_shift_end, ctrl_a, click, set_selection_model,
|
|
726
|
+
setValue, hostToggle, control, acceptRequests
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// the mask only ever names real options
|
|
730
|
+
val SelectionInRange: bool = and(selected >= 0, selected < pow2(optionCount))
|
|
731
|
+
// the active option is always a real option
|
|
732
|
+
val ActiveInRange: bool = and(active >= 0, active <= lastIndex)
|
|
733
|
+
// aria-activedescendant is set exactly while the listbox holds focus
|
|
734
|
+
val ActiveDescendantTracksFocus: bool =
|
|
735
|
+
(ariaActiveDescendant(hasFocus, active) == noOption) == not(hasFocus)
|
|
736
|
+
// APG: a multi-select listbox sets aria-multiselectable=true
|
|
737
|
+
val AriaMultiselectableTrue: bool = ariaMultiselectable == true
|
|
738
|
+
// the anchor is a selected option or absent (uncontrolled)
|
|
739
|
+
val AnchorSelectedOrNone: bool =
|
|
740
|
+
or(controlled, or(anchor == noOption, and(anchor >= 0, anchor <= lastIndex, bit(selected, anchor))))
|
|
741
|
+
val SelectionModelKnown: bool =
|
|
742
|
+
or(selectionModel == "recommended", selectionModel == "alternative")
|
|
743
|
+
// alternative model: a plain move leaves exactly the focused option selected
|
|
744
|
+
val AlternativeMoveCollapses: bool =
|
|
745
|
+
or(controlled, not(isAlt(selectionModel)), lastOp != "move", selected == pow2(active))
|
|
746
|
+
// alternative model: a click selects only the clicked option
|
|
747
|
+
val AlternativeClickCollapses: bool =
|
|
748
|
+
or(controlled, not(isAlt(selectionModel)), lastOp != "click", selected == pow2(active))
|
|
749
|
+
// Shift+Space: everything between the anchor and the active option is selected
|
|
750
|
+
val RangeSelectsAnchorToActive: bool =
|
|
751
|
+
or(
|
|
752
|
+
controlled,
|
|
753
|
+
lastOp != "range",
|
|
754
|
+
and(
|
|
755
|
+
anchor != noOption,
|
|
756
|
+
orMask(selected, rangeMask(loOf(anchor, active), hiOf(anchor, active))) == selected
|
|
757
|
+
)
|
|
758
|
+
)
|
|
759
|
+
// Ctrl+Shift+Home/End: focus and selection reach the edge
|
|
760
|
+
val RangeToEdgeSelectsEdge: bool =
|
|
761
|
+
or(
|
|
762
|
+
controlled,
|
|
763
|
+
and(
|
|
764
|
+
or(lastOp != "range_home", and(active == 0, bit(selected, 0))),
|
|
765
|
+
or(lastOp != "range_end", and(active == lastIndex, bit(selected, lastIndex)))
|
|
766
|
+
)
|
|
767
|
+
)
|
|
768
|
+
// Ctrl+A: select all / unselect all
|
|
769
|
+
val SelectAllOutcome: bool =
|
|
770
|
+
or(
|
|
771
|
+
controlled,
|
|
772
|
+
and(
|
|
773
|
+
or(lastOp != "select_all", selected == allMask),
|
|
774
|
+
or(lastOp != "clear_all", selected == 0)
|
|
775
|
+
)
|
|
776
|
+
)
|
|
777
|
+
val LastOpKnown: bool =
|
|
778
|
+
or(lastOp == "none",
|
|
779
|
+
lastOp == "focus",
|
|
780
|
+
lastOp == "blur",
|
|
781
|
+
lastOp == "move",
|
|
782
|
+
lastOp == "ctrl_move",
|
|
783
|
+
lastOp == "toggle",
|
|
784
|
+
lastOp == "range",
|
|
785
|
+
lastOp == "range_home",
|
|
786
|
+
lastOp == "range_end",
|
|
787
|
+
lastOp == "select_all",
|
|
788
|
+
lastOp == "clear_all",
|
|
789
|
+
lastOp == "click",
|
|
790
|
+
lastOp == "config")
|
|
791
|
+
// callbacks only ever count up from zero
|
|
792
|
+
val RequestsNonNegative: bool = requests >= 0
|
|
793
|
+
// controlled: only the value prop sets the state, so no event commits
|
|
794
|
+
val ControlledFollowsProp: bool = or(not(controlled), selected == propsMask)
|
|
795
|
+
// a rejected request leaves the state as it found it
|
|
796
|
+
val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), selected == base)
|
|
797
|
+
// an accepted request committed exactly its intent
|
|
798
|
+
val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), selected == requested)
|
|
799
|
+
}
|