@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,309 @@
1
+ // APG Disclosure (Show/Hide) — https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/
2
+ //
3
+ // One trigger (role=button) controls one panel. The spec models both the
4
+ // widget state (`expanded`) and what it renders (`ariaExpanded` on the trigger,
5
+ // `panelHidden` on the panel), so the central APG requirement — aria-expanded
6
+ // mirrors the panel's visibility — is a checkable invariant, not a comment.
7
+ //
8
+ // Events are the user-level alphabet shared with the scenario bridge:
9
+ // focus / blur the trigger gains / loses focus
10
+ // click pointer activation (focuses the trigger, then requests a toggle)
11
+ // press Enter or Space on the focused trigger (APG keyboard section)
12
+ // expand/collapse/toggle programmatic control (the same request path)
13
+ // disable/enable the trigger becomes inert (cannot gain focus, ignores
14
+ // click and keys). Focus it already held is browser-defined
15
+ // (Chromium keeps document.activeElement until the next key
16
+ // input), so the spec records it as "unknown" until the next
17
+ // focus / blur / click settles it
18
+ //
19
+ // Request vs commit (map ticket rt1p). Everything a user sees as a change —
20
+ // click, press, expand, collapse, toggle — is a *request* carrying an intent
21
+ // (`requested`). It fires onValueChange (`requests` counts the calls) when the
22
+ // intent differs from the current value, and then:
23
+ // uncontrolled the request is accepted: the state commits the intent
24
+ // controlled, holds the host ignores it ("rejected"): the state is untouched
25
+ // controlled, accepts the host answers with the same value (`accepting`): the
26
+ // state commits through the controlled value, not the event
27
+ // The controlled value (`valueProp`) is the only thing that changes state while
28
+ // `controlled`; `setValue` / `hostToggle` are the host writing it. `control` and
29
+ // `acceptRequests` are configuration events (init is literal-only), like `disable`.
30
+ //
31
+ // Invariants for the model: ControlledFollowsProp (in controlled mode no event
32
+ // commits), RejectedRequestUntouched, AcceptedRequestCommits.
33
+ //
34
+ // `outcome` is "none" | "accepted" | "rejected" for the last request; a host
35
+ // write settles it back to "none". `base` is the value when it was received.
36
+ //
37
+ // ponytail: a single trigger/panel only; groups (DisclosureGroup, expandedKeys)
38
+ // need collections the generator does not have.
39
+ module apg_disclosure {
40
+ import aria.* from "../../shared/aria"
41
+
42
+ var expanded: bool // widget state: the content is shown
43
+ var disabled: bool // the trigger is inert
44
+ var focus: str // "none" | "trigger" | "unknown"
45
+ var ariaExpanded: str // rendered aria-expanded on the trigger: "true" | "false"
46
+ var panelHidden: bool // rendered hidden attribute on the panel
47
+ var requests: int // number of requests that fired onValueChange
48
+ var controlled: bool // the host owns the value (`value` prop present)
49
+ var accepting: bool // controlled host that answers every request with its intent
50
+ var valueProp: bool // the controlled value prop (meaningful while controlled)
51
+ var requested: bool // intent of the last request
52
+ var outcome: str // "none" | "accepted" | "rejected"
53
+ var base: bool // value when the last request was received
54
+
55
+ def moved(before: bool, after: bool): int = if (before == after) 0 else 1
56
+ // controlled host that keeps the old value: the request is rejected
57
+ def holds: bool = and(controlled, not(accepting))
58
+ // controlled host that writes the intent back through the value prop
59
+ def follows: bool = and(controlled, accepting)
60
+ def verdict: str = if (holds) "rejected" else "accepted"
61
+ // the value after a request for `want`
62
+ def landed(h: bool, e: bool, want: bool): bool = if (h) e else want
63
+ // the value prop after a request for `want`
64
+ def propAfter(f: bool, p: bool, want: bool): bool = if (f) want else p
65
+
66
+ action init = all {
67
+ expanded' = false,
68
+ disabled' = false,
69
+ focus' = "none",
70
+ ariaExpanded' = "false",
71
+ panelHidden' = true,
72
+ requests' = 0,
73
+ controlled' = false,
74
+ accepting' = false,
75
+ valueProp' = false,
76
+ requested' = false,
77
+ outcome' = "none",
78
+ base' = false
79
+ }
80
+
81
+ action focusTrigger = all {
82
+ expanded' = expanded,
83
+ disabled' = disabled,
84
+ focus' = if (disabled) focus else "trigger",
85
+ ariaExpanded' = ariaExpanded,
86
+ panelHidden' = panelHidden,
87
+ requests' = requests,
88
+ controlled' = controlled,
89
+ accepting' = accepting,
90
+ valueProp' = valueProp,
91
+ requested' = requested,
92
+ outcome' = outcome,
93
+ base' = base
94
+ }
95
+
96
+ action blurTrigger = all {
97
+ expanded' = expanded,
98
+ disabled' = disabled,
99
+ focus' = "none",
100
+ ariaExpanded' = ariaExpanded,
101
+ panelHidden' = panelHidden,
102
+ requests' = requests,
103
+ controlled' = controlled,
104
+ accepting' = accepting,
105
+ valueProp' = valueProp,
106
+ requested' = requested,
107
+ outcome' = outcome,
108
+ base' = base
109
+ }
110
+
111
+ // request: toggle, fired by a pointer click on an enabled trigger
112
+ action click = all {
113
+ expanded' = if (disabled) expanded else landed(holds, expanded, not(expanded)),
114
+ disabled' = disabled,
115
+ focus' = if (disabled) focus else "trigger",
116
+ ariaExpanded' = if (disabled) ariaExpanded else ariaBoolToken(landed(holds, expanded, not(expanded))),
117
+ panelHidden' = if (disabled) panelHidden else not(landed(holds, expanded, not(expanded))),
118
+ requests' = if (disabled) requests else requests + 1,
119
+ controlled' = controlled,
120
+ accepting' = accepting,
121
+ valueProp' = if (disabled) valueProp else propAfter(follows, valueProp, not(expanded)),
122
+ requested' = if (disabled) requested else not(expanded),
123
+ outcome' = if (disabled) outcome else verdict,
124
+ base' = if (disabled) base else expanded
125
+ }
126
+
127
+ // request: toggle, fired by Enter / Space on the focused, enabled trigger
128
+ action press = all {
129
+ expanded' = if (and(not(disabled), focus == "trigger")) landed(holds, expanded, not(expanded)) else expanded,
130
+ disabled' = disabled,
131
+ focus' = focus,
132
+ ariaExpanded' = if (and(not(disabled), focus == "trigger")) ariaBoolToken(landed(holds, expanded, not(expanded))) else ariaExpanded,
133
+ panelHidden' = if (and(not(disabled), focus == "trigger")) not(landed(holds, expanded, not(expanded))) else panelHidden,
134
+ requests' = if (and(not(disabled), focus == "trigger")) requests + 1 else requests,
135
+ controlled' = controlled,
136
+ accepting' = accepting,
137
+ valueProp' = if (and(not(disabled), focus == "trigger")) propAfter(follows, valueProp, not(expanded)) else valueProp,
138
+ requested' = if (and(not(disabled), focus == "trigger")) not(expanded) else requested,
139
+ outcome' = if (and(not(disabled), focus == "trigger")) verdict else outcome,
140
+ base' = if (and(not(disabled), focus == "trigger")) expanded else base
141
+ }
142
+
143
+ // request: expand (a redundant request fires nothing)
144
+ action expand = all {
145
+ expanded' = if (expanded) expanded else landed(holds, expanded, true),
146
+ disabled' = disabled,
147
+ focus' = focus,
148
+ ariaExpanded' = if (expanded) ariaExpanded else ariaBoolToken(landed(holds, expanded, true)),
149
+ panelHidden' = if (expanded) panelHidden else not(landed(holds, expanded, true)),
150
+ requests' = requests + moved(expanded, true),
151
+ controlled' = controlled,
152
+ accepting' = accepting,
153
+ valueProp' = if (expanded) valueProp else propAfter(follows, valueProp, true),
154
+ requested' = if (expanded) requested else true,
155
+ outcome' = if (expanded) outcome else verdict,
156
+ base' = if (expanded) base else expanded
157
+ }
158
+
159
+ // request: collapse (a redundant request fires nothing)
160
+ action collapse = all {
161
+ expanded' = if (expanded) landed(holds, expanded, false) else expanded,
162
+ disabled' = disabled,
163
+ focus' = focus,
164
+ ariaExpanded' = if (expanded) ariaBoolToken(landed(holds, expanded, false)) else ariaExpanded,
165
+ panelHidden' = if (expanded) not(landed(holds, expanded, false)) else panelHidden,
166
+ requests' = requests + moved(expanded, false),
167
+ controlled' = controlled,
168
+ accepting' = accepting,
169
+ valueProp' = if (expanded) propAfter(follows, valueProp, false) else valueProp,
170
+ requested' = if (expanded) false else requested,
171
+ outcome' = if (expanded) verdict else outcome,
172
+ base' = if (expanded) expanded else base
173
+ }
174
+
175
+ // request: toggle, the programmatic API (ignores `disabled`)
176
+ action toggle = all {
177
+ expanded' = landed(holds, expanded, not(expanded)),
178
+ disabled' = disabled,
179
+ focus' = focus,
180
+ ariaExpanded' = ariaBoolToken(landed(holds, expanded, not(expanded))),
181
+ panelHidden' = not(landed(holds, expanded, not(expanded))),
182
+ requests' = requests + 1,
183
+ controlled' = controlled,
184
+ accepting' = accepting,
185
+ valueProp' = propAfter(follows, valueProp, not(expanded)),
186
+ requested' = not(expanded),
187
+ outcome' = verdict,
188
+ base' = expanded
189
+ }
190
+
191
+ // commit: the host writes the controlled value (a no-op while uncontrolled)
192
+ action setValue = {
193
+ nondet v = Set(true, false).oneOf()
194
+ all {
195
+ expanded' = if (controlled) v else expanded,
196
+ disabled' = disabled,
197
+ focus' = focus,
198
+ ariaExpanded' = if (controlled) ariaBoolToken(v) else ariaExpanded,
199
+ panelHidden' = if (controlled) not(v) else panelHidden,
200
+ requests' = requests,
201
+ controlled' = controlled,
202
+ accepting' = accepting,
203
+ valueProp' = if (controlled) v else valueProp,
204
+ requested' = requested,
205
+ outcome' = if (controlled) "none" else outcome,
206
+ base' = base
207
+ }
208
+ }
209
+
210
+ // commit: the host flips its own copy of the value (an outside button)
211
+ action hostToggle = all {
212
+ expanded' = if (controlled) not(valueProp) else expanded,
213
+ disabled' = disabled,
214
+ focus' = focus,
215
+ ariaExpanded' = if (controlled) ariaBoolToken(not(valueProp)) else ariaExpanded,
216
+ panelHidden' = if (controlled) valueProp else panelHidden,
217
+ requests' = requests,
218
+ controlled' = controlled,
219
+ accepting' = accepting,
220
+ valueProp' = if (controlled) not(valueProp) else valueProp,
221
+ requested' = requested,
222
+ outcome' = if (controlled) "none" else outcome,
223
+ base' = base
224
+ }
225
+
226
+ // configuration: the host starts owning the value, adopting the current one
227
+ action control = all {
228
+ expanded' = expanded,
229
+ disabled' = disabled,
230
+ focus' = focus,
231
+ ariaExpanded' = ariaExpanded,
232
+ panelHidden' = panelHidden,
233
+ requests' = requests,
234
+ controlled' = true,
235
+ accepting' = accepting,
236
+ valueProp' = expanded,
237
+ requested' = requested,
238
+ outcome' = outcome,
239
+ base' = base
240
+ }
241
+
242
+ // configuration: the controlled host answers every request with its intent
243
+ action acceptRequests = all {
244
+ expanded' = expanded,
245
+ disabled' = disabled,
246
+ focus' = focus,
247
+ ariaExpanded' = ariaExpanded,
248
+ panelHidden' = panelHidden,
249
+ requests' = requests,
250
+ controlled' = controlled,
251
+ accepting' = true,
252
+ valueProp' = valueProp,
253
+ requested' = requested,
254
+ outcome' = outcome,
255
+ base' = base
256
+ }
257
+
258
+ action disable = all {
259
+ expanded' = expanded,
260
+ disabled' = true,
261
+ focus' = if (focus == "trigger") "unknown" else focus,
262
+ ariaExpanded' = ariaExpanded,
263
+ panelHidden' = panelHidden,
264
+ requests' = requests,
265
+ controlled' = controlled,
266
+ accepting' = accepting,
267
+ valueProp' = valueProp,
268
+ requested' = requested,
269
+ outcome' = outcome,
270
+ base' = base
271
+ }
272
+
273
+ action enable = all {
274
+ expanded' = expanded,
275
+ disabled' = false,
276
+ focus' = focus,
277
+ ariaExpanded' = ariaExpanded,
278
+ panelHidden' = panelHidden,
279
+ requests' = requests,
280
+ controlled' = controlled,
281
+ accepting' = accepting,
282
+ valueProp' = valueProp,
283
+ requested' = requested,
284
+ outcome' = outcome,
285
+ base' = base
286
+ }
287
+
288
+ action step = any {
289
+ init, focusTrigger, blurTrigger, click, press, expand, collapse, toggle,
290
+ setValue, hostToggle, control, acceptRequests, disable, enable
291
+ }
292
+
293
+ // APG: aria-expanded is "true" exactly when the panel is visible
294
+ val AriaMirrorsPanel: bool = (ariaExpanded == "true") == not(panelHidden)
295
+ // the rendered attributes always follow the widget state
296
+ val RenderFollowsState: bool = (ariaExpanded == "true") == expanded
297
+ // an inert trigger is never *known* to hold focus
298
+ val DisabledNotFocused: bool = or(not(disabled), not(focus == "trigger"))
299
+ // focus is one of the modelled positions
300
+ val FocusKnown: bool = or(focus == "none", or(focus == "trigger", focus == "unknown"))
301
+ // callbacks only ever count up from zero
302
+ val RequestsNonNegative: bool = requests >= 0
303
+ // controlled: only the value prop sets the state, so no event commits
304
+ val ControlledFollowsProp: bool = or(not(controlled), expanded == valueProp)
305
+ // a rejected request leaves the state as it found it
306
+ val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), expanded == base)
307
+ // an accepted request committed exactly its intent
308
+ val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), expanded == requested)
309
+ }