@cssints/apg-specs 0.0.0-stage → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -2
  3. package/dist/apg/accordion.bridge.d.mts +35 -0
  4. package/dist/apg/accordion.bridge.mjs +326 -0
  5. package/dist/apg/accordion.model.json +8397 -0
  6. package/dist/apg/alert.bridge.d.mts +22 -0
  7. package/dist/apg/alert.bridge.mjs +86 -0
  8. package/dist/apg/alert.model.json +871 -0
  9. package/dist/apg/alertdialog.bridge.d.mts +21 -0
  10. package/dist/apg/alertdialog.bridge.mjs +221 -0
  11. package/dist/apg/alertdialog.model.json +5232 -0
  12. package/dist/apg/breadcrumb.bridge.d.mts +26 -0
  13. package/dist/apg/breadcrumb.bridge.mjs +120 -0
  14. package/dist/apg/breadcrumb.model.json +574 -0
  15. package/dist/apg/button.bridge.d.mts +35 -0
  16. package/dist/apg/button.bridge.mjs +208 -0
  17. package/dist/apg/button.model.json +5814 -0
  18. package/dist/apg/carousel.bridge.d.mts +44 -0
  19. package/dist/apg/carousel.bridge.mjs +262 -0
  20. package/dist/apg/carousel.model.json +8963 -0
  21. package/dist/apg/checkbox-group.bridge.d.mts +25 -0
  22. package/dist/apg/checkbox-group.bridge.mjs +276 -0
  23. package/dist/apg/checkbox-group.model.json +3863 -0
  24. package/dist/apg/checkbox.bridge.d.mts +34 -0
  25. package/dist/apg/checkbox.bridge.mjs +253 -0
  26. package/dist/apg/checkbox.model.json +3158 -0
  27. package/dist/apg/combobox-autocomplete.bridge.d.mts +14 -0
  28. package/dist/apg/combobox-autocomplete.bridge.mjs +326 -0
  29. package/dist/apg/combobox-autocomplete.model.json +11586 -0
  30. package/dist/apg/combobox-dialog.bridge.d.mts +17 -0
  31. package/dist/apg/combobox-dialog.bridge.mjs +250 -0
  32. package/dist/apg/combobox-dialog.model.json +8981 -0
  33. package/dist/apg/combobox-grid.bridge.d.mts +17 -0
  34. package/dist/apg/combobox-grid.bridge.mjs +227 -0
  35. package/dist/apg/combobox-grid.model.json +8554 -0
  36. package/dist/apg/combobox-select-only.bridge.d.mts +24 -0
  37. package/dist/apg/combobox-select-only.bridge.mjs +304 -0
  38. package/dist/apg/combobox-select-only.model.json +7846 -0
  39. package/dist/apg/combobox-tree.bridge.d.mts +17 -0
  40. package/dist/apg/combobox-tree.bridge.mjs +211 -0
  41. package/dist/apg/combobox-tree.model.json +9345 -0
  42. package/dist/apg/dialog-modal.bridge.d.mts +35 -0
  43. package/dist/apg/dialog-modal.bridge.mjs +186 -0
  44. package/dist/apg/dialog-modal.model.json +7580 -0
  45. package/dist/apg/disclosure.bridge.d.mts +25 -0
  46. package/dist/apg/disclosure.bridge.mjs +202 -0
  47. package/dist/apg/disclosure.model.json +4703 -0
  48. package/dist/apg/feed.bridge.d.mts +31 -0
  49. package/dist/apg/feed.bridge.mjs +246 -0
  50. package/dist/apg/feed.model.json +4523 -0
  51. package/dist/apg/grid-selection.bridge.d.mts +13 -0
  52. package/dist/apg/grid-selection.bridge.mjs +160 -0
  53. package/dist/apg/grid-selection.model.json +6974 -0
  54. package/dist/apg/grid-sort.bridge.d.mts +12 -0
  55. package/dist/apg/grid-sort.bridge.mjs +138 -0
  56. package/dist/apg/grid-sort.model.json +3799 -0
  57. package/dist/apg/grid.bridge.d.mts +54 -0
  58. package/dist/apg/grid.bridge.mjs +441 -0
  59. package/dist/apg/grid.model.json +3802 -0
  60. package/dist/apg/link.bridge.d.mts +24 -0
  61. package/dist/apg/link.bridge.mjs +126 -0
  62. package/dist/apg/link.model.json +694 -0
  63. package/dist/apg/listbox-multi.bridge.d.mts +10 -0
  64. package/dist/apg/listbox-multi.bridge.mjs +261 -0
  65. package/dist/apg/listbox-multi.model.json +10893 -0
  66. package/dist/apg/listbox-rearrangeable.bridge.d.mts +20 -0
  67. package/dist/apg/listbox-rearrangeable.bridge.mjs +237 -0
  68. package/dist/apg/listbox-rearrangeable.model.json +17592 -0
  69. package/dist/apg/listbox-shared.d.mts +58 -0
  70. package/dist/apg/listbox-shared.mjs +171 -0
  71. package/dist/apg/listbox-single.bridge.d.mts +10 -0
  72. package/dist/apg/listbox-single.bridge.mjs +196 -0
  73. package/dist/apg/listbox-single.model.json +8024 -0
  74. package/dist/apg/listbox-typeahead.bridge.d.mts +10 -0
  75. package/dist/apg/listbox-typeahead.bridge.mjs +153 -0
  76. package/dist/apg/listbox-typeahead.model.json +5615 -0
  77. package/dist/apg/menu-button.bridge.d.mts +29 -0
  78. package/dist/apg/menu-button.bridge.mjs +259 -0
  79. package/dist/apg/menu-button.model.json +5706 -0
  80. package/dist/apg/menu-checkable.bridge.d.mts +25 -0
  81. package/dist/apg/menu-checkable.bridge.mjs +226 -0
  82. package/dist/apg/menu-checkable.model.json +4277 -0
  83. package/dist/apg/menu-menubar.bridge.d.mts +27 -0
  84. package/dist/apg/menu-menubar.bridge.mjs +273 -0
  85. package/dist/apg/menu-menubar.model.json +9540 -0
  86. package/dist/apg/meter.bridge.d.mts +28 -0
  87. package/dist/apg/meter.bridge.mjs +159 -0
  88. package/dist/apg/meter.model.json +1185 -0
  89. package/dist/apg/player.bridge.d.mts +24 -0
  90. package/dist/apg/player.bridge.mjs +342 -0
  91. package/dist/apg/player.model.json +11013 -0
  92. package/dist/apg/radio.bridge.d.mts +35 -0
  93. package/dist/apg/radio.bridge.mjs +371 -0
  94. package/dist/apg/radio.model.json +5011 -0
  95. package/dist/apg/slider-multithumb.bridge.d.mts +34 -0
  96. package/dist/apg/slider-multithumb.bridge.mjs +365 -0
  97. package/dist/apg/slider-multithumb.model.json +5774 -0
  98. package/dist/apg/slider.bridge.d.mts +54 -0
  99. package/dist/apg/slider.bridge.mjs +359 -0
  100. package/dist/apg/slider.model.json +4881 -0
  101. package/dist/apg/spinbutton.bridge.d.mts +41 -0
  102. package/dist/apg/spinbutton.bridge.mjs +327 -0
  103. package/dist/apg/spinbutton.model.json +7571 -0
  104. package/dist/apg/switch.bridge.d.mts +30 -0
  105. package/dist/apg/switch.bridge.mjs +243 -0
  106. package/dist/apg/switch.model.json +3159 -0
  107. package/dist/apg/tabs.bridge.d.mts +41 -0
  108. package/dist/apg/tabs.bridge.mjs +294 -0
  109. package/dist/apg/tabs.model.json +7659 -0
  110. package/dist/apg/timeline.bridge.d.mts +36 -0
  111. package/dist/apg/timeline.bridge.mjs +265 -0
  112. package/dist/apg/timeline.model.json +6189 -0
  113. package/dist/apg/toolbar.bridge.d.mts +25 -0
  114. package/dist/apg/toolbar.bridge.mjs +307 -0
  115. package/dist/apg/toolbar.model.json +6616 -0
  116. package/dist/apg/tooltip.bridge.d.mts +36 -0
  117. package/dist/apg/tooltip.bridge.mjs +208 -0
  118. package/dist/apg/tooltip.model.json +8295 -0
  119. package/dist/apg/treegrid.bridge.d.mts +28 -0
  120. package/dist/apg/treegrid.bridge.mjs +311 -0
  121. package/dist/apg/treegrid.model.json +17358 -0
  122. package/dist/apg/treeview.bridge.d.mts +26 -0
  123. package/dist/apg/treeview.bridge.mjs +396 -0
  124. package/dist/apg/treeview.model.json +13745 -0
  125. package/dist/apg/windowsplitter.bridge.d.mts +36 -0
  126. package/dist/apg/windowsplitter.bridge.mjs +242 -0
  127. package/dist/apg/windowsplitter.model.json +6253 -0
  128. package/dist/canvas/camera.model.json +6568 -0
  129. package/dist/canvas/canvas.bridge.d.mts +190 -0
  130. package/dist/canvas/canvas.bridge.mjs +729 -0
  131. package/dist/canvas/pointer.model.json +17095 -0
  132. package/dist/scenario.d.mts +233 -0
  133. package/dist/scenario.mjs +313 -0
  134. package/package.json +57 -4
  135. package/scenarios/apg/README.md +221 -0
  136. package/scenarios/apg/accordion.base-ui.json +1005 -0
  137. package/scenarios/apg/accordion.react-aria.json +929 -0
  138. package/scenarios/apg/alert.base-ui.json +81 -0
  139. package/scenarios/apg/alert.react-aria.json +98 -0
  140. package/scenarios/apg/alertdialog.base-ui.json +331 -0
  141. package/scenarios/apg/alertdialog.react-aria.json +225 -0
  142. package/scenarios/apg/breadcrumb.react-aria.json +312 -0
  143. package/scenarios/apg/button.ariakit.json +136 -0
  144. package/scenarios/apg/button.base-ui.json +747 -0
  145. package/scenarios/apg/button.react-aria.json +443 -0
  146. package/scenarios/apg/carousel.apg.json +909 -0
  147. package/scenarios/apg/checkbox.ariakit.json +299 -0
  148. package/scenarios/apg/checkbox.base-ui.json +1126 -0
  149. package/scenarios/apg/checkbox.react-aria.json +518 -0
  150. package/scenarios/apg/combobox.ariakit.json +945 -0
  151. package/scenarios/apg/combobox.base-ui.json +2444 -0
  152. package/scenarios/apg/combobox.react-aria.json +845 -0
  153. package/scenarios/apg/dialog-modal.ariakit.json +1102 -0
  154. package/scenarios/apg/dialog-modal.base-ui.json +775 -0
  155. package/scenarios/apg/dialog-modal.react-aria.json +638 -0
  156. package/scenarios/apg/disclosure.apg.json +349 -0
  157. package/scenarios/apg/disclosure.ariakit.json +163 -0
  158. package/scenarios/apg/disclosure.base-ui.json +451 -0
  159. package/scenarios/apg/disclosure.react-aria.json +643 -0
  160. package/scenarios/apg/feed.apg.json +442 -0
  161. package/scenarios/apg/grid.ariakit.json +197 -0
  162. package/scenarios/apg/grid.base-ui.json +720 -0
  163. package/scenarios/apg/grid.react-aria.json +969 -0
  164. package/scenarios/apg/landmarks.ariakit.json +93 -0
  165. package/scenarios/apg/landmarks.react-aria.json +330 -0
  166. package/scenarios/apg/link.ariakit.json +83 -0
  167. package/scenarios/apg/link.base-ui.json +88 -0
  168. package/scenarios/apg/link.react-aria.json +186 -0
  169. package/scenarios/apg/listbox.ariakit.json +78 -0
  170. package/scenarios/apg/listbox.base-ui.json +163 -0
  171. package/scenarios/apg/listbox.react-aria.json +1118 -0
  172. package/scenarios/apg/menu-button.ariakit.json +924 -0
  173. package/scenarios/apg/menu-button.base-ui.json +1155 -0
  174. package/scenarios/apg/menu-button.react-aria.json +1080 -0
  175. package/scenarios/apg/menubar.ariakit.json +758 -0
  176. package/scenarios/apg/menubar.base-ui.json +673 -0
  177. package/scenarios/apg/meter.base-ui.json +243 -0
  178. package/scenarios/apg/meter.react-aria.json +201 -0
  179. package/scenarios/apg/radio.ariakit.json +459 -0
  180. package/scenarios/apg/radio.base-ui.json +581 -0
  181. package/scenarios/apg/radio.react-aria.json +576 -0
  182. package/scenarios/apg/slider-multithumb.base-ui.json +316 -0
  183. package/scenarios/apg/slider-multithumb.react-aria.json +363 -0
  184. package/scenarios/apg/slider.base-ui.json +544 -0
  185. package/scenarios/apg/slider.react-aria.json +456 -0
  186. package/scenarios/apg/spinbutton.base-ui.json +1688 -0
  187. package/scenarios/apg/spinbutton.react-aria.json +426 -0
  188. package/scenarios/apg/switch.base-ui.json +456 -0
  189. package/scenarios/apg/switch.react-aria.json +303 -0
  190. package/scenarios/apg/table.ariakit.json +65 -0
  191. package/scenarios/apg/table.react-aria.json +221 -0
  192. package/scenarios/apg/tabs.ariakit.json +329 -0
  193. package/scenarios/apg/tabs.base-ui.json +875 -0
  194. package/scenarios/apg/tabs.react-aria.json +644 -0
  195. package/scenarios/apg/toolbar.ariakit.json +595 -0
  196. package/scenarios/apg/toolbar.base-ui.json +1189 -0
  197. package/scenarios/apg/toolbar.react-aria.json +383 -0
  198. package/scenarios/apg/tooltip.ariakit.json +441 -0
  199. package/scenarios/apg/tooltip.base-ui.json +411 -0
  200. package/scenarios/apg/tooltip.react-aria.json +454 -0
  201. package/scenarios/apg/treegrid.apg.json +2558 -0
  202. package/scenarios/apg/treegrid.react-aria.json +5094 -0
  203. package/scenarios/apg/treeview.apg.json +928 -0
  204. package/scenarios/apg/windowsplitter.apg.json +557 -0
  205. package/scenarios/canvas/README.md +69 -0
  206. package/scenarios/canvas/camera.contract.json +237 -0
  207. package/scenarios/canvas/camera.excalidraw.json +57 -0
  208. package/scenarios/canvas/marquee.excalidraw.json +122 -0
  209. package/scenarios/canvas/pan.excalidraw.json +46 -0
  210. package/scenarios/canvas/pointer.contract.json +528 -0
  211. package/scenarios/parts.json +291 -0
  212. package/shared/aria.qnt +186 -0
  213. package/shared/details-group.qnt +50 -0
  214. package/shared/focusgroup.qnt +152 -0
  215. package/shared/invokers.qnt +81 -0
  216. package/shared/popover.qnt +71 -0
  217. package/specs/apg/STATIC.md +103 -0
  218. package/specs/apg/accordion.qnt +578 -0
  219. package/specs/apg/alert.qnt +131 -0
  220. package/specs/apg/alertdialog.qnt +455 -0
  221. package/specs/apg/breadcrumb.qnt +88 -0
  222. package/specs/apg/button.qnt +445 -0
  223. package/specs/apg/carousel.qnt +732 -0
  224. package/specs/apg/checkbox-group.qnt +388 -0
  225. package/specs/apg/checkbox.qnt +394 -0
  226. package/specs/apg/combobox-autocomplete.qnt +764 -0
  227. package/specs/apg/combobox-dialog.qnt +627 -0
  228. package/specs/apg/combobox-grid.qnt +632 -0
  229. package/specs/apg/combobox-select-only.qnt +626 -0
  230. package/specs/apg/combobox-tree.qnt +633 -0
  231. package/specs/apg/dialog-modal.qnt +499 -0
  232. package/specs/apg/disclosure.qnt +309 -0
  233. package/specs/apg/feed.qnt +485 -0
  234. package/specs/apg/grid-selection.qnt +512 -0
  235. package/specs/apg/grid-sort.qnt +330 -0
  236. package/specs/apg/grid.qnt +360 -0
  237. package/specs/apg/link.qnt +119 -0
  238. package/specs/apg/listbox-multi.qnt +799 -0
  239. package/specs/apg/listbox-rearrangeable.qnt +801 -0
  240. package/specs/apg/listbox-single.qnt +511 -0
  241. package/specs/apg/listbox-typeahead.qnt +396 -0
  242. package/specs/apg/menu-button.qnt +363 -0
  243. package/specs/apg/menu-checkable.qnt +304 -0
  244. package/specs/apg/menu-menubar.qnt +531 -0
  245. package/specs/apg/meter.qnt +129 -0
  246. package/specs/apg/player.qnt +1081 -0
  247. package/specs/apg/radio.qnt +394 -0
  248. package/specs/apg/slider-multithumb.qnt +441 -0
  249. package/specs/apg/slider.qnt +396 -0
  250. package/specs/apg/spinbutton.qnt +494 -0
  251. package/specs/apg/switch.qnt +389 -0
  252. package/specs/apg/tabs.qnt +412 -0
  253. package/specs/apg/timeline.qnt +612 -0
  254. package/specs/apg/toolbar.qnt +364 -0
  255. package/specs/apg/tooltip.qnt +553 -0
  256. package/specs/apg/treegrid.qnt +983 -0
  257. package/specs/apg/treeview.qnt +1004 -0
  258. package/specs/apg/windowsplitter.qnt +487 -0
  259. package/specs/canvas/camera.qnt +407 -0
  260. package/specs/canvas/pointer.qnt +841 -0
@@ -0,0 +1,396 @@
1
+ // APG Listbox, typeahead — https://www.w3.org/WAI/ARIA/apg/patterns/listbox/
2
+ //
3
+ // Sections modelled:
4
+ // - "Keyboard Interaction": "Type-ahead is recommended for all listboxes":
5
+ // type a character focus moves to the next option whose name starts
6
+ // with that character
7
+ // type several quickly focus moves to the next option whose name starts
8
+ // with the string typed so far
9
+ // and the "Printable Characters" row of every example's keyboard table.
10
+ //
11
+ // The pool is a three-letter alphabet (a=1, b=2, c=3) and six options named
12
+ // 0 "aa" 1 "ab" 2 "b" 3 "ba" 4 "bc" 5 "c"
13
+ // Names are digit strings in base 4 (first char most significant, 0 is never a
14
+ // char), so a prefix test is one integer division. The type-ahead buffer is the
15
+ // same encoding plus its length. The inactivity timer is an event: type_char
16
+ // arms it, `timeout` clears the buffer.
17
+ //
18
+ // Search rule (the pattern text, made precise):
19
+ // - empty buffer: the first option AFTER the active one that starts with the
20
+ // typed char, wrapping; the active option itself is the last candidate, so
21
+ // no other match means focus stays
22
+ // - non-empty buffer: the first option from the active one (inclusive) that
23
+ // starts with the whole buffer, wrapping — so continuing a word keeps focus
24
+ // on an option that still matches
25
+ // - no match: focus does not move (the buffer still grows)
26
+ //
27
+ // Request vs commit (map ticket rt1p). Type-ahead is a *focus move*, and this
28
+ // machine's controllable value is the active option: type_char and the plain
29
+ // moves (arrow_down/up, home, end) are *requests* carrying the intended active
30
+ // index. A request fires onValueChange (`requests` counts the calls) when the
31
+ // intent differs from the current active option, and the active option commits
32
+ // through `landed(holds, active, intent)`:
33
+ // uncontrolled accepted: focus commits the intent
34
+ // controlled, holds rejected: focus is untouched (the type-ahead buffer still
35
+ // grows, exactly as the primitive's internal buffer does)
36
+ // controlled, accepts accepted through the controlled value (`hostWrites`)
37
+ // The controlled value is `valueProp` (the active index); `setValue` /
38
+ // `hostToggle` are the host moving focus. `control` / `acceptRequests` are
39
+ // configuration events (init is literal-only). Toggling a listbox with
40
+ // selection-follows-focus composes this focus move with the selection requests
41
+ // of listbox-single.qnt; this machine models only the focus move.
42
+ //
43
+ // The prefix invariant is scoped to the uncontrolled path: a controlled host
44
+ // owns the active option, so a rejected request may leave focus on an option
45
+ // that no longer matches the (grown) buffer.
46
+ //
47
+ // Deliberate omissions / choices (ponytail):
48
+ // - the buffer saturates at three chars: every name has at most two, so a
49
+ // longer buffer can never match and its exact content is unobservable; this
50
+ // keeps the state space finite
51
+ // - navigation keys and blur clear the buffer (the pattern is silent; the
52
+ // example script keeps the buffer until its timeout fires)
53
+ // - the example script never matches option 0 on a multi-char search; the
54
+ // pattern text does not call for that quirk, so it is not modelled
55
+ // - repeating the same char to cycle ("aaa"), case folding, non-letter chars,
56
+ // the real 500ms duration, and IME composition
57
+ //
58
+ // Generator notes: no parameterized actions and no `let`, so a request spells
59
+ // `landed(...)` out per assignment and every action assigns every var.
60
+ module apg_listbox_typeahead {
61
+ import aria.* from "../../shared/aria"
62
+
63
+ var hasFocus: bool // the listbox is the focused element
64
+ var active: int // the active option (aria-activedescendant target)
65
+ var buf: int // typed chars so far, base-4 digit string
66
+ var bufLen: int // number of chars in the buffer (saturates at maxBuffer)
67
+ var armed: bool // the clear-buffer timer is running
68
+ var matched: bool // the last typed char focused a matching option
69
+ var controlled: bool // the host owns the value (`value` prop present)
70
+ var accepting: bool // controlled host that answers every request with its intent
71
+ var valueProp: int // the controlled value prop (the active index)
72
+ var requested: int // intent of the last request (active index)
73
+ var outcome: str // "none" | "accepted" | "rejected"
74
+ var base: int // active index when the last request was received
75
+ var requests: int // number of requests that fired onValueChange
76
+
77
+ def optionCount: int = 6
78
+ def lastIndex: int = 5
79
+ def noOption: int = 6
80
+ def maxBuffer: int = 3
81
+ // base-4 digit strings: a=1 b=2 c=3
82
+ def nameCode(i: int): int =
83
+ if (i == 0) 5
84
+ else if (i == 1) 6
85
+ else if (i == 2) 2
86
+ else if (i == 3) 9
87
+ else if (i == 4) 11
88
+ else 3
89
+ def nameLen(i: int): int = if (or(i == 2, i == 5)) 1 else 2
90
+ def pow4(n: int): int =
91
+ if (n == 0) 1
92
+ else if (n == 1) 4
93
+ else if (n == 2) 16
94
+ else 64
95
+ def nextIndex(i: int): int = if (i < lastIndex) i + 1 else i
96
+ def prevIndex(i: int): int = if (i > 0) i - 1 else i
97
+
98
+ // option i's name starts with the buffer (len chars, base-4 code)
99
+ def startsWith(i: int, buffer: int, len: int): bool =
100
+ and(len >= 1, len <= nameLen(i), nameCode(i) / pow4(nameLen(i) - len) == buffer)
101
+ // the first matching option, scanning optionCount candidates from start
102
+ // (candidates and the start rule are shared/aria.qnt: ariaCycle0, ariaTaStart0)
103
+ def candidate(start: int, k: int): int = ariaCycle0(optionCount, start, k)
104
+ def matchFrom(start: int, buffer: int, len: int): int =
105
+ if (startsWith(candidate(start, 0), buffer, len)) candidate(start, 0)
106
+ else if (startsWith(candidate(start, 1), buffer, len)) candidate(start, 1)
107
+ else if (startsWith(candidate(start, 2), buffer, len)) candidate(start, 2)
108
+ else if (startsWith(candidate(start, 3), buffer, len)) candidate(start, 3)
109
+ else if (startsWith(candidate(start, 4), buffer, len)) candidate(start, 4)
110
+ else if (startsWith(candidate(start, 5), buffer, len)) candidate(start, 5)
111
+ else noOption
112
+ def nextBuffer(buffer: int, len: int, ch: int): int =
113
+ if (len < maxBuffer) buffer * 4 + ch else buffer
114
+ def nextLength(len: int): int = if (len < maxBuffer) len + 1 else len
115
+ // the option typing ch would focus (noOption = no match)
116
+ def typeTarget(act: int, buffer: int, len: int, ch: int): int =
117
+ matchFrom(
118
+ ariaTaStart0(optionCount, act, len == 0),
119
+ nextBuffer(buffer, len, ch),
120
+ nextLength(len)
121
+ )
122
+
123
+ // --- request / commit (map ticket rt1p) ---------------------------------
124
+ // controlled host that keeps the old value: the request is rejected
125
+ def holds: bool = and(controlled, not(accepting))
126
+ // controlled host that writes the intent back through the value prop
127
+ def hostWrites: bool = and(controlled, accepting)
128
+ def verdict: str = if (holds) "rejected" else "accepted"
129
+ // the active option after a request for `want`
130
+ def landed(h: bool, current: int, want: int): int = if (h) current else want
131
+ // the intended active option of each request
132
+ def typedIntent(ch: int): int =
133
+ if (hasFocus) (if (typeTarget(active, buf, bufLen, ch) == noOption) active else typeTarget(active, buf, bufLen, ch)) else active
134
+ def downIntent: int = if (hasFocus) nextIndex(active) else active
135
+ def upIntent: int = if (hasFocus) prevIndex(active) else active
136
+ def homeIntent: int = if (hasFocus) 0 else active
137
+ def endIntent: int = if (hasFocus) lastIndex else active
138
+
139
+ // --- ARIA contract, derived from the state ------------------------------
140
+ def ariaActiveDescendant(focused: bool, idx: int): int =
141
+ if (focused) idx else noOption
142
+
143
+ action init = all {
144
+ hasFocus' = false,
145
+ active' = 0,
146
+ buf' = 0,
147
+ bufLen' = 0,
148
+ armed' = false,
149
+ matched' = false,
150
+ controlled' = false,
151
+ accepting' = false,
152
+ valueProp' = 0,
153
+ requested' = 0,
154
+ outcome' = "none",
155
+ base' = 0,
156
+ requests' = 0
157
+ }
158
+
159
+ // Tab into the listbox; focus returns to the remembered active option.
160
+ action focus_in = all {
161
+ hasFocus' = true,
162
+ active' = active,
163
+ buf' = buf,
164
+ bufLen' = bufLen,
165
+ armed' = armed,
166
+ matched' = matched,
167
+ controlled' = controlled,
168
+ accepting' = accepting,
169
+ valueProp' = valueProp,
170
+ requested' = requested,
171
+ outcome' = outcome,
172
+ base' = base,
173
+ requests' = requests
174
+ }
175
+
176
+ // Tab out: the buffer is dropped.
177
+ action blur = all {
178
+ hasFocus' = false,
179
+ active' = active,
180
+ buf' = 0,
181
+ bufLen' = 0,
182
+ armed' = false,
183
+ matched' = false,
184
+ controlled' = controlled,
185
+ accepting' = accepting,
186
+ valueProp' = valueProp,
187
+ requested' = requested,
188
+ outcome' = outcome,
189
+ base' = base,
190
+ requests' = requests
191
+ }
192
+
193
+ // A printable char: extend the buffer, re-arm the timer, request a focus move
194
+ // to the search result (stay put when nothing matches).
195
+ action type_char = {
196
+ nondet ch = Set(1, 2, 3).oneOf()
197
+ all {
198
+ hasFocus' = hasFocus,
199
+ active' = landed(holds, active, typedIntent(ch)),
200
+ buf' = if (hasFocus) nextBuffer(buf, bufLen, ch) else buf,
201
+ bufLen' = if (hasFocus) nextLength(bufLen) else bufLen,
202
+ armed' = if (hasFocus) true else armed,
203
+ matched' = if (hasFocus) (and(typeTarget(active, buf, bufLen, ch) != noOption, not(holds))) else matched,
204
+ controlled' = controlled,
205
+ accepting' = accepting,
206
+ valueProp' = if (hostWrites) typedIntent(ch) else valueProp,
207
+ requested' = if (typedIntent(ch) != active) typedIntent(ch) else requested,
208
+ outcome' = if (typedIntent(ch) != active) verdict else outcome,
209
+ base' = if (typedIntent(ch) != active) active else base,
210
+ requests' = if (typedIntent(ch) != active) requests + 1 else requests
211
+ }
212
+ }
213
+
214
+ // The inactivity timer fires: the buffer is cleared.
215
+ action timeout = all {
216
+ hasFocus' = hasFocus,
217
+ active' = active,
218
+ buf' = 0,
219
+ bufLen' = 0,
220
+ armed' = false,
221
+ matched' = false,
222
+ controlled' = controlled,
223
+ accepting' = accepting,
224
+ valueProp' = valueProp,
225
+ requested' = requested,
226
+ outcome' = outcome,
227
+ base' = base,
228
+ requests' = requests
229
+ }
230
+
231
+ // Navigation keys request a focus move and drop the buffer.
232
+ action arrow_down = all {
233
+ hasFocus' = hasFocus,
234
+ active' = landed(holds, active, downIntent),
235
+ buf' = 0,
236
+ bufLen' = 0,
237
+ armed' = false,
238
+ matched' = false,
239
+ controlled' = controlled,
240
+ accepting' = accepting,
241
+ valueProp' = if (hostWrites) downIntent else valueProp,
242
+ requested' = if (downIntent != active) downIntent else requested,
243
+ outcome' = if (downIntent != active) verdict else outcome,
244
+ base' = if (downIntent != active) active else base,
245
+ requests' = if (downIntent != active) requests + 1 else requests
246
+ }
247
+
248
+ action arrow_up = all {
249
+ hasFocus' = hasFocus,
250
+ active' = landed(holds, active, upIntent),
251
+ buf' = 0,
252
+ bufLen' = 0,
253
+ armed' = false,
254
+ matched' = false,
255
+ controlled' = controlled,
256
+ accepting' = accepting,
257
+ valueProp' = if (hostWrites) upIntent else valueProp,
258
+ requested' = if (upIntent != active) upIntent else requested,
259
+ outcome' = if (upIntent != active) verdict else outcome,
260
+ base' = if (upIntent != active) active else base,
261
+ requests' = if (upIntent != active) requests + 1 else requests
262
+ }
263
+
264
+ action home = all {
265
+ hasFocus' = hasFocus,
266
+ active' = landed(holds, active, homeIntent),
267
+ buf' = 0,
268
+ bufLen' = 0,
269
+ armed' = false,
270
+ matched' = false,
271
+ controlled' = controlled,
272
+ accepting' = accepting,
273
+ valueProp' = if (hostWrites) homeIntent else valueProp,
274
+ requested' = if (homeIntent != active) homeIntent else requested,
275
+ outcome' = if (homeIntent != active) verdict else outcome,
276
+ base' = if (homeIntent != active) active else base,
277
+ requests' = if (homeIntent != active) requests + 1 else requests
278
+ }
279
+
280
+ action end = all {
281
+ hasFocus' = hasFocus,
282
+ active' = landed(holds, active, endIntent),
283
+ buf' = 0,
284
+ bufLen' = 0,
285
+ armed' = false,
286
+ matched' = false,
287
+ controlled' = controlled,
288
+ accepting' = accepting,
289
+ valueProp' = if (hostWrites) endIntent else valueProp,
290
+ requested' = if (endIntent != active) endIntent else requested,
291
+ outcome' = if (endIntent != active) verdict else outcome,
292
+ base' = if (endIntent != active) active else base,
293
+ requests' = if (endIntent != active) requests + 1 else requests
294
+ }
295
+
296
+ // commit: the host writes the controlled value (a no-op while uncontrolled)
297
+ action setValue = {
298
+ nondet v = Set(0, 1, 2, 3, 4, 5).oneOf()
299
+ all {
300
+ hasFocus' = hasFocus,
301
+ active' = if (controlled) v else active,
302
+ buf' = buf,
303
+ bufLen' = bufLen,
304
+ armed' = armed,
305
+ matched' = if (controlled) false else matched,
306
+ controlled' = controlled,
307
+ accepting' = accepting,
308
+ valueProp' = if (controlled) v else valueProp,
309
+ requested' = requested,
310
+ outcome' = if (controlled) "none" else outcome,
311
+ base' = base,
312
+ requests' = requests
313
+ }
314
+ }
315
+
316
+ // commit: the host advances its own copy of the value (an outside "next")
317
+ action hostToggle = all {
318
+ hasFocus' = hasFocus,
319
+ active' = if (controlled) (if (active < lastIndex) active + 1 else 0) else active,
320
+ buf' = buf,
321
+ bufLen' = bufLen,
322
+ armed' = armed,
323
+ matched' = if (controlled) false else matched,
324
+ controlled' = controlled,
325
+ accepting' = accepting,
326
+ valueProp' = if (controlled) (if (active < lastIndex) active + 1 else 0) else valueProp,
327
+ requested' = requested,
328
+ outcome' = if (controlled) "none" else outcome,
329
+ base' = base,
330
+ requests' = requests
331
+ }
332
+
333
+ // configuration: the host starts owning the value, adopting the current one
334
+ action control = all {
335
+ hasFocus' = hasFocus,
336
+ active' = active,
337
+ buf' = buf,
338
+ bufLen' = bufLen,
339
+ armed' = armed,
340
+ matched' = matched,
341
+ controlled' = true,
342
+ accepting' = accepting,
343
+ valueProp' = active,
344
+ requested' = requested,
345
+ outcome' = outcome,
346
+ base' = base,
347
+ requests' = requests
348
+ }
349
+
350
+ // configuration: the controlled host answers every request with its intent
351
+ action acceptRequests = all {
352
+ hasFocus' = hasFocus,
353
+ active' = active,
354
+ buf' = buf,
355
+ bufLen' = bufLen,
356
+ armed' = armed,
357
+ matched' = matched,
358
+ controlled' = controlled,
359
+ accepting' = true,
360
+ valueProp' = valueProp,
361
+ requested' = requested,
362
+ outcome' = outcome,
363
+ base' = base,
364
+ requests' = requests
365
+ }
366
+
367
+ action step = any {
368
+ init, focus_in, blur, type_char, timeout, arrow_down, arrow_up, home, end,
369
+ setValue, hostToggle, control, acceptRequests
370
+ }
371
+
372
+ // the active option is always a real option
373
+ val ActiveInRange: bool = and(active >= 0, active <= lastIndex)
374
+ // aria-activedescendant is set exactly while the listbox holds focus
375
+ val ActiveDescendantTracksFocus: bool =
376
+ (ariaActiveDescendant(hasFocus, active) == noOption) == not(hasFocus)
377
+ // the buffer is a well-formed base-4 string of bufLen chars
378
+ val BufferWellFormed: bool =
379
+ and(bufLen >= 0, bufLen <= maxBuffer, buf >= 0, buf < pow4(bufLen))
380
+ // the timer runs exactly while there is something to clear
381
+ val TimerArmedIffBuffered: bool = armed == (bufLen > 0)
382
+ // keys only reach a focused listbox, so only it has a buffer
383
+ val BufferOnlyWhileFocused: bool = or(bufLen == 0, hasFocus)
384
+ // when typing moved (or kept) focus on a match, the focused name starts with
385
+ // the buffer (a rejected request never counts as matched)
386
+ val MatchedImpliesPrefix: bool =
387
+ or(not(matched), and(bufLen > 0, startsWith(active, buf, bufLen)))
388
+ // callbacks only ever count up from zero
389
+ val RequestsNonNegative: bool = requests >= 0
390
+ // controlled: only the value prop sets the state, so no event commits
391
+ val ControlledFollowsProp: bool = or(not(controlled), active == valueProp)
392
+ // a rejected request leaves the state as it found it
393
+ val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), active == base)
394
+ // an accepted request committed exactly its intent
395
+ val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), active == requested)
396
+ }