@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,394 @@
1
+ // WAI-ARIA APG — Checkbox pattern: one checkbox (dual-state, optionally tri-state).
2
+ //
3
+ // Modelled from https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/ :
4
+ // - "About This Pattern": dual-state (checked / not checked) and tri-state
5
+ // (adds partially checked) checkboxes
6
+ // - "Keyboard Interaction": Space changes the state; Enter does nothing
7
+ // - "WAI-ARIA Roles, States, and Properties": role=checkbox; aria-checked is
8
+ // "true" / "false" / "mixed" (mixed only for a tri-state checkbox)
9
+ // and the Checkbox Examples (https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/examples/checkbox/):
10
+ // Space is armed on keydown and flips on keyup (the native checkbox behaviour).
11
+ // The group controller that derives "mixed" from its children is the separate
12
+ // machine specs/apg/checkbox-group.qnt.
13
+ //
14
+ // Attribute contract (derived from state, never stored):
15
+ // role = "checkbox"
16
+ // aria-checked = "true" | "false" | "mixed"; "mixed" only when `allowMixed`
17
+ // Configuration variant: `allowMixed` (dual-state vs tri-state display).
18
+ // Activating a mixed checkbox checks it ("mixed" -> "true"), as the group
19
+ // controller does ("Checking the overall checkbox checks all options").
20
+ //
21
+ // Request vs commit (map ticket rt1p). A user activation — Space, a pointer
22
+ // click — is a *request* carrying the new state (`requested`); it fires
23
+ // onCheckedChange (`requests` counts the calls) and then:
24
+ // uncontrolled the request is accepted: the state commits the intent
25
+ // controlled, holds the host ignores it ("rejected"): the state is untouched
26
+ // controlled, accepts the host answers with the same value (`accepting`): the
27
+ // state commits through the controlled value, not the event
28
+ // The controlled value (`valueProp`, possibly "mixed") is the only thing that
29
+ // changes state while `controlled`; `setValue` / `hostToggle` are the host
30
+ // writing it. `control` / `acceptRequests` are configuration events, `seed`
31
+ // plants an initial value (including "mixed") without counting as a request.
32
+ //
33
+ // Invariants for the model: ControlledFollowsProp (in controlled mode no event
34
+ // commits), RejectedRequestUntouched, AcceptedRequestCommits.
35
+ //
36
+ // `outcome` is "none" | "accepted" | "rejected" for the last request; a host
37
+ // write settles it back to "none". `base` is the value when it was received.
38
+ //
39
+ // Deliberate omissions:
40
+ // - accessible name, aria-labelledby/aria-describedby, group wiring
41
+ // - Enter key (the pattern lists Space only; modelled as a no-op)
42
+ // - required / invalid / read-only (not part of the APG checkbox pattern)
43
+ // - pointer-capture details
44
+ module apg_checkbox {
45
+ var focused: bool // the checkbox has keyboard focus
46
+ var disabled: bool // aria-disabled: focusable, never changes by user input
47
+ var state: str // "true" | "false" | "mixed"
48
+ var allowMixed: bool // config: tri-state checkbox (may display "mixed")
49
+ var spaceArmed: bool // Space went down on the focused checkbox, awaiting keyup
50
+ var pointerArmed: bool // pointer went down on the checkbox, awaiting release
51
+ var requests: int // onChange callbacks fired by user input (not by prop changes)
52
+ var controlled: bool // the host owns the value (`value` prop present)
53
+ var accepting: bool // controlled host that answers every request with its intent
54
+ var valueProp: str // the controlled value prop (meaningful while controlled)
55
+ var requested: str // intent of the last request
56
+ var outcome: str // "none" | "accepted" | "rejected"
57
+ var base: str // value when the last request was received
58
+
59
+ // false -> true, true -> false, mixed -> true
60
+ def nextState(state: str): str = if (state == "true") "false" else "true"
61
+
62
+ def spaceArms(focused: bool, disabled: bool, key: str): bool =
63
+ and(focused, not(disabled), key == "Space")
64
+ def spaceReleaseFires(armed: bool, disabled: bool, key: str): bool =
65
+ and(armed, not(disabled), key == "Space")
66
+ def pointerReleaseFires(armed: bool, disabled: bool): bool =
67
+ and(armed, not(disabled))
68
+
69
+ // ARIA attribute contract: "mixed" is only exposed by a tri-state checkbox
70
+ def ariaChecked(state: str, allowMixed: bool): str =
71
+ if (state == "true") "true"
72
+ else if (and(state == "mixed", allowMixed)) "mixed"
73
+ else "false"
74
+
75
+ // request / commit (map ticket rt1p)
76
+ def holds: bool = and(controlled, not(accepting))
77
+ def follows: bool = and(controlled, accepting)
78
+ def verdict: str = if (holds) "rejected" else "accepted"
79
+ // the value after a request for `want`
80
+ def landed(h: bool, c: str, want: str): str = if (h) c else want
81
+ // the value prop after a request for `want`
82
+ def propAfter(f: bool, p: str, want: str): str = if (f) want else p
83
+
84
+ action init = all {
85
+ focused' = false,
86
+ disabled' = false,
87
+ state' = "false",
88
+ allowMixed' = false,
89
+ spaceArmed' = false,
90
+ pointerArmed' = false,
91
+ requests' = 0,
92
+ controlled' = false,
93
+ accepting' = false,
94
+ valueProp' = "false",
95
+ requested' = "false",
96
+ outcome' = "none",
97
+ base' = "false"
98
+ }
99
+
100
+ action focus = all {
101
+ focused' = true,
102
+ disabled' = disabled,
103
+ state' = state,
104
+ allowMixed' = allowMixed,
105
+ spaceArmed' = spaceArmed,
106
+ pointerArmed' = pointerArmed,
107
+ requests' = requests,
108
+ controlled' = controlled,
109
+ accepting' = accepting,
110
+ valueProp' = valueProp,
111
+ requested' = requested,
112
+ outcome' = outcome,
113
+ base' = base
114
+ }
115
+
116
+ // blur cancels a pending Space press
117
+ action blur = all {
118
+ focused' = false,
119
+ disabled' = disabled,
120
+ state' = state,
121
+ allowMixed' = allowMixed,
122
+ spaceArmed' = false,
123
+ pointerArmed' = pointerArmed,
124
+ requests' = requests,
125
+ controlled' = controlled,
126
+ accepting' = accepting,
127
+ valueProp' = valueProp,
128
+ requested' = requested,
129
+ outcome' = outcome,
130
+ base' = base
131
+ }
132
+
133
+ // only Space is handled on keydown (it arms); Enter and other keys do nothing
134
+ action key_down = {
135
+ nondet key = Set("Space", "Enter", "Other").oneOf()
136
+ all {
137
+ focused' = focused,
138
+ disabled' = disabled,
139
+ state' = state,
140
+ allowMixed' = allowMixed,
141
+ spaceArmed' = if (spaceArms(focused, disabled, key)) true else spaceArmed,
142
+ pointerArmed' = pointerArmed,
143
+ requests' = requests,
144
+ controlled' = controlled,
145
+ accepting' = accepting,
146
+ valueProp' = valueProp,
147
+ requested' = requested,
148
+ outcome' = outcome,
149
+ base' = base
150
+ }
151
+ }
152
+
153
+ // request: Space released on the focused, enabled checkbox
154
+ action key_up = {
155
+ nondet key = Set("Space", "Enter", "Other").oneOf()
156
+ all {
157
+ focused' = focused,
158
+ disabled' = disabled,
159
+ state' = if (spaceReleaseFires(spaceArmed, disabled, key)) landed(holds, state, nextState(state)) else state,
160
+ allowMixed' = allowMixed,
161
+ spaceArmed' = if (key == "Space") false else spaceArmed,
162
+ pointerArmed' = pointerArmed,
163
+ requests' = if (spaceReleaseFires(spaceArmed, disabled, key)) requests + 1 else requests,
164
+ controlled' = controlled,
165
+ accepting' = accepting,
166
+ valueProp' = if (spaceReleaseFires(spaceArmed, disabled, key)) propAfter(follows, valueProp, nextState(state)) else valueProp,
167
+ requested' = if (spaceReleaseFires(spaceArmed, disabled, key)) nextState(state) else requested,
168
+ outcome' = if (spaceReleaseFires(spaceArmed, disabled, key)) verdict else outcome,
169
+ base' = if (spaceReleaseFires(spaceArmed, disabled, key)) state else base
170
+ }
171
+ }
172
+
173
+ action pointer_down = all {
174
+ focused' = true,
175
+ disabled' = disabled,
176
+ state' = state,
177
+ allowMixed' = allowMixed,
178
+ spaceArmed' = spaceArmed,
179
+ pointerArmed' = not(disabled),
180
+ requests' = requests,
181
+ controlled' = controlled,
182
+ accepting' = accepting,
183
+ valueProp' = valueProp,
184
+ requested' = requested,
185
+ outcome' = outcome,
186
+ base' = base
187
+ }
188
+
189
+ // request: pointer released over the checkbox
190
+ action pointer_up = all {
191
+ focused' = focused,
192
+ disabled' = disabled,
193
+ state' = if (pointerReleaseFires(pointerArmed, disabled)) landed(holds, state, nextState(state)) else state,
194
+ allowMixed' = allowMixed,
195
+ spaceArmed' = spaceArmed,
196
+ pointerArmed' = false,
197
+ requests' = if (pointerReleaseFires(pointerArmed, disabled)) requests + 1 else requests,
198
+ controlled' = controlled,
199
+ accepting' = accepting,
200
+ valueProp' = if (pointerReleaseFires(pointerArmed, disabled)) propAfter(follows, valueProp, nextState(state)) else valueProp,
201
+ requested' = if (pointerReleaseFires(pointerArmed, disabled)) nextState(state) else requested,
202
+ outcome' = if (pointerReleaseFires(pointerArmed, disabled)) verdict else outcome,
203
+ base' = if (pointerReleaseFires(pointerArmed, disabled)) state else base
204
+ }
205
+
206
+ action pointer_cancel = all {
207
+ focused' = focused,
208
+ disabled' = disabled,
209
+ state' = state,
210
+ allowMixed' = allowMixed,
211
+ spaceArmed' = spaceArmed,
212
+ pointerArmed' = false,
213
+ requests' = requests,
214
+ controlled' = controlled,
215
+ accepting' = accepting,
216
+ valueProp' = valueProp,
217
+ requested' = requested,
218
+ outcome' = outcome,
219
+ base' = base
220
+ }
221
+
222
+ // commit: the host writes the controlled value (a no-op while uncontrolled).
223
+ // A dual-state checkbox rejects "mixed".
224
+ action setValue = {
225
+ nondet incoming = Set("true", "false", "mixed").oneOf()
226
+ all {
227
+ focused' = focused,
228
+ disabled' = disabled,
229
+ state' = if (and(controlled, or(not(incoming == "mixed"), allowMixed))) incoming else state,
230
+ allowMixed' = allowMixed,
231
+ spaceArmed' = spaceArmed,
232
+ pointerArmed' = pointerArmed,
233
+ requests' = requests,
234
+ controlled' = controlled,
235
+ accepting' = accepting,
236
+ valueProp' = if (and(controlled, or(not(incoming == "mixed"), allowMixed))) incoming else valueProp,
237
+ requested' = requested,
238
+ outcome' = if (controlled) "none" else outcome,
239
+ base' = base
240
+ }
241
+ }
242
+
243
+ // commit: the host flips its own copy of the value (an outside checkbox)
244
+ action hostToggle = all {
245
+ focused' = focused,
246
+ disabled' = disabled,
247
+ state' = if (controlled) nextState(valueProp) else state,
248
+ allowMixed' = allowMixed,
249
+ spaceArmed' = spaceArmed,
250
+ pointerArmed' = pointerArmed,
251
+ requests' = requests,
252
+ controlled' = controlled,
253
+ accepting' = accepting,
254
+ valueProp' = if (controlled) nextState(valueProp) else valueProp,
255
+ requested' = requested,
256
+ outcome' = if (controlled) "none" else outcome,
257
+ base' = base
258
+ }
259
+
260
+ // configuration: the host starts owning the value, adopting the current one
261
+ action control = all {
262
+ focused' = focused,
263
+ disabled' = disabled,
264
+ state' = state,
265
+ allowMixed' = allowMixed,
266
+ spaceArmed' = spaceArmed,
267
+ pointerArmed' = pointerArmed,
268
+ requests' = requests,
269
+ controlled' = true,
270
+ accepting' = accepting,
271
+ valueProp' = state,
272
+ requested' = requested,
273
+ outcome' = outcome,
274
+ base' = base
275
+ }
276
+
277
+ // configuration: the controlled host answers every request with its intent
278
+ action acceptRequests = all {
279
+ focused' = focused,
280
+ disabled' = disabled,
281
+ state' = state,
282
+ allowMixed' = allowMixed,
283
+ spaceArmed' = spaceArmed,
284
+ pointerArmed' = pointerArmed,
285
+ requests' = requests,
286
+ controlled' = controlled,
287
+ accepting' = true,
288
+ valueProp' = valueProp,
289
+ requested' = requested,
290
+ outcome' = outcome,
291
+ base' = base
292
+ }
293
+
294
+ // configuration: plant an initial value (including "mixed") without counting
295
+ // as a request. Inert while controlled: only the value prop may move the state.
296
+ action seed = {
297
+ nondet incoming = Set("true", "false", "mixed").oneOf()
298
+ all {
299
+ focused' = focused,
300
+ disabled' = disabled,
301
+ state' = if (controlled) state else (if (and(incoming == "mixed", not(allowMixed))) state else incoming),
302
+ allowMixed' = allowMixed,
303
+ spaceArmed' = spaceArmed,
304
+ pointerArmed' = pointerArmed,
305
+ requests' = requests,
306
+ controlled' = controlled,
307
+ accepting' = accepting,
308
+ valueProp' = valueProp,
309
+ requested' = requested,
310
+ outcome' = "none",
311
+ base' = base
312
+ }
313
+ }
314
+
315
+ // prop change: aria-disabled. Disabling drops any pending press.
316
+ action set_disabled = {
317
+ nondet value = Set(true, false).oneOf()
318
+ all {
319
+ focused' = focused,
320
+ disabled' = value,
321
+ state' = state,
322
+ allowMixed' = allowMixed,
323
+ spaceArmed' = if (value) false else spaceArmed,
324
+ pointerArmed' = if (value) false else pointerArmed,
325
+ requests' = requests,
326
+ controlled' = controlled,
327
+ accepting' = accepting,
328
+ valueProp' = valueProp,
329
+ requested' = requested,
330
+ outcome' = outcome,
331
+ base' = base
332
+ }
333
+ }
334
+
335
+ // prop change: dual-state <-> tri-state. Going dual-state resolves "mixed".
336
+ action set_allow_mixed = {
337
+ nondet value = Set(true, false).oneOf()
338
+ all {
339
+ focused' = focused,
340
+ disabled' = disabled,
341
+ state' = if (and(not(value), state == "mixed")) "false" else state,
342
+ allowMixed' = value,
343
+ spaceArmed' = spaceArmed,
344
+ pointerArmed' = pointerArmed,
345
+ requests' = requests,
346
+ controlled' = controlled,
347
+ accepting' = accepting,
348
+ valueProp' = if (controlled) (if (and(not(value), valueProp == "mixed")) "false" else valueProp) else valueProp,
349
+ requested' = requested,
350
+ outcome' = "none",
351
+ base' = base
352
+ }
353
+ }
354
+
355
+ action step = any {
356
+ init,
357
+ focus,
358
+ blur,
359
+ key_down,
360
+ key_up,
361
+ pointer_down,
362
+ pointer_up,
363
+ pointer_cancel,
364
+ setValue,
365
+ hostToggle,
366
+ control,
367
+ acceptRequests,
368
+ seed,
369
+ set_disabled,
370
+ set_allow_mixed
371
+ }
372
+
373
+ val StateValid: bool = or(state == "true", or(state == "false", state == "mixed"))
374
+ // a dual-state checkbox is never mixed
375
+ val DualStateNeverMixed: bool = or(allowMixed, state != "mixed")
376
+ // aria-checked is "mixed" exactly when the tri-state checkbox is partially checked
377
+ val AriaCheckedContract: bool = and(
378
+ (ariaChecked(state, allowMixed) == "mixed") == and(allowMixed, state == "mixed"),
379
+ (ariaChecked(state, allowMixed) == "true") == (state == "true"),
380
+ (ariaChecked(state, allowMixed) == "false") == (state == "false")
381
+ )
382
+ // a pending Space press needs a focused, enabled checkbox
383
+ val SpaceArmedImpliesFocusedEnabled: bool =
384
+ or(not(spaceArmed), and(focused, not(disabled)))
385
+ // a pending pointer press never survives disabling
386
+ val PointerArmedImpliesEnabled: bool =
387
+ or(not(pointerArmed), not(disabled))
388
+ // controlled: only the value prop sets the state, so no event commits
389
+ val ControlledFollowsProp: bool = or(not(controlled), state == valueProp)
390
+ // a rejected request leaves the state as it found it
391
+ val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), state == base)
392
+ // an accepted request committed exactly its intent
393
+ val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), state == requested)
394
+ }