@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,445 @@
1
+ // WAI-ARIA APG — Button pattern (command button + toggle button).
2
+ //
3
+ // Modelled from https://www.w3.org/WAI/ARIA/apg/patterns/button/ :
4
+ // - "About This Pattern": ordinary button and toggle button (aria-pressed)
5
+ // - "Keyboard Interaction": Space and Enter activate the button; focus stays
6
+ // on the button after activation (no context change is modelled)
7
+ // - "WAI-ARIA Roles, States, and Properties": role=button, aria-disabled
8
+ // when the action is unavailable, aria-pressed true/false on a toggle
9
+ // and the Button Examples (https://www.w3.org/WAI/ARIA/apg/patterns/button/examples/button/):
10
+ // Enter activates on keydown, Space is armed on keydown and activates on keyup
11
+ // (the native <button> behaviour), so releasing Space after a blur does nothing.
12
+ //
13
+ // Attribute contract (derived from state, never stored):
14
+ // role = "button"
15
+ // aria-pressed = "true" | "false" on a toggle, absent on a command button
16
+ // aria-disabled = "true" when disabled, absent otherwise
17
+ // A disabled button stays focusable (aria-disabled, not the native attribute)
18
+ // but never activates.
19
+ //
20
+ // Request vs commit (map ticket rt1p). A toggle's activation is a *request*
21
+ // carrying the new pressed state (`requested`); it fires onPressedChange
22
+ // (`requests` counts the calls) when the intent differs from the current value,
23
+ // 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
+ // A command button's activation is not a value request: it only counts
29
+ // (`activations`). The controlled value (`valueProp`) is the only thing that
30
+ // changes state while `controlled`; `setValue` / `hostToggle` are the host
31
+ // writing it. `control` / `acceptRequests` are configuration events, `seed`
32
+ // plants an initial value without counting as a request.
33
+ //
34
+ // Invariants for the model: ControlledFollowsProp (in controlled mode no event
35
+ // commits), RejectedRequestUntouched, AcceptedRequestCommits.
36
+ //
37
+ // `outcome` is "none" | "accepted" | "rejected" for the last request; a host
38
+ // write settles it back to "none". `base` is the value when it was received.
39
+ //
40
+ // Deliberate omissions:
41
+ // - accessible name / aria-labelledby / aria-describedby (static wiring, no state)
42
+ // - post-activation focus management (dialog open/close, wizard step, shortcut keys)
43
+ // - menu button (aria-haspopup) — owned by the menu-button pattern
44
+ // - partially pressed (mixed) toggle: only the true/false values the button page lists
45
+ // - Enter key repeat, native `disabled`, and pointer-capture details
46
+ module apg_button {
47
+ import aria.* from "../../shared/aria"
48
+
49
+ var focused: bool // the button has keyboard focus
50
+ var disabled: bool // aria-disabled: focusable, never activates
51
+ var isToggle: bool // config: toggle button (aria-pressed) vs command button
52
+ var pressed: bool // toggle state; always false on a command button
53
+ var toggleCount: int // committed flips since it became a toggle
54
+ var spaceArmed: bool // Space went down on the focused button, awaiting keyup
55
+ var pointerArmed: bool // pointer went down on the button, awaiting release
56
+ var activations: int // onPress callbacks fired (what the consumer observes)
57
+ var requests: int // onPressedChange callbacks fired by user input
58
+ var controlled: bool // the host owns the pressed value (`value` prop present)
59
+ var accepting: bool // controlled host that answers every request with its intent
60
+ var valueProp: bool // the controlled value prop (meaningful while controlled)
61
+ var requested: bool // intent of the last request
62
+ var outcome: str // "none" | "accepted" | "rejected"
63
+ var base: bool // value when the last request was received
64
+
65
+ // guards: the generator supports no preconditions, so they live in the RHS
66
+ def enterFires(focused: bool, disabled: bool, key: str): bool =
67
+ and(focused, not(disabled), key == "Enter")
68
+ def spaceArms(focused: bool, disabled: bool, key: str): bool =
69
+ and(focused, not(disabled), key == "Space")
70
+ def spaceReleaseFires(armed: bool, disabled: bool, key: str): bool =
71
+ and(armed, not(disabled), key == "Space")
72
+ def pointerReleaseFires(armed: bool, disabled: bool): bool =
73
+ and(armed, not(disabled))
74
+
75
+ def b2i(b: bool): int = if (b) 1 else 0
76
+ def moved(before: bool, after: bool): int = if (before == after) 0 else 1
77
+
78
+ // ARIA attribute contract
79
+ def ariaPressed(isToggle: bool, pressed: bool): str =
80
+ if (isToggle) ariaBoolToken(pressed) else "absent"
81
+ def ariaDisabled(disabled: bool): str =
82
+ if (disabled) "true" else "absent"
83
+
84
+ // request / commit (map ticket rt1p)
85
+ def holds: bool = and(controlled, not(accepting))
86
+ def follows: bool = and(controlled, accepting)
87
+ def verdict: str = if (holds) "rejected" else "accepted"
88
+ // the pressed value after a request for `want`
89
+ def landed(h: bool, c: bool, want: bool): bool = if (h) c else want
90
+ // the value prop after a request for `want`
91
+ def propAfter(f: bool, p: bool, want: bool): bool = if (f) want else p
92
+
93
+ action init = all {
94
+ focused' = false,
95
+ disabled' = false,
96
+ isToggle' = false,
97
+ pressed' = false,
98
+ toggleCount' = 0,
99
+ spaceArmed' = false,
100
+ pointerArmed' = false,
101
+ activations' = 0,
102
+ requests' = 0,
103
+ controlled' = false,
104
+ accepting' = false,
105
+ valueProp' = false,
106
+ requested' = false,
107
+ outcome' = "none",
108
+ base' = false
109
+ }
110
+
111
+ action focus = all {
112
+ focused' = true,
113
+ disabled' = disabled,
114
+ isToggle' = isToggle,
115
+ pressed' = pressed,
116
+ toggleCount' = toggleCount,
117
+ spaceArmed' = spaceArmed,
118
+ pointerArmed' = pointerArmed,
119
+ activations' = activations,
120
+ requests' = requests,
121
+ controlled' = controlled,
122
+ accepting' = accepting,
123
+ valueProp' = valueProp,
124
+ requested' = requested,
125
+ outcome' = outcome,
126
+ base' = base
127
+ }
128
+
129
+ // blur cancels a pending Space press (keyup would land elsewhere)
130
+ action blur = all {
131
+ focused' = false,
132
+ disabled' = disabled,
133
+ isToggle' = isToggle,
134
+ pressed' = pressed,
135
+ toggleCount' = toggleCount,
136
+ spaceArmed' = false,
137
+ pointerArmed' = pointerArmed,
138
+ activations' = activations,
139
+ requests' = requests,
140
+ controlled' = controlled,
141
+ accepting' = accepting,
142
+ valueProp' = valueProp,
143
+ requested' = requested,
144
+ outcome' = outcome,
145
+ base' = base
146
+ }
147
+
148
+ // Keyboard Interaction: Enter activates on keydown, Space arms until keyup.
149
+ // "Other" stands for any key the pattern does not handle.
150
+ action key_down = {
151
+ nondet key = Set("Enter", "Space", "Other").oneOf()
152
+ all {
153
+ focused' = focused,
154
+ disabled' = disabled,
155
+ isToggle' = isToggle,
156
+ pressed' = if (and(enterFires(focused, disabled, key), isToggle)) landed(holds, pressed, not(pressed)) else pressed,
157
+ toggleCount' = if (and(enterFires(focused, disabled, key), isToggle)) toggleCount + moved(pressed, landed(holds, pressed, not(pressed))) else toggleCount,
158
+ spaceArmed' = if (spaceArms(focused, disabled, key)) true else spaceArmed,
159
+ pointerArmed' = pointerArmed,
160
+ activations' = if (enterFires(focused, disabled, key)) activations + 1 else activations,
161
+ requests' = if (and(enterFires(focused, disabled, key), isToggle)) requests + 1 else requests,
162
+ controlled' = controlled,
163
+ accepting' = accepting,
164
+ valueProp' = if (and(enterFires(focused, disabled, key), isToggle)) propAfter(follows, valueProp, not(pressed)) else valueProp,
165
+ requested' = if (and(enterFires(focused, disabled, key), isToggle)) not(pressed) else requested,
166
+ outcome' = if (and(enterFires(focused, disabled, key), isToggle)) verdict else outcome,
167
+ base' = if (and(enterFires(focused, disabled, key), isToggle)) pressed else base
168
+ }
169
+ }
170
+
171
+ action key_up = {
172
+ nondet key = Set("Enter", "Space", "Other").oneOf()
173
+ all {
174
+ focused' = focused,
175
+ disabled' = disabled,
176
+ isToggle' = isToggle,
177
+ pressed' = if (and(spaceReleaseFires(spaceArmed, disabled, key), isToggle)) landed(holds, pressed, not(pressed)) else pressed,
178
+ toggleCount' = if (and(spaceReleaseFires(spaceArmed, disabled, key), isToggle)) toggleCount + moved(pressed, landed(holds, pressed, not(pressed))) else toggleCount,
179
+ spaceArmed' = if (key == "Space") false else spaceArmed,
180
+ pointerArmed' = pointerArmed,
181
+ activations' = if (spaceReleaseFires(spaceArmed, disabled, key)) activations + 1 else activations,
182
+ requests' = if (and(spaceReleaseFires(spaceArmed, disabled, key), isToggle)) requests + 1 else requests,
183
+ controlled' = controlled,
184
+ accepting' = accepting,
185
+ valueProp' = if (and(spaceReleaseFires(spaceArmed, disabled, key), isToggle)) propAfter(follows, valueProp, not(pressed)) else valueProp,
186
+ requested' = if (and(spaceReleaseFires(spaceArmed, disabled, key), isToggle)) not(pressed) else requested,
187
+ outcome' = if (and(spaceReleaseFires(spaceArmed, disabled, key), isToggle)) verdict else outcome,
188
+ base' = if (and(spaceReleaseFires(spaceArmed, disabled, key), isToggle)) pressed else base
189
+ }
190
+ }
191
+
192
+ // pointer down focuses the button (also when aria-disabled) and arms a click
193
+ action pointer_down = all {
194
+ focused' = true,
195
+ disabled' = disabled,
196
+ isToggle' = isToggle,
197
+ pressed' = pressed,
198
+ toggleCount' = toggleCount,
199
+ spaceArmed' = spaceArmed,
200
+ pointerArmed' = not(disabled),
201
+ activations' = activations,
202
+ requests' = requests,
203
+ controlled' = controlled,
204
+ accepting' = accepting,
205
+ valueProp' = valueProp,
206
+ requested' = requested,
207
+ outcome' = outcome,
208
+ base' = base
209
+ }
210
+
211
+ // pointer released over the button: the click activates
212
+ action pointer_up = all {
213
+ focused' = focused,
214
+ disabled' = disabled,
215
+ isToggle' = isToggle,
216
+ pressed' = if (and(pointerReleaseFires(pointerArmed, disabled), isToggle)) landed(holds, pressed, not(pressed)) else pressed,
217
+ toggleCount' = if (and(pointerReleaseFires(pointerArmed, disabled), isToggle)) toggleCount + moved(pressed, landed(holds, pressed, not(pressed))) else toggleCount,
218
+ spaceArmed' = spaceArmed,
219
+ pointerArmed' = false,
220
+ activations' = if (pointerReleaseFires(pointerArmed, disabled)) activations + 1 else activations,
221
+ requests' = if (and(pointerReleaseFires(pointerArmed, disabled), isToggle)) requests + 1 else requests,
222
+ controlled' = controlled,
223
+ accepting' = accepting,
224
+ valueProp' = if (and(pointerReleaseFires(pointerArmed, disabled), isToggle)) propAfter(follows, valueProp, not(pressed)) else valueProp,
225
+ requested' = if (and(pointerReleaseFires(pointerArmed, disabled), isToggle)) not(pressed) else requested,
226
+ outcome' = if (and(pointerReleaseFires(pointerArmed, disabled), isToggle)) verdict else outcome,
227
+ base' = if (and(pointerReleaseFires(pointerArmed, disabled), isToggle)) pressed else base
228
+ }
229
+
230
+ // pointer left / cancelled before release: no click
231
+ action pointer_cancel = all {
232
+ focused' = focused,
233
+ disabled' = disabled,
234
+ isToggle' = isToggle,
235
+ pressed' = pressed,
236
+ toggleCount' = toggleCount,
237
+ spaceArmed' = spaceArmed,
238
+ pointerArmed' = false,
239
+ activations' = activations,
240
+ requests' = requests,
241
+ controlled' = controlled,
242
+ accepting' = accepting,
243
+ valueProp' = valueProp,
244
+ requested' = requested,
245
+ outcome' = outcome,
246
+ base' = base
247
+ }
248
+
249
+ // commit: the host writes the controlled value (a no-op while uncontrolled)
250
+ action setValue = {
251
+ nondet v = Set(true, false).oneOf()
252
+ all {
253
+ focused' = focused,
254
+ disabled' = disabled,
255
+ isToggle' = isToggle,
256
+ pressed' = if (and(controlled, isToggle)) v else pressed,
257
+ toggleCount' = if (and(controlled, isToggle)) toggleCount + moved(pressed, v) else toggleCount,
258
+ spaceArmed' = spaceArmed,
259
+ pointerArmed' = pointerArmed,
260
+ activations' = activations,
261
+ requests' = requests,
262
+ controlled' = controlled,
263
+ accepting' = accepting,
264
+ valueProp' = if (and(controlled, isToggle)) v else valueProp,
265
+ requested' = requested,
266
+ outcome' = if (controlled) "none" else outcome,
267
+ base' = base
268
+ }
269
+ }
270
+
271
+ // commit: the host flips its own copy of the value (an outside toggle)
272
+ action hostToggle = all {
273
+ focused' = focused,
274
+ disabled' = disabled,
275
+ isToggle' = isToggle,
276
+ pressed' = if (and(controlled, isToggle)) not(valueProp) else pressed,
277
+ toggleCount' = if (and(controlled, isToggle)) toggleCount + 1 else toggleCount,
278
+ spaceArmed' = spaceArmed,
279
+ pointerArmed' = pointerArmed,
280
+ activations' = activations,
281
+ requests' = requests,
282
+ controlled' = controlled,
283
+ accepting' = accepting,
284
+ valueProp' = if (and(controlled, isToggle)) not(valueProp) else valueProp,
285
+ requested' = requested,
286
+ outcome' = if (controlled) "none" else outcome,
287
+ base' = base
288
+ }
289
+
290
+ // configuration: the host starts owning the pressed value, adopting the current one
291
+ action control = all {
292
+ focused' = focused,
293
+ disabled' = disabled,
294
+ isToggle' = isToggle,
295
+ pressed' = pressed,
296
+ toggleCount' = toggleCount,
297
+ spaceArmed' = spaceArmed,
298
+ pointerArmed' = pointerArmed,
299
+ activations' = activations,
300
+ requests' = requests,
301
+ controlled' = true,
302
+ accepting' = accepting,
303
+ valueProp' = pressed,
304
+ requested' = requested,
305
+ outcome' = outcome,
306
+ base' = base
307
+ }
308
+
309
+ // configuration: the controlled host answers every request with its intent
310
+ action acceptRequests = all {
311
+ focused' = focused,
312
+ disabled' = disabled,
313
+ isToggle' = isToggle,
314
+ pressed' = pressed,
315
+ toggleCount' = toggleCount,
316
+ spaceArmed' = spaceArmed,
317
+ pointerArmed' = pointerArmed,
318
+ activations' = activations,
319
+ requests' = requests,
320
+ controlled' = controlled,
321
+ accepting' = true,
322
+ valueProp' = valueProp,
323
+ requested' = requested,
324
+ outcome' = outcome,
325
+ base' = base
326
+ }
327
+
328
+ // configuration: plant an initial value without counting as a request.
329
+ // Inert while controlled: only the value prop may move the state there.
330
+ action seed = {
331
+ nondet v = Set(true, false).oneOf()
332
+ all {
333
+ focused' = focused,
334
+ disabled' = disabled,
335
+ isToggle' = isToggle,
336
+ pressed' = if (controlled) pressed else (if (isToggle) v else pressed),
337
+ toggleCount' = if (controlled) toggleCount else (if (isToggle) b2i(v) else toggleCount),
338
+ spaceArmed' = spaceArmed,
339
+ pointerArmed' = pointerArmed,
340
+ activations' = activations,
341
+ requests' = requests,
342
+ controlled' = controlled,
343
+ accepting' = accepting,
344
+ valueProp' = valueProp,
345
+ requested' = requested,
346
+ outcome' = "none",
347
+ base' = base
348
+ }
349
+ }
350
+
351
+ // prop change: aria-disabled. Disabling drops any pending press.
352
+ action set_disabled = {
353
+ nondet value = Set(true, false).oneOf()
354
+ all {
355
+ focused' = focused,
356
+ disabled' = value,
357
+ isToggle' = isToggle,
358
+ pressed' = pressed,
359
+ toggleCount' = toggleCount,
360
+ spaceArmed' = if (value) false else spaceArmed,
361
+ pointerArmed' = if (value) false else pointerArmed,
362
+ activations' = activations,
363
+ requests' = requests,
364
+ controlled' = controlled,
365
+ accepting' = accepting,
366
+ valueProp' = valueProp,
367
+ requested' = requested,
368
+ outcome' = outcome,
369
+ base' = base
370
+ }
371
+ }
372
+
373
+ // prop change: command button <-> toggle button. A command button has no
374
+ // pressed state, so leaving toggle mode resets it; a controlled toggle
375
+ // re-adopts the value prop.
376
+ action set_toggle = {
377
+ nondet value = Set(true, false).oneOf()
378
+ all {
379
+ focused' = focused,
380
+ disabled' = disabled,
381
+ isToggle' = value,
382
+ pressed' = if (value) (if (controlled) valueProp else pressed) else false,
383
+ toggleCount' = if (value) (if (controlled) b2i(valueProp) else toggleCount) else 0,
384
+ spaceArmed' = spaceArmed,
385
+ pointerArmed' = pointerArmed,
386
+ activations' = activations,
387
+ requests' = requests,
388
+ controlled' = controlled,
389
+ accepting' = accepting,
390
+ valueProp' = if (controlled) (if (value) valueProp else false) else valueProp,
391
+ requested' = requested,
392
+ outcome' = "none",
393
+ base' = base
394
+ }
395
+ }
396
+
397
+ action step = any {
398
+ init,
399
+ focus,
400
+ blur,
401
+ key_down,
402
+ key_up,
403
+ pointer_down,
404
+ pointer_up,
405
+ pointer_cancel,
406
+ setValue,
407
+ hostToggle,
408
+ control,
409
+ acceptRequests,
410
+ seed,
411
+ set_disabled,
412
+ set_toggle
413
+ }
414
+
415
+ // aria-pressed is present exactly on a toggle button
416
+ val PressedAttrPresentIffToggle: bool =
417
+ (ariaPressed(isToggle, pressed) != "absent") == isToggle
418
+ // ...and mirrors the pressed state
419
+ val PressedAttrMirrorsState: bool =
420
+ or(not(isToggle), (ariaPressed(isToggle, pressed) == "true") == pressed)
421
+ // aria-disabled="true" exactly when disabled, otherwise absent
422
+ val DisabledAttrContract: bool = and(
423
+ (ariaDisabled(disabled) == "true") == disabled,
424
+ (ariaDisabled(disabled) == "absent") == not(disabled)
425
+ )
426
+ // a command button never carries pressed state
427
+ val CommandButtonNeverPressed: bool = or(isToggle, not(pressed))
428
+ // pressed is the parity of the committed flips since toggle mode began
429
+ val PressedIsToggleParity: bool = pressed == (toggleCount % 2 == 1)
430
+ // a request only ever rides an activation
431
+ val RequestsWithinActivations: bool = requests <= activations
432
+ // a pending Space press needs a focused, enabled button
433
+ val SpaceArmedImpliesFocusedEnabled: bool =
434
+ or(not(spaceArmed), and(focused, not(disabled)))
435
+ // a pending pointer press never survives disabling
436
+ val PointerArmedImpliesEnabled: bool =
437
+ or(not(pointerArmed), not(disabled))
438
+ // controlled: only the value prop sets the state, so no event commits
439
+ val ControlledFollowsProp: bool =
440
+ or(not(controlled), or(not(isToggle), pressed == valueProp))
441
+ // a rejected request leaves the state as it found it
442
+ val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), pressed == base)
443
+ // an accepted request committed exactly its intent
444
+ val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), pressed == requested)
445
+ }