@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,983 @@
1
+ // APG Treegrid — https://www.w3.org/WAI/ARIA/apg/patterns/treegrid/
2
+ // and its example, the E-mail Inbox treegrid:
3
+ // https://www.w3.org/WAI/ARIA/apg/patterns/treegrid/examples/treegrid-1/
4
+ //
5
+ // Modelled (APG sections: About This Pattern, Keyboard Interaction, WAI-ARIA
6
+ // Roles/States/Properties):
7
+ // - a fixed tree of 7 rows in document order x 3 columns, the parent map being
8
+ // the only tree data (Inbox-style: A > {A.1, A.2 > {A.2.1}}, B > {B.1, B.2}):
9
+ // row 0 A parent - row 4 B parent -
10
+ // row 1 A.1 parent 0 row 5 B.1 parent 4
11
+ // row 2 A.2 parent 0 row 6 B.2 parent 4
12
+ // row 3 A.2.1 parent 2
13
+ // every ARIA fact is DERIVED from that map: hasChildren, aria-level,
14
+ // aria-setsize, aria-posinset, aria-expanded (only on parent rows), and row
15
+ // visibility (a row is rendered only while all its ancestors are expanded).
16
+ // - the three navigation models of the example: `rowsFirst` (Tab lands on the
17
+ // row, Right enters the cells), `cellsFirst` (Tab lands on the first cell,
18
+ // Left from the first cell still reaches the row) and `cellsOnly` (rows are
19
+ // never focused; Enter on the first cell of a parent row toggles it).
20
+ // - roving tabindex: exactly one rendered row-or-cell is the tab stop, and the
21
+ // tab stop is remembered while focus is outside the grid.
22
+ // - keys: Right / Left (expand / collapse a focused row, otherwise move between
23
+ // cells), Down / Up over VISIBLE rows (same column in cell mode), Home / End
24
+ // (first / last visible row in row mode, first / last cell in cell mode),
25
+ // Ctrl+Home / Ctrl+End (first / last visible row; same column in cell mode),
26
+ // Enter (cellsOnly toggle), plus Tab in / out and pointer clicks (cell focus,
27
+ // expander toggle - which relocates focus to the toggled row when it would
28
+ // hide the focused row).
29
+ // - selection (rows): `single` selection follows focus (aria-selected on one
30
+ // row only, absent elsewhere); `multi` selection is independent of focus
31
+ // (aria-multiselectable, aria-selected true/false on every row): Shift+Space
32
+ // toggles the focused row, Ctrl+A selects all rows, Shift+Down / Shift+Up on
33
+ // a focused row extend the selection to the neighbouring visible row.
34
+ //
35
+ // Request vs commit (map ticket rt1p). The controlled value of a treegrid is the
36
+ // selected-row set; expansion stays uncontrolled (like a menu's open state). Every
37
+ // user-intent event that changes the selection - Tab in (single follows focus),
38
+ // Down / Up / Home / End / Ctrl+Home / Ctrl+End (single follows focus), a cell
39
+ // click, an expander toggle that relocates focus, Shift+Space, Ctrl+A and the
40
+ // Shift+arrow extensions - is a *request* carrying the intended `selMask`
41
+ // (packed into `requested`). It fires onValueChange (`requests` counts the calls)
42
+ // when the intent differs from the current selection, and then:
43
+ // uncontrolled the request is accepted: the rows commit the intent
44
+ // controlled, holds the host ignores it ("rejected"): the selection is untouched
45
+ // controlled, accepts the host answers with the same value (`accepting`): the
46
+ // rows commit through the controlled value, not the event
47
+ // The controlled value `selMask` is spelled out per member (`valueProp0..6`, bit
48
+ // r = row r) because the generator has no collections; it is the only thing that
49
+ // changes the selection while `controlled`, and `setValue` / `hostToggle` are the
50
+ // host writing it. `control` and `acceptRequests` are configuration events (init
51
+ // is literal-only). Configuration events (`configureNav` / `configureSel`) reset
52
+ // the uncontrolled expansion and focus but leave a controlled selection alone.
53
+ // Expansion keys (Right / Left / Enter) and focus movement do not change the
54
+ // controlled value, so they commit unconditionally.
55
+ //
56
+ // Invariants for the model: ControlledFollowsProp (in controlled mode no event
57
+ // commits the selection), RejectedRequestUntouched, AcceptedRequestCommits. The
58
+ // single-select invariants are scoped to the uncontrolled path: a controlled host
59
+ // owns the selection and may hand the grid any set.
60
+ //
61
+ // `outcome` is "none" | "accepted" | "rejected" for the last request; a host
62
+ // write settles it back to "none". `base` is the packed selection when the last
63
+ // request was received.
64
+ //
65
+ // Deliberately omitted (recorded gaps, see the bean summary):
66
+ // - Page Up / Page Down: the row count is "author-determined".
67
+ // - Tab to focusable elements inside cells and editing inside cells (grid
68
+ // "Editing and Navigating Inside a Cell"); cells are the focus targets.
69
+ // - cell / column selection (Ctrl+Space, Shift+Left/Right, cell-level Shift+Up/
70
+ // Down), aria-sort, aria-readonly, aria-rowcount / aria-colcount / *index.
71
+ // - row type headers (columnheader / rowheader), aria-label(ledby).
72
+ // - dynamic trees: the parent map is constant. The generator has no
73
+ // collections (str / int / bool vars only), so per-parent expansion is three
74
+ // bool vars and the controlled selected-row set is one bool per row.
75
+ // - Left on a collapsed / leaf row does NOT move to the parent: that is what
76
+ // APG says ("focus does not move"); implementations that jump to the parent
77
+ // diverge from APG.
78
+ module apg_treegrid {
79
+ import aria.* from "../../shared/aria"
80
+
81
+ var navModel: str // "rowsFirst" | "cellsFirst" | "cellsOnly"
82
+ var selMode: str // "single" | "multi"
83
+ var exp0: bool // row 0 expanded
84
+ var exp2: bool // row 2 expanded
85
+ var exp4: bool // row 4 expanded
86
+ var inGrid: bool // DOM focus is inside the treegrid
87
+ var kind: str // the roving item is a "row" or a "cell"
88
+ var frow: int // roving / focused row (0..6)
89
+ var fcol: int // roving / focused column (0..2); always 0 on a row
90
+ var selMask: int // selected rows, bit r set = row r selected (0..127)
91
+ var controlled: bool // the host owns the selection (`value` prop present)
92
+ var accepting: bool // controlled host that answers every request with its intent
93
+ var valueProp0: bool // controlled selection member 0 (row 0), ...
94
+ var valueProp1: bool
95
+ var valueProp2: bool
96
+ var valueProp3: bool
97
+ var valueProp4: bool
98
+ var valueProp5: bool
99
+ var valueProp6: bool
100
+ var requested: int // intent of the last request, packed selection
101
+ var outcome: str // "none" | "accepted" | "rejected"
102
+ var base: int // packed selection when the last request was received
103
+ var requests: int // number of requests that fired onSelectionChange
104
+
105
+ // ---- constants as nullary vals --------------------------------------------
106
+ val ROWS: int = 7
107
+ val COLS: int = 3
108
+
109
+ // ---- small arithmetic -----------------------------------------------------
110
+ def b(x: bool): int = if (x) 1 else 0
111
+ def minI(x: int, y: int): int = if (x < y) x else y
112
+
113
+ def bit(r: int): int =
114
+ if (r == 0) 1 else if (r == 1) 2 else if (r == 2) 4 else if (r == 3) 8
115
+ else if (r == 4) 16 else if (r == 5) 32 else if (r == 6) 64 else 0
116
+ def bitSet(mask: int, r: int): bool =
117
+ if (bit(r) == 0) false else (mask / bit(r)) % 2 == 1
118
+ def popcount(mask: int): int =
119
+ b(bitSet(mask, 0)) + b(bitSet(mask, 1)) + b(bitSet(mask, 2)) + b(bitSet(mask, 3))
120
+ + b(bitSet(mask, 4)) + b(bitSet(mask, 5)) + b(bitSet(mask, 6))
121
+ def withBit(mask: int, r: int): int = if (bitSet(mask, r)) mask else mask + bit(r)
122
+ def flipBit(mask: int, r: int): int =
123
+ if (bitSet(mask, r)) mask - bit(r) else mask + bit(r)
124
+
125
+ // ---- the tree: ONE parent map, everything else derived --------------------
126
+ def parentOf(r: int): int =
127
+ if (r == 1) 0 else if (r == 2) 0 else if (r == 3) 2
128
+ else if (r == 5) 4 else if (r == 6) 4 else -1
129
+ def childCount(p: int): int =
130
+ b(parentOf(0) == p) + b(parentOf(1) == p) + b(parentOf(2) == p) + b(parentOf(3) == p)
131
+ + b(parentOf(4) == p) + b(parentOf(5) == p) + b(parentOf(6) == p)
132
+ def hasChildren(r: int): bool = childCount(r) > 0
133
+
134
+ // ---- ARIA contract derived from the tree ----------------------------------
135
+ def ariaLevel(r: int): int =
136
+ if (parentOf(r) < 0) 1 else if (parentOf(parentOf(r)) < 0) 2 else 3
137
+ // siblings share a parent; the roots (parent -1) form the top-level set
138
+ def ariaSetSize(r: int): int = childCount(parentOf(r))
139
+ def ariaPosInSet(r: int): int =
140
+ 1 + b(0 < r and parentOf(0) == parentOf(r)) + b(1 < r and parentOf(1) == parentOf(r))
141
+ + b(2 < r and parentOf(2) == parentOf(r)) + b(3 < r and parentOf(3) == parentOf(r))
142
+ + b(4 < r and parentOf(4) == parentOf(r)) + b(5 < r and parentOf(5) == parentOf(r))
143
+
144
+ // ---- expansion and visibility ---------------------------------------------
145
+ // expansion of row p under a hypothetical (a, c, d) = (exp0, exp2, exp4)
146
+ def expW(p: int, a: bool, c: bool, d: bool): bool =
147
+ if (p == 0) a else if (p == 2) c else if (p == 4) d else false
148
+ def isExp(p: int): bool = expW(p, exp0, exp2, exp4)
149
+ // the children of p are rendered: p and every ancestor of p are expanded
150
+ // (depth is at most 3, so one grandparent step covers it)
151
+ def openW(p: int, a: bool, c: bool, d: bool): bool =
152
+ if (p < 0) true
153
+ else if (not(expW(p, a, c, d))) false
154
+ else if (parentOf(p) < 0) true
155
+ else expW(parentOf(p), a, c, d)
156
+ def visibleW(r: int, a: bool, c: bool, d: bool): bool =
157
+ if (r < 0 or r >= ROWS) false else openW(parentOf(r), a, c, d)
158
+ def visible(r: int): bool = visibleW(r, exp0, exp2, exp4)
159
+
160
+ // aria-expanded exists only on parent rows
161
+ def ariaExpandedOf(r: int): str =
162
+ if (hasChildren(r)) ariaBoolToken(isExp(r)) else "absent"
163
+ def canExpand(r: int): bool = hasChildren(r) and not(isExp(r))
164
+ def canCollapse(r: int): bool = hasChildren(r) and isExp(r)
165
+ def toggleIf(target: int, r: int, cur: bool): bool = if (target == r) not(cur) else cur
166
+
167
+ // ---- selection ARIA --------------------------------------------------------
168
+ def multiselectable(sel: str): bool = ariaSelIsMulti(sel)
169
+ def ariaSelectedOf(sel: str, mask: int, r: int): str =
170
+ if (bitSet(mask, r)) "true" else if (sel == "multi") "false" else "absent"
171
+ // single selection follows focus; multi leaves the selection alone
172
+ def follow(sel: str, mask: int, r: int): int = if (sel == "single") bit(r) else mask
173
+
174
+ // ---- roving tabindex -------------------------------------------------------
175
+ def rowFocusSupported(nav: str): bool = nav != "cellsOnly"
176
+ def isTabRow(r: int, k: str, fr: int): bool = k == "row" and fr == r
177
+ def isTabCell(r: int, c: int, k: str, fr: int, fc: int): bool =
178
+ k == "cell" and fr == r and fc == c
179
+ def rowTabindex(r: int, k: str, fr: int): int = if (isTabRow(r, k, fr)) 0 else -1
180
+ def cellTabindex(r: int, c: int, k: str, fr: int, fc: int): int =
181
+ if (isTabCell(r, c, k, fr, fc)) 0 else -1
182
+ def tabStopsIn(r: int, k: str, fr: int, fc: int): int =
183
+ b(isTabRow(r, k, fr)) + b(isTabCell(r, 0, k, fr, fc)) + b(isTabCell(r, 1, k, fr, fc))
184
+ + b(isTabCell(r, 2, k, fr, fc))
185
+ // tab stops among the RENDERED rows only
186
+ val tabStops: int =
187
+ b(visible(0)) * tabStopsIn(0, kind, frow, fcol) + b(visible(1)) * tabStopsIn(1, kind, frow, fcol)
188
+ + b(visible(2)) * tabStopsIn(2, kind, frow, fcol) + b(visible(3)) * tabStopsIn(3, kind, frow, fcol)
189
+ + b(visible(4)) * tabStopsIn(4, kind, frow, fcol) + b(visible(5)) * tabStopsIn(5, kind, frow, fcol)
190
+ + b(visible(6)) * tabStopsIn(6, kind, frow, fcol)
191
+
192
+ // ---- navigation over VISIBLE rows ------------------------------------------
193
+ def nextVis(r: int): int =
194
+ if (visible(r + 1)) r + 1 else if (visible(r + 2)) r + 2 else if (visible(r + 3)) r + 3
195
+ else if (visible(r + 4)) r + 4 else if (visible(r + 5)) r + 5 else if (visible(r + 6)) r + 6
196
+ else r
197
+ def prevVis(r: int): int =
198
+ if (visible(r - 1)) r - 1 else if (visible(r - 2)) r - 2 else if (visible(r - 3)) r - 3
199
+ else if (visible(r - 4)) r - 4 else if (visible(r - 5)) r - 5 else if (visible(r - 6)) r - 6
200
+ else r
201
+ val lastVis: int = prevVis(ROWS)
202
+ // target rows of the jump keys (the cell-mode forms keep the row)
203
+ val homeRow: int = if (kind == "row") 0 else frow
204
+ val endRow: int = if (kind == "row") lastVis else frow
205
+
206
+ // a click on the expander of row r is ignored unless that row is rendered
207
+ def clickTarget(r: int): int = if (visible(r)) r else -1
208
+ // collapsing may hide the focused row: focus relocates to the toggled row
209
+ def rowAfterToggle(r: int): int =
210
+ if (visibleW(frow, toggleIf(clickTarget(r), 0, exp0), toggleIf(clickTarget(r), 2, exp2),
211
+ toggleIf(clickTarget(r), 4, exp4))) frow else r
212
+
213
+ // ---- request / commit (map ticket rt1p) -----------------------------------
214
+ def holds: bool = and(controlled, not(accepting))
215
+ def follows: bool = and(controlled, accepting)
216
+ def verdict: str = if (holds) "rejected" else "accepted"
217
+ // the selection after a request for `want`
218
+ def landed(h: bool, current: int, want: int): int = if (h) current else want
219
+ // a controlled member after a request for `want`
220
+ def propAfter(f: bool, p: bool, want: bool): bool = if (f) want else p
221
+ // the packed controlled value, spelled out one member at a time
222
+ def propMask: int =
223
+ b(valueProp0) + 2 * b(valueProp1) + 4 * b(valueProp2) + 8 * b(valueProp3)
224
+ + 16 * b(valueProp4) + 32 * b(valueProp5) + 64 * b(valueProp6)
225
+ // a request is real only when its intent differs from the current selection
226
+ def reqMoves(want: int): bool = want != selMask
227
+ // the intended selection of each request action
228
+ def focusWant: int = follow(selMode, selMask, frow)
229
+ def downWant: int = if (inGrid) follow(selMode, selMask, nextVis(frow)) else selMask
230
+ def upWant: int = if (inGrid) follow(selMode, selMask, prevVis(frow)) else selMask
231
+ def homeWant: int = if (inGrid) follow(selMode, selMask, homeRow) else selMask
232
+ def endWant: int = if (inGrid) follow(selMode, selMask, endRow) else selMask
233
+ def ctrlHomeWant: int = if (inGrid) follow(selMode, selMask, 0) else selMask
234
+ def ctrlEndWant: int = if (inGrid) follow(selMode, selMask, lastVis) else selMask
235
+ def cellWant(cell: int): int =
236
+ if (visible(cell / COLS)) follow(selMode, selMask, cell / COLS) else selMask
237
+ def toggleRowWant(r: int): int =
238
+ if (and(selMode == "single", and(selMask != 0, visible(r))))
239
+ bit(rowAfterToggle(r)) else selMask
240
+ def toggleSelectWant: int =
241
+ if (inGrid and selMode == "multi") flipBit(selMask, frow) else selMask
242
+ def selectAllWant: int = if (inGrid and selMode == "multi") 127 else selMask
243
+ def extendDownWant: int =
244
+ if (inGrid and selMode == "multi" and kind == "row")
245
+ withBit(withBit(selMask, frow), nextVis(frow)) else selMask
246
+ def extendUpWant: int =
247
+ if (inGrid and selMode == "multi" and kind == "row")
248
+ withBit(withBit(selMask, frow), prevVis(frow)) else selMask
249
+
250
+ // ---- actions ---------------------------------------------------------------
251
+
252
+ action init = all {
253
+ navModel' = "rowsFirst",
254
+ selMode' = "single",
255
+ exp0' = false,
256
+ exp2' = false,
257
+ exp4' = false,
258
+ inGrid' = false,
259
+ kind' = "row",
260
+ frow' = 0,
261
+ fcol' = 0,
262
+ selMask' = 0,
263
+ controlled' = false,
264
+ accepting' = false,
265
+ valueProp0' = false,
266
+ valueProp1' = false,
267
+ valueProp2' = false,
268
+ valueProp3' = false,
269
+ valueProp4' = false,
270
+ valueProp5' = false,
271
+ valueProp6' = false,
272
+ requested' = 0,
273
+ outcome' = "none",
274
+ base' = 0,
275
+ requests' = 0
276
+ }
277
+
278
+ // pick the navigation model; the grid restarts collapsed with the model's entry
279
+ // point (a controlled selection is owned by the host and stays put)
280
+ action configureNav = {
281
+ nondet nav = Set("rowsFirst", "cellsFirst", "cellsOnly").oneOf()
282
+ all {
283
+ navModel' = nav,
284
+ selMode' = selMode,
285
+ exp0' = false,
286
+ exp2' = false,
287
+ exp4' = false,
288
+ inGrid' = false,
289
+ kind' = if (nav == "rowsFirst") "row" else "cell",
290
+ frow' = 0,
291
+ fcol' = 0,
292
+ selMask' = if (controlled) selMask else 0,
293
+ controlled' = controlled,
294
+ accepting' = accepting,
295
+ valueProp0' = valueProp0,
296
+ valueProp1' = valueProp1,
297
+ valueProp2' = valueProp2,
298
+ valueProp3' = valueProp3,
299
+ valueProp4' = valueProp4,
300
+ valueProp5' = valueProp5,
301
+ valueProp6' = valueProp6,
302
+ requested' = 0,
303
+ outcome' = "none",
304
+ base' = 0,
305
+ requests' = requests
306
+ }
307
+ }
308
+
309
+ // pick the selection variant; the grid restarts with the navigation model kept
310
+ action configureSel = {
311
+ nondet sel = Set("single", "multi").oneOf()
312
+ all {
313
+ navModel' = navModel,
314
+ selMode' = sel,
315
+ exp0' = false,
316
+ exp2' = false,
317
+ exp4' = false,
318
+ inGrid' = false,
319
+ kind' = if (navModel == "rowsFirst") "row" else "cell",
320
+ frow' = 0,
321
+ fcol' = 0,
322
+ selMask' = if (controlled) selMask else 0,
323
+ controlled' = controlled,
324
+ accepting' = accepting,
325
+ valueProp0' = valueProp0,
326
+ valueProp1' = valueProp1,
327
+ valueProp2' = valueProp2,
328
+ valueProp3' = valueProp3,
329
+ valueProp4' = valueProp4,
330
+ valueProp5' = valueProp5,
331
+ valueProp6' = valueProp6,
332
+ requested' = 0,
333
+ outcome' = "none",
334
+ base' = 0,
335
+ requests' = requests
336
+ }
337
+ }
338
+
339
+ // request: Tab into the grid (single selection follows the remembered tab stop)
340
+ action focusIn = all {
341
+ navModel' = navModel,
342
+ selMode' = selMode,
343
+ exp0' = exp0,
344
+ exp2' = exp2,
345
+ exp4' = exp4,
346
+ inGrid' = true,
347
+ kind' = kind,
348
+ frow' = frow,
349
+ fcol' = fcol,
350
+ selMask' = landed(holds, selMask, focusWant),
351
+ controlled' = controlled,
352
+ accepting' = accepting,
353
+ valueProp0' = propAfter(follows, valueProp0, bitSet(focusWant, 0)),
354
+ valueProp1' = propAfter(follows, valueProp1, bitSet(focusWant, 1)),
355
+ valueProp2' = propAfter(follows, valueProp2, bitSet(focusWant, 2)),
356
+ valueProp3' = propAfter(follows, valueProp3, bitSet(focusWant, 3)),
357
+ valueProp4' = propAfter(follows, valueProp4, bitSet(focusWant, 4)),
358
+ valueProp5' = propAfter(follows, valueProp5, bitSet(focusWant, 5)),
359
+ valueProp6' = propAfter(follows, valueProp6, bitSet(focusWant, 6)),
360
+ requested' = if (reqMoves(focusWant)) focusWant else requested,
361
+ outcome' = if (reqMoves(focusWant)) verdict else outcome,
362
+ base' = if (reqMoves(focusWant)) selMask else base,
363
+ requests' = if (reqMoves(focusWant)) requests + 1 else requests
364
+ }
365
+
366
+ // Tab out of the grid: the tab stop is remembered
367
+ action blur = all {
368
+ navModel' = navModel,
369
+ selMode' = selMode,
370
+ exp0' = exp0,
371
+ exp2' = exp2,
372
+ exp4' = exp4,
373
+ inGrid' = false,
374
+ kind' = kind,
375
+ frow' = frow,
376
+ fcol' = fcol,
377
+ selMask' = selMask,
378
+ controlled' = controlled,
379
+ accepting' = accepting,
380
+ valueProp0' = valueProp0,
381
+ valueProp1' = valueProp1,
382
+ valueProp2' = valueProp2,
383
+ valueProp3' = valueProp3,
384
+ valueProp4' = valueProp4,
385
+ valueProp5' = valueProp5,
386
+ valueProp6' = valueProp6,
387
+ requested' = requested,
388
+ outcome' = outcome,
389
+ base' = base,
390
+ requests' = requests
391
+ }
392
+
393
+ // Right: collapsed row expands; expanded / leaf row -> first cell; cell -> next cell
394
+ action right = all {
395
+ navModel' = navModel,
396
+ selMode' = selMode,
397
+ exp0' = toggleIf(if (inGrid and kind == "row" and canExpand(frow)) frow else -1, 0, exp0),
398
+ exp2' = toggleIf(if (inGrid and kind == "row" and canExpand(frow)) frow else -1, 2, exp2),
399
+ exp4' = toggleIf(if (inGrid and kind == "row" and canExpand(frow)) frow else -1, 4, exp4),
400
+ inGrid' = inGrid,
401
+ kind' = if (inGrid and kind == "row" and not(canExpand(frow))) "cell" else kind,
402
+ frow' = frow,
403
+ fcol' = if (inGrid and kind == "cell") minI(fcol + 1, COLS - 1) else fcol,
404
+ selMask' = selMask,
405
+ controlled' = controlled,
406
+ accepting' = accepting,
407
+ valueProp0' = valueProp0,
408
+ valueProp1' = valueProp1,
409
+ valueProp2' = valueProp2,
410
+ valueProp3' = valueProp3,
411
+ valueProp4' = valueProp4,
412
+ valueProp5' = valueProp5,
413
+ valueProp6' = valueProp6,
414
+ requested' = requested,
415
+ outcome' = outcome,
416
+ base' = base,
417
+ requests' = requests
418
+ }
419
+
420
+ // Left: expanded row collapses; first cell -> row (when rows are focusable);
421
+ // other cell -> previous cell; collapsed / leaf row stays
422
+ action left = all {
423
+ navModel' = navModel,
424
+ selMode' = selMode,
425
+ exp0' = toggleIf(if (inGrid and kind == "row" and canCollapse(frow)) frow else -1, 0, exp0),
426
+ exp2' = toggleIf(if (inGrid and kind == "row" and canCollapse(frow)) frow else -1, 2, exp2),
427
+ exp4' = toggleIf(if (inGrid and kind == "row" and canCollapse(frow)) frow else -1, 4, exp4),
428
+ inGrid' = inGrid,
429
+ kind' = if (inGrid and kind == "cell" and fcol == 0 and rowFocusSupported(navModel)) "row" else kind,
430
+ frow' = frow,
431
+ fcol' = if (inGrid and kind == "cell" and fcol > 0) fcol - 1 else fcol,
432
+ selMask' = selMask,
433
+ controlled' = controlled,
434
+ accepting' = accepting,
435
+ valueProp0' = valueProp0,
436
+ valueProp1' = valueProp1,
437
+ valueProp2' = valueProp2,
438
+ valueProp3' = valueProp3,
439
+ valueProp4' = valueProp4,
440
+ valueProp5' = valueProp5,
441
+ valueProp6' = valueProp6,
442
+ requested' = requested,
443
+ outcome' = outcome,
444
+ base' = base,
445
+ requests' = requests
446
+ }
447
+
448
+ // request: Down, next VISIBLE row (same column in cell mode)
449
+ action down = all {
450
+ navModel' = navModel,
451
+ selMode' = selMode,
452
+ exp0' = exp0,
453
+ exp2' = exp2,
454
+ exp4' = exp4,
455
+ inGrid' = inGrid,
456
+ kind' = kind,
457
+ frow' = if (inGrid) nextVis(frow) else frow,
458
+ fcol' = fcol,
459
+ selMask' = landed(holds, selMask, downWant),
460
+ controlled' = controlled,
461
+ accepting' = accepting,
462
+ valueProp0' = propAfter(follows, valueProp0, bitSet(downWant, 0)),
463
+ valueProp1' = propAfter(follows, valueProp1, bitSet(downWant, 1)),
464
+ valueProp2' = propAfter(follows, valueProp2, bitSet(downWant, 2)),
465
+ valueProp3' = propAfter(follows, valueProp3, bitSet(downWant, 3)),
466
+ valueProp4' = propAfter(follows, valueProp4, bitSet(downWant, 4)),
467
+ valueProp5' = propAfter(follows, valueProp5, bitSet(downWant, 5)),
468
+ valueProp6' = propAfter(follows, valueProp6, bitSet(downWant, 6)),
469
+ requested' = if (reqMoves(downWant)) downWant else requested,
470
+ outcome' = if (reqMoves(downWant)) verdict else outcome,
471
+ base' = if (reqMoves(downWant)) selMask else base,
472
+ requests' = if (reqMoves(downWant)) requests + 1 else requests
473
+ }
474
+
475
+ // request: Up, previous VISIBLE row (same column in cell mode)
476
+ action up = all {
477
+ navModel' = navModel,
478
+ selMode' = selMode,
479
+ exp0' = exp0,
480
+ exp2' = exp2,
481
+ exp4' = exp4,
482
+ inGrid' = inGrid,
483
+ kind' = kind,
484
+ frow' = if (inGrid) prevVis(frow) else frow,
485
+ fcol' = fcol,
486
+ selMask' = landed(holds, selMask, upWant),
487
+ controlled' = controlled,
488
+ accepting' = accepting,
489
+ valueProp0' = propAfter(follows, valueProp0, bitSet(upWant, 0)),
490
+ valueProp1' = propAfter(follows, valueProp1, bitSet(upWant, 1)),
491
+ valueProp2' = propAfter(follows, valueProp2, bitSet(upWant, 2)),
492
+ valueProp3' = propAfter(follows, valueProp3, bitSet(upWant, 3)),
493
+ valueProp4' = propAfter(follows, valueProp4, bitSet(upWant, 4)),
494
+ valueProp5' = propAfter(follows, valueProp5, bitSet(upWant, 5)),
495
+ valueProp6' = propAfter(follows, valueProp6, bitSet(upWant, 6)),
496
+ requested' = if (reqMoves(upWant)) upWant else requested,
497
+ outcome' = if (reqMoves(upWant)) verdict else outcome,
498
+ base' = if (reqMoves(upWant)) selMask else base,
499
+ requests' = if (reqMoves(upWant)) requests + 1 else requests
500
+ }
501
+
502
+ // request: Home; row -> first row, cell -> first cell of the row
503
+ action home = all {
504
+ navModel' = navModel,
505
+ selMode' = selMode,
506
+ exp0' = exp0,
507
+ exp2' = exp2,
508
+ exp4' = exp4,
509
+ inGrid' = inGrid,
510
+ kind' = kind,
511
+ frow' = if (inGrid) homeRow else frow,
512
+ fcol' = if (inGrid and kind == "cell") 0 else fcol,
513
+ selMask' = landed(holds, selMask, homeWant),
514
+ controlled' = controlled,
515
+ accepting' = accepting,
516
+ valueProp0' = propAfter(follows, valueProp0, bitSet(homeWant, 0)),
517
+ valueProp1' = propAfter(follows, valueProp1, bitSet(homeWant, 1)),
518
+ valueProp2' = propAfter(follows, valueProp2, bitSet(homeWant, 2)),
519
+ valueProp3' = propAfter(follows, valueProp3, bitSet(homeWant, 3)),
520
+ valueProp4' = propAfter(follows, valueProp4, bitSet(homeWant, 4)),
521
+ valueProp5' = propAfter(follows, valueProp5, bitSet(homeWant, 5)),
522
+ valueProp6' = propAfter(follows, valueProp6, bitSet(homeWant, 6)),
523
+ requested' = if (reqMoves(homeWant)) homeWant else requested,
524
+ outcome' = if (reqMoves(homeWant)) verdict else outcome,
525
+ base' = if (reqMoves(homeWant)) selMask else base,
526
+ requests' = if (reqMoves(homeWant)) requests + 1 else requests
527
+ }
528
+
529
+ // request: End; row -> last visible row, cell -> last cell of the row
530
+ action end = all {
531
+ navModel' = navModel,
532
+ selMode' = selMode,
533
+ exp0' = exp0,
534
+ exp2' = exp2,
535
+ exp4' = exp4,
536
+ inGrid' = inGrid,
537
+ kind' = kind,
538
+ frow' = if (inGrid) endRow else frow,
539
+ fcol' = if (inGrid and kind == "cell") COLS - 1 else fcol,
540
+ selMask' = landed(holds, selMask, endWant),
541
+ controlled' = controlled,
542
+ accepting' = accepting,
543
+ valueProp0' = propAfter(follows, valueProp0, bitSet(endWant, 0)),
544
+ valueProp1' = propAfter(follows, valueProp1, bitSet(endWant, 1)),
545
+ valueProp2' = propAfter(follows, valueProp2, bitSet(endWant, 2)),
546
+ valueProp3' = propAfter(follows, valueProp3, bitSet(endWant, 3)),
547
+ valueProp4' = propAfter(follows, valueProp4, bitSet(endWant, 4)),
548
+ valueProp5' = propAfter(follows, valueProp5, bitSet(endWant, 5)),
549
+ valueProp6' = propAfter(follows, valueProp6, bitSet(endWant, 6)),
550
+ requested' = if (reqMoves(endWant)) endWant else requested,
551
+ outcome' = if (reqMoves(endWant)) verdict else outcome,
552
+ base' = if (reqMoves(endWant)) selMask else base,
553
+ requests' = if (reqMoves(endWant)) requests + 1 else requests
554
+ }
555
+
556
+ // request: Ctrl+Home, first row (cell mode keeps the column)
557
+ action ctrlHome = all {
558
+ navModel' = navModel,
559
+ selMode' = selMode,
560
+ exp0' = exp0,
561
+ exp2' = exp2,
562
+ exp4' = exp4,
563
+ inGrid' = inGrid,
564
+ kind' = kind,
565
+ frow' = if (inGrid) 0 else frow,
566
+ fcol' = fcol,
567
+ selMask' = landed(holds, selMask, ctrlHomeWant),
568
+ controlled' = controlled,
569
+ accepting' = accepting,
570
+ valueProp0' = propAfter(follows, valueProp0, bitSet(ctrlHomeWant, 0)),
571
+ valueProp1' = propAfter(follows, valueProp1, bitSet(ctrlHomeWant, 1)),
572
+ valueProp2' = propAfter(follows, valueProp2, bitSet(ctrlHomeWant, 2)),
573
+ valueProp3' = propAfter(follows, valueProp3, bitSet(ctrlHomeWant, 3)),
574
+ valueProp4' = propAfter(follows, valueProp4, bitSet(ctrlHomeWant, 4)),
575
+ valueProp5' = propAfter(follows, valueProp5, bitSet(ctrlHomeWant, 5)),
576
+ valueProp6' = propAfter(follows, valueProp6, bitSet(ctrlHomeWant, 6)),
577
+ requested' = if (reqMoves(ctrlHomeWant)) ctrlHomeWant else requested,
578
+ outcome' = if (reqMoves(ctrlHomeWant)) verdict else outcome,
579
+ base' = if (reqMoves(ctrlHomeWant)) selMask else base,
580
+ requests' = if (reqMoves(ctrlHomeWant)) requests + 1 else requests
581
+ }
582
+
583
+ // request: Ctrl+End, last visible row (cell mode keeps the column)
584
+ action ctrlEnd = all {
585
+ navModel' = navModel,
586
+ selMode' = selMode,
587
+ exp0' = exp0,
588
+ exp2' = exp2,
589
+ exp4' = exp4,
590
+ inGrid' = inGrid,
591
+ kind' = kind,
592
+ frow' = if (inGrid) lastVis else frow,
593
+ fcol' = fcol,
594
+ selMask' = landed(holds, selMask, ctrlEndWant),
595
+ controlled' = controlled,
596
+ accepting' = accepting,
597
+ valueProp0' = propAfter(follows, valueProp0, bitSet(ctrlEndWant, 0)),
598
+ valueProp1' = propAfter(follows, valueProp1, bitSet(ctrlEndWant, 1)),
599
+ valueProp2' = propAfter(follows, valueProp2, bitSet(ctrlEndWant, 2)),
600
+ valueProp3' = propAfter(follows, valueProp3, bitSet(ctrlEndWant, 3)),
601
+ valueProp4' = propAfter(follows, valueProp4, bitSet(ctrlEndWant, 4)),
602
+ valueProp5' = propAfter(follows, valueProp5, bitSet(ctrlEndWant, 5)),
603
+ valueProp6' = propAfter(follows, valueProp6, bitSet(ctrlEndWant, 6)),
604
+ requested' = if (reqMoves(ctrlEndWant)) ctrlEndWant else requested,
605
+ outcome' = if (reqMoves(ctrlEndWant)) verdict else outcome,
606
+ base' = if (reqMoves(ctrlEndWant)) selMask else base,
607
+ requests' = if (reqMoves(ctrlEndWant)) requests + 1 else requests
608
+ }
609
+
610
+ // Enter: with cells-only focus, on the first cell of a parent row, toggles its children
611
+ action enter = all {
612
+ navModel' = navModel,
613
+ selMode' = selMode,
614
+ exp0' = toggleIf(if (inGrid and navModel == "cellsOnly" and kind == "cell" and fcol == 0 and hasChildren(frow)) frow else -1, 0, exp0),
615
+ exp2' = toggleIf(if (inGrid and navModel == "cellsOnly" and kind == "cell" and fcol == 0 and hasChildren(frow)) frow else -1, 2, exp2),
616
+ exp4' = toggleIf(if (inGrid and navModel == "cellsOnly" and kind == "cell" and fcol == 0 and hasChildren(frow)) frow else -1, 4, exp4),
617
+ inGrid' = inGrid,
618
+ kind' = kind,
619
+ frow' = frow,
620
+ fcol' = fcol,
621
+ selMask' = selMask,
622
+ controlled' = controlled,
623
+ accepting' = accepting,
624
+ valueProp0' = valueProp0,
625
+ valueProp1' = valueProp1,
626
+ valueProp2' = valueProp2,
627
+ valueProp3' = valueProp3,
628
+ valueProp4' = valueProp4,
629
+ valueProp5' = valueProp5,
630
+ valueProp6' = valueProp6,
631
+ requested' = requested,
632
+ outcome' = outcome,
633
+ base' = base,
634
+ requests' = requests
635
+ }
636
+
637
+ // request: click the expander of parent row r; single selection follows the
638
+ // relocated focus
639
+ action toggleRow = {
640
+ nondet r = Set(0, 2, 4).oneOf()
641
+ all {
642
+ navModel' = navModel,
643
+ selMode' = selMode,
644
+ exp0' = toggleIf(clickTarget(r), 0, exp0),
645
+ exp2' = toggleIf(clickTarget(r), 2, exp2),
646
+ exp4' = toggleIf(clickTarget(r), 4, exp4),
647
+ inGrid' = inGrid,
648
+ kind' = kind,
649
+ frow' = rowAfterToggle(r),
650
+ fcol' = fcol,
651
+ selMask' = landed(holds, selMask, toggleRowWant(r)),
652
+ controlled' = controlled,
653
+ accepting' = accepting,
654
+ valueProp0' = propAfter(follows, valueProp0, bitSet(toggleRowWant(r), 0)),
655
+ valueProp1' = propAfter(follows, valueProp1, bitSet(toggleRowWant(r), 1)),
656
+ valueProp2' = propAfter(follows, valueProp2, bitSet(toggleRowWant(r), 2)),
657
+ valueProp3' = propAfter(follows, valueProp3, bitSet(toggleRowWant(r), 3)),
658
+ valueProp4' = propAfter(follows, valueProp4, bitSet(toggleRowWant(r), 4)),
659
+ valueProp5' = propAfter(follows, valueProp5, bitSet(toggleRowWant(r), 5)),
660
+ valueProp6' = propAfter(follows, valueProp6, bitSet(toggleRowWant(r), 6)),
661
+ requested' = if (reqMoves(toggleRowWant(r))) toggleRowWant(r) else requested,
662
+ outcome' = if (reqMoves(toggleRowWant(r))) verdict else outcome,
663
+ base' = if (reqMoves(toggleRowWant(r))) selMask else base,
664
+ requests' = if (reqMoves(toggleRowWant(r))) requests + 1 else requests
665
+ }
666
+ }
667
+
668
+ // request: click cell number (row * COLS + col); an unrendered cell cannot be clicked
669
+ action clickCell = {
670
+ nondet cell = Set(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20).oneOf()
671
+ all {
672
+ navModel' = navModel,
673
+ selMode' = selMode,
674
+ exp0' = exp0,
675
+ exp2' = exp2,
676
+ exp4' = exp4,
677
+ inGrid' = if (visible(cell / COLS)) true else inGrid,
678
+ kind' = if (visible(cell / COLS)) "cell" else kind,
679
+ frow' = if (visible(cell / COLS)) cell / COLS else frow,
680
+ fcol' = if (visible(cell / COLS)) cell % COLS else fcol,
681
+ selMask' = landed(holds, selMask, cellWant(cell)),
682
+ controlled' = controlled,
683
+ accepting' = accepting,
684
+ valueProp0' = propAfter(follows, valueProp0, bitSet(cellWant(cell), 0)),
685
+ valueProp1' = propAfter(follows, valueProp1, bitSet(cellWant(cell), 1)),
686
+ valueProp2' = propAfter(follows, valueProp2, bitSet(cellWant(cell), 2)),
687
+ valueProp3' = propAfter(follows, valueProp3, bitSet(cellWant(cell), 3)),
688
+ valueProp4' = propAfter(follows, valueProp4, bitSet(cellWant(cell), 4)),
689
+ valueProp5' = propAfter(follows, valueProp5, bitSet(cellWant(cell), 5)),
690
+ valueProp6' = propAfter(follows, valueProp6, bitSet(cellWant(cell), 6)),
691
+ requested' = if (reqMoves(cellWant(cell))) cellWant(cell) else requested,
692
+ outcome' = if (reqMoves(cellWant(cell))) verdict else outcome,
693
+ base' = if (reqMoves(cellWant(cell))) selMask else base,
694
+ requests' = if (reqMoves(cellWant(cell))) requests + 1 else requests
695
+ }
696
+ }
697
+
698
+ // request: Shift+Space, multi toggles the focused row's selection
699
+ action toggleSelect = all {
700
+ navModel' = navModel,
701
+ selMode' = selMode,
702
+ exp0' = exp0,
703
+ exp2' = exp2,
704
+ exp4' = exp4,
705
+ inGrid' = inGrid,
706
+ kind' = kind,
707
+ frow' = frow,
708
+ fcol' = fcol,
709
+ selMask' = landed(holds, selMask, toggleSelectWant),
710
+ controlled' = controlled,
711
+ accepting' = accepting,
712
+ valueProp0' = propAfter(follows, valueProp0, bitSet(toggleSelectWant, 0)),
713
+ valueProp1' = propAfter(follows, valueProp1, bitSet(toggleSelectWant, 1)),
714
+ valueProp2' = propAfter(follows, valueProp2, bitSet(toggleSelectWant, 2)),
715
+ valueProp3' = propAfter(follows, valueProp3, bitSet(toggleSelectWant, 3)),
716
+ valueProp4' = propAfter(follows, valueProp4, bitSet(toggleSelectWant, 4)),
717
+ valueProp5' = propAfter(follows, valueProp5, bitSet(toggleSelectWant, 5)),
718
+ valueProp6' = propAfter(follows, valueProp6, bitSet(toggleSelectWant, 6)),
719
+ requested' = if (reqMoves(toggleSelectWant)) toggleSelectWant else requested,
720
+ outcome' = if (reqMoves(toggleSelectWant)) verdict else outcome,
721
+ base' = if (reqMoves(toggleSelectWant)) selMask else base,
722
+ requests' = if (reqMoves(toggleSelectWant)) requests + 1 else requests
723
+ }
724
+
725
+ // request: Ctrl+A, multi selects every row
726
+ action selectAll = all {
727
+ navModel' = navModel,
728
+ selMode' = selMode,
729
+ exp0' = exp0,
730
+ exp2' = exp2,
731
+ exp4' = exp4,
732
+ inGrid' = inGrid,
733
+ kind' = kind,
734
+ frow' = frow,
735
+ fcol' = fcol,
736
+ selMask' = landed(holds, selMask, selectAllWant),
737
+ controlled' = controlled,
738
+ accepting' = accepting,
739
+ valueProp0' = propAfter(follows, valueProp0, bitSet(selectAllWant, 0)),
740
+ valueProp1' = propAfter(follows, valueProp1, bitSet(selectAllWant, 1)),
741
+ valueProp2' = propAfter(follows, valueProp2, bitSet(selectAllWant, 2)),
742
+ valueProp3' = propAfter(follows, valueProp3, bitSet(selectAllWant, 3)),
743
+ valueProp4' = propAfter(follows, valueProp4, bitSet(selectAllWant, 4)),
744
+ valueProp5' = propAfter(follows, valueProp5, bitSet(selectAllWant, 5)),
745
+ valueProp6' = propAfter(follows, valueProp6, bitSet(selectAllWant, 6)),
746
+ requested' = if (reqMoves(selectAllWant)) selectAllWant else requested,
747
+ outcome' = if (reqMoves(selectAllWant)) verdict else outcome,
748
+ base' = if (reqMoves(selectAllWant)) selMask else base,
749
+ requests' = if (reqMoves(selectAllWant)) requests + 1 else requests
750
+ }
751
+
752
+ // request: Shift+Down on a focused row (multi), move down and extend the selection
753
+ action extendDown = all {
754
+ navModel' = navModel,
755
+ selMode' = selMode,
756
+ exp0' = exp0,
757
+ exp2' = exp2,
758
+ exp4' = exp4,
759
+ inGrid' = inGrid,
760
+ kind' = kind,
761
+ frow' = if (inGrid and selMode == "multi" and kind == "row") nextVis(frow) else frow,
762
+ fcol' = fcol,
763
+ selMask' = landed(holds, selMask, extendDownWant),
764
+ controlled' = controlled,
765
+ accepting' = accepting,
766
+ valueProp0' = propAfter(follows, valueProp0, bitSet(extendDownWant, 0)),
767
+ valueProp1' = propAfter(follows, valueProp1, bitSet(extendDownWant, 1)),
768
+ valueProp2' = propAfter(follows, valueProp2, bitSet(extendDownWant, 2)),
769
+ valueProp3' = propAfter(follows, valueProp3, bitSet(extendDownWant, 3)),
770
+ valueProp4' = propAfter(follows, valueProp4, bitSet(extendDownWant, 4)),
771
+ valueProp5' = propAfter(follows, valueProp5, bitSet(extendDownWant, 5)),
772
+ valueProp6' = propAfter(follows, valueProp6, bitSet(extendDownWant, 6)),
773
+ requested' = if (reqMoves(extendDownWant)) extendDownWant else requested,
774
+ outcome' = if (reqMoves(extendDownWant)) verdict else outcome,
775
+ base' = if (reqMoves(extendDownWant)) selMask else base,
776
+ requests' = if (reqMoves(extendDownWant)) requests + 1 else requests
777
+ }
778
+
779
+ // request: Shift+Up on a focused row (multi), move up and extend the selection
780
+ action extendUp = all {
781
+ navModel' = navModel,
782
+ selMode' = selMode,
783
+ exp0' = exp0,
784
+ exp2' = exp2,
785
+ exp4' = exp4,
786
+ inGrid' = inGrid,
787
+ kind' = kind,
788
+ frow' = if (inGrid and selMode == "multi" and kind == "row") prevVis(frow) else frow,
789
+ fcol' = fcol,
790
+ selMask' = landed(holds, selMask, extendUpWant),
791
+ controlled' = controlled,
792
+ accepting' = accepting,
793
+ valueProp0' = propAfter(follows, valueProp0, bitSet(extendUpWant, 0)),
794
+ valueProp1' = propAfter(follows, valueProp1, bitSet(extendUpWant, 1)),
795
+ valueProp2' = propAfter(follows, valueProp2, bitSet(extendUpWant, 2)),
796
+ valueProp3' = propAfter(follows, valueProp3, bitSet(extendUpWant, 3)),
797
+ valueProp4' = propAfter(follows, valueProp4, bitSet(extendUpWant, 4)),
798
+ valueProp5' = propAfter(follows, valueProp5, bitSet(extendUpWant, 5)),
799
+ valueProp6' = propAfter(follows, valueProp6, bitSet(extendUpWant, 6)),
800
+ requested' = if (reqMoves(extendUpWant)) extendUpWant else requested,
801
+ outcome' = if (reqMoves(extendUpWant)) verdict else outcome,
802
+ base' = if (reqMoves(extendUpWant)) selMask else base,
803
+ requests' = if (reqMoves(extendUpWant)) requests + 1 else requests
804
+ }
805
+
806
+ // commit: the host writes the controlled selection (a no-op while uncontrolled).
807
+ // The pool is one row at a time: a host sets a single row or clears.
808
+ action setValue = {
809
+ nondet v = Set(0, 1, 2, 4, 8, 16, 32, 64).oneOf()
810
+ all {
811
+ navModel' = navModel,
812
+ selMode' = selMode,
813
+ exp0' = exp0,
814
+ exp2' = exp2,
815
+ exp4' = exp4,
816
+ inGrid' = inGrid,
817
+ kind' = kind,
818
+ frow' = frow,
819
+ fcol' = fcol,
820
+ selMask' = if (controlled) v else selMask,
821
+ controlled' = controlled,
822
+ accepting' = accepting,
823
+ valueProp0' = if (controlled) bitSet(v, 0) else valueProp0,
824
+ valueProp1' = if (controlled) bitSet(v, 1) else valueProp1,
825
+ valueProp2' = if (controlled) bitSet(v, 2) else valueProp2,
826
+ valueProp3' = if (controlled) bitSet(v, 3) else valueProp3,
827
+ valueProp4' = if (controlled) bitSet(v, 4) else valueProp4,
828
+ valueProp5' = if (controlled) bitSet(v, 5) else valueProp5,
829
+ valueProp6' = if (controlled) bitSet(v, 6) else valueProp6,
830
+ requested' = requested,
831
+ outcome' = if (controlled) "none" else outcome,
832
+ base' = base,
833
+ requests' = requests
834
+ }
835
+ }
836
+
837
+ // commit: the host flips one row of its own copy of the selection
838
+ action hostToggle = {
839
+ nondet r = Set(0, 1, 2, 3, 4, 5, 6).oneOf()
840
+ all {
841
+ navModel' = navModel,
842
+ selMode' = selMode,
843
+ exp0' = exp0,
844
+ exp2' = exp2,
845
+ exp4' = exp4,
846
+ inGrid' = inGrid,
847
+ kind' = kind,
848
+ frow' = frow,
849
+ fcol' = fcol,
850
+ selMask' = if (controlled) flipBit(propMask, r) else selMask,
851
+ controlled' = controlled,
852
+ accepting' = accepting,
853
+ valueProp0' = if (and(controlled, r == 0)) not(valueProp0) else valueProp0,
854
+ valueProp1' = if (and(controlled, r == 1)) not(valueProp1) else valueProp1,
855
+ valueProp2' = if (and(controlled, r == 2)) not(valueProp2) else valueProp2,
856
+ valueProp3' = if (and(controlled, r == 3)) not(valueProp3) else valueProp3,
857
+ valueProp4' = if (and(controlled, r == 4)) not(valueProp4) else valueProp4,
858
+ valueProp5' = if (and(controlled, r == 5)) not(valueProp5) else valueProp5,
859
+ valueProp6' = if (and(controlled, r == 6)) not(valueProp6) else valueProp6,
860
+ requested' = requested,
861
+ outcome' = if (controlled) "none" else outcome,
862
+ base' = base,
863
+ requests' = requests
864
+ }
865
+ }
866
+
867
+ // configuration: the host starts owning the selection, adopting the current rows
868
+ action control = all {
869
+ navModel' = navModel,
870
+ selMode' = selMode,
871
+ exp0' = exp0,
872
+ exp2' = exp2,
873
+ exp4' = exp4,
874
+ inGrid' = inGrid,
875
+ kind' = kind,
876
+ frow' = frow,
877
+ fcol' = fcol,
878
+ selMask' = selMask,
879
+ controlled' = true,
880
+ accepting' = accepting,
881
+ valueProp0' = bitSet(selMask, 0),
882
+ valueProp1' = bitSet(selMask, 1),
883
+ valueProp2' = bitSet(selMask, 2),
884
+ valueProp3' = bitSet(selMask, 3),
885
+ valueProp4' = bitSet(selMask, 4),
886
+ valueProp5' = bitSet(selMask, 5),
887
+ valueProp6' = bitSet(selMask, 6),
888
+ requested' = requested,
889
+ outcome' = outcome,
890
+ base' = base,
891
+ requests' = requests
892
+ }
893
+
894
+ // configuration: the controlled host answers every request with its intent
895
+ action acceptRequests = all {
896
+ navModel' = navModel,
897
+ selMode' = selMode,
898
+ exp0' = exp0,
899
+ exp2' = exp2,
900
+ exp4' = exp4,
901
+ inGrid' = inGrid,
902
+ kind' = kind,
903
+ frow' = frow,
904
+ fcol' = fcol,
905
+ selMask' = selMask,
906
+ controlled' = controlled,
907
+ accepting' = true,
908
+ valueProp0' = valueProp0,
909
+ valueProp1' = valueProp1,
910
+ valueProp2' = valueProp2,
911
+ valueProp3' = valueProp3,
912
+ valueProp4' = valueProp4,
913
+ valueProp5' = valueProp5,
914
+ valueProp6' = valueProp6,
915
+ requested' = requested,
916
+ outcome' = outcome,
917
+ base' = base,
918
+ requests' = requests
919
+ }
920
+
921
+ action step = any {
922
+ init, configureNav, configureSel, focusIn, blur,
923
+ right, left, down, up, home, end, ctrlHome, ctrlEnd, enter,
924
+ toggleRow, clickCell, toggleSelect, selectAll, extendDown, extendUp,
925
+ setValue, hostToggle, control, acceptRequests
926
+ }
927
+
928
+ // ---- invariant helpers -----------------------------------------------------
929
+ def childMirror(k: int): bool =
930
+ parentOf(k) < 0 or not(visible(parentOf(k))) or visible(k) == isExp(parentOf(k))
931
+ def rowContractOk(r: int): bool =
932
+ ariaLevel(r) >= 1 and ariaLevel(r) <= 3
933
+ and ariaPosInSet(r) >= 1 and ariaPosInSet(r) <= ariaSetSize(r)
934
+ and ariaLevel(r) == (if (parentOf(r) < 0) 1 else ariaLevel(parentOf(r)) + 1)
935
+ and (ariaExpandedOf(r) == "absent") == not(hasChildren(r))
936
+
937
+ // ---- invariants ------------------------------------------------------------
938
+ // the focused / tab-stop row is always rendered (never inside a collapsed subtree)
939
+ val FocusedRowVisible: bool = visible(frow)
940
+ // roving tabindex: exactly one rendered row-or-cell has tabindex=0
941
+ val ExactlyOneTabStop: bool = tabStops == 1
942
+ // the roving position stays inside the grid
943
+ val PositionInRange: bool = frow >= 0 and frow < ROWS and fcol >= 0 and fcol < COLS
944
+ // a focused row has no column
945
+ val RowFocusHasNoColumn: bool = kind != "row" or fcol == 0
946
+ // kind is one of the two modelled item types
947
+ val KindKnown: bool = kind == "row" or kind == "cell"
948
+ // cells-only models never focus a row
949
+ val CellsOnlyNeverFocusesRow: bool = navModel != "cellsOnly" or kind == "cell"
950
+ // top-level rows are always rendered
951
+ val TopLevelAlwaysVisible: bool = visible(0) and visible(4)
952
+ // aria-expanded mirrors child visibility: children of a rendered parent row are
953
+ // rendered exactly when it is expanded
954
+ val AriaExpandedMirrorsChildren: bool =
955
+ childMirror(0) and childMirror(1) and childMirror(2) and childMirror(3)
956
+ and childMirror(4) and childMirror(5) and childMirror(6)
957
+ // level / setsize / posinset / aria-expanded are well-formed for every row
958
+ val AriaContractWellFormed: bool =
959
+ rowContractOk(0) and rowContractOk(1) and rowContractOk(2) and rowContractOk(3)
960
+ and rowContractOk(4) and rowContractOk(5) and rowContractOk(6)
961
+ // the selection bitmask stays inside the 7 rows
962
+ val SelectionInRange: bool = selMask >= 0 and selMask < 128
963
+ // single-select: aria-selected on at most one row, and it follows the focus
964
+ // (uncontrolled: a controlled host owns the selection)
965
+ val SingleSelectAtMostOne: bool =
966
+ ariaSingleHoldsAtMostOne(controlled, selMode != "single", popcount(selMask))
967
+ val SingleSelectFollowsFocus: bool =
968
+ or(controlled, selMode != "single" or selMask == 0 or selMask == bit(frow))
969
+ val SingleSelectedWhenFocused: bool =
970
+ or(controlled, selMode != "single" or not(inGrid) or selMask == bit(frow))
971
+ // models and variants are the modelled ones
972
+ val ModesKnown: bool =
973
+ (navModel == "rowsFirst" or navModel == "cellsFirst" or navModel == "cellsOnly")
974
+ and ariaSelModeKnown(selMode)
975
+ // callbacks only ever count up from zero
976
+ val RequestsNonNegative: bool = requests >= 0
977
+ // controlled: only the value prop sets the selection, so no event commits
978
+ val ControlledFollowsProp: bool = or(not(controlled), selMask == propMask)
979
+ // a rejected request leaves the selection as it found it
980
+ val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), selMask == base)
981
+ // an accepted request committed exactly its intent
982
+ val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), selMask == requested)
983
+ }