@cssints/apg-specs 0.0.0-stage → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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,511 @@
|
|
|
1
|
+
// APG Listbox, single-select — https://www.w3.org/WAI/ARIA/apg/patterns/listbox/
|
|
2
|
+
//
|
|
3
|
+
// Sections modelled:
|
|
4
|
+
// - "Keyboard Interaction": what a single-select listbox does when it receives
|
|
5
|
+
// focus (first option, or the selected option), Down/Up/Home/End, and the
|
|
6
|
+
// note on "selection follows focus" (optional in the pattern, always on in
|
|
7
|
+
// the scrollable and grouped examples).
|
|
8
|
+
// - "WAI-ARIA Roles, States, and Properties": listbox/option/group, at most
|
|
9
|
+
// one option selected, aria-multiselectable false, aria-selected per option,
|
|
10
|
+
// aria-activedescendant (DOM focus stays on the listbox; the active option
|
|
11
|
+
// is the visual focus).
|
|
12
|
+
// - Examples: listbox-scrollable (single-select + follows focus),
|
|
13
|
+
// listbox-grouped (two option groups of three; groups are not focusable, so
|
|
14
|
+
// arrows cross group boundaries), listbox-rearrangeable example 1 (the
|
|
15
|
+
// single-select side; reordering lives in listbox-rearrangeable.qnt).
|
|
16
|
+
//
|
|
17
|
+
// State: whether the listbox holds focus, the active option, the selection (a
|
|
18
|
+
// bitmask — a set cannot be a state var in the supported subset), and the
|
|
19
|
+
// followsFocus prop. Selection is a mask rather than an index so "at most one
|
|
20
|
+
// selected" is a real invariant, not a tautology of the encoding.
|
|
21
|
+
//
|
|
22
|
+
// Events (user-level alphabet):
|
|
23
|
+
// focus_in / blur Tab into / out of the listbox
|
|
24
|
+
// arrow_down/arrow_up next / previous option (clamped, no wrap)
|
|
25
|
+
// home / end first / last option
|
|
26
|
+
// space select the active option (the way to select when
|
|
27
|
+
// selection does not follow focus)
|
|
28
|
+
// click(index) pointer: focus and select that option
|
|
29
|
+
// set_follows_focus prop change; turning it on while focused syncs the
|
|
30
|
+
// selection to the active option
|
|
31
|
+
//
|
|
32
|
+
// Request vs commit (map ticket rt1p). Every event a user would see as a change
|
|
33
|
+
// — focus_in (when selection follows focus), arrow_down/up, home, end, space,
|
|
34
|
+
// click, set_follows_focus (when it syncs) — is a *request* carrying the
|
|
35
|
+
// intended selection mask. It fires onValueChange (`requests` counts the calls)
|
|
36
|
+
// when the intent differs from the current selection, and then:
|
|
37
|
+
// uncontrolled the request is accepted: the selection commits the intent
|
|
38
|
+
// controlled, holds the host ignores it ("rejected"): the selection is untouched
|
|
39
|
+
// controlled, accepts the host answers with the same value (`accepting`): the
|
|
40
|
+
// selection commits through the controlled value
|
|
41
|
+
// The controlled value is `valueProp0..valueProp5` (one bool per option), the
|
|
42
|
+
// only thing that changes the selection while `controlled`; `setValue` /
|
|
43
|
+
// `hostToggle` are the host writing it. `control` and `acceptRequests` are
|
|
44
|
+
// configuration events (init is literal-only). The active option stays
|
|
45
|
+
// widget-internal: a rejected request still moves the visual focus, exactly as
|
|
46
|
+
// the primitive does.
|
|
47
|
+
//
|
|
48
|
+
// Invariants for the model: ControlledFollowsProp (in controlled mode no event
|
|
49
|
+
// commits), RejectedRequestUntouched, AcceptedRequestCommits. The mode invariants
|
|
50
|
+
// (at most one selected, selection follows focus) are scoped to the uncontrolled
|
|
51
|
+
// path: a controlled host owns the value and may hand the widget any set.
|
|
52
|
+
//
|
|
53
|
+
// Deliberate omissions (ponytail):
|
|
54
|
+
// - typeahead lives in listbox-typeahead.qnt (it only produces a focus move,
|
|
55
|
+
// which composes with the moves here)
|
|
56
|
+
// - scrolling the active option into view, aria-orientation=horizontal (arrow
|
|
57
|
+
// keys swap), aria-checked, aria-setsize/aria-posinset (dynamic loading),
|
|
58
|
+
// labels (aria-label/aria-labelledby), disabled options
|
|
59
|
+
// - an empty listbox (aria-activedescendant="") — the option count is fixed
|
|
60
|
+
// - the optional "first option auto-selected on entry" when selection does not
|
|
61
|
+
// follow focus: entry never selects unless followsFocus is on
|
|
62
|
+
// - focus wrap-around: the pattern does not specify it, the examples clamp
|
|
63
|
+
//
|
|
64
|
+
// Generator notes: no parameterized actions and no `let`, so a request spells
|
|
65
|
+
// `landed(...)` out per assignment and every action assigns every var.
|
|
66
|
+
module apg_listbox_single {
|
|
67
|
+
import aria.* from "../../shared/aria"
|
|
68
|
+
|
|
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 followsFocus: bool // prop: selection follows focus
|
|
73
|
+
var controlled: bool // the host owns the value (`value` prop present)
|
|
74
|
+
var accepting: bool // controlled host that answers every request with its intent
|
|
75
|
+
var valueProp0: bool // the controlled value prop, one bool per option
|
|
76
|
+
var valueProp1: bool
|
|
77
|
+
var valueProp2: bool
|
|
78
|
+
var valueProp3: bool
|
|
79
|
+
var valueProp4: bool
|
|
80
|
+
var valueProp5: bool
|
|
81
|
+
var requested: int // intent of the last request (selection mask)
|
|
82
|
+
var outcome: str // "none" | "accepted" | "rejected"
|
|
83
|
+
var base: int // selection when the last request was received
|
|
84
|
+
var requests: int // number of requests that fired onValueChange
|
|
85
|
+
|
|
86
|
+
// six options in two groups of three (listbox-grouped example)
|
|
87
|
+
def optionCount: int = 6
|
|
88
|
+
def lastIndex: int = 5
|
|
89
|
+
def noOption: int = 6
|
|
90
|
+
def groupSize: int = 3
|
|
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 if (i == 5) 32
|
|
99
|
+
else 64
|
|
100
|
+
// bit i of a selection mask: option i is selected
|
|
101
|
+
def bit(mask: int, i: int): bool = (mask / pow2(i)) % 2 == 1
|
|
102
|
+
def popcount(mask: int): int =
|
|
103
|
+
(if (bit(mask, 0)) 1 else 0)
|
|
104
|
+
+ (if (bit(mask, 1)) 1 else 0)
|
|
105
|
+
+ (if (bit(mask, 2)) 1 else 0)
|
|
106
|
+
+ (if (bit(mask, 3)) 1 else 0)
|
|
107
|
+
+ (if (bit(mask, 4)) 1 else 0)
|
|
108
|
+
+ (if (bit(mask, 5)) 1 else 0)
|
|
109
|
+
def firstSelected(mask: int): int =
|
|
110
|
+
if (bit(mask, 0)) 0
|
|
111
|
+
else if (bit(mask, 1)) 1
|
|
112
|
+
else if (bit(mask, 2)) 2
|
|
113
|
+
else if (bit(mask, 3)) 3
|
|
114
|
+
else if (bit(mask, 4)) 4
|
|
115
|
+
else if (bit(mask, 5)) 5
|
|
116
|
+
else noOption
|
|
117
|
+
// Tab entry: the selected option when there is one, else the first option
|
|
118
|
+
def entryIndex(mask: int): int = if (mask == 0) 0 else firstSelected(mask)
|
|
119
|
+
def nextIndex(i: int): int = if (i < lastIndex) i + 1 else i
|
|
120
|
+
def prevIndex(i: int): int = if (i > 0) i - 1 else i
|
|
121
|
+
def groupOf(i: int): int = i / groupSize
|
|
122
|
+
// selection after the active option moved to target
|
|
123
|
+
def selectionAfterMove(follow: bool, mask: int, target: int): int =
|
|
124
|
+
if (follow) pow2(target) else mask
|
|
125
|
+
|
|
126
|
+
// --- request / commit (map ticket rt1p) ---------------------------------
|
|
127
|
+
def b2i(b: bool): int = if (b) 1 else 0
|
|
128
|
+
// the controlled value prop, packed into a mask
|
|
129
|
+
def propsMask: int =
|
|
130
|
+
b2i(valueProp0)
|
|
131
|
+
+ 2 * b2i(valueProp1)
|
|
132
|
+
+ 4 * b2i(valueProp2)
|
|
133
|
+
+ 8 * b2i(valueProp3)
|
|
134
|
+
+ 16 * b2i(valueProp4)
|
|
135
|
+
+ 32 * b2i(valueProp5)
|
|
136
|
+
// the host flips one selected option of its own copy of the value
|
|
137
|
+
def flipAt(n: int, i: int): int = if (bit(n, i)) n - pow2(i) else n + pow2(i)
|
|
138
|
+
// controlled host that keeps the old value: the request is rejected
|
|
139
|
+
def holds: bool = and(controlled, not(accepting))
|
|
140
|
+
// controlled host that writes the intent back through the value prop
|
|
141
|
+
def hostWrites: bool = and(controlled, accepting)
|
|
142
|
+
def verdict: str = if (holds) "rejected" else "accepted"
|
|
143
|
+
// the value after a request for `want`
|
|
144
|
+
def landed(h: bool, current: int, want: int): int = if (h) current else want
|
|
145
|
+
// one value-prop member after a request for `want`
|
|
146
|
+
def propBit(writes: bool, current: bool, want: int, i: int): bool =
|
|
147
|
+
if (writes) bit(want, i) else current
|
|
148
|
+
// the intended selection of each request
|
|
149
|
+
def focusIntent: int =
|
|
150
|
+
if (and(not(hasFocus), followsFocus)) pow2(entryIndex(selected)) else selected
|
|
151
|
+
def downIntent: int =
|
|
152
|
+
if (hasFocus) selectionAfterMove(followsFocus, selected, nextIndex(active)) else selected
|
|
153
|
+
def upIntent: int =
|
|
154
|
+
if (hasFocus) selectionAfterMove(followsFocus, selected, prevIndex(active)) else selected
|
|
155
|
+
def homeIntent: int = if (hasFocus) selectionAfterMove(followsFocus, selected, 0) else selected
|
|
156
|
+
def endIntent: int = if (hasFocus) selectionAfterMove(followsFocus, selected, lastIndex) else selected
|
|
157
|
+
def spaceIntent: int = if (hasFocus) pow2(active) else selected
|
|
158
|
+
def followsIntent(follow: bool): int =
|
|
159
|
+
if (and(follow, hasFocus)) pow2(active) else selected
|
|
160
|
+
|
|
161
|
+
// --- ARIA contract, derived from the state ------------------------------
|
|
162
|
+
def ariaMultiselectable: bool = false
|
|
163
|
+
def ariaActiveDescendant(focused: bool, idx: int): int =
|
|
164
|
+
if (focused) idx else noOption
|
|
165
|
+
def ariaSelected(mask: int, i: int): bool = bit(mask, i)
|
|
166
|
+
// the listbox itself is the one tab stop; options never take tabindex
|
|
167
|
+
def listboxTabIndex: int = 0
|
|
168
|
+
|
|
169
|
+
action init = all {
|
|
170
|
+
hasFocus' = false,
|
|
171
|
+
active' = 0,
|
|
172
|
+
selected' = 0,
|
|
173
|
+
followsFocus' = false,
|
|
174
|
+
controlled' = false,
|
|
175
|
+
accepting' = false,
|
|
176
|
+
valueProp0' = false,
|
|
177
|
+
valueProp1' = false,
|
|
178
|
+
valueProp2' = false,
|
|
179
|
+
valueProp3' = false,
|
|
180
|
+
valueProp4' = false,
|
|
181
|
+
valueProp5' = false,
|
|
182
|
+
requested' = 0,
|
|
183
|
+
outcome' = "none",
|
|
184
|
+
base' = 0,
|
|
185
|
+
requests' = 0
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// mount: the uncontrolled initial selection (the host's `defaultValue`),
|
|
189
|
+
// active at the first option, exactly as the primitive mounts
|
|
190
|
+
action seed = {
|
|
191
|
+
nondet index = Set(0, 1, 2, 3, 4, 5, 6).oneOf()
|
|
192
|
+
all {
|
|
193
|
+
hasFocus' = false,
|
|
194
|
+
active' = 0,
|
|
195
|
+
selected' = if (index == noOption) 0 else pow2(index),
|
|
196
|
+
followsFocus' = followsFocus,
|
|
197
|
+
controlled' = false,
|
|
198
|
+
accepting' = false,
|
|
199
|
+
valueProp0' = false,
|
|
200
|
+
valueProp1' = false,
|
|
201
|
+
valueProp2' = false,
|
|
202
|
+
valueProp3' = false,
|
|
203
|
+
valueProp4' = false,
|
|
204
|
+
valueProp5' = false,
|
|
205
|
+
requested' = 0,
|
|
206
|
+
outcome' = "none",
|
|
207
|
+
base' = 0,
|
|
208
|
+
requests' = 0
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Tab into the listbox: focus the selected option, else the first. With
|
|
213
|
+
// followsFocus the entry option is selected too; already focused is a no-op.
|
|
214
|
+
action focus_in = all {
|
|
215
|
+
hasFocus' = true,
|
|
216
|
+
active' = if (hasFocus) active else entryIndex(selected),
|
|
217
|
+
selected' = landed(holds, selected, focusIntent),
|
|
218
|
+
followsFocus' = followsFocus,
|
|
219
|
+
controlled' = controlled,
|
|
220
|
+
accepting' = accepting,
|
|
221
|
+
valueProp0' = propBit(hostWrites, valueProp0, focusIntent, 0),
|
|
222
|
+
valueProp1' = propBit(hostWrites, valueProp1, focusIntent, 1),
|
|
223
|
+
valueProp2' = propBit(hostWrites, valueProp2, focusIntent, 2),
|
|
224
|
+
valueProp3' = propBit(hostWrites, valueProp3, focusIntent, 3),
|
|
225
|
+
valueProp4' = propBit(hostWrites, valueProp4, focusIntent, 4),
|
|
226
|
+
valueProp5' = propBit(hostWrites, valueProp5, focusIntent, 5),
|
|
227
|
+
requested' = if (focusIntent != selected) focusIntent else requested,
|
|
228
|
+
outcome' = if (focusIntent != selected) verdict else outcome,
|
|
229
|
+
base' = if (focusIntent != selected) selected else base,
|
|
230
|
+
requests' = if (focusIntent != selected) requests + 1 else requests
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Tab out: the selection is kept.
|
|
234
|
+
action blur = all {
|
|
235
|
+
hasFocus' = false,
|
|
236
|
+
active' = active,
|
|
237
|
+
selected' = selected,
|
|
238
|
+
followsFocus' = followsFocus,
|
|
239
|
+
controlled' = controlled,
|
|
240
|
+
accepting' = accepting,
|
|
241
|
+
valueProp0' = valueProp0,
|
|
242
|
+
valueProp1' = valueProp1,
|
|
243
|
+
valueProp2' = valueProp2,
|
|
244
|
+
valueProp3' = valueProp3,
|
|
245
|
+
valueProp4' = valueProp4,
|
|
246
|
+
valueProp5' = valueProp5,
|
|
247
|
+
requested' = requested,
|
|
248
|
+
outcome' = outcome,
|
|
249
|
+
base' = base,
|
|
250
|
+
requests' = requests
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
action arrow_down = all {
|
|
254
|
+
hasFocus' = hasFocus,
|
|
255
|
+
active' = if (hasFocus) nextIndex(active) else active,
|
|
256
|
+
selected' = landed(holds, selected, downIntent),
|
|
257
|
+
followsFocus' = followsFocus,
|
|
258
|
+
controlled' = controlled,
|
|
259
|
+
accepting' = accepting,
|
|
260
|
+
valueProp0' = propBit(hostWrites, valueProp0, downIntent, 0),
|
|
261
|
+
valueProp1' = propBit(hostWrites, valueProp1, downIntent, 1),
|
|
262
|
+
valueProp2' = propBit(hostWrites, valueProp2, downIntent, 2),
|
|
263
|
+
valueProp3' = propBit(hostWrites, valueProp3, downIntent, 3),
|
|
264
|
+
valueProp4' = propBit(hostWrites, valueProp4, downIntent, 4),
|
|
265
|
+
valueProp5' = propBit(hostWrites, valueProp5, downIntent, 5),
|
|
266
|
+
requested' = if (downIntent != selected) downIntent else requested,
|
|
267
|
+
outcome' = if (downIntent != selected) verdict else outcome,
|
|
268
|
+
base' = if (downIntent != selected) selected else base,
|
|
269
|
+
requests' = if (downIntent != selected) requests + 1 else requests
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
action arrow_up = all {
|
|
273
|
+
hasFocus' = hasFocus,
|
|
274
|
+
active' = if (hasFocus) prevIndex(active) else active,
|
|
275
|
+
selected' = landed(holds, selected, upIntent),
|
|
276
|
+
followsFocus' = followsFocus,
|
|
277
|
+
controlled' = controlled,
|
|
278
|
+
accepting' = accepting,
|
|
279
|
+
valueProp0' = propBit(hostWrites, valueProp0, upIntent, 0),
|
|
280
|
+
valueProp1' = propBit(hostWrites, valueProp1, upIntent, 1),
|
|
281
|
+
valueProp2' = propBit(hostWrites, valueProp2, upIntent, 2),
|
|
282
|
+
valueProp3' = propBit(hostWrites, valueProp3, upIntent, 3),
|
|
283
|
+
valueProp4' = propBit(hostWrites, valueProp4, upIntent, 4),
|
|
284
|
+
valueProp5' = propBit(hostWrites, valueProp5, upIntent, 5),
|
|
285
|
+
requested' = if (upIntent != selected) upIntent else requested,
|
|
286
|
+
outcome' = if (upIntent != selected) verdict else outcome,
|
|
287
|
+
base' = if (upIntent != selected) selected else base,
|
|
288
|
+
requests' = if (upIntent != selected) requests + 1 else requests
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
action home = all {
|
|
292
|
+
hasFocus' = hasFocus,
|
|
293
|
+
active' = if (hasFocus) 0 else active,
|
|
294
|
+
selected' = landed(holds, selected, homeIntent),
|
|
295
|
+
followsFocus' = followsFocus,
|
|
296
|
+
controlled' = controlled,
|
|
297
|
+
accepting' = accepting,
|
|
298
|
+
valueProp0' = propBit(hostWrites, valueProp0, homeIntent, 0),
|
|
299
|
+
valueProp1' = propBit(hostWrites, valueProp1, homeIntent, 1),
|
|
300
|
+
valueProp2' = propBit(hostWrites, valueProp2, homeIntent, 2),
|
|
301
|
+
valueProp3' = propBit(hostWrites, valueProp3, homeIntent, 3),
|
|
302
|
+
valueProp4' = propBit(hostWrites, valueProp4, homeIntent, 4),
|
|
303
|
+
valueProp5' = propBit(hostWrites, valueProp5, homeIntent, 5),
|
|
304
|
+
requested' = if (homeIntent != selected) homeIntent else requested,
|
|
305
|
+
outcome' = if (homeIntent != selected) verdict else outcome,
|
|
306
|
+
base' = if (homeIntent != selected) selected else base,
|
|
307
|
+
requests' = if (homeIntent != selected) requests + 1 else requests
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
action end = all {
|
|
311
|
+
hasFocus' = hasFocus,
|
|
312
|
+
active' = if (hasFocus) lastIndex else active,
|
|
313
|
+
selected' = landed(holds, selected, endIntent),
|
|
314
|
+
followsFocus' = followsFocus,
|
|
315
|
+
controlled' = controlled,
|
|
316
|
+
accepting' = accepting,
|
|
317
|
+
valueProp0' = propBit(hostWrites, valueProp0, endIntent, 0),
|
|
318
|
+
valueProp1' = propBit(hostWrites, valueProp1, endIntent, 1),
|
|
319
|
+
valueProp2' = propBit(hostWrites, valueProp2, endIntent, 2),
|
|
320
|
+
valueProp3' = propBit(hostWrites, valueProp3, endIntent, 3),
|
|
321
|
+
valueProp4' = propBit(hostWrites, valueProp4, endIntent, 4),
|
|
322
|
+
valueProp5' = propBit(hostWrites, valueProp5, endIntent, 5),
|
|
323
|
+
requested' = if (endIntent != selected) endIntent else requested,
|
|
324
|
+
outcome' = if (endIntent != selected) verdict else outcome,
|
|
325
|
+
base' = if (endIntent != selected) selected else base,
|
|
326
|
+
requests' = if (endIntent != selected) requests + 1 else requests
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Select the active option (single-select: replaces the selection).
|
|
330
|
+
action space = all {
|
|
331
|
+
hasFocus' = hasFocus,
|
|
332
|
+
active' = active,
|
|
333
|
+
selected' = landed(holds, selected, spaceIntent),
|
|
334
|
+
followsFocus' = followsFocus,
|
|
335
|
+
controlled' = controlled,
|
|
336
|
+
accepting' = accepting,
|
|
337
|
+
valueProp0' = propBit(hostWrites, valueProp0, spaceIntent, 0),
|
|
338
|
+
valueProp1' = propBit(hostWrites, valueProp1, spaceIntent, 1),
|
|
339
|
+
valueProp2' = propBit(hostWrites, valueProp2, spaceIntent, 2),
|
|
340
|
+
valueProp3' = propBit(hostWrites, valueProp3, spaceIntent, 3),
|
|
341
|
+
valueProp4' = propBit(hostWrites, valueProp4, spaceIntent, 4),
|
|
342
|
+
valueProp5' = propBit(hostWrites, valueProp5, spaceIntent, 5),
|
|
343
|
+
requested' = if (spaceIntent != selected) spaceIntent else requested,
|
|
344
|
+
outcome' = if (spaceIntent != selected) verdict else outcome,
|
|
345
|
+
base' = if (spaceIntent != selected) selected else base,
|
|
346
|
+
requests' = if (spaceIntent != selected) requests + 1 else requests
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Pointer: focus the option and select it.
|
|
350
|
+
action click = {
|
|
351
|
+
nondet index = Set(0, 1, 2, 3, 4, 5).oneOf()
|
|
352
|
+
all {
|
|
353
|
+
hasFocus' = true,
|
|
354
|
+
active' = index,
|
|
355
|
+
selected' = landed(holds, selected, pow2(index)),
|
|
356
|
+
followsFocus' = followsFocus,
|
|
357
|
+
controlled' = controlled,
|
|
358
|
+
accepting' = accepting,
|
|
359
|
+
valueProp0' = propBit(hostWrites, valueProp0, pow2(index), 0),
|
|
360
|
+
valueProp1' = propBit(hostWrites, valueProp1, pow2(index), 1),
|
|
361
|
+
valueProp2' = propBit(hostWrites, valueProp2, pow2(index), 2),
|
|
362
|
+
valueProp3' = propBit(hostWrites, valueProp3, pow2(index), 3),
|
|
363
|
+
valueProp4' = propBit(hostWrites, valueProp4, pow2(index), 4),
|
|
364
|
+
valueProp5' = propBit(hostWrites, valueProp5, pow2(index), 5),
|
|
365
|
+
requested' = if (pow2(index) != selected) pow2(index) else requested,
|
|
366
|
+
outcome' = if (pow2(index) != selected) verdict else outcome,
|
|
367
|
+
base' = if (pow2(index) != selected) selected else base,
|
|
368
|
+
requests' = if (pow2(index) != selected) requests + 1 else requests
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
action set_follows_focus = {
|
|
373
|
+
nondet follow = Set(true, false).oneOf()
|
|
374
|
+
all {
|
|
375
|
+
hasFocus' = hasFocus,
|
|
376
|
+
active' = active,
|
|
377
|
+
selected' = landed(holds, selected, followsIntent(follow)),
|
|
378
|
+
followsFocus' = follow,
|
|
379
|
+
controlled' = controlled,
|
|
380
|
+
accepting' = accepting,
|
|
381
|
+
valueProp0' = propBit(hostWrites, valueProp0, followsIntent(follow), 0),
|
|
382
|
+
valueProp1' = propBit(hostWrites, valueProp1, followsIntent(follow), 1),
|
|
383
|
+
valueProp2' = propBit(hostWrites, valueProp2, followsIntent(follow), 2),
|
|
384
|
+
valueProp3' = propBit(hostWrites, valueProp3, followsIntent(follow), 3),
|
|
385
|
+
valueProp4' = propBit(hostWrites, valueProp4, followsIntent(follow), 4),
|
|
386
|
+
valueProp5' = propBit(hostWrites, valueProp5, followsIntent(follow), 5),
|
|
387
|
+
requested' = if (followsIntent(follow) != selected) followsIntent(follow) else requested,
|
|
388
|
+
outcome' = if (followsIntent(follow) != selected) verdict else outcome,
|
|
389
|
+
base' = if (followsIntent(follow) != selected) selected else base,
|
|
390
|
+
requests' = if (followsIntent(follow) != selected) requests + 1 else requests
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// commit: the host writes the controlled value (a no-op while uncontrolled)
|
|
395
|
+
action setValue = {
|
|
396
|
+
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, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63).oneOf()
|
|
397
|
+
all {
|
|
398
|
+
hasFocus' = hasFocus,
|
|
399
|
+
active' = active,
|
|
400
|
+
selected' = if (controlled) v else selected,
|
|
401
|
+
followsFocus' = followsFocus,
|
|
402
|
+
controlled' = controlled,
|
|
403
|
+
accepting' = accepting,
|
|
404
|
+
valueProp0' = if (controlled) bit(v, 0) else valueProp0,
|
|
405
|
+
valueProp1' = if (controlled) bit(v, 1) else valueProp1,
|
|
406
|
+
valueProp2' = if (controlled) bit(v, 2) else valueProp2,
|
|
407
|
+
valueProp3' = if (controlled) bit(v, 3) else valueProp3,
|
|
408
|
+
valueProp4' = if (controlled) bit(v, 4) else valueProp4,
|
|
409
|
+
valueProp5' = if (controlled) bit(v, 5) else valueProp5,
|
|
410
|
+
requested' = requested,
|
|
411
|
+
outcome' = if (controlled) "none" else outcome,
|
|
412
|
+
base' = base,
|
|
413
|
+
requests' = requests
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// commit: the host flips one option of its own copy of the value
|
|
418
|
+
action hostToggle = {
|
|
419
|
+
nondet panel = Set(0, 1, 2, 3, 4, 5).oneOf()
|
|
420
|
+
all {
|
|
421
|
+
hasFocus' = hasFocus,
|
|
422
|
+
active' = active,
|
|
423
|
+
selected' = if (controlled) flipAt(propsMask, panel) else selected,
|
|
424
|
+
followsFocus' = followsFocus,
|
|
425
|
+
controlled' = controlled,
|
|
426
|
+
accepting' = accepting,
|
|
427
|
+
valueProp0' = if (controlled) bit(flipAt(propsMask, panel), 0) else valueProp0,
|
|
428
|
+
valueProp1' = if (controlled) bit(flipAt(propsMask, panel), 1) else valueProp1,
|
|
429
|
+
valueProp2' = if (controlled) bit(flipAt(propsMask, panel), 2) else valueProp2,
|
|
430
|
+
valueProp3' = if (controlled) bit(flipAt(propsMask, panel), 3) else valueProp3,
|
|
431
|
+
valueProp4' = if (controlled) bit(flipAt(propsMask, panel), 4) else valueProp4,
|
|
432
|
+
valueProp5' = if (controlled) bit(flipAt(propsMask, panel), 5) else valueProp5,
|
|
433
|
+
requested' = requested,
|
|
434
|
+
outcome' = if (controlled) "none" else outcome,
|
|
435
|
+
base' = base,
|
|
436
|
+
requests' = requests
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// configuration: the host starts owning the value, adopting the current one
|
|
441
|
+
action control = all {
|
|
442
|
+
hasFocus' = hasFocus,
|
|
443
|
+
active' = active,
|
|
444
|
+
selected' = selected,
|
|
445
|
+
followsFocus' = followsFocus,
|
|
446
|
+
controlled' = true,
|
|
447
|
+
accepting' = accepting,
|
|
448
|
+
valueProp0' = bit(selected, 0),
|
|
449
|
+
valueProp1' = bit(selected, 1),
|
|
450
|
+
valueProp2' = bit(selected, 2),
|
|
451
|
+
valueProp3' = bit(selected, 3),
|
|
452
|
+
valueProp4' = bit(selected, 4),
|
|
453
|
+
valueProp5' = bit(selected, 5),
|
|
454
|
+
requested' = requested,
|
|
455
|
+
outcome' = outcome,
|
|
456
|
+
base' = base,
|
|
457
|
+
requests' = requests
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// configuration: the controlled host answers every request with its intent
|
|
461
|
+
action acceptRequests = all {
|
|
462
|
+
hasFocus' = hasFocus,
|
|
463
|
+
active' = active,
|
|
464
|
+
selected' = selected,
|
|
465
|
+
followsFocus' = followsFocus,
|
|
466
|
+
controlled' = controlled,
|
|
467
|
+
accepting' = true,
|
|
468
|
+
valueProp0' = valueProp0,
|
|
469
|
+
valueProp1' = valueProp1,
|
|
470
|
+
valueProp2' = valueProp2,
|
|
471
|
+
valueProp3' = valueProp3,
|
|
472
|
+
valueProp4' = valueProp4,
|
|
473
|
+
valueProp5' = valueProp5,
|
|
474
|
+
requested' = requested,
|
|
475
|
+
outcome' = outcome,
|
|
476
|
+
base' = base,
|
|
477
|
+
requests' = requests
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
action step = any {
|
|
481
|
+
init, seed, focus_in, blur, arrow_down, arrow_up, home, end, space, click,
|
|
482
|
+
set_follows_focus, setValue, hostToggle, control, acceptRequests
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// APG: no more than one option is selected without aria-multiselectable
|
|
486
|
+
// (uncontrolled: a controlled host owns the value and may hand us any set)
|
|
487
|
+
val SingleSelectAtMostOne: bool = ariaSingleHoldsAtMostOne(controlled, false, popcount(selected))
|
|
488
|
+
// the mask only ever names real options
|
|
489
|
+
val SelectionInRange: bool = and(selected >= 0, selected < pow2(optionCount))
|
|
490
|
+
// the active option is always a real option
|
|
491
|
+
val ActiveInRange: bool = and(active >= 0, active <= lastIndex)
|
|
492
|
+
// aria-activedescendant is set exactly while the listbox holds focus
|
|
493
|
+
val ActiveDescendantTracksFocus: bool =
|
|
494
|
+
(ariaActiveDescendant(hasFocus, active) == noOption) == not(hasFocus)
|
|
495
|
+
// selection follows focus: the focused option is the selected one (uncontrolled)
|
|
496
|
+
val SelectionFollowsFocus: bool =
|
|
497
|
+
or(controlled, not(followsFocus), not(hasFocus), selected == pow2(active))
|
|
498
|
+
// a single-select listbox never advertises aria-multiselectable
|
|
499
|
+
val AriaMultiselectableFalse: bool = ariaMultiselectable == false
|
|
500
|
+
// every option group holds at least one option
|
|
501
|
+
val GroupsNonEmpty: bool =
|
|
502
|
+
and(groupOf(0) == 0, groupOf(groupSize - 1) == 0, groupOf(groupSize) == 1, groupOf(lastIndex) == 1)
|
|
503
|
+
// callbacks only ever count up from zero
|
|
504
|
+
val RequestsNonNegative: bool = requests >= 0
|
|
505
|
+
// controlled: only the value prop sets the state, so no event commits
|
|
506
|
+
val ControlledFollowsProp: bool = or(not(controlled), selected == propsMask)
|
|
507
|
+
// a rejected request leaves the state as it found it
|
|
508
|
+
val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), selected == base)
|
|
509
|
+
// an accepted request committed exactly its intent
|
|
510
|
+
val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), selected == requested)
|
|
511
|
+
}
|