@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,732 @@
1
+ // WAI-ARIA APG — Carousel (Slide Show or Image Rotator) pattern.
2
+ //
3
+ // Modelled (https://www.w3.org/WAI/ARIA/apg/patterns/carousel/):
4
+ // - "About This Pattern": prev/next controls; optional slide picker; for an
5
+ // auto-rotating carousel a rotation control, stop on keyboard focus, stop
6
+ // while the mouse hovers.
7
+ // - "Keyboard Interaction": rotation stops when any element receives focus
8
+ // and does not resume unless the rotation control is activated; Tab /
9
+ // Shift+Tab walk the page tab sequence with the rotation control FIRST;
10
+ // buttons activate with Enter/Space without moving focus.
11
+ // - "WAI-ARIA Roles, States, and Properties": basic + tabbed variants; the
12
+ // slide wrapper's aria-live is "off" while rotating, "polite" otherwise;
13
+ // the rotation control label flips between Stop/Start and carries no
14
+ // aria-pressed; slides are group (basic) or tabpanel (tabbed); roving
15
+ // tabindex (only the selected tab is in the tab sequence).
16
+ // - Examples (…/examples/carousel-1-prev-next/ and …/carousel-2-tablist/):
17
+ // hover pause resumes on leave; an explicit Start ignores focus/hover until
18
+ // Stop; reduced motion / "pause on load" starts paused; "disable
19
+ // auto-rotation" removes the control; tabs use Left/Right (wrapping), Home,
20
+ // End with automatic activation.
21
+ //
22
+ // Configuration: props are events (set_slide_count, set_tabbed,
23
+ // set_auto_rotate, set_sticky_focus_pause, set_reduced_motion), so a Solid
24
+ // component can be driven through every variant. `stickyFocusPause` encodes the
25
+ // pattern text (true) vs the basic example (false: rotation resumes on blur).
26
+ //
27
+ // Position encoding for `focus`: 0 none, 1 rotation control, 2 previous,
28
+ // 3 next, 4..6 the tabs, 7 a link inside the slide.
29
+ //
30
+ // Request vs commit (map ticket rt1p). The shown slide is the value. Every
31
+ // event a user sees as a slide change — `tick`, `click_prev`, `click_next`,
32
+ // `click_tab`, the tab keys (`key_arrow_*`, `key_home`, `key_end`) and
33
+ // `key_activate` on a prev/next control — is a *request* carrying an intent
34
+ // (`requested`); it fires onIndexChange (`requests` counts the calls) when the
35
+ // intent differs from the current `current`, and then:
36
+ // uncontrolled the request is accepted: the slide commits the intent
37
+ // controlled, holds the host ignores it ("rejected"): the slide is untouched
38
+ // controlled, accepts the host answers with the same value (`accepting`): the
39
+ // slide commits through the controlled value, not the event
40
+ // `valueProp` is the only thing that changes the slide while `controlled`;
41
+ // `setValue` is the host writing it. `control` / `acceptRequests` are
42
+ // configuration events (init is literal-only). DOM focus still moves with the
43
+ // request (roving focus / tab walking), independent of the value. Invariants
44
+ // for the model: ControlledFollowsProp, RejectedRequestUntouched,
45
+ // AcceptedRequestCommits.
46
+ //
47
+ // ARIA contract (derived defs, evaluated by the invariants below):
48
+ // ariaLive(rotating) -> aria-live on the slide wrapper
49
+ // rotationLabel(rotating) -> accessible name of the rotation control
50
+ // slideRole(tabbed) -> role of each slide container
51
+ // slideHidden / tabSelected / tabStop -> per-slide hidden, aria-selected, and
52
+ // tabindex=0 (a non-stop tab has tabindex=-1; negatives avoided in the spec)
53
+ // The rotation control is rendered iff autoRotate.
54
+ //
55
+ // Deliberate omissions:
56
+ // - Static attributes (role region|group + aria-roledescription="carousel" +
57
+ // label on the container, aria-roledescription="slide" and "N of M" labels,
58
+ // aria-atomic="false", tablist label, aria-controls) are constants, not state.
59
+ // - Discrepancy: the pattern text says tabpanel slides have NO
60
+ // aria-roledescription; the tabbed example sets it to "slide". Static, so
61
+ // left to the implementation (follow the example).
62
+ // - Grouped variant (picker buttons with aria-disabled) is not modelled.
63
+ // - Pointer clicks on controls do not move focus (Safari); focus_control covers
64
+ // click-focus. Timer duration and slide transition animation are not modelled.
65
+ // - Slide content is opaque; focus inside it is the single position 7.
66
+ // - Re-enabling autoRotate leaves rotation paused (user must Start).
67
+ //
68
+ // Generator notes: the evaluator resolves def-calls but guards take their vars
69
+ // as parameters; no action preconditions, so guards live in the assignment RHS.
70
+ module apg_carousel {
71
+ var slides: int // prop: slide count (1..3)
72
+ var current: int // index of the displayed slide (0-based)
73
+ var tabbed: bool // prop: tabbed slide picker rendered (tablist/tab/tabpanel)
74
+ var autoRotate: bool // prop: auto-rotation feature enabled (false = no rotation control rendered)
75
+ var stickyFocusPause: bool // prop: focus entry stops rotation until the control restarts it (pattern text); false = resume on blur (basic example)
76
+ var rotate: bool // user intent: rotation wanted (page-load default true; cleared by Stop, reduced motion, sticky focus entry)
77
+ var forced: bool // user explicitly started rotation: focus/hover are ignored until Stop
78
+ var focus: int // focus position: 0 none, 1 rotation, 2 prev, 3 next, 4..6 tab 0..2, 7 link inside slide
79
+ var hover: bool // pointer is over the carousel
80
+ var controlled: bool // the host owns the shown slide (`value` prop present)
81
+ var accepting: bool // controlled host that answers every request with its intent
82
+ var valueProp: int // the controlled value prop (meaningful while controlled)
83
+ var requested: int // intent of the last request
84
+ var requests: int // number of requests that fired onIndexChange
85
+ var outcome: str // "none" | "accepted" | "rejected"
86
+ var base: int // slide when the last request was received
87
+
88
+ def isTab(f: int): bool = and(f >= 4, f <= 6)
89
+ def prevIdx(i: int, n: int): int = if (i <= 0) n - 1 else i - 1
90
+ def nextIdx(i: int, n: int): int = if (i + 1 >= n) 0 else i + 1
91
+ def clampIdx(i: int, n: int): int = if (i >= n) n - 1 else i
92
+
93
+ // rotation actually runs: feature on, user wants it, and either the user
94
+ // explicitly started it or neither focus nor hover is inside the carousel
95
+ def rotating(auto: bool, rot: bool, frc: bool, f: int, hov: bool): bool =
96
+ and(and(auto, rot), or(frc, and(f == 0, not(hov))))
97
+
98
+ // focus moving from outside to inside stops rotation under the sticky policy
99
+ def stopsOnEnter(sticky: bool, f0: int, f1: int): bool =
100
+ and(sticky, and(f0 == 0, f1 != 0))
101
+
102
+ // the rotation control: Stop when rotating, else Start; both flags follow
103
+ def toggled(auto: bool, going: bool, cur: bool): bool = if (auto) not(going) else cur
104
+
105
+ // Tab / Shift+Tab; from the last / first stop focus leaves the carousel (0)
106
+ def tabFwd(f: int, auto: bool, tb: bool, cur: int): int =
107
+ if (f == 0) (if (auto) 1 else 2)
108
+ else if (f == 1) 2
109
+ else if (f == 2) 3
110
+ else if (f == 3) (if (tb) 4 + cur else 7)
111
+ else if (f == 7) 0
112
+ else 7
113
+ def tabBack(f: int, auto: bool, tb: bool, cur: int): int =
114
+ if (f == 0) 7
115
+ else if (f == 7) (if (tb) 4 + cur else 3)
116
+ else if (isTab(f)) 3
117
+ else if (f == 3) 2
118
+ else if (f == 2) (if (auto) 1 else 0)
119
+ else 0
120
+
121
+ // direct focus: unavailable targets leave focus where it is
122
+ def focusTarget(t: int, f: int, auto: bool, tb: bool, cur: int): int =
123
+ if (t == 1) (if (auto) 1 else f)
124
+ else if (t == 4) (if (tb) 4 + cur else f)
125
+ else t
126
+
127
+ // ---- request / commit (map ticket rt1p) ----------------------------------
128
+ def moved(before: int, after: int): int = if (before == after) 0 else 1
129
+ // the slide each request would show
130
+ def tickWant(auto: bool, rot: bool, frc: bool, f: int, hov: bool, cur: int, n: int): int =
131
+ if (rotating(auto, rot, frc, f, hov)) nextIdx(cur, n) else cur
132
+ def activateWant(f: int, cur: int, n: int): int =
133
+ if (f == 2) prevIdx(cur, n) else if (f == 3) nextIdx(cur, n) else cur
134
+ def arrowRightWant(f: int, cur: int, n: int): int =
135
+ if (isTab(f)) nextIdx(f - 4, n) else cur
136
+ def arrowLeftWant(f: int, cur: int, n: int): int =
137
+ if (isTab(f)) prevIdx(f - 4, n) else cur
138
+ def homeWant(f: int, cur: int): int = if (isTab(f)) 0 else cur
139
+ def endWant(f: int, cur: int, n: int): int = if (isTab(f)) n - 1 else cur
140
+ def tabClickWant(tb: bool, i: int, cur: int, n: int): int =
141
+ if (and(tb, i < n)) i else cur
142
+ // controlled host that keeps the old value: the request is rejected
143
+ def holds: bool = and(controlled, not(accepting))
144
+ // controlled host that writes the intent back through the value prop
145
+ def follows: bool = and(controlled, accepting)
146
+ def verdict: str = if (holds) "rejected" else "accepted"
147
+ // the slide after a request for `want`
148
+ def landed(h: bool, cur: int, want: int): int = if (h) cur else want
149
+ // the value prop after a request for `want`
150
+ def propAfter(f: bool, p: int, want: int): int = if (f) want else p
151
+
152
+ // ARIA contract
153
+ def ariaLive(going: bool): str = if (going) "off" else "polite"
154
+ def rotationLabel(going: bool): str =
155
+ if (going) "Stop automatic slide show" else "Start automatic slide show"
156
+ def slideRole(tb: bool): str = if (tb) "tabpanel" else "group"
157
+ def slideShown(i: int, n: int, cur: int): bool = and(i < n, i == cur)
158
+ def slideHidden(i: int, n: int, cur: int): bool = not(slideShown(i, n, cur))
159
+ def tabSelected(i: int, n: int, cur: int): bool = slideShown(i, n, cur)
160
+ def tabStop(i: int, n: int, cur: int): bool = tabSelected(i, n, cur)
161
+ def b2i(b: bool): int = if (b) 1 else 0
162
+ def shownCount(n: int, cur: int): int =
163
+ b2i(slideShown(0, n, cur)) + b2i(slideShown(1, n, cur)) + b2i(slideShown(2, n, cur))
164
+ def stopCount(n: int, cur: int): int =
165
+ b2i(tabStop(0, n, cur)) + b2i(tabStop(1, n, cur)) + b2i(tabStop(2, n, cur))
166
+
167
+ // page load: rotating, nothing focused or hovered, basic (untabbed) carousel
168
+ action init = all {
169
+ slides' = 3,
170
+ current' = 0,
171
+ tabbed' = false,
172
+ autoRotate' = true,
173
+ stickyFocusPause' = true,
174
+ rotate' = true,
175
+ forced' = false,
176
+ focus' = 0,
177
+ hover' = false,
178
+ controlled' = false,
179
+ accepting' = false,
180
+ valueProp' = 0,
181
+ requested' = 0,
182
+ requests' = 0,
183
+ outcome' = "none",
184
+ base' = 0
185
+ }
186
+
187
+ // request: the rotation timer advances the slide only while rotating
188
+ action tick = all {
189
+ slides' = slides,
190
+ current' = landed(holds, current, tickWant(autoRotate, rotate, forced, focus, hover, current, slides)),
191
+ tabbed' = tabbed,
192
+ autoRotate' = autoRotate,
193
+ stickyFocusPause' = stickyFocusPause,
194
+ rotate' = rotate,
195
+ forced' = forced,
196
+ focus' = focus,
197
+ hover' = hover,
198
+ controlled' = controlled,
199
+ accepting' = accepting,
200
+ valueProp' = propAfter(follows, valueProp, tickWant(autoRotate, rotate, forced, focus, hover, current, slides)),
201
+ requested' = if (moved(current, tickWant(autoRotate, rotate, forced, focus, hover, current, slides)) == 1) tickWant(autoRotate, rotate, forced, focus, hover, current, slides) else requested,
202
+ requests' = requests + moved(current, tickWant(autoRotate, rotate, forced, focus, hover, current, slides)),
203
+ outcome' = if (moved(current, tickWant(autoRotate, rotate, forced, focus, hover, current, slides)) == 1) verdict else outcome,
204
+ base' = if (moved(current, tickWant(autoRotate, rotate, forced, focus, hover, current, slides)) == 1) current else base
205
+ }
206
+
207
+ // mouse enters the carousel: rotation pauses (derived)
208
+ action pointer_enter = all {
209
+ slides' = slides,
210
+ current' = current,
211
+ tabbed' = tabbed,
212
+ autoRotate' = autoRotate,
213
+ stickyFocusPause' = stickyFocusPause,
214
+ rotate' = rotate,
215
+ forced' = forced,
216
+ focus' = focus,
217
+ hover' = true,
218
+ controlled' = controlled,
219
+ accepting' = accepting,
220
+ valueProp' = valueProp,
221
+ requested' = requested,
222
+ requests' = requests,
223
+ outcome' = outcome,
224
+ base' = base
225
+ }
226
+
227
+ // mouse leaves the carousel
228
+ action pointer_leave = all {
229
+ slides' = slides,
230
+ current' = current,
231
+ tabbed' = tabbed,
232
+ autoRotate' = autoRotate,
233
+ stickyFocusPause' = stickyFocusPause,
234
+ rotate' = rotate,
235
+ forced' = forced,
236
+ focus' = focus,
237
+ hover' = false,
238
+ controlled' = controlled,
239
+ accepting' = accepting,
240
+ valueProp' = valueProp,
241
+ requested' = requested,
242
+ requests' = requests,
243
+ outcome' = outcome,
244
+ base' = base
245
+ }
246
+
247
+ // Tab: next stop in the tab sequence (rotation, prev, next, selected tab, slide link)
248
+ action key_tab = all {
249
+ slides' = slides,
250
+ current' = current,
251
+ tabbed' = tabbed,
252
+ autoRotate' = autoRotate,
253
+ stickyFocusPause' = stickyFocusPause,
254
+ rotate' = if (stopsOnEnter(stickyFocusPause, focus, tabFwd(focus, autoRotate, tabbed, current))) false else rotate,
255
+ forced' = if (stopsOnEnter(stickyFocusPause, focus, tabFwd(focus, autoRotate, tabbed, current))) false else forced,
256
+ focus' = tabFwd(focus, autoRotate, tabbed, current),
257
+ hover' = hover,
258
+ controlled' = controlled,
259
+ accepting' = accepting,
260
+ valueProp' = valueProp,
261
+ requested' = requested,
262
+ requests' = requests,
263
+ outcome' = outcome,
264
+ base' = base
265
+ }
266
+
267
+ // Shift+Tab: previous stop in the tab sequence
268
+ action key_shift_tab = all {
269
+ slides' = slides,
270
+ current' = current,
271
+ tabbed' = tabbed,
272
+ autoRotate' = autoRotate,
273
+ stickyFocusPause' = stickyFocusPause,
274
+ rotate' = if (stopsOnEnter(stickyFocusPause, focus, tabBack(focus, autoRotate, tabbed, current))) false else rotate,
275
+ forced' = if (stopsOnEnter(stickyFocusPause, focus, tabBack(focus, autoRotate, tabbed, current))) false else forced,
276
+ focus' = tabBack(focus, autoRotate, tabbed, current),
277
+ hover' = hover,
278
+ controlled' = controlled,
279
+ accepting' = accepting,
280
+ valueProp' = valueProp,
281
+ requested' = requested,
282
+ requests' = requests,
283
+ outcome' = outcome,
284
+ base' = base
285
+ }
286
+
287
+ // focus lands directly on a control (click focus, AT virtual cursor, script); t: 1 rotation, 2 prev, 3 next, 4 selected tab, 7 slide link
288
+ action focus_control = {
289
+ nondet t = Set(1, 2, 3, 4, 7).oneOf()
290
+ all {
291
+ slides' = slides,
292
+ current' = current,
293
+ tabbed' = tabbed,
294
+ autoRotate' = autoRotate,
295
+ stickyFocusPause' = stickyFocusPause,
296
+ rotate' = if (stopsOnEnter(stickyFocusPause, focus, focusTarget(t, focus, autoRotate, tabbed, current))) false else rotate,
297
+ forced' = if (stopsOnEnter(stickyFocusPause, focus, focusTarget(t, focus, autoRotate, tabbed, current))) false else forced,
298
+ focus' = focusTarget(t, focus, autoRotate, tabbed, current),
299
+ hover' = hover,
300
+ controlled' = controlled,
301
+ accepting' = accepting,
302
+ valueProp' = valueProp,
303
+ requested' = requested,
304
+ requests' = requests,
305
+ outcome' = outcome,
306
+ base' = base
307
+ }
308
+ }
309
+
310
+ // focus leaves the carousel
311
+ action blur_out = all {
312
+ slides' = slides,
313
+ current' = current,
314
+ tabbed' = tabbed,
315
+ autoRotate' = autoRotate,
316
+ stickyFocusPause' = stickyFocusPause,
317
+ rotate' = rotate,
318
+ forced' = forced,
319
+ focus' = 0,
320
+ hover' = hover,
321
+ controlled' = controlled,
322
+ accepting' = accepting,
323
+ valueProp' = valueProp,
324
+ requested' = requested,
325
+ requests' = requests,
326
+ outcome' = outcome,
327
+ base' = base
328
+ }
329
+
330
+ // request: Enter/Space on the focused element toggles rotation, or goes
331
+ // prev/next (focus does not move); on a tab it is a no-op (already selected)
332
+ action key_activate = all {
333
+ slides' = slides,
334
+ current' = landed(holds, current, activateWant(focus, current, slides)),
335
+ tabbed' = tabbed,
336
+ autoRotate' = autoRotate,
337
+ stickyFocusPause' = stickyFocusPause,
338
+ rotate' = if (focus == 1) toggled(autoRotate, rotating(autoRotate, rotate, forced, focus, hover), rotate) else rotate,
339
+ forced' = if (focus == 1) toggled(autoRotate, rotating(autoRotate, rotate, forced, focus, hover), forced) else forced,
340
+ focus' = focus,
341
+ hover' = hover,
342
+ controlled' = controlled,
343
+ accepting' = accepting,
344
+ valueProp' = propAfter(follows, valueProp, activateWant(focus, current, slides)),
345
+ requested' = if (moved(current, activateWant(focus, current, slides)) == 1) activateWant(focus, current, slides) else requested,
346
+ requests' = requests + moved(current, activateWant(focus, current, slides)),
347
+ outcome' = if (moved(current, activateWant(focus, current, slides)) == 1) verdict else outcome,
348
+ base' = if (moved(current, activateWant(focus, current, slides)) == 1) current else base
349
+ }
350
+
351
+ // request: Right Arrow on a tab focuses + shows the next tab (wraps)
352
+ action key_arrow_right = all {
353
+ slides' = slides,
354
+ current' = landed(holds, current, arrowRightWant(focus, current, slides)),
355
+ tabbed' = tabbed,
356
+ autoRotate' = autoRotate,
357
+ stickyFocusPause' = stickyFocusPause,
358
+ rotate' = rotate,
359
+ forced' = forced,
360
+ focus' = if (isTab(focus)) 4 + nextIdx(focus - 4, slides) else focus,
361
+ hover' = hover,
362
+ controlled' = controlled,
363
+ accepting' = accepting,
364
+ valueProp' = propAfter(follows, valueProp, arrowRightWant(focus, current, slides)),
365
+ requested' = if (moved(current, arrowRightWant(focus, current, slides)) == 1) arrowRightWant(focus, current, slides) else requested,
366
+ requests' = requests + moved(current, arrowRightWant(focus, current, slides)),
367
+ outcome' = if (moved(current, arrowRightWant(focus, current, slides)) == 1) verdict else outcome,
368
+ base' = if (moved(current, arrowRightWant(focus, current, slides)) == 1) current else base
369
+ }
370
+
371
+ // request: Left Arrow on a tab focuses + shows the previous tab (wraps)
372
+ action key_arrow_left = all {
373
+ slides' = slides,
374
+ current' = landed(holds, current, arrowLeftWant(focus, current, slides)),
375
+ tabbed' = tabbed,
376
+ autoRotate' = autoRotate,
377
+ stickyFocusPause' = stickyFocusPause,
378
+ rotate' = rotate,
379
+ forced' = forced,
380
+ focus' = if (isTab(focus)) 4 + prevIdx(focus - 4, slides) else focus,
381
+ hover' = hover,
382
+ controlled' = controlled,
383
+ accepting' = accepting,
384
+ valueProp' = propAfter(follows, valueProp, arrowLeftWant(focus, current, slides)),
385
+ requested' = if (moved(current, arrowLeftWant(focus, current, slides)) == 1) arrowLeftWant(focus, current, slides) else requested,
386
+ requests' = requests + moved(current, arrowLeftWant(focus, current, slides)),
387
+ outcome' = if (moved(current, arrowLeftWant(focus, current, slides)) == 1) verdict else outcome,
388
+ base' = if (moved(current, arrowLeftWant(focus, current, slides)) == 1) current else base
389
+ }
390
+
391
+ // request: Home on a tab shows the first slide
392
+ action key_home = all {
393
+ slides' = slides,
394
+ current' = landed(holds, current, homeWant(focus, current)),
395
+ tabbed' = tabbed,
396
+ autoRotate' = autoRotate,
397
+ stickyFocusPause' = stickyFocusPause,
398
+ rotate' = rotate,
399
+ forced' = forced,
400
+ focus' = if (isTab(focus)) 4 + 0 else focus,
401
+ hover' = hover,
402
+ controlled' = controlled,
403
+ accepting' = accepting,
404
+ valueProp' = propAfter(follows, valueProp, homeWant(focus, current)),
405
+ requested' = if (moved(current, homeWant(focus, current)) == 1) homeWant(focus, current) else requested,
406
+ requests' = requests + moved(current, homeWant(focus, current)),
407
+ outcome' = if (moved(current, homeWant(focus, current)) == 1) verdict else outcome,
408
+ base' = if (moved(current, homeWant(focus, current)) == 1) current else base
409
+ }
410
+
411
+ // request: End on a tab shows the last slide
412
+ action key_end = all {
413
+ slides' = slides,
414
+ current' = landed(holds, current, endWant(focus, current, slides)),
415
+ tabbed' = tabbed,
416
+ autoRotate' = autoRotate,
417
+ stickyFocusPause' = stickyFocusPause,
418
+ rotate' = rotate,
419
+ forced' = forced,
420
+ focus' = if (isTab(focus)) 4 + slides - 1 else focus,
421
+ hover' = hover,
422
+ controlled' = controlled,
423
+ accepting' = accepting,
424
+ valueProp' = propAfter(follows, valueProp, endWant(focus, current, slides)),
425
+ requested' = if (moved(current, endWant(focus, current, slides)) == 1) endWant(focus, current, slides) else requested,
426
+ requests' = requests + moved(current, endWant(focus, current, slides)),
427
+ outcome' = if (moved(current, endWant(focus, current, slides)) == 1) verdict else outcome,
428
+ base' = if (moved(current, endWant(focus, current, slides)) == 1) current else base
429
+ }
430
+
431
+ // pointer click on the rotation control (focus unchanged): Stop when rotating, else explicit Start (focus/hover ignored)
432
+ action click_rotation = all {
433
+ slides' = slides,
434
+ current' = current,
435
+ tabbed' = tabbed,
436
+ autoRotate' = autoRotate,
437
+ stickyFocusPause' = stickyFocusPause,
438
+ rotate' = toggled(autoRotate, rotating(autoRotate, rotate, forced, focus, hover), rotate),
439
+ forced' = toggled(autoRotate, rotating(autoRotate, rotate, forced, focus, hover), forced),
440
+ focus' = focus,
441
+ hover' = hover,
442
+ controlled' = controlled,
443
+ accepting' = accepting,
444
+ valueProp' = valueProp,
445
+ requested' = requested,
446
+ requests' = requests,
447
+ outcome' = outcome,
448
+ base' = base
449
+ }
450
+
451
+ // request: pointer click on Previous slide
452
+ action click_prev = all {
453
+ slides' = slides,
454
+ current' = landed(holds, current, prevIdx(current, slides)),
455
+ tabbed' = tabbed,
456
+ autoRotate' = autoRotate,
457
+ stickyFocusPause' = stickyFocusPause,
458
+ rotate' = rotate,
459
+ forced' = forced,
460
+ focus' = focus,
461
+ hover' = hover,
462
+ controlled' = controlled,
463
+ accepting' = accepting,
464
+ valueProp' = propAfter(follows, valueProp, prevIdx(current, slides)),
465
+ requested' = if (moved(current, prevIdx(current, slides)) == 1) prevIdx(current, slides) else requested,
466
+ requests' = requests + moved(current, prevIdx(current, slides)),
467
+ outcome' = if (moved(current, prevIdx(current, slides)) == 1) verdict else outcome,
468
+ base' = if (moved(current, prevIdx(current, slides)) == 1) current else base
469
+ }
470
+
471
+ // request: pointer click on Next slide
472
+ action click_next = all {
473
+ slides' = slides,
474
+ current' = landed(holds, current, nextIdx(current, slides)),
475
+ tabbed' = tabbed,
476
+ autoRotate' = autoRotate,
477
+ stickyFocusPause' = stickyFocusPause,
478
+ rotate' = rotate,
479
+ forced' = forced,
480
+ focus' = focus,
481
+ hover' = hover,
482
+ controlled' = controlled,
483
+ accepting' = accepting,
484
+ valueProp' = propAfter(follows, valueProp, nextIdx(current, slides)),
485
+ requested' = if (moved(current, nextIdx(current, slides)) == 1) nextIdx(current, slides) else requested,
486
+ requests' = requests + moved(current, nextIdx(current, slides)),
487
+ outcome' = if (moved(current, nextIdx(current, slides)) == 1) verdict else outcome,
488
+ base' = if (moved(current, nextIdx(current, slides)) == 1) current else base
489
+ }
490
+
491
+ // request: pointer click on tab i (tabbed only) shows that slide
492
+ action click_tab = {
493
+ nondet i = Set(0, 1, 2).oneOf()
494
+ all {
495
+ slides' = slides,
496
+ current' = landed(holds, current, tabClickWant(tabbed, i, current, slides)),
497
+ tabbed' = tabbed,
498
+ autoRotate' = autoRotate,
499
+ stickyFocusPause' = stickyFocusPause,
500
+ rotate' = rotate,
501
+ forced' = forced,
502
+ focus' = focus,
503
+ hover' = hover,
504
+ controlled' = controlled,
505
+ accepting' = accepting,
506
+ valueProp' = propAfter(follows, valueProp, tabClickWant(tabbed, i, current, slides)),
507
+ requested' = if (moved(current, tabClickWant(tabbed, i, current, slides)) == 1) tabClickWant(tabbed, i, current, slides) else requested,
508
+ requests' = requests + moved(current, tabClickWant(tabbed, i, current, slides)),
509
+ outcome' = if (moved(current, tabClickWant(tabbed, i, current, slides)) == 1) verdict else outcome,
510
+ base' = if (moved(current, tabClickWant(tabbed, i, current, slides)) == 1) current else base
511
+ }
512
+ }
513
+
514
+ // prop change: number of slides; current slide and tab focus clamp into range.
515
+ // While controlled the value prop clamps with it, and a pending request settles.
516
+ action set_slide_count = {
517
+ nondet m = Set(1, 2, 3).oneOf()
518
+ all {
519
+ slides' = m,
520
+ current' = clampIdx(current, m),
521
+ tabbed' = tabbed,
522
+ autoRotate' = autoRotate,
523
+ stickyFocusPause' = stickyFocusPause,
524
+ rotate' = rotate,
525
+ forced' = forced,
526
+ focus' = if (and(isTab(focus), focus - 4 >= m)) 4 + m - 1 else focus,
527
+ hover' = hover,
528
+ controlled' = controlled,
529
+ accepting' = accepting,
530
+ valueProp' = if (controlled) clampIdx(valueProp, m) else valueProp,
531
+ requested' = requested,
532
+ requests' = requests,
533
+ outcome' = "none",
534
+ base' = base
535
+ }
536
+ }
537
+
538
+ // prop change: tabbed variant; the tablist unmounting drops tab focus
539
+ action set_tabbed = {
540
+ nondet on = Set(true, false).oneOf()
541
+ all {
542
+ slides' = slides,
543
+ current' = current,
544
+ tabbed' = on,
545
+ autoRotate' = autoRotate,
546
+ stickyFocusPause' = stickyFocusPause,
547
+ rotate' = rotate,
548
+ forced' = forced,
549
+ focus' = if (and(not(on), isTab(focus))) 0 else focus,
550
+ hover' = hover,
551
+ controlled' = controlled,
552
+ accepting' = accepting,
553
+ valueProp' = valueProp,
554
+ requested' = requested,
555
+ requests' = requests,
556
+ outcome' = outcome,
557
+ base' = base
558
+ }
559
+ }
560
+
561
+ // prop change: auto-rotation feature; disabling stops rotation and unmounts the rotation control
562
+ action set_auto_rotate = {
563
+ nondet on = Set(true, false).oneOf()
564
+ all {
565
+ slides' = slides,
566
+ current' = current,
567
+ tabbed' = tabbed,
568
+ autoRotate' = on,
569
+ stickyFocusPause' = stickyFocusPause,
570
+ rotate' = if (on) rotate else false,
571
+ forced' = if (on) forced else false,
572
+ focus' = if (and(not(on), focus == 1)) 0 else focus,
573
+ hover' = hover,
574
+ controlled' = controlled,
575
+ accepting' = accepting,
576
+ valueProp' = valueProp,
577
+ requested' = requested,
578
+ requests' = requests,
579
+ outcome' = outcome,
580
+ base' = base
581
+ }
582
+ }
583
+
584
+ // prop change: focus-pause policy; turning sticky on while focus is inside stops rotation unless user-started
585
+ action set_sticky_focus_pause = {
586
+ nondet on = Set(true, false).oneOf()
587
+ all {
588
+ slides' = slides,
589
+ current' = current,
590
+ tabbed' = tabbed,
591
+ autoRotate' = autoRotate,
592
+ stickyFocusPause' = on,
593
+ rotate' = if (and(on, and(focus != 0, not(forced)))) false else rotate,
594
+ forced' = forced,
595
+ focus' = focus,
596
+ hover' = hover,
597
+ controlled' = controlled,
598
+ accepting' = accepting,
599
+ valueProp' = valueProp,
600
+ requested' = requested,
601
+ requests' = requests,
602
+ outcome' = outcome,
603
+ base' = base
604
+ }
605
+ }
606
+
607
+ // prefers-reduced-motion (or "pause on load"): rotation paused until the user starts it
608
+ action set_reduced_motion = all {
609
+ slides' = slides,
610
+ current' = current,
611
+ tabbed' = tabbed,
612
+ autoRotate' = autoRotate,
613
+ stickyFocusPause' = stickyFocusPause,
614
+ rotate' = false,
615
+ forced' = false,
616
+ focus' = focus,
617
+ hover' = hover,
618
+ controlled' = controlled,
619
+ accepting' = accepting,
620
+ valueProp' = valueProp,
621
+ requested' = requested,
622
+ requests' = requests,
623
+ outcome' = outcome,
624
+ base' = base
625
+ }
626
+
627
+ // commit: the host writes the controlled slide (a no-op while uncontrolled)
628
+ action setValue = {
629
+ nondet v = Set(0, 1, 2).oneOf()
630
+ all {
631
+ slides' = slides,
632
+ current' = if (controlled) clampIdx(v, slides) else current,
633
+ tabbed' = tabbed,
634
+ autoRotate' = autoRotate,
635
+ stickyFocusPause' = stickyFocusPause,
636
+ rotate' = rotate,
637
+ forced' = forced,
638
+ focus' = focus,
639
+ hover' = hover,
640
+ controlled' = controlled,
641
+ accepting' = accepting,
642
+ valueProp' = if (controlled) clampIdx(v, slides) else valueProp,
643
+ requested' = requested,
644
+ requests' = requests,
645
+ outcome' = if (controlled) "none" else outcome,
646
+ base' = base
647
+ }
648
+ }
649
+
650
+ // configuration: the host starts owning the slide, adopting the current one
651
+ action control = all {
652
+ slides' = slides,
653
+ current' = current,
654
+ tabbed' = tabbed,
655
+ autoRotate' = autoRotate,
656
+ stickyFocusPause' = stickyFocusPause,
657
+ rotate' = rotate,
658
+ forced' = forced,
659
+ focus' = focus,
660
+ hover' = hover,
661
+ controlled' = true,
662
+ accepting' = accepting,
663
+ valueProp' = current,
664
+ requested' = requested,
665
+ requests' = requests,
666
+ outcome' = outcome,
667
+ base' = base
668
+ }
669
+
670
+ // configuration: the controlled host answers every request with its intent
671
+ action acceptRequests = all {
672
+ slides' = slides,
673
+ current' = current,
674
+ tabbed' = tabbed,
675
+ autoRotate' = autoRotate,
676
+ stickyFocusPause' = stickyFocusPause,
677
+ rotate' = rotate,
678
+ forced' = forced,
679
+ focus' = focus,
680
+ hover' = hover,
681
+ controlled' = controlled,
682
+ accepting' = true,
683
+ valueProp' = valueProp,
684
+ requested' = requested,
685
+ requests' = requests,
686
+ outcome' = outcome,
687
+ base' = base
688
+ }
689
+
690
+ action step = any { init, tick, pointer_enter, pointer_leave, key_tab, key_shift_tab, focus_control, blur_out, key_activate, key_arrow_right, key_arrow_left, key_home, key_end, click_rotation, click_prev, click_next, click_tab, set_slide_count, set_tabbed, set_auto_rotate, set_sticky_focus_pause, set_reduced_motion, setValue, control, acceptRequests }
691
+
692
+ // APG: never rotating while focus or hover is inside, unless the user
693
+ // explicitly started rotation
694
+ val NeverRotatingWhileEngaged: bool =
695
+ or(not(rotating(autoRotate, rotate, forced, focus, hover)), or(forced, and(focus == 0, not(hover))))
696
+ // an explicit Start implies rotation is wanted
697
+ val ForcedImpliesRotate: bool = or(not(forced), rotate)
698
+ // with the feature off nothing is wanted and nothing is forced
699
+ val DisabledIsIdle: bool = or(autoRotate, and(not(rotate), not(forced)))
700
+ // pattern text: focus inside stops rotation and it stays stopped (unless the user restarted it)
701
+ val StickyFocusStaysStopped: bool =
702
+ or(not(stickyFocusPause), or(focus == 0, or(forced, not(rotate))))
703
+ // aria-live is off exactly while rotating
704
+ val LiveOffIffRotating: bool = (ariaLive(rotating(autoRotate, rotate, forced, focus, hover)) == "off") == rotating(autoRotate, rotate, forced, focus, hover)
705
+ // the rotation label announces the action the button will perform
706
+ val RotationLabelMatchesAction: bool =
707
+ (rotationLabel(rotating(autoRotate, rotate, forced, focus, hover)) == "Stop automatic slide show") == rotating(autoRotate, rotate, forced, focus, hover)
708
+ // the rotation control is the first stop in the tab sequence
709
+ val RotationControlFirst: bool =
710
+ or(not(autoRotate), tabFwd(0, autoRotate, tabbed, current) == 1)
711
+ // slide/tab bookkeeping
712
+ val CurrentInRange: bool = and(current >= 0, current < slides)
713
+ val SlideCountValid: bool = and(slides >= 1, slides <= 3)
714
+ val ExactlyOneSlideShown: bool = shownCount(slides, current) == 1
715
+ val TabbedHasOneTabStop: bool = or(not(tabbed), stopCount(slides, current) == 1)
716
+ val SlideRoleMatchesVariant: bool = (slideRole(tabbed) == "tabpanel") == tabbed
717
+ // focus only ever sits on something that is rendered
718
+ val FocusValid: bool = and(
719
+ focus >= 0,
720
+ focus <= 7,
721
+ or(focus != 1, autoRotate),
722
+ or(not(isTab(focus)), and(tabbed, focus - 4 < slides))
723
+ )
724
+ // callbacks only ever count up from zero
725
+ val RequestsNonNegative: bool = requests >= 0
726
+ // controlled: only the value prop sets the slide, so no event commits
727
+ val ControlledFollowsProp: bool = or(not(controlled), current == valueProp)
728
+ // a rejected request leaves the slide as it found it
729
+ val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), current == base)
730
+ // an accepted request committed exactly its intent
731
+ val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), current == requested)
732
+ }