@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.
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,801 @@
1
+ // APG Listbox, rearrangeable options —
2
+ // https://www.w3.org/WAI/ARIA/apg/patterns/listbox/examples/listbox-rearrangeable/
3
+ // (and the pattern page https://www.w3.org/WAI/ARIA/apg/patterns/listbox/)
4
+ //
5
+ // Two listboxes, A and B, share four options (ids 0..3); every option sits in
6
+ // exactly one list. Initially A holds all four in order. The machine covers
7
+ // both examples of the page:
8
+ // example 1 (single-select, multi = false) Up/Down reorder the focused option
9
+ // inside its list (Alt+Up / Alt+Down); "Not Important"/"Important"
10
+ // move it to the other list (Delete); selection follows focus.
11
+ // example 2 (multi-select, multi = true) Space toggles; "Add"/"Remove" move
12
+ // every selected option of the focused list to the other list
13
+ // (Enter / Delete); arrows move focus only.
14
+ // The same transfer rule serves both: it moves the SELECTED options of the
15
+ // focused list, and in single-select mode "the selected option" is the focused
16
+ // one because selection follows focus.
17
+ //
18
+ // Keyboard facts modelled from the example's "Accessibility Features" and
19
+ // keyboard tables:
20
+ // - Tab entry: the first selected option, else the first option
21
+ // - Down/Up/Home/End move focus (and select it in the single-select example)
22
+ // - after a reorder, focus stays on the moved option
23
+ // - after a transfer, focus in the source list lands on the first remaining
24
+ // option after the removed one; if none follows, on the last remaining one
25
+ // - the options keep their order when they move to the other list (appended)
26
+ //
27
+ // State: the option ids in each list's four slots (4 = empty slot, lists are
28
+ // packed to the left), the active option of each list, which list is focused,
29
+ // the selection as a bitmask over option ids (selection is a property of the
30
+ // option, so it survives reordering without any remapping), and the mode.
31
+ //
32
+ // Request vs commit (map ticket rt1p). Every event a user would see as a
33
+ // selection change — focus_a/focus_b (the single-select entry select), the plain
34
+ // moves, Space, transfer (which deselects the moved options) and set_multi (the
35
+ // switch back to single-select) — is a *request* carrying the intended selection
36
+ // mask. It fires onValueChange (`requests` counts the calls) when the intent
37
+ // differs from the current selection, and the selection commits through
38
+ // `landed(holds, sel, intent)`:
39
+ // uncontrolled accepted: the selection commits the intent
40
+ // controlled, holds rejected: the selection is untouched
41
+ // controlled, accepts accepted through the controlled value (hostWrites)
42
+ // The controlled value is `valueProp0..valueProp3` (one bool per option id), the
43
+ // only thing that changes the selection while `controlled`; `setValue` /
44
+ // `hostToggle` are the host writing it. `control` / `acceptRequests` are
45
+ // configuration events (init is literal-only). A rejected transfer moves nothing
46
+ // at all (its list surgery is gated on the request landing), so a rejected
47
+ // request leaves the whole widget consistent.
48
+ //
49
+ // Choices (ponytail): a transferred option arrives deselected (selection is a
50
+ // per-list concept); toggling the mode to single-select collapses each list's
51
+ // selection to its active option; Alt+Up/Down are only handled in single-select
52
+ // mode (example 2 has no reorder buttons); the toolbar buttons are modelled by
53
+ // their keyboard shortcuts on the focused list, so a transfer with nothing
54
+ // selected or no focused list is a no-op.
55
+ //
56
+ // Deliberate omissions: the live-region "Last change" announcements, the
57
+ // toolbar roving tabindex, aria-keyshortcuts, typeahead (listbox-typeahead.qnt),
58
+ // scrolling, disabled buttons, and add/remove of brand-new options (the option
59
+ // set is fixed at four).
60
+ //
61
+ // Generator notes: no parameterized actions and no `let`, so a request spells
62
+ // `landed(...)` out per assignment and every action assigns every var.
63
+ module apg_listbox_rearrangeable {
64
+ import aria.* from "../../shared/aria"
65
+
66
+ var a0: int // list A, slot 0..3: option id (noItem = empty)
67
+ var a1: int // list A, slot 1
68
+ var a2: int // list A, slot 2
69
+ var a3: int // list A, slot 3
70
+ var b0: int // list B, slot 0..3: option id (noItem = empty)
71
+ var b1: int // list B, slot 1
72
+ var b2: int // list B, slot 2
73
+ var b3: int // list B, slot 3
74
+ var activeA: int // active option of list A (noItem = list empty)
75
+ var activeB: int // active option of list B (noItem = list empty)
76
+ var focusList: str // "none" | "a" | "b": the listbox holding focus
77
+ var sel: int // selection bitmask over option ids
78
+ var multi: bool // mode: multi-select (example 2) vs single-select (example 1)
79
+ var controlled: bool // the host owns the value (`value` prop present)
80
+ var accepting: bool // controlled host that answers every request with its intent
81
+ var valueProp0: bool // the controlled value prop, one bool per option id
82
+ var valueProp1: bool
83
+ var valueProp2: bool
84
+ var valueProp3: bool
85
+ var requested: int // intent of the last request (selection mask)
86
+ var outcome: str // "none" | "accepted" | "rejected"
87
+ var base: int // selection when the last request was received
88
+ var requests: int // number of requests that fired onValueChange
89
+
90
+ def itemCount: int = 4
91
+ // "no option" / "empty slot" / "not found" sentinel
92
+ def noItem: int = 4
93
+
94
+ def pow2(i: int): int =
95
+ if (i == 0) 1
96
+ else if (i == 1) 2
97
+ else if (i == 2) 4
98
+ else if (i == 3) 8
99
+ else 16
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
+ def toggle(mask: int, i: int): int =
108
+ if (bit(mask, i)) mask - pow2(i) else mask + pow2(i)
109
+ def focusBit(opt: int): int = if (opt == noItem) 0 else pow2(opt)
110
+
111
+ // --- one list, as four slots ----------------------------------------------
112
+ def slotAt(s0: int, s1: int, s2: int, s3: int, k: int): int =
113
+ if (k == 0) s0
114
+ else if (k == 1) s1
115
+ else if (k == 2) s2
116
+ else if (k == 3) s3
117
+ else noItem
118
+ def lenOf(s0: int, s1: int, s2: int, s3: int): int =
119
+ (if (s0 != noItem) 1 else 0)
120
+ + (if (s1 != noItem) 1 else 0)
121
+ + (if (s2 != noItem) 1 else 0)
122
+ + (if (s3 != noItem) 1 else 0)
123
+ def posOf(s0: int, s1: int, s2: int, s3: int, opt: int): int =
124
+ if (opt == noItem) noItem
125
+ else if (s0 == opt) 0
126
+ else if (s1 == opt) 1
127
+ else if (s2 == opt) 2
128
+ else if (s3 == opt) 3
129
+ else noItem
130
+ def holds(s0: int, s1: int, s2: int, s3: int, opt: int): bool =
131
+ and(opt != noItem, or(s0 == opt, s1 == opt, s2 == opt, s3 == opt))
132
+ def selOf(mask: int, opt: int): int = if (opt == noItem) 0 else (if (bit(mask, opt)) pow2(opt) else 0)
133
+ // the selected options that are in this list, as a mask
134
+ def selIn(mask: int, s0: int, s1: int, s2: int, s3: int): int =
135
+ selOf(mask, s0) + selOf(mask, s1) + selOf(mask, s2) + selOf(mask, s3)
136
+ // Tab entry: the first selected option of the list, else its first option
137
+ def entryItem(mask: int, s0: int, s1: int, s2: int, s3: int): int =
138
+ if (selOf(mask, s0) != 0) s0
139
+ else if (selOf(mask, s1) != 0) s1
140
+ else if (selOf(mask, s2) != 0) s2
141
+ else if (selOf(mask, s3) != 0) s3
142
+ else s0
143
+
144
+ // --- keyboard targets inside a list -------------------------------------
145
+ def nextItem(s0: int, s1: int, s2: int, s3: int, act: int): int =
146
+ if (posOf(s0, s1, s2, s3, act) == noItem) noItem
147
+ else slotAt(s0, s1, s2, s3, if (posOf(s0, s1, s2, s3, act) + 1 < lenOf(s0, s1, s2, s3)) posOf(s0, s1, s2, s3, act) + 1 else posOf(s0, s1, s2, s3, act))
148
+ def prevItem(s0: int, s1: int, s2: int, s3: int, act: int): int =
149
+ if (posOf(s0, s1, s2, s3, act) == noItem) noItem
150
+ else slotAt(s0, s1, s2, s3, if (posOf(s0, s1, s2, s3, act) > 0) posOf(s0, s1, s2, s3, act) - 1 else posOf(s0, s1, s2, s3, act))
151
+ def lastItem(s0: int, s1: int, s2: int, s3: int): int = slotAt(s0, s1, s2, s3, lenOf(s0, s1, s2, s3) - 1)
152
+
153
+ // --- reordering (Alt+Up / Alt+Down) ---------------------------------------
154
+ def canUp(s0: int, s1: int, s2: int, s3: int, act: int): bool = and(act != noItem, posOf(s0, s1, s2, s3, act) > 0)
155
+ def canDown(s0: int, s1: int, s2: int, s3: int, act: int): bool =
156
+ and(act != noItem, posOf(s0, s1, s2, s3, act) + 1 < lenOf(s0, s1, s2, s3))
157
+ def swapUpSlot(s0: int, s1: int, s2: int, s3: int, act: int, k: int): int =
158
+ if (not(canUp(s0, s1, s2, s3, act))) slotAt(s0, s1, s2, s3, k)
159
+ else if (k == posOf(s0, s1, s2, s3, act) - 1) slotAt(s0, s1, s2, s3, posOf(s0, s1, s2, s3, act))
160
+ else if (k == posOf(s0, s1, s2, s3, act)) slotAt(s0, s1, s2, s3, posOf(s0, s1, s2, s3, act) - 1)
161
+ else slotAt(s0, s1, s2, s3, k)
162
+ def swapDownSlot(s0: int, s1: int, s2: int, s3: int, act: int, k: int): int =
163
+ if (not(canDown(s0, s1, s2, s3, act))) slotAt(s0, s1, s2, s3, k)
164
+ else if (k == posOf(s0, s1, s2, s3, act)) slotAt(s0, s1, s2, s3, posOf(s0, s1, s2, s3, act) + 1)
165
+ else if (k == posOf(s0, s1, s2, s3, act) + 1) slotAt(s0, s1, s2, s3, posOf(s0, s1, s2, s3, act))
166
+ else slotAt(s0, s1, s2, s3, k)
167
+
168
+ // --- transferring the selected options to the other list -------------------
169
+ def keeps(mask: int, opt: int): bool = and(opt != noItem, not(bit(mask, opt)))
170
+ def moves(mask: int, opt: int): bool = and(opt != noItem, bit(mask, opt))
171
+ def keepsN(mask: int, opt: int): int = if (keeps(mask, opt)) 1 else 0
172
+ def movesN(mask: int, opt: int): int = if (moves(mask, opt)) 1 else 0
173
+ // how many kept / moved options sit in slots before slot j
174
+ def keptBefore(mask: int, s0: int, s1: int, s2: int, s3: int, j: int): int =
175
+ if (j == 0) 0
176
+ else if (j == 1) keepsN(mask, s0)
177
+ else if (j == 2) keepsN(mask, s0) + keepsN(mask, s1)
178
+ else keepsN(mask, s0) + keepsN(mask, s1) + keepsN(mask, s2)
179
+ def movedBefore(mask: int, s0: int, s1: int, s2: int, s3: int, j: int): int =
180
+ if (j == 0) 0
181
+ else if (j == 1) movesN(mask, s0)
182
+ else if (j == 2) movesN(mask, s0) + movesN(mask, s1)
183
+ else movesN(mask, s0) + movesN(mask, s1) + movesN(mask, s2)
184
+ // the k-th option that stays / leaves, in slot order (noItem = none)
185
+ def keptAt(mask: int, s0: int, s1: int, s2: int, s3: int, k: int): int =
186
+ if (and(keeps(mask, s0), keptBefore(mask, s0, s1, s2, s3, 0) == k)) s0
187
+ else if (and(keeps(mask, s1), keptBefore(mask, s0, s1, s2, s3, 1) == k)) s1
188
+ else if (and(keeps(mask, s2), keptBefore(mask, s0, s1, s2, s3, 2) == k)) s2
189
+ else if (and(keeps(mask, s3), keptBefore(mask, s0, s1, s2, s3, 3) == k)) s3
190
+ else noItem
191
+ def movedAt(mask: int, s0: int, s1: int, s2: int, s3: int, k: int): int =
192
+ if (and(moves(mask, s0), movedBefore(mask, s0, s1, s2, s3, 0) == k)) s0
193
+ else if (and(moves(mask, s1), movedBefore(mask, s0, s1, s2, s3, 1) == k)) s1
194
+ else if (and(moves(mask, s2), movedBefore(mask, s0, s1, s2, s3, 2) == k)) s2
195
+ else if (and(moves(mask, s3), movedBefore(mask, s0, s1, s2, s3, 3) == k)) s3
196
+ else noItem
197
+ def movedCount(mask: int, s0: int, s1: int, s2: int, s3: int): int = popcount(selIn(mask, s0, s1, s2, s3))
198
+ // the destination list: its options, then the moved ones in source order
199
+ def appendAt(mask: int, t0: int, t1: int, t2: int, t3: int, s0: int, s1: int, s2: int, s3: int, k: int): int =
200
+ if (k < lenOf(t0, t1, t2, t3)) slotAt(t0, t1, t2, t3, k)
201
+ else movedAt(mask, s0, s1, s2, s3, k - lenOf(t0, t1, t2, t3))
202
+ // the source list's focus after its selected options left: stay if the
203
+ // focused option stays, else the first kept option after it, else the last
204
+ // kept option before it
205
+ def focusAfterRemoval(mask: int, s0: int, s1: int, s2: int, s3: int, act: int): int =
206
+ if (act == noItem) noItem
207
+ else if (not(moves(mask, act))) act
208
+ else if (and(posOf(s0, s1, s2, s3, act) < 1, keeps(mask, s1))) s1
209
+ else if (and(posOf(s0, s1, s2, s3, act) < 2, keeps(mask, s2))) s2
210
+ else if (and(posOf(s0, s1, s2, s3, act) < 3, keeps(mask, s3))) s3
211
+ else if (and(posOf(s0, s1, s2, s3, act) > 2, keeps(mask, s2))) s2
212
+ else if (and(posOf(s0, s1, s2, s3, act) > 1, keeps(mask, s1))) s1
213
+ else if (and(posOf(s0, s1, s2, s3, act) > 0, keeps(mask, s0))) s0
214
+ else noItem
215
+
216
+ // --- request / commit (map ticket rt1p) ---------------------------------
217
+ def b2i(b: bool): int = if (b) 1 else 0
218
+ // the controlled value prop, packed into a mask
219
+ def propsMask: int =
220
+ b2i(valueProp0) + 2 * b2i(valueProp1) + 4 * b2i(valueProp2) + 8 * b2i(valueProp3)
221
+ // the host flips one selected option of its own copy of the value
222
+ def flipAt(n: int, i: int): int = if (bit(n, i)) n - pow2(i) else n + pow2(i)
223
+ // controlled host that keeps the old value: the request is rejected
224
+ def rejects: bool = and(controlled, not(accepting))
225
+ // controlled host that writes the intent back through the value prop
226
+ def hostWrites: bool = and(controlled, accepting)
227
+ def verdict: str = if (rejects) "rejected" else "accepted"
228
+ // the value after a request for `want`
229
+ def landed(reject: bool, current: int, want: int): int = if (reject) current else want
230
+ // one value-prop member after a request for `want`
231
+ def propBit(writes: bool, current: bool, want: int, i: int): bool =
232
+ if (writes) bit(want, i) else current
233
+ // the intended selection of each request
234
+ def focusAIntent: int =
235
+ if (and(not(multi), not(focusList == "a"))) selIn(sel, b0, b1, b2, b3) + focusBit(entryItem(sel, a0, a1, a2, a3)) else sel
236
+ def focusBIntent: int =
237
+ if (and(not(multi), not(focusList == "b"))) selIn(sel, a0, a1, a2, a3) + focusBit(entryItem(sel, b0, b1, b2, b3)) else sel
238
+ def downIntent: int =
239
+ if (and(not(multi), focusList == "a")) selIn(sel, b0, b1, b2, b3) + focusBit(nextItem(a0, a1, a2, a3, activeA))
240
+ else if (and(not(multi), focusList == "b")) selIn(sel, a0, a1, a2, a3) + focusBit(nextItem(b0, b1, b2, b3, activeB))
241
+ else sel
242
+ def upIntent: int =
243
+ if (and(not(multi), focusList == "a")) selIn(sel, b0, b1, b2, b3) + focusBit(prevItem(a0, a1, a2, a3, activeA))
244
+ else if (and(not(multi), focusList == "b")) selIn(sel, a0, a1, a2, a3) + focusBit(prevItem(b0, b1, b2, b3, activeB))
245
+ else sel
246
+ def homeIntent: int =
247
+ if (and(not(multi), focusList == "a")) selIn(sel, b0, b1, b2, b3) + focusBit(a0)
248
+ else if (and(not(multi), focusList == "b")) selIn(sel, a0, a1, a2, a3) + focusBit(b0)
249
+ else sel
250
+ def endIntent: int =
251
+ if (and(not(multi), focusList == "a")) selIn(sel, b0, b1, b2, b3) + focusBit(lastItem(a0, a1, a2, a3))
252
+ else if (and(not(multi), focusList == "b")) selIn(sel, a0, a1, a2, a3) + focusBit(lastItem(b0, b1, b2, b3))
253
+ else sel
254
+ def spaceIntent: int =
255
+ if (and(multi, focusList == "a", activeA != noItem)) toggle(sel, activeA)
256
+ else if (and(multi, focusList == "b", activeB != noItem)) toggle(sel, activeB)
257
+ else sel
258
+ def transferIntent: int =
259
+ if (and(focusList == "a", movedCount(sel, a0, a1, a2, a3) > 0)) selIn(sel, b0, b1, b2, b3) + (if (multi) 0 else focusBit(focusAfterRemoval(sel, a0, a1, a2, a3, activeA)))
260
+ else if (and(focusList == "b", movedCount(sel, b0, b1, b2, b3) > 0)) selIn(sel, a0, a1, a2, a3) + (if (multi) 0 else focusBit(focusAfterRemoval(sel, b0, b1, b2, b3, activeB)))
261
+ else sel
262
+ def setMultiIntent(flag: bool): int =
263
+ if (flag) sel else focusBit(activeA) + focusBit(activeB)
264
+
265
+ // --- ARIA contract, derived from the state ------------------------------
266
+ def ariaSelected(mask: int, opt: int): bool = bit(mask, opt)
267
+ def ariaActiveDescendant(focusedList: str, which: str, act: int): int =
268
+ if (focusedList == which) act else noItem
269
+ def listboxTabIndex: int = 0
270
+
271
+ // --- invariant helpers ----------------------------------------------------
272
+ def occurrences(opt: int): int =
273
+ (if (a0 == opt) 1 else 0)
274
+ + (if (a1 == opt) 1 else 0)
275
+ + (if (a2 == opt) 1 else 0)
276
+ + (if (a3 == opt) 1 else 0)
277
+ + (if (b0 == opt) 1 else 0)
278
+ + (if (b1 == opt) 1 else 0)
279
+ + (if (b2 == opt) 1 else 0)
280
+ + (if (b3 == opt) 1 else 0)
281
+ def packed(s0: int, s1: int, s2: int, s3: int): bool =
282
+ and(or(s1 == noItem, s0 != noItem), or(s2 == noItem, s1 != noItem), or(s3 == noItem, s2 != noItem))
283
+ def activeValid(s0: int, s1: int, s2: int, s3: int, act: int): bool =
284
+ if (s0 == noItem) act == noItem else holds(s0, s1, s2, s3, act)
285
+
286
+ action init = all {
287
+ a0' = 0,
288
+ a1' = 1,
289
+ a2' = 2,
290
+ a3' = 3,
291
+ b0' = noItem,
292
+ b1' = noItem,
293
+ b2' = noItem,
294
+ b3' = noItem,
295
+ activeA' = 0,
296
+ activeB' = noItem,
297
+ focusList' = "none",
298
+ sel' = 0,
299
+ multi' = false,
300
+ controlled' = false,
301
+ accepting' = false,
302
+ valueProp0' = false,
303
+ valueProp1' = false,
304
+ valueProp2' = false,
305
+ valueProp3' = false,
306
+ requested' = 0,
307
+ outcome' = "none",
308
+ base' = 0,
309
+ requests' = 0
310
+ }
311
+
312
+ // Tab into list A (first selected option, else first option).
313
+ action focus_a = all {
314
+ a0' = a0,
315
+ a1' = a1,
316
+ a2' = a2,
317
+ a3' = a3,
318
+ b0' = b0,
319
+ b1' = b1,
320
+ b2' = b2,
321
+ b3' = b3,
322
+ activeA' = if (focusList == "a") activeA else entryItem(sel, a0, a1, a2, a3),
323
+ activeB' = activeB,
324
+ focusList' = "a",
325
+ sel' = landed(rejects, sel, focusAIntent),
326
+ multi' = multi,
327
+ controlled' = controlled,
328
+ accepting' = accepting,
329
+ valueProp0' = propBit(hostWrites, valueProp0, focusAIntent, 0),
330
+ valueProp1' = propBit(hostWrites, valueProp1, focusAIntent, 1),
331
+ valueProp2' = propBit(hostWrites, valueProp2, focusAIntent, 2),
332
+ valueProp3' = propBit(hostWrites, valueProp3, focusAIntent, 3),
333
+ requested' = if (focusAIntent != sel) focusAIntent else requested,
334
+ outcome' = if (focusAIntent != sel) verdict else outcome,
335
+ base' = if (focusAIntent != sel) sel else base,
336
+ requests' = if (focusAIntent != sel) requests + 1 else requests
337
+ }
338
+
339
+ // Tab into list B.
340
+ action focus_b = all {
341
+ a0' = a0,
342
+ a1' = a1,
343
+ a2' = a2,
344
+ a3' = a3,
345
+ b0' = b0,
346
+ b1' = b1,
347
+ b2' = b2,
348
+ b3' = b3,
349
+ activeA' = activeA,
350
+ activeB' = if (focusList == "b") activeB else entryItem(sel, b0, b1, b2, b3),
351
+ focusList' = "b",
352
+ sel' = landed(rejects, sel, focusBIntent),
353
+ multi' = multi,
354
+ controlled' = controlled,
355
+ accepting' = accepting,
356
+ valueProp0' = propBit(hostWrites, valueProp0, focusBIntent, 0),
357
+ valueProp1' = propBit(hostWrites, valueProp1, focusBIntent, 1),
358
+ valueProp2' = propBit(hostWrites, valueProp2, focusBIntent, 2),
359
+ valueProp3' = propBit(hostWrites, valueProp3, focusBIntent, 3),
360
+ requested' = if (focusBIntent != sel) focusBIntent else requested,
361
+ outcome' = if (focusBIntent != sel) verdict else outcome,
362
+ base' = if (focusBIntent != sel) sel else base,
363
+ requests' = if (focusBIntent != sel) requests + 1 else requests
364
+ }
365
+
366
+ // Tab out of whichever list holds focus.
367
+ action blur = all {
368
+ a0' = a0,
369
+ a1' = a1,
370
+ a2' = a2,
371
+ a3' = a3,
372
+ b0' = b0,
373
+ b1' = b1,
374
+ b2' = b2,
375
+ b3' = b3,
376
+ activeA' = activeA,
377
+ activeB' = activeB,
378
+ focusList' = "none",
379
+ sel' = sel,
380
+ multi' = multi,
381
+ controlled' = controlled,
382
+ accepting' = accepting,
383
+ valueProp0' = valueProp0,
384
+ valueProp1' = valueProp1,
385
+ valueProp2' = valueProp2,
386
+ valueProp3' = valueProp3,
387
+ requested' = requested,
388
+ outcome' = outcome,
389
+ base' = base,
390
+ requests' = requests
391
+ }
392
+
393
+ // Move focus inside the focused list (and select it in single-select mode).
394
+ action arrow_down = all {
395
+ a0' = a0,
396
+ a1' = a1,
397
+ a2' = a2,
398
+ a3' = a3,
399
+ b0' = b0,
400
+ b1' = b1,
401
+ b2' = b2,
402
+ b3' = b3,
403
+ activeA' = if (focusList == "a") nextItem(a0, a1, a2, a3, activeA) else activeA,
404
+ activeB' = if (focusList == "b") nextItem(b0, b1, b2, b3, activeB) else activeB,
405
+ focusList' = focusList,
406
+ sel' = landed(rejects, sel, downIntent),
407
+ multi' = multi,
408
+ controlled' = controlled,
409
+ accepting' = accepting,
410
+ valueProp0' = propBit(hostWrites, valueProp0, downIntent, 0),
411
+ valueProp1' = propBit(hostWrites, valueProp1, downIntent, 1),
412
+ valueProp2' = propBit(hostWrites, valueProp2, downIntent, 2),
413
+ valueProp3' = propBit(hostWrites, valueProp3, downIntent, 3),
414
+ requested' = if (downIntent != sel) downIntent else requested,
415
+ outcome' = if (downIntent != sel) verdict else outcome,
416
+ base' = if (downIntent != sel) sel else base,
417
+ requests' = if (downIntent != sel) requests + 1 else requests
418
+ }
419
+
420
+ action arrow_up = all {
421
+ a0' = a0,
422
+ a1' = a1,
423
+ a2' = a2,
424
+ a3' = a3,
425
+ b0' = b0,
426
+ b1' = b1,
427
+ b2' = b2,
428
+ b3' = b3,
429
+ activeA' = if (focusList == "a") prevItem(a0, a1, a2, a3, activeA) else activeA,
430
+ activeB' = if (focusList == "b") prevItem(b0, b1, b2, b3, activeB) else activeB,
431
+ focusList' = focusList,
432
+ sel' = landed(rejects, sel, upIntent),
433
+ multi' = multi,
434
+ controlled' = controlled,
435
+ accepting' = accepting,
436
+ valueProp0' = propBit(hostWrites, valueProp0, upIntent, 0),
437
+ valueProp1' = propBit(hostWrites, valueProp1, upIntent, 1),
438
+ valueProp2' = propBit(hostWrites, valueProp2, upIntent, 2),
439
+ valueProp3' = propBit(hostWrites, valueProp3, upIntent, 3),
440
+ requested' = if (upIntent != sel) upIntent else requested,
441
+ outcome' = if (upIntent != sel) verdict else outcome,
442
+ base' = if (upIntent != sel) sel else base,
443
+ requests' = if (upIntent != sel) requests + 1 else requests
444
+ }
445
+
446
+ action home = all {
447
+ a0' = a0,
448
+ a1' = a1,
449
+ a2' = a2,
450
+ a3' = a3,
451
+ b0' = b0,
452
+ b1' = b1,
453
+ b2' = b2,
454
+ b3' = b3,
455
+ activeA' = if (focusList == "a") a0 else activeA,
456
+ activeB' = if (focusList == "b") b0 else activeB,
457
+ focusList' = focusList,
458
+ sel' = landed(rejects, sel, homeIntent),
459
+ multi' = multi,
460
+ controlled' = controlled,
461
+ accepting' = accepting,
462
+ valueProp0' = propBit(hostWrites, valueProp0, homeIntent, 0),
463
+ valueProp1' = propBit(hostWrites, valueProp1, homeIntent, 1),
464
+ valueProp2' = propBit(hostWrites, valueProp2, homeIntent, 2),
465
+ valueProp3' = propBit(hostWrites, valueProp3, homeIntent, 3),
466
+ requested' = if (homeIntent != sel) homeIntent else requested,
467
+ outcome' = if (homeIntent != sel) verdict else outcome,
468
+ base' = if (homeIntent != sel) sel else base,
469
+ requests' = if (homeIntent != sel) requests + 1 else requests
470
+ }
471
+
472
+ action end = all {
473
+ a0' = a0,
474
+ a1' = a1,
475
+ a2' = a2,
476
+ a3' = a3,
477
+ b0' = b0,
478
+ b1' = b1,
479
+ b2' = b2,
480
+ b3' = b3,
481
+ activeA' = if (focusList == "a") lastItem(a0, a1, a2, a3) else activeA,
482
+ activeB' = if (focusList == "b") lastItem(b0, b1, b2, b3) else activeB,
483
+ focusList' = focusList,
484
+ sel' = landed(rejects, sel, endIntent),
485
+ multi' = multi,
486
+ controlled' = controlled,
487
+ accepting' = accepting,
488
+ valueProp0' = propBit(hostWrites, valueProp0, endIntent, 0),
489
+ valueProp1' = propBit(hostWrites, valueProp1, endIntent, 1),
490
+ valueProp2' = propBit(hostWrites, valueProp2, endIntent, 2),
491
+ valueProp3' = propBit(hostWrites, valueProp3, endIntent, 3),
492
+ requested' = if (endIntent != sel) endIntent else requested,
493
+ outcome' = if (endIntent != sel) verdict else outcome,
494
+ base' = if (endIntent != sel) sel else base,
495
+ requests' = if (endIntent != sel) requests + 1 else requests
496
+ }
497
+
498
+ // Multi-select: toggle the active option. Single-select: selection already
499
+ // follows focus, nothing to do.
500
+ action space = all {
501
+ a0' = a0,
502
+ a1' = a1,
503
+ a2' = a2,
504
+ a3' = a3,
505
+ b0' = b0,
506
+ b1' = b1,
507
+ b2' = b2,
508
+ b3' = b3,
509
+ activeA' = activeA,
510
+ activeB' = activeB,
511
+ focusList' = focusList,
512
+ sel' = landed(rejects, sel, spaceIntent),
513
+ multi' = multi,
514
+ controlled' = controlled,
515
+ accepting' = accepting,
516
+ valueProp0' = propBit(hostWrites, valueProp0, spaceIntent, 0),
517
+ valueProp1' = propBit(hostWrites, valueProp1, spaceIntent, 1),
518
+ valueProp2' = propBit(hostWrites, valueProp2, spaceIntent, 2),
519
+ valueProp3' = propBit(hostWrites, valueProp3, spaceIntent, 3),
520
+ requested' = if (spaceIntent != sel) spaceIntent else requested,
521
+ outcome' = if (spaceIntent != sel) verdict else outcome,
522
+ base' = if (spaceIntent != sel) sel else base,
523
+ requests' = if (spaceIntent != sel) requests + 1 else requests
524
+ }
525
+
526
+ // Alt+Up (single-select): the focused option swaps with its predecessor;
527
+ // focus stays on it.
528
+ action move_up = all {
529
+ a0' = if (and(not(multi), focusList == "a")) swapUpSlot(a0, a1, a2, a3, activeA, 0) else a0,
530
+ a1' = if (and(not(multi), focusList == "a")) swapUpSlot(a0, a1, a2, a3, activeA, 1) else a1,
531
+ a2' = if (and(not(multi), focusList == "a")) swapUpSlot(a0, a1, a2, a3, activeA, 2) else a2,
532
+ a3' = if (and(not(multi), focusList == "a")) swapUpSlot(a0, a1, a2, a3, activeA, 3) else a3,
533
+ b0' = if (and(not(multi), focusList == "b")) swapUpSlot(b0, b1, b2, b3, activeB, 0) else b0,
534
+ b1' = if (and(not(multi), focusList == "b")) swapUpSlot(b0, b1, b2, b3, activeB, 1) else b1,
535
+ b2' = if (and(not(multi), focusList == "b")) swapUpSlot(b0, b1, b2, b3, activeB, 2) else b2,
536
+ b3' = if (and(not(multi), focusList == "b")) swapUpSlot(b0, b1, b2, b3, activeB, 3) else b3,
537
+ activeA' = activeA,
538
+ activeB' = activeB,
539
+ focusList' = focusList,
540
+ sel' = sel,
541
+ multi' = multi,
542
+ controlled' = controlled,
543
+ accepting' = accepting,
544
+ valueProp0' = valueProp0,
545
+ valueProp1' = valueProp1,
546
+ valueProp2' = valueProp2,
547
+ valueProp3' = valueProp3,
548
+ requested' = requested,
549
+ outcome' = outcome,
550
+ base' = base,
551
+ requests' = requests
552
+ }
553
+
554
+ // Alt+Down (single-select).
555
+ action move_down = all {
556
+ a0' = if (and(not(multi), focusList == "a")) swapDownSlot(a0, a1, a2, a3, activeA, 0) else a0,
557
+ a1' = if (and(not(multi), focusList == "a")) swapDownSlot(a0, a1, a2, a3, activeA, 1) else a1,
558
+ a2' = if (and(not(multi), focusList == "a")) swapDownSlot(a0, a1, a2, a3, activeA, 2) else a2,
559
+ a3' = if (and(not(multi), focusList == "a")) swapDownSlot(a0, a1, a2, a3, activeA, 3) else a3,
560
+ b0' = if (and(not(multi), focusList == "b")) swapDownSlot(b0, b1, b2, b3, activeB, 0) else b0,
561
+ b1' = if (and(not(multi), focusList == "b")) swapDownSlot(b0, b1, b2, b3, activeB, 1) else b1,
562
+ b2' = if (and(not(multi), focusList == "b")) swapDownSlot(b0, b1, b2, b3, activeB, 2) else b2,
563
+ b3' = if (and(not(multi), focusList == "b")) swapDownSlot(b0, b1, b2, b3, activeB, 3) else b3,
564
+ activeA' = activeA,
565
+ activeB' = activeB,
566
+ focusList' = focusList,
567
+ sel' = sel,
568
+ multi' = multi,
569
+ controlled' = controlled,
570
+ accepting' = accepting,
571
+ valueProp0' = valueProp0,
572
+ valueProp1' = valueProp1,
573
+ valueProp2' = valueProp2,
574
+ valueProp3' = valueProp3,
575
+ requested' = requested,
576
+ outcome' = outcome,
577
+ base' = base,
578
+ requests' = requests
579
+ }
580
+
581
+ // Delete/Enter: move the selected options of the focused list to the other
582
+ // list; they arrive deselected, source focus lands on the next remaining option.
583
+ action transfer = all {
584
+ a0' = if (rejects) a0 else (if (and(focusList == "a", movedCount(sel, a0, a1, a2, a3) > 0)) keptAt(sel, a0, a1, a2, a3, 0) else if (and(focusList == "b", movedCount(sel, b0, b1, b2, b3) > 0)) appendAt(sel, a0, a1, a2, a3, b0, b1, b2, b3, 0) else a0),
585
+ a1' = if (rejects) a1 else (if (and(focusList == "a", movedCount(sel, a0, a1, a2, a3) > 0)) keptAt(sel, a0, a1, a2, a3, 1) else if (and(focusList == "b", movedCount(sel, b0, b1, b2, b3) > 0)) appendAt(sel, a0, a1, a2, a3, b0, b1, b2, b3, 1) else a1),
586
+ a2' = if (rejects) a2 else (if (and(focusList == "a", movedCount(sel, a0, a1, a2, a3) > 0)) keptAt(sel, a0, a1, a2, a3, 2) else if (and(focusList == "b", movedCount(sel, b0, b1, b2, b3) > 0)) appendAt(sel, a0, a1, a2, a3, b0, b1, b2, b3, 2) else a2),
587
+ a3' = if (rejects) a3 else (if (and(focusList == "a", movedCount(sel, a0, a1, a2, a3) > 0)) keptAt(sel, a0, a1, a2, a3, 3) else if (and(focusList == "b", movedCount(sel, b0, b1, b2, b3) > 0)) appendAt(sel, a0, a1, a2, a3, b0, b1, b2, b3, 3) else a3),
588
+ b0' = if (rejects) b0 else (if (and(focusList == "b", movedCount(sel, b0, b1, b2, b3) > 0)) keptAt(sel, b0, b1, b2, b3, 0) else if (and(focusList == "a", movedCount(sel, a0, a1, a2, a3) > 0)) appendAt(sel, b0, b1, b2, b3, a0, a1, a2, a3, 0) else b0),
589
+ b1' = if (rejects) b1 else (if (and(focusList == "b", movedCount(sel, b0, b1, b2, b3) > 0)) keptAt(sel, b0, b1, b2, b3, 1) else if (and(focusList == "a", movedCount(sel, a0, a1, a2, a3) > 0)) appendAt(sel, b0, b1, b2, b3, a0, a1, a2, a3, 1) else b1),
590
+ b2' = if (rejects) b2 else (if (and(focusList == "b", movedCount(sel, b0, b1, b2, b3) > 0)) keptAt(sel, b0, b1, b2, b3, 2) else if (and(focusList == "a", movedCount(sel, a0, a1, a2, a3) > 0)) appendAt(sel, b0, b1, b2, b3, a0, a1, a2, a3, 2) else b2),
591
+ b3' = if (rejects) b3 else (if (and(focusList == "b", movedCount(sel, b0, b1, b2, b3) > 0)) keptAt(sel, b0, b1, b2, b3, 3) else if (and(focusList == "a", movedCount(sel, a0, a1, a2, a3) > 0)) appendAt(sel, b0, b1, b2, b3, a0, a1, a2, a3, 3) else b3),
592
+ activeA' = if (rejects) activeA else (if (and(focusList == "a", movedCount(sel, a0, a1, a2, a3) > 0)) focusAfterRemoval(sel, a0, a1, a2, a3, activeA) else if (and(focusList == "b", movedCount(sel, b0, b1, b2, b3) > 0)) (if (activeA == noItem) movedAt(sel, b0, b1, b2, b3, 0) else activeA) else activeA),
593
+ activeB' = if (rejects) activeB else (if (and(focusList == "b", movedCount(sel, b0, b1, b2, b3) > 0)) focusAfterRemoval(sel, b0, b1, b2, b3, activeB) else if (and(focusList == "a", movedCount(sel, a0, a1, a2, a3) > 0)) (if (activeB == noItem) movedAt(sel, a0, a1, a2, a3, 0) else activeB) else activeB),
594
+ focusList' = focusList,
595
+ sel' = landed(rejects, sel, transferIntent),
596
+ multi' = multi,
597
+ controlled' = controlled,
598
+ accepting' = accepting,
599
+ valueProp0' = propBit(hostWrites, valueProp0, transferIntent, 0),
600
+ valueProp1' = propBit(hostWrites, valueProp1, transferIntent, 1),
601
+ valueProp2' = propBit(hostWrites, valueProp2, transferIntent, 2),
602
+ valueProp3' = propBit(hostWrites, valueProp3, transferIntent, 3),
603
+ requested' = if (transferIntent != sel) transferIntent else requested,
604
+ outcome' = if (transferIntent != sel) verdict else outcome,
605
+ base' = if (transferIntent != sel) sel else base,
606
+ requests' = if (transferIntent != sel) requests + 1 else requests
607
+ }
608
+
609
+ // Mode prop; switching to single-select collapses each list to its active
610
+ // option.
611
+ action set_multi = {
612
+ nondet flag = Set(true, false).oneOf()
613
+ all {
614
+ a0' = a0,
615
+ a1' = a1,
616
+ a2' = a2,
617
+ a3' = a3,
618
+ b0' = b0,
619
+ b1' = b1,
620
+ b2' = b2,
621
+ b3' = b3,
622
+ activeA' = activeA,
623
+ activeB' = activeB,
624
+ focusList' = focusList,
625
+ sel' = landed(rejects, sel, setMultiIntent(flag)),
626
+ multi' = flag,
627
+ controlled' = controlled,
628
+ accepting' = accepting,
629
+ valueProp0' = propBit(hostWrites, valueProp0, setMultiIntent(flag), 0),
630
+ valueProp1' = propBit(hostWrites, valueProp1, setMultiIntent(flag), 1),
631
+ valueProp2' = propBit(hostWrites, valueProp2, setMultiIntent(flag), 2),
632
+ valueProp3' = propBit(hostWrites, valueProp3, setMultiIntent(flag), 3),
633
+ requested' = if (setMultiIntent(flag) != sel) setMultiIntent(flag) else requested,
634
+ outcome' = if (setMultiIntent(flag) != sel) verdict else outcome,
635
+ base' = if (setMultiIntent(flag) != sel) sel else base,
636
+ requests' = if (setMultiIntent(flag) != sel) requests + 1 else requests
637
+ }
638
+ }
639
+
640
+ // commit: the host writes the controlled value (a no-op while uncontrolled)
641
+ action setValue = {
642
+ nondet v = Set(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15).oneOf()
643
+ all {
644
+ a0' = a0,
645
+ a1' = a1,
646
+ a2' = a2,
647
+ a3' = a3,
648
+ b0' = b0,
649
+ b1' = b1,
650
+ b2' = b2,
651
+ b3' = b3,
652
+ activeA' = activeA,
653
+ activeB' = activeB,
654
+ focusList' = focusList,
655
+ sel' = if (controlled) v else sel,
656
+ multi' = multi,
657
+ controlled' = controlled,
658
+ accepting' = accepting,
659
+ valueProp0' = if (controlled) bit(v, 0) else valueProp0,
660
+ valueProp1' = if (controlled) bit(v, 1) else valueProp1,
661
+ valueProp2' = if (controlled) bit(v, 2) else valueProp2,
662
+ valueProp3' = if (controlled) bit(v, 3) else valueProp3,
663
+ requested' = requested,
664
+ outcome' = if (controlled) "none" else outcome,
665
+ base' = base,
666
+ requests' = requests
667
+ }
668
+ }
669
+
670
+ // commit: the host flips one option of its own copy of the value
671
+ action hostToggle = {
672
+ nondet panel = Set(0, 1, 2, 3).oneOf()
673
+ all {
674
+ a0' = a0,
675
+ a1' = a1,
676
+ a2' = a2,
677
+ a3' = a3,
678
+ b0' = b0,
679
+ b1' = b1,
680
+ b2' = b2,
681
+ b3' = b3,
682
+ activeA' = activeA,
683
+ activeB' = activeB,
684
+ focusList' = focusList,
685
+ sel' = if (controlled) flipAt(propsMask, panel) else sel,
686
+ multi' = multi,
687
+ controlled' = controlled,
688
+ accepting' = accepting,
689
+ valueProp0' = if (controlled) bit(flipAt(propsMask, panel), 0) else valueProp0,
690
+ valueProp1' = if (controlled) bit(flipAt(propsMask, panel), 1) else valueProp1,
691
+ valueProp2' = if (controlled) bit(flipAt(propsMask, panel), 2) else valueProp2,
692
+ valueProp3' = if (controlled) bit(flipAt(propsMask, panel), 3) else valueProp3,
693
+ requested' = requested,
694
+ outcome' = if (controlled) "none" else outcome,
695
+ base' = base,
696
+ requests' = requests
697
+ }
698
+ }
699
+
700
+ // configuration: the host starts owning the value, adopting the current one
701
+ action control = all {
702
+ a0' = a0,
703
+ a1' = a1,
704
+ a2' = a2,
705
+ a3' = a3,
706
+ b0' = b0,
707
+ b1' = b1,
708
+ b2' = b2,
709
+ b3' = b3,
710
+ activeA' = activeA,
711
+ activeB' = activeB,
712
+ focusList' = focusList,
713
+ sel' = sel,
714
+ multi' = multi,
715
+ controlled' = true,
716
+ accepting' = accepting,
717
+ valueProp0' = bit(sel, 0),
718
+ valueProp1' = bit(sel, 1),
719
+ valueProp2' = bit(sel, 2),
720
+ valueProp3' = bit(sel, 3),
721
+ requested' = requested,
722
+ outcome' = outcome,
723
+ base' = base,
724
+ requests' = requests
725
+ }
726
+
727
+ // configuration: the controlled host answers every request with its intent
728
+ action acceptRequests = all {
729
+ a0' = a0,
730
+ a1' = a1,
731
+ a2' = a2,
732
+ a3' = a3,
733
+ b0' = b0,
734
+ b1' = b1,
735
+ b2' = b2,
736
+ b3' = b3,
737
+ activeA' = activeA,
738
+ activeB' = activeB,
739
+ focusList' = focusList,
740
+ sel' = sel,
741
+ multi' = multi,
742
+ controlled' = controlled,
743
+ accepting' = true,
744
+ valueProp0' = valueProp0,
745
+ valueProp1' = valueProp1,
746
+ valueProp2' = valueProp2,
747
+ valueProp3' = valueProp3,
748
+ requested' = requested,
749
+ outcome' = outcome,
750
+ base' = base,
751
+ requests' = requests
752
+ }
753
+
754
+ action step = any {
755
+ init, focus_a, focus_b, blur, arrow_down, arrow_up, home, end, space,
756
+ move_up, move_down, transfer, set_multi, setValue, hostToggle, control,
757
+ acceptRequests
758
+ }
759
+
760
+ // every option is in exactly one list, exactly once
761
+ val OptionsPartitioned: bool =
762
+ and(occurrences(0) == 1, occurrences(1) == 1, occurrences(2) == 1, occurrences(3) == 1)
763
+ // lists are packed to the left: no hole before an option
764
+ val ListsPacked: bool = and(packed(a0, a1, a2, a3), packed(b0, b1, b2, b3))
765
+ // a list's active option is one of its own options (noItem exactly when it is empty)
766
+ val ActiveInOwnList: bool = and(activeValid(a0, a1, a2, a3, activeA), activeValid(b0, b1, b2, b3, activeB))
767
+ // the mask only ever names real options
768
+ val SelectionInRange: bool = and(sel >= 0, sel < pow2(itemCount))
769
+ val FocusListKnown: bool = or(focusList == "none", focusList == "a", focusList == "b")
770
+ // APG: no more than one option selected without aria-multiselectable (per list)
771
+ // (uncontrolled: a controlled host owns the value and may hand us any set)
772
+ val SingleSelectOnePerList: bool =
773
+ and(
774
+ ariaSingleHoldsAtMostOne(controlled, multi, popcount(selIn(sel, a0, a1, a2, a3))),
775
+ ariaSingleHoldsAtMostOne(controlled, multi, popcount(selIn(sel, b0, b1, b2, b3)))
776
+ )
777
+ // single-select: the focused list's selection is its active option
778
+ val SingleSelectFollowsFocus: bool =
779
+ or(
780
+ controlled,
781
+ multi,
782
+ and(
783
+ or(focusList != "a", selIn(sel, a0, a1, a2, a3) == focusBit(activeA)),
784
+ or(focusList != "b", selIn(sel, b0, b1, b2, b3) == focusBit(activeB))
785
+ )
786
+ )
787
+ // aria-activedescendant is set on exactly the focused list
788
+ val ActiveDescendantOnFocusedListOnly: bool =
789
+ and(
790
+ (ariaActiveDescendant(focusList, "a", activeA) != noItem) == and(focusList == "a", activeA != noItem),
791
+ (ariaActiveDescendant(focusList, "b", activeB) != noItem) == and(focusList == "b", activeB != noItem)
792
+ )
793
+ // callbacks only ever count up from zero
794
+ val RequestsNonNegative: bool = requests >= 0
795
+ // controlled: only the value prop sets the state, so no event commits
796
+ val ControlledFollowsProp: bool = or(not(controlled), sel == propsMask)
797
+ // a rejected request leaves the state as it found it
798
+ val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), sel == base)
799
+ // an accepted request committed exactly its intent
800
+ val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), sel == requested)
801
+ }