@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.
Files changed (260) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -2
  3. package/dist/apg/accordion.bridge.d.mts +35 -0
  4. package/dist/apg/accordion.bridge.mjs +326 -0
  5. package/dist/apg/accordion.model.json +8397 -0
  6. package/dist/apg/alert.bridge.d.mts +22 -0
  7. package/dist/apg/alert.bridge.mjs +86 -0
  8. package/dist/apg/alert.model.json +871 -0
  9. package/dist/apg/alertdialog.bridge.d.mts +21 -0
  10. package/dist/apg/alertdialog.bridge.mjs +221 -0
  11. package/dist/apg/alertdialog.model.json +5232 -0
  12. package/dist/apg/breadcrumb.bridge.d.mts +26 -0
  13. package/dist/apg/breadcrumb.bridge.mjs +120 -0
  14. package/dist/apg/breadcrumb.model.json +574 -0
  15. package/dist/apg/button.bridge.d.mts +35 -0
  16. package/dist/apg/button.bridge.mjs +208 -0
  17. package/dist/apg/button.model.json +5814 -0
  18. package/dist/apg/carousel.bridge.d.mts +44 -0
  19. package/dist/apg/carousel.bridge.mjs +262 -0
  20. package/dist/apg/carousel.model.json +8963 -0
  21. package/dist/apg/checkbox-group.bridge.d.mts +25 -0
  22. package/dist/apg/checkbox-group.bridge.mjs +276 -0
  23. package/dist/apg/checkbox-group.model.json +3863 -0
  24. package/dist/apg/checkbox.bridge.d.mts +34 -0
  25. package/dist/apg/checkbox.bridge.mjs +253 -0
  26. package/dist/apg/checkbox.model.json +3158 -0
  27. package/dist/apg/combobox-autocomplete.bridge.d.mts +14 -0
  28. package/dist/apg/combobox-autocomplete.bridge.mjs +326 -0
  29. package/dist/apg/combobox-autocomplete.model.json +11586 -0
  30. package/dist/apg/combobox-dialog.bridge.d.mts +17 -0
  31. package/dist/apg/combobox-dialog.bridge.mjs +250 -0
  32. package/dist/apg/combobox-dialog.model.json +8981 -0
  33. package/dist/apg/combobox-grid.bridge.d.mts +17 -0
  34. package/dist/apg/combobox-grid.bridge.mjs +227 -0
  35. package/dist/apg/combobox-grid.model.json +8554 -0
  36. package/dist/apg/combobox-select-only.bridge.d.mts +24 -0
  37. package/dist/apg/combobox-select-only.bridge.mjs +304 -0
  38. package/dist/apg/combobox-select-only.model.json +7846 -0
  39. package/dist/apg/combobox-tree.bridge.d.mts +17 -0
  40. package/dist/apg/combobox-tree.bridge.mjs +211 -0
  41. package/dist/apg/combobox-tree.model.json +9345 -0
  42. package/dist/apg/dialog-modal.bridge.d.mts +35 -0
  43. package/dist/apg/dialog-modal.bridge.mjs +186 -0
  44. package/dist/apg/dialog-modal.model.json +7580 -0
  45. package/dist/apg/disclosure.bridge.d.mts +25 -0
  46. package/dist/apg/disclosure.bridge.mjs +202 -0
  47. package/dist/apg/disclosure.model.json +4703 -0
  48. package/dist/apg/feed.bridge.d.mts +31 -0
  49. package/dist/apg/feed.bridge.mjs +246 -0
  50. package/dist/apg/feed.model.json +4523 -0
  51. package/dist/apg/grid-selection.bridge.d.mts +13 -0
  52. package/dist/apg/grid-selection.bridge.mjs +160 -0
  53. package/dist/apg/grid-selection.model.json +6974 -0
  54. package/dist/apg/grid-sort.bridge.d.mts +12 -0
  55. package/dist/apg/grid-sort.bridge.mjs +138 -0
  56. package/dist/apg/grid-sort.model.json +3799 -0
  57. package/dist/apg/grid.bridge.d.mts +54 -0
  58. package/dist/apg/grid.bridge.mjs +441 -0
  59. package/dist/apg/grid.model.json +3802 -0
  60. package/dist/apg/link.bridge.d.mts +24 -0
  61. package/dist/apg/link.bridge.mjs +126 -0
  62. package/dist/apg/link.model.json +694 -0
  63. package/dist/apg/listbox-multi.bridge.d.mts +10 -0
  64. package/dist/apg/listbox-multi.bridge.mjs +261 -0
  65. package/dist/apg/listbox-multi.model.json +10893 -0
  66. package/dist/apg/listbox-rearrangeable.bridge.d.mts +20 -0
  67. package/dist/apg/listbox-rearrangeable.bridge.mjs +237 -0
  68. package/dist/apg/listbox-rearrangeable.model.json +17592 -0
  69. package/dist/apg/listbox-shared.d.mts +58 -0
  70. package/dist/apg/listbox-shared.mjs +171 -0
  71. package/dist/apg/listbox-single.bridge.d.mts +10 -0
  72. package/dist/apg/listbox-single.bridge.mjs +196 -0
  73. package/dist/apg/listbox-single.model.json +8024 -0
  74. package/dist/apg/listbox-typeahead.bridge.d.mts +10 -0
  75. package/dist/apg/listbox-typeahead.bridge.mjs +153 -0
  76. package/dist/apg/listbox-typeahead.model.json +5615 -0
  77. package/dist/apg/menu-button.bridge.d.mts +29 -0
  78. package/dist/apg/menu-button.bridge.mjs +259 -0
  79. package/dist/apg/menu-button.model.json +5706 -0
  80. package/dist/apg/menu-checkable.bridge.d.mts +25 -0
  81. package/dist/apg/menu-checkable.bridge.mjs +226 -0
  82. package/dist/apg/menu-checkable.model.json +4277 -0
  83. package/dist/apg/menu-menubar.bridge.d.mts +27 -0
  84. package/dist/apg/menu-menubar.bridge.mjs +273 -0
  85. package/dist/apg/menu-menubar.model.json +9540 -0
  86. package/dist/apg/meter.bridge.d.mts +28 -0
  87. package/dist/apg/meter.bridge.mjs +159 -0
  88. package/dist/apg/meter.model.json +1185 -0
  89. package/dist/apg/player.bridge.d.mts +24 -0
  90. package/dist/apg/player.bridge.mjs +342 -0
  91. package/dist/apg/player.model.json +11013 -0
  92. package/dist/apg/radio.bridge.d.mts +35 -0
  93. package/dist/apg/radio.bridge.mjs +371 -0
  94. package/dist/apg/radio.model.json +5011 -0
  95. package/dist/apg/slider-multithumb.bridge.d.mts +34 -0
  96. package/dist/apg/slider-multithumb.bridge.mjs +365 -0
  97. package/dist/apg/slider-multithumb.model.json +5774 -0
  98. package/dist/apg/slider.bridge.d.mts +54 -0
  99. package/dist/apg/slider.bridge.mjs +359 -0
  100. package/dist/apg/slider.model.json +4881 -0
  101. package/dist/apg/spinbutton.bridge.d.mts +41 -0
  102. package/dist/apg/spinbutton.bridge.mjs +327 -0
  103. package/dist/apg/spinbutton.model.json +7571 -0
  104. package/dist/apg/switch.bridge.d.mts +30 -0
  105. package/dist/apg/switch.bridge.mjs +243 -0
  106. package/dist/apg/switch.model.json +3159 -0
  107. package/dist/apg/tabs.bridge.d.mts +41 -0
  108. package/dist/apg/tabs.bridge.mjs +294 -0
  109. package/dist/apg/tabs.model.json +7659 -0
  110. package/dist/apg/timeline.bridge.d.mts +36 -0
  111. package/dist/apg/timeline.bridge.mjs +265 -0
  112. package/dist/apg/timeline.model.json +6189 -0
  113. package/dist/apg/toolbar.bridge.d.mts +25 -0
  114. package/dist/apg/toolbar.bridge.mjs +307 -0
  115. package/dist/apg/toolbar.model.json +6616 -0
  116. package/dist/apg/tooltip.bridge.d.mts +36 -0
  117. package/dist/apg/tooltip.bridge.mjs +208 -0
  118. package/dist/apg/tooltip.model.json +8295 -0
  119. package/dist/apg/treegrid.bridge.d.mts +28 -0
  120. package/dist/apg/treegrid.bridge.mjs +311 -0
  121. package/dist/apg/treegrid.model.json +17358 -0
  122. package/dist/apg/treeview.bridge.d.mts +26 -0
  123. package/dist/apg/treeview.bridge.mjs +396 -0
  124. package/dist/apg/treeview.model.json +13745 -0
  125. package/dist/apg/windowsplitter.bridge.d.mts +36 -0
  126. package/dist/apg/windowsplitter.bridge.mjs +242 -0
  127. package/dist/apg/windowsplitter.model.json +6253 -0
  128. package/dist/canvas/camera.model.json +6568 -0
  129. package/dist/canvas/canvas.bridge.d.mts +190 -0
  130. package/dist/canvas/canvas.bridge.mjs +729 -0
  131. package/dist/canvas/pointer.model.json +17095 -0
  132. package/dist/scenario.d.mts +233 -0
  133. package/dist/scenario.mjs +313 -0
  134. package/package.json +57 -4
  135. package/scenarios/apg/README.md +221 -0
  136. package/scenarios/apg/accordion.base-ui.json +1005 -0
  137. package/scenarios/apg/accordion.react-aria.json +929 -0
  138. package/scenarios/apg/alert.base-ui.json +81 -0
  139. package/scenarios/apg/alert.react-aria.json +98 -0
  140. package/scenarios/apg/alertdialog.base-ui.json +331 -0
  141. package/scenarios/apg/alertdialog.react-aria.json +225 -0
  142. package/scenarios/apg/breadcrumb.react-aria.json +312 -0
  143. package/scenarios/apg/button.ariakit.json +136 -0
  144. package/scenarios/apg/button.base-ui.json +747 -0
  145. package/scenarios/apg/button.react-aria.json +443 -0
  146. package/scenarios/apg/carousel.apg.json +909 -0
  147. package/scenarios/apg/checkbox.ariakit.json +299 -0
  148. package/scenarios/apg/checkbox.base-ui.json +1126 -0
  149. package/scenarios/apg/checkbox.react-aria.json +518 -0
  150. package/scenarios/apg/combobox.ariakit.json +945 -0
  151. package/scenarios/apg/combobox.base-ui.json +2444 -0
  152. package/scenarios/apg/combobox.react-aria.json +845 -0
  153. package/scenarios/apg/dialog-modal.ariakit.json +1102 -0
  154. package/scenarios/apg/dialog-modal.base-ui.json +775 -0
  155. package/scenarios/apg/dialog-modal.react-aria.json +638 -0
  156. package/scenarios/apg/disclosure.apg.json +349 -0
  157. package/scenarios/apg/disclosure.ariakit.json +163 -0
  158. package/scenarios/apg/disclosure.base-ui.json +451 -0
  159. package/scenarios/apg/disclosure.react-aria.json +643 -0
  160. package/scenarios/apg/feed.apg.json +442 -0
  161. package/scenarios/apg/grid.ariakit.json +197 -0
  162. package/scenarios/apg/grid.base-ui.json +720 -0
  163. package/scenarios/apg/grid.react-aria.json +969 -0
  164. package/scenarios/apg/landmarks.ariakit.json +93 -0
  165. package/scenarios/apg/landmarks.react-aria.json +330 -0
  166. package/scenarios/apg/link.ariakit.json +83 -0
  167. package/scenarios/apg/link.base-ui.json +88 -0
  168. package/scenarios/apg/link.react-aria.json +186 -0
  169. package/scenarios/apg/listbox.ariakit.json +78 -0
  170. package/scenarios/apg/listbox.base-ui.json +163 -0
  171. package/scenarios/apg/listbox.react-aria.json +1118 -0
  172. package/scenarios/apg/menu-button.ariakit.json +924 -0
  173. package/scenarios/apg/menu-button.base-ui.json +1155 -0
  174. package/scenarios/apg/menu-button.react-aria.json +1080 -0
  175. package/scenarios/apg/menubar.ariakit.json +758 -0
  176. package/scenarios/apg/menubar.base-ui.json +673 -0
  177. package/scenarios/apg/meter.base-ui.json +243 -0
  178. package/scenarios/apg/meter.react-aria.json +201 -0
  179. package/scenarios/apg/radio.ariakit.json +459 -0
  180. package/scenarios/apg/radio.base-ui.json +581 -0
  181. package/scenarios/apg/radio.react-aria.json +576 -0
  182. package/scenarios/apg/slider-multithumb.base-ui.json +316 -0
  183. package/scenarios/apg/slider-multithumb.react-aria.json +363 -0
  184. package/scenarios/apg/slider.base-ui.json +544 -0
  185. package/scenarios/apg/slider.react-aria.json +456 -0
  186. package/scenarios/apg/spinbutton.base-ui.json +1688 -0
  187. package/scenarios/apg/spinbutton.react-aria.json +426 -0
  188. package/scenarios/apg/switch.base-ui.json +456 -0
  189. package/scenarios/apg/switch.react-aria.json +303 -0
  190. package/scenarios/apg/table.ariakit.json +65 -0
  191. package/scenarios/apg/table.react-aria.json +221 -0
  192. package/scenarios/apg/tabs.ariakit.json +329 -0
  193. package/scenarios/apg/tabs.base-ui.json +875 -0
  194. package/scenarios/apg/tabs.react-aria.json +644 -0
  195. package/scenarios/apg/toolbar.ariakit.json +595 -0
  196. package/scenarios/apg/toolbar.base-ui.json +1189 -0
  197. package/scenarios/apg/toolbar.react-aria.json +383 -0
  198. package/scenarios/apg/tooltip.ariakit.json +441 -0
  199. package/scenarios/apg/tooltip.base-ui.json +411 -0
  200. package/scenarios/apg/tooltip.react-aria.json +454 -0
  201. package/scenarios/apg/treegrid.apg.json +2558 -0
  202. package/scenarios/apg/treegrid.react-aria.json +5094 -0
  203. package/scenarios/apg/treeview.apg.json +928 -0
  204. package/scenarios/apg/windowsplitter.apg.json +557 -0
  205. package/scenarios/canvas/README.md +69 -0
  206. package/scenarios/canvas/camera.contract.json +237 -0
  207. package/scenarios/canvas/camera.excalidraw.json +57 -0
  208. package/scenarios/canvas/marquee.excalidraw.json +122 -0
  209. package/scenarios/canvas/pan.excalidraw.json +46 -0
  210. package/scenarios/canvas/pointer.contract.json +528 -0
  211. package/scenarios/parts.json +291 -0
  212. package/shared/aria.qnt +186 -0
  213. package/shared/details-group.qnt +50 -0
  214. package/shared/focusgroup.qnt +152 -0
  215. package/shared/invokers.qnt +81 -0
  216. package/shared/popover.qnt +71 -0
  217. package/specs/apg/STATIC.md +103 -0
  218. package/specs/apg/accordion.qnt +578 -0
  219. package/specs/apg/alert.qnt +131 -0
  220. package/specs/apg/alertdialog.qnt +455 -0
  221. package/specs/apg/breadcrumb.qnt +88 -0
  222. package/specs/apg/button.qnt +445 -0
  223. package/specs/apg/carousel.qnt +732 -0
  224. package/specs/apg/checkbox-group.qnt +388 -0
  225. package/specs/apg/checkbox.qnt +394 -0
  226. package/specs/apg/combobox-autocomplete.qnt +764 -0
  227. package/specs/apg/combobox-dialog.qnt +627 -0
  228. package/specs/apg/combobox-grid.qnt +632 -0
  229. package/specs/apg/combobox-select-only.qnt +626 -0
  230. package/specs/apg/combobox-tree.qnt +633 -0
  231. package/specs/apg/dialog-modal.qnt +499 -0
  232. package/specs/apg/disclosure.qnt +309 -0
  233. package/specs/apg/feed.qnt +485 -0
  234. package/specs/apg/grid-selection.qnt +512 -0
  235. package/specs/apg/grid-sort.qnt +330 -0
  236. package/specs/apg/grid.qnt +360 -0
  237. package/specs/apg/link.qnt +119 -0
  238. package/specs/apg/listbox-multi.qnt +799 -0
  239. package/specs/apg/listbox-rearrangeable.qnt +801 -0
  240. package/specs/apg/listbox-single.qnt +511 -0
  241. package/specs/apg/listbox-typeahead.qnt +396 -0
  242. package/specs/apg/menu-button.qnt +363 -0
  243. package/specs/apg/menu-checkable.qnt +304 -0
  244. package/specs/apg/menu-menubar.qnt +531 -0
  245. package/specs/apg/meter.qnt +129 -0
  246. package/specs/apg/player.qnt +1081 -0
  247. package/specs/apg/radio.qnt +394 -0
  248. package/specs/apg/slider-multithumb.qnt +441 -0
  249. package/specs/apg/slider.qnt +396 -0
  250. package/specs/apg/spinbutton.qnt +494 -0
  251. package/specs/apg/switch.qnt +389 -0
  252. package/specs/apg/tabs.qnt +412 -0
  253. package/specs/apg/timeline.qnt +612 -0
  254. package/specs/apg/toolbar.qnt +364 -0
  255. package/specs/apg/tooltip.qnt +553 -0
  256. package/specs/apg/treegrid.qnt +983 -0
  257. package/specs/apg/treeview.qnt +1004 -0
  258. package/specs/apg/windowsplitter.qnt +487 -0
  259. package/specs/canvas/camera.qnt +407 -0
  260. 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
+ }