@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,499 @@
1
+ // APG Dialog (Modal) pattern — the layered modal-dialog stack.
2
+ //
3
+ // Source: https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
4
+ // sections: About This Pattern, Keyboard Interaction (+ Notes 1-3),
5
+ // WAI-ARIA Roles, States, and Properties
6
+ // example: https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/dialog/
7
+ // (four dialogs: Add Delivery Address -> Verification Result / Address Added
8
+ // -> End of the Road; the dialog table below is that example's content).
9
+ //
10
+ // One page behind up to MAX_DEPTH stacked modal dialogs. Focus is a single
11
+ // encoded target `layer * 100 + position`: layer 0 is the page, layer k the
12
+ // k-th open dialog; position 0 is a static `tabindex=-1` element (only dialogs
13
+ // with a static start have one), positions 1..tabCount are the tab sequence.
14
+ //
15
+ // Modelled
16
+ // - open: activating an invoker moves focus INTO the new dialog, at the
17
+ // dialog's configured initial focus (Keyboard Interaction, Note 1).
18
+ // - Tab / Shift+Tab: move within the top dialog's tab sequence and wrap
19
+ // first<->last; they never leave the dialog (focus trap). A static
20
+ // `tabindex=-1` start element is focusable but not in the tab sequence.
21
+ // - Escape: closes the top dialog only; focus returns to its invoker
22
+ // (Note 2), popping one layer of the nested stack.
23
+ // - activate (Enter/Space on the focused element): open a nested dialog,
24
+ // close the top dialog, or replace it (Add: the Address Added dialog
25
+ // replaces Add Delivery Address and inherits its return-focus target).
26
+ // - focus_attempt: programmatic focus / pointer click on any element; the
27
+ // trap refuses targets outside the top dialog (the dialog container itself
28
+ // is never focusable) and focus stays put.
29
+ // - pointer_outside: a click on the inert content; a no-op unless the
30
+ // CLOSE_ON_OUTSIDE variant is enabled ("in some implementations, attempts
31
+ // to interact with the inert content cause the dialog to close").
32
+ // - remove_invoker: the invoking element no longer exists; the return
33
+ // target becomes a logical-workflow fallback element (Note 2).
34
+ // - ARIA contract (derived defs): role=dialog, aria-modal=true, accessible
35
+ // name via aria-labelledby or aria-label, optional aria-describedby, and
36
+ // the inert state of every layer beneath the top.
37
+ //
38
+ // Request vs commit (map ticket rt1p). Whether any dialog is open is the
39
+ // controlled value: `escape`, `activate` and `pointer_outside` are requests
40
+ // carrying the open intent (`requested`). A request fires onValueChange
41
+ // (`requests` counts the calls) when the intent differs from the current open
42
+ // state, and then:
43
+ // uncontrolled the request is accepted: the stack commits the intent
44
+ // controlled, holds the host ignores it ("rejected"): the stack is untouched
45
+ // controlled, accepts the host writes the intent back (`accepting`)
46
+ // `valueProp` is the host's copy (meaningful while `controlled`); `setValue` /
47
+ // `hostToggle` are the host writing it, the only commit while controlled.
48
+ // `control` / `acceptRequests` configure the host (init is literal-only). A
49
+ // rejected open/close request leaves the whole stack (depth, layers, return
50
+ // targets, focus) untouched.
51
+ //
52
+ // Invariants for the model: ControlledFollowsProp (in controlled mode no event
53
+ // commits), RejectedRequestUntouched, AcceptedRequestCommits.
54
+ //
55
+ // Deliberately omitted
56
+ // - the "work-flow design" exception that returns focus to a different
57
+ // element than the invoker (only the invoker-removed fallback is modelled);
58
+ // - the visible close-button recommendation (Note 3) is modelled as content
59
+ // of the example dialogs, not enforced as an invariant;
60
+ // - legacy `aria-hidden` handling of the inert layer, visual dimming, and
61
+ // page scrolling;
62
+ // - window blur/focus and focus escaping into browser chrome;
63
+ // - dialog 2's aria-describedby/static-start advice interplay (the config
64
+ // records what the example does, no invariant forbids the combination);
65
+ // - the Alert Dialog variant: see alertdialog.qnt.
66
+ module apg_dialog_modal {
67
+ var depth: int // number of open dialogs: 0 = page only .. MAX_DEPTH
68
+ var l1: int // dialog id open at layer 1 (0 = none)
69
+ var l2: int
70
+ var l3: int
71
+ var r1: int // focus target to restore when layer 1 closes (0 = unused)
72
+ var r2: int
73
+ var r3: int
74
+ var focus: int // layer * 100 + position
75
+ var requests: int // number of requests that fired onValueChange
76
+ var controlled: bool // the host owns the open state (`value` prop present)
77
+ var accepting: bool // controlled host that answers every request with its intent
78
+ var valueProp: bool // the controlled value prop (meaningful while controlled)
79
+ var requested: bool // intent of the last request
80
+ var outcome: str // "none" | "accepted" | "rejected"
81
+ var base: bool // open state when the last request was received
82
+
83
+ // --- configuration: variants as constants and tables -----------------------
84
+ def MAX_DEPTH: int = 3
85
+ def CLOSE_ON_OUTSIDE: bool = false
86
+
87
+ // tab-sequence length per dialog id (0 = the page: invoker, link, fallback)
88
+ def tabCount(d: int): int =
89
+ if (d == 0) 3
90
+ else if (d == 1) 8 // street, city, state, zip, instructions, Verify, Add, Cancel
91
+ else if (d == 2) 2 // help link, Close (the first paragraph is the static start)
92
+ else if (d == 3) 2 // profile link, OK
93
+ else 1 // Close
94
+ // a `tabindex=-1` static element at the start of the dialog (position 0)
95
+ def hasStatic(d: int): bool = d == 2
96
+ // initial focus per dialog (Keyboard Interaction, Note 1)
97
+ def initialFocus(d: int): int =
98
+ if (d == 1) 1 // first input
99
+ else if (d == 2) 0 // first paragraph: first interactive element is out of view
100
+ else if (d == 3) 2 // OK, the most used control
101
+ else 1 // sole focusable element
102
+ // dialog opened by activating position `pos` of dialog `d` (0 = none)
103
+ def opens(d: int, pos: int): int =
104
+ if (and(d == 0, pos == 1)) 1 // Add Delivery Address button
105
+ else if (and(d == 1, pos == 6)) 2 // Verify Address
106
+ else if (and(d == 1, pos == 7)) 3 // Add (replaces dialog 1)
107
+ else if (and(d == 2, pos == 1)) 4 // link to help
108
+ else if (and(d == 3, pos == 1)) 4 // profile link
109
+ else 0
110
+ // activating position `pos` of dialog `d` closes it
111
+ def closes(d: int, pos: int): bool =
112
+ or(
113
+ and(d == 1, or(pos == 7, pos == 8)),
114
+ and(d == 2, pos == 2),
115
+ and(d == 3, pos == 2),
116
+ and(d == 4, pos == 1)
117
+ )
118
+ // fallback focus when the invoker is gone: page 3, otherwise first tabbable
119
+ def fallbackPos(d: int): int = if (d == 0) 3 else 1
120
+
121
+ // --- ARIA contract of a dialog id ------------------------------------------
122
+ def dialogRole(d: int): str = "dialog"
123
+ def ariaModal(d: int): bool = true
124
+ // aria-labelledby vs aria-label: the example labels every dialog by title
125
+ def labelling(d: int): str = "labelledby"
126
+ def hasAccessibleName(d: int): bool =
127
+ or(labelling(d) == "labelledby", labelling(d) == "label")
128
+ // optional; the example omits it from dialog 1 (no static descriptive text)
129
+ def ariaDescribedBy(d: int): bool = d != 1
130
+ def contractOk(d: int): bool =
131
+ and(dialogRole(d) == "dialog", ariaModal(d), hasAccessibleName(d))
132
+ // every layer beneath the top is inert; so is the page once a dialog is open
133
+ def isInert(layer: int): bool = layer < depth
134
+
135
+ // --- derived state ---------------------------------------------------------
136
+ def dlgAt(layer: int): int =
137
+ if (layer == 1) l1 else if (layer == 2) l2 else if (layer == 3) l3 else 0
138
+ def retAt(layer: int): int =
139
+ if (layer == 1) r1 else if (layer == 2) r2 else if (layer == 3) r3 else 0
140
+ def topDialog: int = dlgAt(depth)
141
+ def topRet: int = retAt(depth)
142
+ def focusLayer: int = focus / 100
143
+ def focusPos: int = focus % 100
144
+
145
+ def validPos(d: int, pos: int): bool =
146
+ if (d == 0) and(pos >= 1, pos <= tabCount(0))
147
+ else and(pos <= tabCount(d), or(pos >= 1, and(pos == 0, hasStatic(d))))
148
+ def validTarget(target: int): bool =
149
+ and(target / 100 == depth, validPos(topDialog, target % 100))
150
+ def nextPos(pos: int, n: int): int = if (pos >= n) 1 else pos + 1
151
+ def prevPos(pos: int, n: int): int = if (pos <= 1) n else pos - 1
152
+
153
+ // --- transition kernel -----------------------------------------------------
154
+ // ev: 1 escape, 2 activate, 3 pointer_outside, 4 tab, 5 shift_tab,
155
+ // 6 focus_attempt, 7 remove_invoker
156
+ // mode: 0 none, 1 close top, 2 open nested, 3 replace top
157
+ def activateOpens: int = opens(topDialog, focusPos)
158
+ def activateCloses: bool = closes(topDialog, focusPos)
159
+ def activateMode: int =
160
+ if (and(activateOpens > 0, activateCloses)) 3
161
+ else if (and(activateOpens > 0, depth < MAX_DEPTH)) 2
162
+ else if (activateCloses) 1
163
+ else 0
164
+ def modeOf(ev: int): int =
165
+ if (ev == 1) (if (depth > 0) 1 else 0)
166
+ else if (ev == 2) activateMode
167
+ else if (ev == 3) (if (and(CLOSE_ON_OUTSIDE, depth > 0)) 1 else 0)
168
+ else 0
169
+ def newDlg(ev: int): int = if (ev == 2) activateOpens else 0
170
+
171
+ def nextDepth(ev: int): int =
172
+ if (modeOf(ev) == 1) depth - 1
173
+ else if (modeOf(ev) == 2) depth + 1
174
+ else depth
175
+ def nextL(k: int, cur: int, ev: int): int =
176
+ if (and(modeOf(ev) == 1, depth == k)) 0
177
+ else if (and(modeOf(ev) == 2, depth + 1 == k)) newDlg(ev)
178
+ else if (and(modeOf(ev) == 3, depth == k)) newDlg(ev)
179
+ else cur
180
+ def nextR(k: int, cur: int, ev: int): int =
181
+ if (and(modeOf(ev) == 1, depth == k)) 0
182
+ else if (and(modeOf(ev) == 2, depth + 1 == k)) focus
183
+ else if (and(ev == 7, and(depth > 0, depth == k))) (k - 1) * 100 + fallbackPos(dlgAt(k - 1))
184
+ else cur
185
+ def nextFocus(ev: int, arg: int): int =
186
+ if (modeOf(ev) == 1) topRet
187
+ else if (modeOf(ev) == 2) (depth + 1) * 100 + initialFocus(newDlg(ev))
188
+ else if (modeOf(ev) == 3) depth * 100 + initialFocus(newDlg(ev))
189
+ else if (ev == 4) depth * 100 + nextPos(focusPos, tabCount(topDialog))
190
+ else if (ev == 5) depth * 100 + prevPos(focusPos, tabCount(topDialog))
191
+ else if (and(ev == 6, validTarget(arg))) arg
192
+ else focus
193
+
194
+ // --- request / commit (map ticket rt1p) ----------------------------------
195
+ def moved(a: bool, b: bool): int = if (a == b) 0 else 1
196
+ def holds: bool = and(controlled, not(accepting))
197
+ def follows: bool = and(controlled, accepting)
198
+ def verdict: str = if (holds) "rejected" else "accepted"
199
+ def landed(h: bool, v: bool, want: bool): bool = if (h) v else want
200
+ def propAfter(f: bool, p: bool, want: bool): bool = if (f) want else p
201
+ // the open state is "any dialog is open"; only a change of it is a request
202
+ def openNow: bool = depth > 0
203
+ def wantOpenEscape: bool = nextDepth(1) > 0
204
+ def wantOpenActivate: bool = nextDepth(2) > 0
205
+ def wantOpenOutside: bool = nextDepth(3) > 0
206
+ def rejectEscape: bool = and(holds, wantOpenEscape != openNow)
207
+ def rejectActivate: bool = and(holds, wantOpenActivate != openNow)
208
+ def rejectOutside: bool = and(holds, wantOpenOutside != openNow)
209
+
210
+ // --- host writes (the only commit while controlled) ----------------------
211
+ def setDepth(c: bool, v: bool, d: int): int =
212
+ if (c) (if (v) (if (d > 0) d else 1) else 0) else d
213
+ def openingSet(c: bool, v: bool, d: int): bool = and(c, and(v, d == 0))
214
+ def closingSet(c: bool, v: bool, d: int): bool = and(c, and(not(v), d > 0))
215
+ def toggleDepth(c: bool, d: int): int = if (c) (if (d > 0) 0 else 1) else d
216
+
217
+ action init = all {
218
+ depth' = 0,
219
+ l1' = 0,
220
+ l2' = 0,
221
+ l3' = 0,
222
+ r1' = 0,
223
+ r2' = 0,
224
+ r3' = 0,
225
+ focus' = 1,
226
+ requests' = 0,
227
+ controlled' = false,
228
+ accepting' = false,
229
+ valueProp' = false,
230
+ requested' = false,
231
+ outcome' = "none",
232
+ base' = false
233
+ }
234
+
235
+ // request: Escape closes the top dialog only
236
+ action escape = all {
237
+ depth' = if (rejectEscape) depth else nextDepth(1),
238
+ l1' = if (rejectEscape) l1 else nextL(1, l1, 1),
239
+ l2' = if (rejectEscape) l2 else nextL(2, l2, 1),
240
+ l3' = if (rejectEscape) l3 else nextL(3, l3, 1),
241
+ r1' = if (rejectEscape) r1 else nextR(1, r1, 1),
242
+ r2' = if (rejectEscape) r2 else nextR(2, r2, 1),
243
+ r3' = if (rejectEscape) r3 else nextR(3, r3, 1),
244
+ focus' = if (rejectEscape) focus else nextFocus(1, 0),
245
+ requests' = requests + moved(openNow, wantOpenEscape),
246
+ controlled' = controlled,
247
+ accepting' = accepting,
248
+ valueProp' = propAfter(follows, valueProp, wantOpenEscape),
249
+ requested' = if (openNow == wantOpenEscape) requested else wantOpenEscape,
250
+ outcome' = if (openNow == wantOpenEscape) outcome else verdict,
251
+ base' = if (openNow == wantOpenEscape) base else openNow
252
+ }
253
+
254
+ // request: activate opens a nested dialog, closes the top, or replaces it
255
+ action activate = all {
256
+ depth' = if (rejectActivate) depth else nextDepth(2),
257
+ l1' = if (rejectActivate) l1 else nextL(1, l1, 2),
258
+ l2' = if (rejectActivate) l2 else nextL(2, l2, 2),
259
+ l3' = if (rejectActivate) l3 else nextL(3, l3, 2),
260
+ r1' = if (rejectActivate) r1 else nextR(1, r1, 2),
261
+ r2' = if (rejectActivate) r2 else nextR(2, r2, 2),
262
+ r3' = if (rejectActivate) r3 else nextR(3, r3, 2),
263
+ focus' = if (rejectActivate) focus else nextFocus(2, 0),
264
+ requests' = requests + moved(openNow, wantOpenActivate),
265
+ controlled' = controlled,
266
+ accepting' = accepting,
267
+ valueProp' = propAfter(follows, valueProp, wantOpenActivate),
268
+ requested' = if (openNow == wantOpenActivate) requested else wantOpenActivate,
269
+ outcome' = if (openNow == wantOpenActivate) outcome else verdict,
270
+ base' = if (openNow == wantOpenActivate) base else openNow
271
+ }
272
+
273
+ // request: a click on the inert content (only acts with CLOSE_ON_OUTSIDE)
274
+ action pointer_outside = all {
275
+ depth' = if (rejectOutside) depth else nextDepth(3),
276
+ l1' = if (rejectOutside) l1 else nextL(1, l1, 3),
277
+ l2' = if (rejectOutside) l2 else nextL(2, l2, 3),
278
+ l3' = if (rejectOutside) l3 else nextL(3, l3, 3),
279
+ r1' = if (rejectOutside) r1 else nextR(1, r1, 3),
280
+ r2' = if (rejectOutside) r2 else nextR(2, r2, 3),
281
+ r3' = if (rejectOutside) r3 else nextR(3, r3, 3),
282
+ focus' = if (rejectOutside) focus else nextFocus(3, 0),
283
+ requests' = requests + moved(openNow, wantOpenOutside),
284
+ controlled' = controlled,
285
+ accepting' = accepting,
286
+ valueProp' = propAfter(follows, valueProp, wantOpenOutside),
287
+ requested' = if (openNow == wantOpenOutside) requested else wantOpenOutside,
288
+ outcome' = if (openNow == wantOpenOutside) outcome else verdict,
289
+ base' = if (openNow == wantOpenOutside) base else openNow
290
+ }
291
+
292
+ action tab = all {
293
+ depth' = nextDepth(4),
294
+ l1' = nextL(1, l1, 4),
295
+ l2' = nextL(2, l2, 4),
296
+ l3' = nextL(3, l3, 4),
297
+ r1' = nextR(1, r1, 4),
298
+ r2' = nextR(2, r2, 4),
299
+ r3' = nextR(3, r3, 4),
300
+ focus' = nextFocus(4, 0),
301
+ requests' = requests,
302
+ controlled' = controlled,
303
+ accepting' = accepting,
304
+ valueProp' = valueProp,
305
+ requested' = requested,
306
+ outcome' = outcome,
307
+ base' = base
308
+ }
309
+
310
+ action shift_tab = all {
311
+ depth' = nextDepth(5),
312
+ l1' = nextL(1, l1, 5),
313
+ l2' = nextL(2, l2, 5),
314
+ l3' = nextL(3, l3, 5),
315
+ r1' = nextR(1, r1, 5),
316
+ r2' = nextR(2, r2, 5),
317
+ r3' = nextR(3, r3, 5),
318
+ focus' = nextFocus(5, 0),
319
+ requests' = requests,
320
+ controlled' = controlled,
321
+ accepting' = accepting,
322
+ valueProp' = valueProp,
323
+ requested' = requested,
324
+ outcome' = outcome,
325
+ base' = base
326
+ }
327
+
328
+ // pointer click / programmatic focus on any encoded target (valid or not)
329
+ action focus_attempt = {
330
+ nondet target = Set(0, 1, 3, 4, 100, 101, 102, 106, 108, 109, 200, 201, 202, 203, 300, 301, 302).oneOf()
331
+ all {
332
+ depth' = nextDepth(6),
333
+ l1' = nextL(1, l1, 6),
334
+ l2' = nextL(2, l2, 6),
335
+ l3' = nextL(3, l3, 6),
336
+ r1' = nextR(1, r1, 6),
337
+ r2' = nextR(2, r2, 6),
338
+ r3' = nextR(3, r3, 6),
339
+ focus' = nextFocus(6, target),
340
+ requests' = requests,
341
+ controlled' = controlled,
342
+ accepting' = accepting,
343
+ valueProp' = valueProp,
344
+ requested' = requested,
345
+ outcome' = outcome,
346
+ base' = base
347
+ }
348
+ }
349
+
350
+ action remove_invoker = all {
351
+ depth' = nextDepth(7),
352
+ l1' = nextL(1, l1, 7),
353
+ l2' = nextL(2, l2, 7),
354
+ l3' = nextL(3, l3, 7),
355
+ r1' = nextR(1, r1, 7),
356
+ r2' = nextR(2, r2, 7),
357
+ r3' = nextR(3, r3, 7),
358
+ focus' = nextFocus(7, 0),
359
+ requests' = requests,
360
+ controlled' = controlled,
361
+ accepting' = accepting,
362
+ valueProp' = valueProp,
363
+ requested' = requested,
364
+ outcome' = outcome,
365
+ base' = base
366
+ }
367
+
368
+ // commit: the host writes the controlled open state (a no-op while uncontrolled)
369
+ action setValue = {
370
+ nondet v = Set(true, false).oneOf()
371
+ all {
372
+ depth' = if (controlled) setDepth(true, v, depth) else depth,
373
+ l1' = if (openingSet(controlled, v, depth)) 1
374
+ else if (closingSet(controlled, v, depth)) 0 else l1,
375
+ l2' = if (closingSet(controlled, v, depth)) 0 else l2,
376
+ l3' = if (closingSet(controlled, v, depth)) 0 else l3,
377
+ r1' = if (openingSet(controlled, v, depth)) 1
378
+ else if (closingSet(controlled, v, depth)) 0 else r1,
379
+ r2' = if (closingSet(controlled, v, depth)) 0 else r2,
380
+ r3' = if (closingSet(controlled, v, depth)) 0 else r3,
381
+ focus' = if (openingSet(controlled, v, depth)) (100 + initialFocus(1))
382
+ else if (closingSet(controlled, v, depth)) 1 else focus,
383
+ requests' = requests,
384
+ controlled' = controlled,
385
+ accepting' = accepting,
386
+ valueProp' = if (controlled) (setDepth(true, v, depth) > 0) else valueProp,
387
+ requested' = requested,
388
+ outcome' = if (controlled) "none" else outcome,
389
+ base' = base
390
+ }
391
+ }
392
+
393
+ // commit: the host flips its own copy of the open state
394
+ action hostToggle = all {
395
+ depth' = if (controlled) toggleDepth(true, depth) else depth,
396
+ l1' = if (openingSet(controlled, not(openNow), depth)) 1
397
+ else if (closingSet(controlled, not(openNow), depth)) 0 else l1,
398
+ l2' = if (closingSet(controlled, not(openNow), depth)) 0 else l2,
399
+ l3' = if (closingSet(controlled, not(openNow), depth)) 0 else l3,
400
+ r1' = if (openingSet(controlled, not(openNow), depth)) 1
401
+ else if (closingSet(controlled, not(openNow), depth)) 0 else r1,
402
+ r2' = if (closingSet(controlled, not(openNow), depth)) 0 else r2,
403
+ r3' = if (closingSet(controlled, not(openNow), depth)) 0 else r3,
404
+ focus' = if (openingSet(controlled, not(openNow), depth)) (100 + initialFocus(1))
405
+ else if (closingSet(controlled, not(openNow), depth)) 1 else focus,
406
+ requests' = requests,
407
+ controlled' = controlled,
408
+ accepting' = accepting,
409
+ valueProp' = if (controlled) (toggleDepth(true, depth) > 0) else valueProp,
410
+ requested' = requested,
411
+ outcome' = if (controlled) "none" else outcome,
412
+ base' = base
413
+ }
414
+
415
+ // configuration: the host starts owning the open state, adopting the current one
416
+ action control = all {
417
+ depth' = depth,
418
+ l1' = l1,
419
+ l2' = l2,
420
+ l3' = l3,
421
+ r1' = r1,
422
+ r2' = r2,
423
+ r3' = r3,
424
+ focus' = focus,
425
+ requests' = requests,
426
+ controlled' = true,
427
+ accepting' = accepting,
428
+ valueProp' = openNow,
429
+ requested' = requested,
430
+ outcome' = outcome,
431
+ base' = base
432
+ }
433
+
434
+ // configuration: the controlled host answers every request with its intent
435
+ action acceptRequests = all {
436
+ depth' = depth,
437
+ l1' = l1,
438
+ l2' = l2,
439
+ l3' = l3,
440
+ r1' = r1,
441
+ r2' = r2,
442
+ r3' = r3,
443
+ focus' = focus,
444
+ requests' = requests,
445
+ controlled' = controlled,
446
+ accepting' = true,
447
+ valueProp' = valueProp,
448
+ requested' = requested,
449
+ outcome' = outcome,
450
+ base' = base
451
+ }
452
+
453
+ action step = any {
454
+ init, escape, activate, pointer_outside, tab, shift_tab, focus_attempt,
455
+ remove_invoker, setValue, hostToggle, control, acceptRequests
456
+ }
457
+
458
+ // --- invariants ------------------------------------------------------------
459
+ // focus never leaves the top modal while it is open (page when none is)
460
+ val FocusInTopLayer: bool = focusLayer == depth
461
+ // nothing beneath the top layer holds focus (aria-modal: those layers are inert)
462
+ val NoFocusInInertLayer: bool = not(isInert(focusLayer))
463
+ // focus sits on a real element; the dialog container itself is never focused
464
+ val FocusPositionValid: bool = validPos(topDialog, focusPos)
465
+ val DepthBounded: bool = and(depth >= 0, depth <= MAX_DEPTH)
466
+ // exactly layers 1..depth hold a dialog and a return target; the rest are empty
467
+ val StackContiguous: bool = and(
468
+ or(and(depth >= 1, l1 > 0), and(depth < 1, l1 == 0, r1 == 0)),
469
+ or(and(depth >= 2, l2 > 0), and(depth < 2, l2 == 0, r2 == 0)),
470
+ or(and(depth >= 3, l3 > 0), and(depth < 3, l3 == 0, r3 == 0))
471
+ )
472
+ // each open layer's return target is a real element in the layer beneath it
473
+ val ReturnTargetsValid: bool = and(
474
+ or(depth < 1, and(r1 / 100 == 0, validPos(dlgAt(0), r1 % 100))),
475
+ or(depth < 2, and(r2 / 100 == 1, validPos(dlgAt(1), r2 % 100))),
476
+ or(depth < 3, and(r3 / 100 == 2, validPos(dlgAt(2), r3 % 100)))
477
+ )
478
+ // role=dialog, aria-modal=true and an accessible name on every open dialog
479
+ val OpenDialogsHonourContract: bool = and(
480
+ or(depth < 1, contractOk(l1)),
481
+ or(depth < 2, contractOk(l2)),
482
+ or(depth < 3, contractOk(l3))
483
+ )
484
+ // configuration sanity: every dialog's initial focus is a focusable position
485
+ val InitialFocusConfigValid: bool = and(
486
+ validPos(1, initialFocus(1)),
487
+ validPos(2, initialFocus(2)),
488
+ validPos(3, initialFocus(3)),
489
+ validPos(4, initialFocus(4))
490
+ )
491
+ // callbacks only ever count up from zero
492
+ val RequestsNonNegative: bool = requests >= 0
493
+ // controlled: only the value prop sets the state, so no event commits
494
+ val ControlledFollowsProp: bool = or(not(controlled), openNow == valueProp)
495
+ // a rejected request leaves the state as it found it
496
+ val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), openNow == base)
497
+ // an accepted request committed exactly its intent
498
+ val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), openNow == requested)
499
+ }