@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,1004 @@
1
+ // APG Tree View — https://www.w3.org/WAI/ARIA/apg/patterns/treeview/
2
+ // (and the File Directory Treeview example, single/multi-select variants).
3
+ //
4
+ // Modelled APG sections:
5
+ // * "Keyboard Interaction": Down/Up (next/previous *visible* node, collapsed
6
+ // subtrees skipped), Right (open a closed parent, else move to its first
7
+ // child, else nothing), Left (close an open parent, else move to the
8
+ // parent, else nothing), Home/End (first / last visible node), Enter
9
+ // (activate; opens/closes a parent), type-ahead (1-2 characters), `*`
10
+ // (open every sibling of the focused node), and the multi-select keys
11
+ // Space (toggle), Shift+Down / Shift+Up (move + toggle the arrived-at
12
+ // node), Ctrl+A (select all; a second press clears).
13
+ // * "WAI-ARIA Roles, States, and Properties": aria-expanded only on parent
14
+ // nodes (absent on end nodes), aria-selected on every node,
15
+ // aria-multiselectable on the tree, aria-level / aria-setsize /
16
+ // aria-posinset on every treeitem, roving tabindex (exactly one tabbable
17
+ // treeitem). These are derived defs (`ariaExpanded`, `ariaSelected`,
18
+ // `ariaLevel`, `ariaSetsize`, `ariaPosinset`, `tabIndex`,
19
+ // `ariaMultiselectable`), never stored vars, so they cannot drift from
20
+ // the widget state.
21
+ //
22
+ // Request vs commit (map ticket rt1p). The controlled value of a tree view is the
23
+ // selected-node set; expansion stays uncontrolled (a `defaultExpanded` /
24
+ // `onExpandedChange` pair, like a menu's open state). Every user-intent event that
25
+ // changes the selection - Space, Enter (single-select), a click, Shift+Down /
26
+ // Shift+Up (multi), Ctrl+A (multi) - is a *request* carrying the intended
27
+ // `selected` mask (packed into `requested`). It fires onSelectionChange
28
+ // (`requests` counts the calls) when the intent differs from the current
29
+ // selection, and then:
30
+ // uncontrolled the request is accepted: the nodes commit the intent
31
+ // controlled, holds the host ignores it ("rejected"): the selection is untouched
32
+ // controlled, accepts the host answers with the same value (`accepting`): the
33
+ // nodes commit through the controlled value, not the event
34
+ // The controlled value `selected` is spelled out per member (`valueProp0..6`, bit
35
+ // n-1 = node n) because the generator has no collections; it is the only thing
36
+ // that changes the selection while `controlled`, and `setValue` / `hostToggle` are
37
+ // the host writing it. `control` and `acceptRequests` are configuration events
38
+ // (init is literal-only). `configure` (the multi-select switch) resets the
39
+ // uncontrolled selection only. Expansion keys move focus and toggle expansion but
40
+ // do not change the controlled value, so they commit unconditionally.
41
+ //
42
+ // Invariants for the model: ControlledFollowsProp (in controlled mode no event
43
+ // commits the selection), RejectedRequestUntouched, AcceptedRequestCommits. The
44
+ // single-select invariant is scoped to the uncontrolled path: a controlled host
45
+ // owns the selection and may hand the tree any set.
46
+ //
47
+ // `outcome` is "none" | "accepted" | "rejected" for the last request; a host
48
+ // write settles it back to "none". `base` is the selection when the last request
49
+ // was received.
50
+ //
51
+ // Tree shape (a fixed constant, ids are pre-order = document order):
52
+ //
53
+ // 1 Projects level 1, parent
54
+ // 2 Reports level 2, parent
55
+ // 3 Roadmap level 3, end node
56
+ // 4 Summary level 3, end node
57
+ // 5 Notes level 2, end node
58
+ // 6 Photos level 1, parent
59
+ // 7 Trip level 2, end node
60
+ //
61
+ // Encoding (the generator only supports str/int/bool vars): `expanded` and
62
+ // `selected` are bitmasks over node ids (bit n-1, weight 2^(n-1)); `focus` is
63
+ // the roving-tabindex node (always one of 1..7, even while the tree is not
64
+ // focused); `hasFocus` says whether DOM focus is inside the tree. Key events
65
+ // are no-ops while `hasFocus` is false. `ta1`/`ta2` are the type-ahead buffer
66
+ // ("" = empty). `activated` is the last node whose activation callback fired
67
+ // (0 = none).
68
+ //
69
+ // Deliberate omissions (ponytail):
70
+ // * selection-follows-focus (single-select here selects on Enter/Space/click
71
+ // only), Shift+Space / Ctrl+Shift+Home/End range selection (needs an
72
+ // anchor), Ctrl+Arrow, Ctrl+click, and the "focus the first selected node
73
+ // when the tree receives focus" rule (Tab returns to the roving node).
74
+ // * type-ahead is capped at two characters (a third character restarts the
75
+ // buffer), a repeated identical character does not cycle, and the timeout
76
+ // is an explicit `typeaheadReset` event.
77
+ // * trees deeper than 3 levels (visibility unrolls two ancestor checks),
78
+ // dynamic/lazy children, the Navigation Treeview example (aria-current
79
+ // links), aria-disabled nodes, and aria-owns/group wrappers.
80
+ // * Ctrl+A selects every node (as the APG text says), including nodes inside
81
+ // collapsed subtrees.
82
+ module apg_treeview {
83
+ import aria.* from "../../shared/aria"
84
+
85
+ var multi: bool // aria-multiselectable
86
+ var expanded: int // bitmask: parent nodes whose children are shown
87
+ var selected: int // bitmask: aria-selected nodes
88
+ var focus: int // roving-tabindex node, 1..7
89
+ var hasFocus: bool // DOM focus is inside the tree
90
+ var ta1: str // type-ahead buffer, first character ("" = empty)
91
+ var ta2: str // type-ahead buffer, second character
92
+ var activated: int // last activated node, 0 = none
93
+ var controlled: bool // the host owns the selection (`value` prop present)
94
+ var accepting: bool // controlled host that answers every request with its intent
95
+ var valueProp0: bool // controlled selection member node 1, ...
96
+ var valueProp1: bool
97
+ var valueProp2: bool
98
+ var valueProp3: bool
99
+ var valueProp4: bool
100
+ var valueProp5: bool
101
+ var valueProp6: bool
102
+ var requested: int // intent of the last request, packed selection
103
+ var outcome: str // "none" | "accepted" | "rejected"
104
+ var base: int // selection when the last request was received
105
+ var requests: int // number of requests that fired onSelectionChange
106
+
107
+ // ---- the constant tree --------------------------------------------------
108
+
109
+ val lastNode: int = 7
110
+ val allMask: int = 127
111
+
112
+ def parentOf(n: int): int =
113
+ if (n == 2) 1
114
+ else if (n == 3) 2
115
+ else if (n == 4) 2
116
+ else if (n == 5) 1
117
+ else if (n == 7) 6
118
+ else 0
119
+
120
+ def firstChild(n: int): int =
121
+ if (n == 1) 2
122
+ else if (n == 2) 3
123
+ else if (n == 6) 7
124
+ else 0
125
+
126
+ def isParent(n: int): bool = firstChild(n) != 0
127
+
128
+ // first two characters of each node's name, lowercased (for type-ahead)
129
+ def label1(n: int): str =
130
+ if (n == 1) "p"
131
+ else if (n == 2) "r"
132
+ else if (n == 3) "r"
133
+ else if (n == 4) "s"
134
+ else if (n == 5) "n"
135
+ else if (n == 6) "p"
136
+ else "t"
137
+
138
+ def label2(n: int): str =
139
+ if (n == 1) "r"
140
+ else if (n == 2) "e"
141
+ else if (n == 3) "o"
142
+ else if (n == 4) "u"
143
+ else if (n == 5) "o"
144
+ else if (n == 6) "h"
145
+ else "r"
146
+
147
+ // ---- bitmask helpers ----------------------------------------------------
148
+
149
+ def weight(n: int): int =
150
+ if (n == 1) 1
151
+ else if (n == 2) 2
152
+ else if (n == 3) 4
153
+ else if (n == 4) 8
154
+ else if (n == 5) 16
155
+ else if (n == 6) 32
156
+ else if (n == 7) 64
157
+ else 0
158
+
159
+ def bit(mask: int, n: int): bool =
160
+ if (and(n >= 1, n <= 7)) ((mask / weight(n)) % 2) == 1 else false
161
+
162
+ def setBit(mask: int, n: int, on: bool): int =
163
+ if (bit(mask, n) == on) mask
164
+ else if (on) mask + weight(n)
165
+ else mask - weight(n)
166
+
167
+ def toggleBit(mask: int, n: int): int = setBit(mask, n, not(bit(mask, n)))
168
+
169
+ def bitCount(mask: int, n: int): int = if (bit(mask, n)) 1 else 0
170
+
171
+ def popcount(mask: int): int =
172
+ bitCount(mask, 1) + bitCount(mask, 2) + bitCount(mask, 3) + bitCount(mask, 4)
173
+ + bitCount(mask, 5) + bitCount(mask, 6) + bitCount(mask, 7)
174
+
175
+ // ---- visibility and navigation -----------------------------------------
176
+
177
+ // the node's parent is shown-open (roots, and the pseudo-parent 0, always are)
178
+ def parentOpen(n: int, e: int): bool = or(parentOf(n) == 0, bit(e, parentOf(n)))
179
+
180
+ // focusable: every ancestor is expanded (depth <= 3 => two ancestor checks)
181
+ def visible(n: int, e: int): bool =
182
+ and(n >= 1, n <= 7, parentOpen(n, e), parentOpen(parentOf(n), e))
183
+
184
+ def visibleAfter(n: int, k: int, e: int): bool = and(n + k <= 7, visible(n + k, e))
185
+ def visibleBefore(n: int, k: int, e: int): bool = and(n - k >= 1, visible(n - k, e))
186
+
187
+ // Down: the next visible node in document order; the last node stays put
188
+ def nextVisible(n: int, e: int): int =
189
+ if (visibleAfter(n, 1, e)) n + 1
190
+ else if (visibleAfter(n, 2, e)) n + 2
191
+ else if (visibleAfter(n, 3, e)) n + 3
192
+ else if (visibleAfter(n, 4, e)) n + 4
193
+ else if (visibleAfter(n, 5, e)) n + 5
194
+ else if (visibleAfter(n, 6, e)) n + 6
195
+ else n
196
+
197
+ // Up: the previous visible node; the first node stays put
198
+ def prevVisible(n: int, e: int): int =
199
+ if (visibleBefore(n, 1, e)) n - 1
200
+ else if (visibleBefore(n, 2, e)) n - 2
201
+ else if (visibleBefore(n, 3, e)) n - 3
202
+ else if (visibleBefore(n, 4, e)) n - 4
203
+ else if (visibleBefore(n, 5, e)) n - 5
204
+ else if (visibleBefore(n, 6, e)) n - 6
205
+ else n
206
+
207
+ // End: the last node focusable without opening anything (node 1 is a root)
208
+ def lastVisible(e: int): int =
209
+ if (visible(7, e)) 7
210
+ else if (visible(6, e)) 6
211
+ else if (visible(5, e)) 5
212
+ else if (visible(4, e)) 4
213
+ else if (visible(3, e)) 3
214
+ else if (visible(2, e)) 2
215
+ else 1
216
+
217
+ // Right on a closed parent opens it (focus stays); on an open parent focus
218
+ // moves to its first child; on an end node nothing happens
219
+ def rightExpands(f: int, e: int): bool = and(isParent(f), not(bit(e, f)))
220
+ def rightTarget(f: int, e: int): int =
221
+ if (and(isParent(f), bit(e, f))) firstChild(f) else f
222
+
223
+ // Left on an open parent closes it (focus stays); otherwise focus moves to
224
+ // the parent; on a root nothing happens
225
+ def leftCollapses(f: int, e: int): bool = and(isParent(f), bit(e, f))
226
+ def leftTarget(f: int, e: int): int =
227
+ if (leftCollapses(f, e)) f
228
+ else if (parentOf(f) == 0) f
229
+ else parentOf(f)
230
+
231
+ // `*`: open every parent that shares the focused node's parent
232
+ def expandIfSibling(e: int, m: int, n: int): int =
233
+ if (and(isParent(m), parentOf(m) == parentOf(n))) setBit(e, m, true) else e
234
+
235
+ def expandSiblings(n: int, e: int): int =
236
+ expandIfSibling(
237
+ expandIfSibling(
238
+ expandIfSibling(
239
+ expandIfSibling(
240
+ expandIfSibling(
241
+ expandIfSibling(expandIfSibling(e, 1, n), 2, n), 3, n),
242
+ 4, n),
243
+ 5, n),
244
+ 6, n),
245
+ 7, n)
246
+
247
+ // ---- type-ahead ---------------------------------------------------------
248
+
249
+ // append `ch` to the buffer; a full buffer restarts with `ch`
250
+ def nextC1(a: str, b: str, ch: str): str =
251
+ if (a == "") ch else if (b == "") a else ch
252
+ def nextC2(a: str, b: str, ch: str): str =
253
+ if (a == "") "" else if (b == "") ch else ""
254
+
255
+ def matches(n: int, c1: str, c2: str, e: int): bool =
256
+ and(visible(n, e), label1(n) == c1, or(c2 == "", label2(n) == c2))
257
+
258
+ // k-th candidate in cyclic id order starting at s (shared/aria.qnt)
259
+ def cand(s: int, k: int): int = ariaCycle1(7, s, k)
260
+
261
+ def firstMatch(s: int, c1: str, c2: str, e: int): int =
262
+ if (matches(cand(s, 0), c1, c2, e)) cand(s, 0)
263
+ else if (matches(cand(s, 1), c1, c2, e)) cand(s, 1)
264
+ else if (matches(cand(s, 2), c1, c2, e)) cand(s, 2)
265
+ else if (matches(cand(s, 3), c1, c2, e)) cand(s, 3)
266
+ else if (matches(cand(s, 4), c1, c2, e)) cand(s, 4)
267
+ else if (matches(cand(s, 5), c1, c2, e)) cand(s, 5)
268
+ else if (matches(cand(s, 6), c1, c2, e)) cand(s, 6)
269
+ else 0
270
+
271
+ // one character searches from the node after focus; more characters search
272
+ // from the focused node itself (so refining a match keeps focus on it)
273
+ def typeStart(f: int, c2: str): int = ariaTaStart1(7, f, c2 == "")
274
+
275
+ // 0 = no visible node matches (focus stays)
276
+ def typeTarget(f: int, c1: str, c2: str, e: int): int =
277
+ firstMatch(typeStart(f, c2), c1, c2, e)
278
+
279
+ // ---- selection ----------------------------------------------------------
280
+
281
+ def shiftMoves(f: int, e: int): bool = nextVisible(f, e) != f
282
+ def shiftUpMoves(f: int, e: int): bool = prevVisible(f, e) != f
283
+
284
+ // ---- request / commit (map ticket rt1p) ---------------------------------
285
+
286
+ def holds: bool = and(controlled, not(accepting))
287
+ def follows: bool = and(controlled, accepting)
288
+ def verdict: str = if (holds) "rejected" else "accepted"
289
+ // the selection after a request for `want`
290
+ def landed(h: bool, current: int, want: int): int = if (h) current else want
291
+ // a controlled member after a request for `want`
292
+ def propAfter(f: bool, p: bool, want: bool): bool = if (f) want else p
293
+ // the packed controlled value, spelled out one member at a time (node i+1)
294
+ def propMask: int =
295
+ (if (valueProp0) 1 else 0) + (if (valueProp1) 2 else 0)
296
+ + (if (valueProp2) 4 else 0) + (if (valueProp3) 8 else 0)
297
+ + (if (valueProp4) 16 else 0) + (if (valueProp5) 32 else 0)
298
+ + (if (valueProp6) 64 else 0)
299
+ // the selection component `i` (0..6) of a mask
300
+ def at(mask: int, i: int): bool = bit(mask, i + 1)
301
+ // a request is real only when its intent differs from the current selection
302
+ def reqMoves(want: int): bool = want != selected
303
+ // the intended selection of each request action
304
+ def spaceWant: int =
305
+ if (hasFocus) (if (multi) toggleBit(selected, focus) else weight(focus)) else selected
306
+ def enterWant: int =
307
+ if (and(hasFocus, not(multi))) weight(focus) else selected
308
+ def shiftDownWant: int =
309
+ if (and(hasFocus, multi, shiftMoves(focus, expanded)))
310
+ toggleBit(selected, nextVisible(focus, expanded)) else selected
311
+ def shiftUpWant: int =
312
+ if (and(hasFocus, multi, shiftUpMoves(focus, expanded)))
313
+ toggleBit(selected, prevVisible(focus, expanded)) else selected
314
+ def ctrlAWant: int =
315
+ if (and(hasFocus, multi)) (if (selected == allMask) 0 else allMask) else selected
316
+ def clickWant(node: int): int =
317
+ if (visible(node, expanded)) weight(node) else selected
318
+
319
+ // ---- the ARIA contract (derived) ----------------------------------------
320
+
321
+ // "none" means the attribute is absent (end nodes never carry aria-expanded)
322
+ def ariaExpanded(n: int, e: int): str =
323
+ if (not(isParent(n))) "none"
324
+ else ariaBoolToken(bit(e, n))
325
+
326
+ def ariaSelected(n: int, s: int): str = ariaBoolToken(bit(s, n))
327
+ def ariaMultiselectable(m: bool): str = ariaBoolToken(m)
328
+
329
+ def ariaLevel(n: int): int =
330
+ if (parentOf(n) == 0) 1
331
+ else if (parentOf(parentOf(n)) == 0) 2
332
+ else 3
333
+
334
+ def sibling(m: int, n: int): int = if (parentOf(m) == parentOf(n)) 1 else 0
335
+ def siblingBefore(m: int, n: int): int = if (m < n) sibling(m, n) else 0
336
+
337
+ def ariaSetsize(n: int): int =
338
+ sibling(1, n) + sibling(2, n) + sibling(3, n) + sibling(4, n)
339
+ + sibling(5, n) + sibling(6, n) + sibling(7, n)
340
+
341
+ def ariaPosinset(n: int): int =
342
+ 1 + siblingBefore(1, n) + siblingBefore(2, n) + siblingBefore(3, n)
343
+ + siblingBefore(4, n) + siblingBefore(5, n) + siblingBefore(6, n)
344
+ + siblingBefore(7, n)
345
+
346
+ // roving tabindex: 0 on the focus node, -1 elsewhere
347
+ def tabIndex(n: int, f: int): int = if (n == f) 0 else -1
348
+ def tabbable(n: int, f: int): int = if (tabIndex(n, f) == 0) 1 else 0
349
+ def tabbableCount(f: int): int =
350
+ tabbable(1, f) + tabbable(2, f) + tabbable(3, f) + tabbable(4, f)
351
+ + tabbable(5, f) + tabbable(6, f) + tabbable(7, f)
352
+
353
+ // ---- actions ------------------------------------------------------------
354
+
355
+ action init = all {
356
+ multi' = false,
357
+ expanded' = 0,
358
+ selected' = 0,
359
+ focus' = 1,
360
+ hasFocus' = false,
361
+ ta1' = "",
362
+ ta2' = "",
363
+ activated' = 0,
364
+ controlled' = false,
365
+ accepting' = false,
366
+ valueProp0' = false,
367
+ valueProp1' = false,
368
+ valueProp2' = false,
369
+ valueProp3' = false,
370
+ valueProp4' = false,
371
+ valueProp5' = false,
372
+ valueProp6' = false,
373
+ requested' = 0,
374
+ outcome' = "none",
375
+ base' = 0,
376
+ requests' = 0
377
+ }
378
+
379
+ // switch between single- and multi-select; the uncontrolled selection resets
380
+ action configure = {
381
+ nondet m = Set(true, false).oneOf()
382
+ all {
383
+ multi' = m,
384
+ expanded' = expanded,
385
+ selected' = if (controlled) selected else (if (m == multi) selected else 0),
386
+ focus' = focus,
387
+ hasFocus' = hasFocus,
388
+ ta1' = "",
389
+ ta2' = "",
390
+ activated' = activated,
391
+ controlled' = controlled,
392
+ accepting' = accepting,
393
+ valueProp0' = valueProp0,
394
+ valueProp1' = valueProp1,
395
+ valueProp2' = valueProp2,
396
+ valueProp3' = valueProp3,
397
+ valueProp4' = valueProp4,
398
+ valueProp5' = valueProp5,
399
+ valueProp6' = valueProp6,
400
+ requested' = 0,
401
+ outcome' = "none",
402
+ base' = 0,
403
+ requests' = requests
404
+ }
405
+ }
406
+
407
+ // Tab into the tree: the roving node receives focus
408
+ action focusTree = all {
409
+ multi' = multi,
410
+ expanded' = expanded,
411
+ selected' = selected,
412
+ focus' = focus,
413
+ hasFocus' = true,
414
+ ta1' = ta1,
415
+ ta2' = ta2,
416
+ activated' = activated,
417
+ controlled' = controlled,
418
+ accepting' = accepting,
419
+ valueProp0' = valueProp0,
420
+ valueProp1' = valueProp1,
421
+ valueProp2' = valueProp2,
422
+ valueProp3' = valueProp3,
423
+ valueProp4' = valueProp4,
424
+ valueProp5' = valueProp5,
425
+ valueProp6' = valueProp6,
426
+ requested' = requested,
427
+ outcome' = outcome,
428
+ base' = base,
429
+ requests' = requests
430
+ }
431
+
432
+ action blurTree = all {
433
+ multi' = multi,
434
+ expanded' = expanded,
435
+ selected' = selected,
436
+ focus' = focus,
437
+ hasFocus' = false,
438
+ ta1' = "",
439
+ ta2' = "",
440
+ activated' = activated,
441
+ controlled' = controlled,
442
+ accepting' = accepting,
443
+ valueProp0' = valueProp0,
444
+ valueProp1' = valueProp1,
445
+ valueProp2' = valueProp2,
446
+ valueProp3' = valueProp3,
447
+ valueProp4' = valueProp4,
448
+ valueProp5' = valueProp5,
449
+ valueProp6' = valueProp6,
450
+ requested' = requested,
451
+ outcome' = outcome,
452
+ base' = base,
453
+ requests' = requests
454
+ }
455
+
456
+ action pressDown = all {
457
+ multi' = multi,
458
+ expanded' = expanded,
459
+ selected' = selected,
460
+ focus' = if (hasFocus) nextVisible(focus, expanded) else focus,
461
+ hasFocus' = hasFocus,
462
+ ta1' = "",
463
+ ta2' = "",
464
+ activated' = activated,
465
+ controlled' = controlled,
466
+ accepting' = accepting,
467
+ valueProp0' = valueProp0,
468
+ valueProp1' = valueProp1,
469
+ valueProp2' = valueProp2,
470
+ valueProp3' = valueProp3,
471
+ valueProp4' = valueProp4,
472
+ valueProp5' = valueProp5,
473
+ valueProp6' = valueProp6,
474
+ requested' = requested,
475
+ outcome' = outcome,
476
+ base' = base,
477
+ requests' = requests
478
+ }
479
+
480
+ action pressUp = all {
481
+ multi' = multi,
482
+ expanded' = expanded,
483
+ selected' = selected,
484
+ focus' = if (hasFocus) prevVisible(focus, expanded) else focus,
485
+ hasFocus' = hasFocus,
486
+ ta1' = "",
487
+ ta2' = "",
488
+ activated' = activated,
489
+ controlled' = controlled,
490
+ accepting' = accepting,
491
+ valueProp0' = valueProp0,
492
+ valueProp1' = valueProp1,
493
+ valueProp2' = valueProp2,
494
+ valueProp3' = valueProp3,
495
+ valueProp4' = valueProp4,
496
+ valueProp5' = valueProp5,
497
+ valueProp6' = valueProp6,
498
+ requested' = requested,
499
+ outcome' = outcome,
500
+ base' = base,
501
+ requests' = requests
502
+ }
503
+
504
+ action pressRight = all {
505
+ multi' = multi,
506
+ expanded' = if (and(hasFocus, rightExpands(focus, expanded))) setBit(expanded, focus, true) else expanded,
507
+ selected' = selected,
508
+ focus' = if (hasFocus) rightTarget(focus, expanded) else focus,
509
+ hasFocus' = hasFocus,
510
+ ta1' = "",
511
+ ta2' = "",
512
+ activated' = activated,
513
+ controlled' = controlled,
514
+ accepting' = accepting,
515
+ valueProp0' = valueProp0,
516
+ valueProp1' = valueProp1,
517
+ valueProp2' = valueProp2,
518
+ valueProp3' = valueProp3,
519
+ valueProp4' = valueProp4,
520
+ valueProp5' = valueProp5,
521
+ valueProp6' = valueProp6,
522
+ requested' = requested,
523
+ outcome' = outcome,
524
+ base' = base,
525
+ requests' = requests
526
+ }
527
+
528
+ action pressLeft = all {
529
+ multi' = multi,
530
+ expanded' = if (and(hasFocus, leftCollapses(focus, expanded))) setBit(expanded, focus, false) else expanded,
531
+ selected' = selected,
532
+ focus' = if (hasFocus) leftTarget(focus, expanded) else focus,
533
+ hasFocus' = hasFocus,
534
+ ta1' = "",
535
+ ta2' = "",
536
+ activated' = activated,
537
+ controlled' = controlled,
538
+ accepting' = accepting,
539
+ valueProp0' = valueProp0,
540
+ valueProp1' = valueProp1,
541
+ valueProp2' = valueProp2,
542
+ valueProp3' = valueProp3,
543
+ valueProp4' = valueProp4,
544
+ valueProp5' = valueProp5,
545
+ valueProp6' = valueProp6,
546
+ requested' = requested,
547
+ outcome' = outcome,
548
+ base' = base,
549
+ requests' = requests
550
+ }
551
+
552
+ action pressHome = all {
553
+ multi' = multi,
554
+ expanded' = expanded,
555
+ selected' = selected,
556
+ focus' = if (hasFocus) 1 else focus,
557
+ hasFocus' = hasFocus,
558
+ ta1' = "",
559
+ ta2' = "",
560
+ activated' = activated,
561
+ controlled' = controlled,
562
+ accepting' = accepting,
563
+ valueProp0' = valueProp0,
564
+ valueProp1' = valueProp1,
565
+ valueProp2' = valueProp2,
566
+ valueProp3' = valueProp3,
567
+ valueProp4' = valueProp4,
568
+ valueProp5' = valueProp5,
569
+ valueProp6' = valueProp6,
570
+ requested' = requested,
571
+ outcome' = outcome,
572
+ base' = base,
573
+ requests' = requests
574
+ }
575
+
576
+ action pressEnd = all {
577
+ multi' = multi,
578
+ expanded' = expanded,
579
+ selected' = selected,
580
+ focus' = if (hasFocus) lastVisible(expanded) else focus,
581
+ hasFocus' = hasFocus,
582
+ ta1' = "",
583
+ ta2' = "",
584
+ activated' = activated,
585
+ controlled' = controlled,
586
+ accepting' = accepting,
587
+ valueProp0' = valueProp0,
588
+ valueProp1' = valueProp1,
589
+ valueProp2' = valueProp2,
590
+ valueProp3' = valueProp3,
591
+ valueProp4' = valueProp4,
592
+ valueProp5' = valueProp5,
593
+ valueProp6' = valueProp6,
594
+ requested' = requested,
595
+ outcome' = outcome,
596
+ base' = base,
597
+ requests' = requests
598
+ }
599
+
600
+ // request: Enter activates the focused node. A parent also opens/closes
601
+ // (uncontrolled); in single-select the activated node becomes the selection.
602
+ action pressEnter = all {
603
+ multi' = multi,
604
+ expanded' = if (and(hasFocus, isParent(focus))) toggleBit(expanded, focus) else expanded,
605
+ selected' = landed(holds, selected, enterWant),
606
+ focus' = focus,
607
+ hasFocus' = hasFocus,
608
+ ta1' = "",
609
+ ta2' = "",
610
+ activated' = if (hasFocus) focus else activated,
611
+ controlled' = controlled,
612
+ accepting' = accepting,
613
+ valueProp0' = propAfter(follows, valueProp0, at(enterWant, 0)),
614
+ valueProp1' = propAfter(follows, valueProp1, at(enterWant, 1)),
615
+ valueProp2' = propAfter(follows, valueProp2, at(enterWant, 2)),
616
+ valueProp3' = propAfter(follows, valueProp3, at(enterWant, 3)),
617
+ valueProp4' = propAfter(follows, valueProp4, at(enterWant, 4)),
618
+ valueProp5' = propAfter(follows, valueProp5, at(enterWant, 5)),
619
+ valueProp6' = propAfter(follows, valueProp6, at(enterWant, 6)),
620
+ requested' = if (reqMoves(enterWant)) enterWant else requested,
621
+ outcome' = if (reqMoves(enterWant)) verdict else outcome,
622
+ base' = if (reqMoves(enterWant)) selected else base,
623
+ requests' = if (reqMoves(enterWant)) requests + 1 else requests
624
+ }
625
+
626
+ // request: Space - single-select selects the focused node, multi-select toggles it
627
+ action pressSpace = all {
628
+ multi' = multi,
629
+ expanded' = expanded,
630
+ selected' = landed(holds, selected, spaceWant),
631
+ focus' = focus,
632
+ hasFocus' = hasFocus,
633
+ ta1' = "",
634
+ ta2' = "",
635
+ activated' = activated,
636
+ controlled' = controlled,
637
+ accepting' = accepting,
638
+ valueProp0' = propAfter(follows, valueProp0, at(spaceWant, 0)),
639
+ valueProp1' = propAfter(follows, valueProp1, at(spaceWant, 1)),
640
+ valueProp2' = propAfter(follows, valueProp2, at(spaceWant, 2)),
641
+ valueProp3' = propAfter(follows, valueProp3, at(spaceWant, 3)),
642
+ valueProp4' = propAfter(follows, valueProp4, at(spaceWant, 4)),
643
+ valueProp5' = propAfter(follows, valueProp5, at(spaceWant, 5)),
644
+ valueProp6' = propAfter(follows, valueProp6, at(spaceWant, 6)),
645
+ requested' = if (reqMoves(spaceWant)) spaceWant else requested,
646
+ outcome' = if (reqMoves(spaceWant)) verdict else outcome,
647
+ base' = if (reqMoves(spaceWant)) selected else base,
648
+ requests' = if (reqMoves(spaceWant)) requests + 1 else requests
649
+ }
650
+
651
+ // `*`: open all siblings of the focused node (uncontrolled expansion)
652
+ action pressStar = all {
653
+ multi' = multi,
654
+ expanded' = if (hasFocus) expandSiblings(focus, expanded) else expanded,
655
+ selected' = selected,
656
+ focus' = focus,
657
+ hasFocus' = hasFocus,
658
+ ta1' = "",
659
+ ta2' = "",
660
+ activated' = activated,
661
+ controlled' = controlled,
662
+ accepting' = accepting,
663
+ valueProp0' = valueProp0,
664
+ valueProp1' = valueProp1,
665
+ valueProp2' = valueProp2,
666
+ valueProp3' = valueProp3,
667
+ valueProp4' = valueProp4,
668
+ valueProp5' = valueProp5,
669
+ valueProp6' = valueProp6,
670
+ requested' = requested,
671
+ outcome' = outcome,
672
+ base' = base,
673
+ requests' = requests
674
+ }
675
+
676
+ // a printable character: type-ahead over the visible nodes
677
+ action typeChar = {
678
+ nondet ch = Set("p", "r", "n", "s", "t", "h", "e", "o", "u", "x").oneOf()
679
+ all {
680
+ multi' = multi,
681
+ expanded' = expanded,
682
+ selected' = selected,
683
+ focus' = if (and(hasFocus, typeTarget(focus, nextC1(ta1, ta2, ch), nextC2(ta1, ta2, ch), expanded) != 0)) typeTarget(focus, nextC1(ta1, ta2, ch), nextC2(ta1, ta2, ch), expanded) else focus,
684
+ hasFocus' = hasFocus,
685
+ ta1' = if (hasFocus) nextC1(ta1, ta2, ch) else ta1,
686
+ ta2' = if (hasFocus) nextC2(ta1, ta2, ch) else ta2,
687
+ activated' = activated,
688
+ controlled' = controlled,
689
+ accepting' = accepting,
690
+ valueProp0' = valueProp0,
691
+ valueProp1' = valueProp1,
692
+ valueProp2' = valueProp2,
693
+ valueProp3' = valueProp3,
694
+ valueProp4' = valueProp4,
695
+ valueProp5' = valueProp5,
696
+ valueProp6' = valueProp6,
697
+ requested' = requested,
698
+ outcome' = outcome,
699
+ base' = base,
700
+ requests' = requests
701
+ }
702
+ }
703
+
704
+ // the type-ahead timeout elapsed
705
+ action typeaheadReset = all {
706
+ multi' = multi,
707
+ expanded' = expanded,
708
+ selected' = selected,
709
+ focus' = focus,
710
+ hasFocus' = hasFocus,
711
+ ta1' = "",
712
+ ta2' = "",
713
+ activated' = activated,
714
+ controlled' = controlled,
715
+ accepting' = accepting,
716
+ valueProp0' = valueProp0,
717
+ valueProp1' = valueProp1,
718
+ valueProp2' = valueProp2,
719
+ valueProp3' = valueProp3,
720
+ valueProp4' = valueProp4,
721
+ valueProp5' = valueProp5,
722
+ valueProp6' = valueProp6,
723
+ requested' = requested,
724
+ outcome' = outcome,
725
+ base' = base,
726
+ requests' = requests
727
+ }
728
+
729
+ // request: Shift+Down - move to the next node and (multi) toggle its selection
730
+ action pressShiftDown = all {
731
+ multi' = multi,
732
+ expanded' = expanded,
733
+ selected' = landed(holds, selected, shiftDownWant),
734
+ focus' = if (hasFocus) nextVisible(focus, expanded) else focus,
735
+ hasFocus' = hasFocus,
736
+ ta1' = "",
737
+ ta2' = "",
738
+ activated' = activated,
739
+ controlled' = controlled,
740
+ accepting' = accepting,
741
+ valueProp0' = propAfter(follows, valueProp0, at(shiftDownWant, 0)),
742
+ valueProp1' = propAfter(follows, valueProp1, at(shiftDownWant, 1)),
743
+ valueProp2' = propAfter(follows, valueProp2, at(shiftDownWant, 2)),
744
+ valueProp3' = propAfter(follows, valueProp3, at(shiftDownWant, 3)),
745
+ valueProp4' = propAfter(follows, valueProp4, at(shiftDownWant, 4)),
746
+ valueProp5' = propAfter(follows, valueProp5, at(shiftDownWant, 5)),
747
+ valueProp6' = propAfter(follows, valueProp6, at(shiftDownWant, 6)),
748
+ requested' = if (reqMoves(shiftDownWant)) shiftDownWant else requested,
749
+ outcome' = if (reqMoves(shiftDownWant)) verdict else outcome,
750
+ base' = if (reqMoves(shiftDownWant)) selected else base,
751
+ requests' = if (reqMoves(shiftDownWant)) requests + 1 else requests
752
+ }
753
+
754
+ // request: Shift+Up - move to the previous node and (multi) toggle its selection
755
+ action pressShiftUp = all {
756
+ multi' = multi,
757
+ expanded' = expanded,
758
+ selected' = landed(holds, selected, shiftUpWant),
759
+ focus' = if (hasFocus) prevVisible(focus, expanded) else focus,
760
+ hasFocus' = hasFocus,
761
+ ta1' = "",
762
+ ta2' = "",
763
+ activated' = activated,
764
+ controlled' = controlled,
765
+ accepting' = accepting,
766
+ valueProp0' = propAfter(follows, valueProp0, at(shiftUpWant, 0)),
767
+ valueProp1' = propAfter(follows, valueProp1, at(shiftUpWant, 1)),
768
+ valueProp2' = propAfter(follows, valueProp2, at(shiftUpWant, 2)),
769
+ valueProp3' = propAfter(follows, valueProp3, at(shiftUpWant, 3)),
770
+ valueProp4' = propAfter(follows, valueProp4, at(shiftUpWant, 4)),
771
+ valueProp5' = propAfter(follows, valueProp5, at(shiftUpWant, 5)),
772
+ valueProp6' = propAfter(follows, valueProp6, at(shiftUpWant, 6)),
773
+ requested' = if (reqMoves(shiftUpWant)) shiftUpWant else requested,
774
+ outcome' = if (reqMoves(shiftUpWant)) verdict else outcome,
775
+ base' = if (reqMoves(shiftUpWant)) selected else base,
776
+ requests' = if (reqMoves(shiftUpWant)) requests + 1 else requests
777
+ }
778
+
779
+ // request: Ctrl+A (multi) selects every node; pressed again, clear
780
+ action pressCtrlA = all {
781
+ multi' = multi,
782
+ expanded' = expanded,
783
+ selected' = landed(holds, selected, ctrlAWant),
784
+ focus' = focus,
785
+ hasFocus' = hasFocus,
786
+ ta1' = "",
787
+ ta2' = "",
788
+ activated' = activated,
789
+ controlled' = controlled,
790
+ accepting' = accepting,
791
+ valueProp0' = propAfter(follows, valueProp0, at(ctrlAWant, 0)),
792
+ valueProp1' = propAfter(follows, valueProp1, at(ctrlAWant, 1)),
793
+ valueProp2' = propAfter(follows, valueProp2, at(ctrlAWant, 2)),
794
+ valueProp3' = propAfter(follows, valueProp3, at(ctrlAWant, 3)),
795
+ valueProp4' = propAfter(follows, valueProp4, at(ctrlAWant, 4)),
796
+ valueProp5' = propAfter(follows, valueProp5, at(ctrlAWant, 5)),
797
+ valueProp6' = propAfter(follows, valueProp6, at(ctrlAWant, 6)),
798
+ requested' = if (reqMoves(ctrlAWant)) ctrlAWant else requested,
799
+ outcome' = if (reqMoves(ctrlAWant)) verdict else outcome,
800
+ base' = if (reqMoves(ctrlAWant)) selected else base,
801
+ requests' = if (reqMoves(ctrlAWant)) requests + 1 else requests
802
+ }
803
+
804
+ // request: pointer click on a node - only a visible node can be hit. It takes
805
+ // focus, becomes the sole selection, is activated, and a parent toggles.
806
+ action click = {
807
+ nondet node = Set(1, 2, 3, 4, 5, 6, 7).oneOf()
808
+ all {
809
+ multi' = multi,
810
+ expanded' = if (and(visible(node, expanded), isParent(node))) toggleBit(expanded, node) else expanded,
811
+ selected' = landed(holds, selected, clickWant(node)),
812
+ focus' = if (visible(node, expanded)) node else focus,
813
+ hasFocus' = if (visible(node, expanded)) true else hasFocus,
814
+ ta1' = "",
815
+ ta2' = "",
816
+ activated' = if (visible(node, expanded)) node else activated,
817
+ controlled' = controlled,
818
+ accepting' = accepting,
819
+ valueProp0' = propAfter(follows, valueProp0, at(clickWant(node), 0)),
820
+ valueProp1' = propAfter(follows, valueProp1, at(clickWant(node), 1)),
821
+ valueProp2' = propAfter(follows, valueProp2, at(clickWant(node), 2)),
822
+ valueProp3' = propAfter(follows, valueProp3, at(clickWant(node), 3)),
823
+ valueProp4' = propAfter(follows, valueProp4, at(clickWant(node), 4)),
824
+ valueProp5' = propAfter(follows, valueProp5, at(clickWant(node), 5)),
825
+ valueProp6' = propAfter(follows, valueProp6, at(clickWant(node), 6)),
826
+ requested' = if (reqMoves(clickWant(node))) clickWant(node) else requested,
827
+ outcome' = if (reqMoves(clickWant(node))) verdict else outcome,
828
+ base' = if (reqMoves(clickWant(node))) selected else base,
829
+ requests' = if (reqMoves(clickWant(node))) requests + 1 else requests
830
+ }
831
+ }
832
+
833
+ // commit: the host writes the controlled selection (a no-op while uncontrolled).
834
+ // The pool is one node at a time: a host sets a single node or clears.
835
+ action setValue = {
836
+ nondet v = Set(0, 1, 2, 4, 8, 16, 32, 64).oneOf()
837
+ all {
838
+ multi' = multi,
839
+ expanded' = expanded,
840
+ selected' = if (controlled) v else selected,
841
+ focus' = focus,
842
+ hasFocus' = hasFocus,
843
+ ta1' = ta1,
844
+ ta2' = ta2,
845
+ activated' = activated,
846
+ controlled' = controlled,
847
+ accepting' = accepting,
848
+ valueProp0' = if (controlled) at(v, 0) else valueProp0,
849
+ valueProp1' = if (controlled) at(v, 1) else valueProp1,
850
+ valueProp2' = if (controlled) at(v, 2) else valueProp2,
851
+ valueProp3' = if (controlled) at(v, 3) else valueProp3,
852
+ valueProp4' = if (controlled) at(v, 4) else valueProp4,
853
+ valueProp5' = if (controlled) at(v, 5) else valueProp5,
854
+ valueProp6' = if (controlled) at(v, 6) else valueProp6,
855
+ requested' = requested,
856
+ outcome' = if (controlled) "none" else outcome,
857
+ base' = base,
858
+ requests' = requests
859
+ }
860
+ }
861
+
862
+ // commit: the host flips one node of its own copy of the selection
863
+ action hostToggle = {
864
+ nondet n = Set(1, 2, 3, 4, 5, 6, 7).oneOf()
865
+ all {
866
+ multi' = multi,
867
+ expanded' = expanded,
868
+ selected' = if (controlled) toggleBit(propMask, n) else selected,
869
+ focus' = focus,
870
+ hasFocus' = hasFocus,
871
+ ta1' = ta1,
872
+ ta2' = ta2,
873
+ activated' = activated,
874
+ controlled' = controlled,
875
+ accepting' = accepting,
876
+ valueProp0' = if (and(controlled, n == 1)) not(valueProp0) else valueProp0,
877
+ valueProp1' = if (and(controlled, n == 2)) not(valueProp1) else valueProp1,
878
+ valueProp2' = if (and(controlled, n == 3)) not(valueProp2) else valueProp2,
879
+ valueProp3' = if (and(controlled, n == 4)) not(valueProp3) else valueProp3,
880
+ valueProp4' = if (and(controlled, n == 5)) not(valueProp4) else valueProp4,
881
+ valueProp5' = if (and(controlled, n == 6)) not(valueProp5) else valueProp5,
882
+ valueProp6' = if (and(controlled, n == 7)) not(valueProp6) else valueProp6,
883
+ requested' = requested,
884
+ outcome' = if (controlled) "none" else outcome,
885
+ base' = base,
886
+ requests' = requests
887
+ }
888
+ }
889
+
890
+ // configuration: the host starts owning the selection, adopting the current nodes
891
+ action control = all {
892
+ multi' = multi,
893
+ expanded' = expanded,
894
+ selected' = selected,
895
+ focus' = focus,
896
+ hasFocus' = hasFocus,
897
+ ta1' = ta1,
898
+ ta2' = ta2,
899
+ activated' = activated,
900
+ controlled' = true,
901
+ accepting' = accepting,
902
+ valueProp0' = at(selected, 0),
903
+ valueProp1' = at(selected, 1),
904
+ valueProp2' = at(selected, 2),
905
+ valueProp3' = at(selected, 3),
906
+ valueProp4' = at(selected, 4),
907
+ valueProp5' = at(selected, 5),
908
+ valueProp6' = at(selected, 6),
909
+ requested' = requested,
910
+ outcome' = outcome,
911
+ base' = base,
912
+ requests' = requests
913
+ }
914
+
915
+ // configuration: the controlled host answers every request with its intent
916
+ action acceptRequests = all {
917
+ multi' = multi,
918
+ expanded' = expanded,
919
+ selected' = selected,
920
+ focus' = focus,
921
+ hasFocus' = hasFocus,
922
+ ta1' = ta1,
923
+ ta2' = ta2,
924
+ activated' = activated,
925
+ controlled' = controlled,
926
+ accepting' = true,
927
+ valueProp0' = valueProp0,
928
+ valueProp1' = valueProp1,
929
+ valueProp2' = valueProp2,
930
+ valueProp3' = valueProp3,
931
+ valueProp4' = valueProp4,
932
+ valueProp5' = valueProp5,
933
+ valueProp6' = valueProp6,
934
+ requested' = requested,
935
+ outcome' = outcome,
936
+ base' = base,
937
+ requests' = requests
938
+ }
939
+
940
+ action step = any {
941
+ init, configure, focusTree, blurTree,
942
+ pressDown, pressUp, pressRight, pressLeft, pressHome, pressEnd,
943
+ pressEnter, pressSpace, pressStar, typeChar, typeaheadReset,
944
+ pressShiftDown, pressShiftUp, pressCtrlA, click,
945
+ setValue, hostToggle, control, acceptRequests
946
+ }
947
+
948
+ // ---- invariants ---------------------------------------------------------
949
+
950
+ // the roving node exists
951
+ val FocusKnown: bool = and(focus >= 1, focus <= 7)
952
+ // APG: focus only ever rests on a focusable node — every ancestor is open
953
+ val FocusVisible: bool = visible(focus, expanded)
954
+ // roving tabindex: exactly one treeitem has tabindex=0
955
+ val ExactlyOneTabbable: bool = tabbableCount(focus) == 1
956
+ // masks stay within the node set
957
+ val MasksInRange: bool = and(expanded >= 0, expanded <= allMask, selected >= 0, selected <= allMask)
958
+
959
+ def leafNeverOpen(n: int, e: int): bool = or(isParent(n), not(bit(e, n)))
960
+ // end nodes are never recorded as expanded
961
+ val LeafNeverExpanded: bool = and(
962
+ leafNeverOpen(1, expanded), leafNeverOpen(2, expanded), leafNeverOpen(3, expanded),
963
+ leafNeverOpen(4, expanded), leafNeverOpen(5, expanded), leafNeverOpen(6, expanded),
964
+ leafNeverOpen(7, expanded)
965
+ )
966
+
967
+ // aria-expanded is rendered on exactly the parent nodes
968
+ def ariaExpandedOnlyOnParent(n: int, e: int): bool =
969
+ isParent(n) == (ariaExpanded(n, e) != "none")
970
+ val AriaExpandedOnlyOnParents: bool = and(
971
+ ariaExpandedOnlyOnParent(1, expanded), ariaExpandedOnlyOnParent(2, expanded),
972
+ ariaExpandedOnlyOnParent(3, expanded), ariaExpandedOnlyOnParent(4, expanded),
973
+ ariaExpandedOnlyOnParent(5, expanded), ariaExpandedOnlyOnParent(6, expanded),
974
+ ariaExpandedOnlyOnParent(7, expanded)
975
+ )
976
+
977
+ // aria-level is one deeper than the parent's; posinset lies in 1..setsize
978
+ def ariaPositionOk(n: int): bool =
979
+ and(
980
+ ariaLevel(n) == (if (parentOf(n) == 0) 1 else ariaLevel(parentOf(n)) + 1),
981
+ ariaPosinset(n) >= 1,
982
+ ariaPosinset(n) <= ariaSetsize(n)
983
+ )
984
+ val AriaPositionConsistent: bool = and(
985
+ ariaPositionOk(1), ariaPositionOk(2), ariaPositionOk(3), ariaPositionOk(4),
986
+ ariaPositionOk(5), ariaPositionOk(6), ariaPositionOk(7)
987
+ )
988
+
989
+ // a single-select tree never has more than one aria-selected node (uncontrolled:
990
+ // a controlled host owns the selection and may hand us any set)
991
+ val SingleSelectAtMostOne: bool = ariaSingleHoldsAtMostOne(controlled, multi, popcount(selected))
992
+ // the type-ahead buffer fills left to right
993
+ val TypeaheadBufferShape: bool = or(ta1 != "", ta2 == "")
994
+ // activation only ever reports a real node
995
+ val ActivatedKnown: bool = and(activated >= 0, activated <= 7)
996
+ // callbacks only ever count up from zero
997
+ val RequestsNonNegative: bool = requests >= 0
998
+ // controlled: only the value prop sets the selection, so no event commits
999
+ val ControlledFollowsProp: bool = or(not(controlled), selected == propMask)
1000
+ // a rejected request leaves the selection as it found it
1001
+ val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), selected == base)
1002
+ // an accepted request committed exactly its intent
1003
+ val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), selected == requested)
1004
+ }