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