@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,553 @@
1
+ // APG Tooltip — https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/ (work in
2
+ // progress upstream) plus WCAG 1.4.13 Content on Hover or Focus.
3
+ //
4
+ // Two triggers ("a", "b") each own one role=tooltip element. The tooltip shows
5
+ // on pointer hover or keyboard focus after a delay, hides after a grace period
6
+ // once neither the trigger nor the tooltip is engaged, and is:
7
+ // dismissable Escape hides it without moving pointer or focus, and it stays
8
+ // hidden until the trigger is left (no hover, no focus) and
9
+ // re-entered
10
+ // hoverable the pointer may travel onto the tooltip; hovering it keeps it
11
+ // open (the grace period is cancelled)
12
+ // persistent it never hides by itself while engaged: only the hide timer
13
+ // after disengagement, or Escape, removes it
14
+ //
15
+ // Timers are events, not clocks: `showTimer` and `hideTimer` are the delay
16
+ // and grace expiries, firing when the matching timer is armed. "Armed" is state:
17
+ // show timer `pendA`/`pendB`, set when a trigger is entered
18
+ // hide timer derived: a tooltip is visible and neither its trigger nor the
19
+ // tooltip is engaged (see `hidable`)
20
+ //
21
+ // Variant — global warm-up (`warmEnabled`): while another tooltip is visible
22
+ // or one was hidden within the warm window (`warm`, cleared by `warmExpire`), a
23
+ // newly entered trigger opens immediately and skips the show delay.
24
+ //
25
+ // Rendered attributes are state too: `describedA`/`describedB` mirror
26
+ // aria-describedby on each trigger. This spec removes the attribute while the
27
+ // tooltip is hidden; keeping it permanently is also valid per APG.
28
+ //
29
+ // Request vs commit (map ticket rt1p). The visible tooltip is the controlled
30
+ // value, packed as 0 = none, 1 = a, 2 = b. Every user-intent event (focusIn,
31
+ // focusOut, pointerEnter, pointerLeave, showTimer, hideTimer, escape) is a
32
+ // request carrying the visible value it wants (`requested`). It fires
33
+ // onValueChange (`requests` counts the calls) when the intent differs from the
34
+ // current value, and then:
35
+ // uncontrolled the request is accepted: the tooltip commits the intent
36
+ // controlled, holds the host ignores it ("rejected"): `shown` is untouched
37
+ // controlled, accepts the host writes the intent back (`accepting`)
38
+ // `valueProp` is the host's copy (meaningful while `controlled`); `setValue` /
39
+ // `hostToggle` are the host writing it, the only commit while controlled.
40
+ // `control` / `acceptRequests` configure the host (init is literal-only). The
41
+ // non-visible state (focus, pointer, pending timers) still tracks the event, so
42
+ // only the visible commit is gated.
43
+ //
44
+ // Invariants for the model: ControlledFollowsProp (in controlled mode no event
45
+ // commits), RejectedRequestUntouched, AcceptedRequestCommits.
46
+ //
47
+ // Events (user-level alphabet): focusIn(t) / focusOut, pointerEnter(t) /
48
+ // pointerLeave, pointerEnterTip, showTimer(t), hideTimer, escape, warmExpire,
49
+ // setWarmup(on).
50
+ //
51
+ // What interest invokers (`interestfor`, popover=hint) would provide natively:
52
+ // - show/hide on hover and focus with `interest-delay-start`/`-end`
53
+ // (the show delay and hide grace period above)
54
+ // - hoverable content: interest stays while the target is hovered
55
+ // - popover=hint: Escape light dismiss, and a hint replaces another open hint
56
+ // (at most one visible)
57
+ // Not provided natively: the global warm-up variant, the "stay dismissed until
58
+ // the trigger is left and re-entered" memory, and the aria-describedby wiring.
59
+ //
60
+ // ponytail: two triggers stand in for N; a tooltip with interactive content
61
+ // and tooltip-owned focus are out of scope (APG says tooltips never take focus).
62
+ module apg_tooltip {
63
+ var focus: str // "none" | "a" | "b"
64
+ var pointer: str // "none" | "a" | "b" | "tip" (over the visible tooltip)
65
+ var shownA: bool // tooltip a is visible
66
+ var shownB: bool // tooltip b is visible
67
+ var pendA: bool // show timer armed for a
68
+ var pendB: bool // show timer armed for b
69
+ var dismA: bool // a was dismissed with Escape and is still engaged
70
+ var dismB: bool // b was dismissed with Escape and is still engaged
71
+ var warm: bool // a tooltip was hidden recently (warm-up window open)
72
+ var warmEnabled: bool // variant switch: global warm-up on
73
+ var describedA: bool // aria-describedby on trigger a
74
+ var describedB: bool // aria-describedby on trigger b
75
+ var requests: int // number of requests that fired onValueChange
76
+ var controlled: bool // the host owns the visible tooltip (`value` prop present)
77
+ var accepting: bool // controlled host that answers every request with its intent
78
+ var valueProp: int // the controlled value prop (0 none, 1 a, 2 b)
79
+ var requested: int // visible value of the last request
80
+ var outcome: str // "none" | "accepted" | "rejected"
81
+ var base: int // visible value when the last request was received
82
+
83
+ val visibleT: str = if (shownA) "a" else if (shownB) "b" else "none"
84
+
85
+ def isShown(t: str): bool = if (t == "a") shownA else shownB
86
+ def isPend(t: str): bool = if (t == "a") pendA else pendB
87
+ def isDism(t: str): bool = if (t == "a") dismA else dismB
88
+ def otherT(t: str): str = if (t == "a") "b" else "a"
89
+ def entryOf(current: str, t: str): str = if (current == t) "none" else t
90
+ def quickOf(on: bool, w: bool, a: bool, b: bool): bool = and(on, or(w, or(a, b)))
91
+
92
+ // the trigger itself has pointer hover or focus
93
+ def trigEng(t: str, f: str, p: str): bool = or(f == t, p == t)
94
+ // the trigger or its visible tooltip is engaged
95
+ def engaged(t: str, f: str, p: str): bool =
96
+ or(trigEng(t, f, p), and(p == "tip", visibleT == t))
97
+ // Escape memory survives only while the trigger stays engaged
98
+ def dismNext(t: str, f: str, p: str): bool = and(isDism(t), trigEng(t, f, p))
99
+
100
+ // entering `e` with the warm-up variant on opens it with no delay
101
+ def immediateShow(t: str, f: str, p: str, e: str): bool =
102
+ and(
103
+ and(e == t, quickOf(warmEnabled, warm, shownA, shownB)),
104
+ and(not(dismNext(t, f, p)), not(isShown(t)))
105
+ )
106
+ def shownNext(t: str, f: str, p: str, e: str): bool =
107
+ or(immediateShow(t, f, p, e), and(isShown(t), not(immediateShow(otherT(t), f, p, e))))
108
+ def visNext(f: str, p: str, e: str): str =
109
+ if (shownNext("a", f, p, e)) "a" else if (shownNext("b", f, p, e)) "b" else "none"
110
+ def pendNext(t: str, f: str, p: str, e: str): bool =
111
+ and(
112
+ trigEng(t, f, p),
113
+ and(
114
+ not(shownNext(t, f, p, e)),
115
+ and(not(dismNext(t, f, p)), or(isPend(t), e == t))
116
+ )
117
+ )
118
+ // the pointer rests on a tooltip only while that same tooltip stays visible
119
+ def ptrNext(p: str, f: str, e: str): str =
120
+ if (and(p == "tip", visNext(f, p, e) != visibleT)) "none" else p
121
+
122
+ // the visible tooltip lost trigger and tooltip engagement: grace is running
123
+ def hidable(f: str, p: str): bool =
124
+ and(visibleT != "none", not(engaged(visibleT, f, p)))
125
+
126
+ // --- request / commit (map ticket rt1p) ----------------------------------
127
+ def visOf(a: bool, b: bool): int = if (a) 1 else if (b) 2 else 0
128
+ def visNow: int = visOf(shownA, shownB)
129
+ def moved(a: int, b: int): int = if (a == b) 0 else 1
130
+ def holds: bool = and(controlled, not(accepting))
131
+ def follows: bool = and(controlled, accepting)
132
+ def verdict: str = if (holds) "rejected" else "accepted"
133
+ def landed(h: bool, v: int, want: int): int = if (h) v else want
134
+ def propAfter(f: bool, p: int, want: int): int = if (f) want else p
135
+ // the visible value an event without a host would commit
136
+ def wantVis(f: str, p: str, e: str): int =
137
+ if (shownNext("a", f, p, e)) 1 else if (shownNext("b", f, p, e)) 2 else 0
138
+ // an Escape that would hide a visible tooltip is the request a host can veto
139
+ def rejectHide: bool = and(holds, visNow != 0)
140
+ // the host's own flip: none <-> tooltip a
141
+ def toggleVis: int = if (valueProp == 0) 1 else 0
142
+
143
+ action init = all {
144
+ focus' = "none",
145
+ pointer' = "none",
146
+ shownA' = false,
147
+ shownB' = false,
148
+ pendA' = false,
149
+ pendB' = false,
150
+ dismA' = false,
151
+ dismB' = false,
152
+ warm' = false,
153
+ warmEnabled' = false,
154
+ describedA' = false,
155
+ describedB' = false,
156
+ requests' = 0,
157
+ controlled' = false,
158
+ accepting' = false,
159
+ valueProp' = 0,
160
+ requested' = 0,
161
+ outcome' = "none",
162
+ base' = 0
163
+ }
164
+
165
+ action focusIn = {
166
+ nondet t = Set("a", "b").oneOf()
167
+ all {
168
+ focus' = t,
169
+ pointer' = ptrNext(pointer, t, entryOf(focus, t)),
170
+ shownA' = if (holds) shownA else (wantVis(t, pointer, entryOf(focus, t)) == 1),
171
+ shownB' = if (holds) shownB else (wantVis(t, pointer, entryOf(focus, t)) == 2),
172
+ pendA' = pendNext("a", t, pointer, entryOf(focus, t)),
173
+ pendB' = pendNext("b", t, pointer, entryOf(focus, t)),
174
+ dismA' = dismNext("a", t, pointer),
175
+ dismB' = dismNext("b", t, pointer),
176
+ warm' = warm,
177
+ warmEnabled' = warmEnabled,
178
+ describedA' = if (holds) describedA else (wantVis(t, pointer, entryOf(focus, t)) == 1),
179
+ describedB' = if (holds) describedB else (wantVis(t, pointer, entryOf(focus, t)) == 2),
180
+ requests' = requests + moved(visNow, wantVis(t, pointer, entryOf(focus, t))),
181
+ controlled' = controlled,
182
+ accepting' = accepting,
183
+ valueProp' = propAfter(follows, valueProp, wantVis(t, pointer, entryOf(focus, t))),
184
+ requested' = if (visNow == wantVis(t, pointer, entryOf(focus, t))) requested else wantVis(t, pointer, entryOf(focus, t)),
185
+ outcome' = if (visNow == wantVis(t, pointer, entryOf(focus, t))) outcome else verdict,
186
+ base' = if (visNow == wantVis(t, pointer, entryOf(focus, t))) base else visNow
187
+ }
188
+ }
189
+
190
+ action focusOut = all {
191
+ focus' = "none",
192
+ pointer' = ptrNext(pointer, "none", "none"),
193
+ shownA' = if (holds) shownA else (wantVis("none", pointer, "none") == 1),
194
+ shownB' = if (holds) shownB else (wantVis("none", pointer, "none") == 2),
195
+ pendA' = pendNext("a", "none", pointer, "none"),
196
+ pendB' = pendNext("b", "none", pointer, "none"),
197
+ dismA' = dismNext("a", "none", pointer),
198
+ dismB' = dismNext("b", "none", pointer),
199
+ warm' = warm,
200
+ warmEnabled' = warmEnabled,
201
+ describedA' = if (holds) describedA else (wantVis("none", pointer, "none") == 1),
202
+ describedB' = if (holds) describedB else (wantVis("none", pointer, "none") == 2),
203
+ requests' = requests + moved(visNow, wantVis("none", pointer, "none")),
204
+ controlled' = controlled,
205
+ accepting' = accepting,
206
+ valueProp' = propAfter(follows, valueProp, wantVis("none", pointer, "none")),
207
+ requested' = if (visNow == wantVis("none", pointer, "none")) requested else wantVis("none", pointer, "none"),
208
+ outcome' = if (visNow == wantVis("none", pointer, "none")) outcome else verdict,
209
+ base' = if (visNow == wantVis("none", pointer, "none")) base else visNow
210
+ }
211
+
212
+ action pointerEnter = {
213
+ nondet t = Set("a", "b").oneOf()
214
+ all {
215
+ focus' = focus,
216
+ pointer' = ptrNext(t, focus, entryOf(pointer, t)),
217
+ shownA' = if (holds) shownA else (wantVis(focus, t, entryOf(pointer, t)) == 1),
218
+ shownB' = if (holds) shownB else (wantVis(focus, t, entryOf(pointer, t)) == 2),
219
+ pendA' = pendNext("a", focus, t, entryOf(pointer, t)),
220
+ pendB' = pendNext("b", focus, t, entryOf(pointer, t)),
221
+ dismA' = dismNext("a", focus, t),
222
+ dismB' = dismNext("b", focus, t),
223
+ warm' = warm,
224
+ warmEnabled' = warmEnabled,
225
+ describedA' = if (holds) describedA else (wantVis(focus, t, entryOf(pointer, t)) == 1),
226
+ describedB' = if (holds) describedB else (wantVis(focus, t, entryOf(pointer, t)) == 2),
227
+ requests' = requests + moved(visNow, wantVis(focus, t, entryOf(pointer, t))),
228
+ controlled' = controlled,
229
+ accepting' = accepting,
230
+ valueProp' = propAfter(follows, valueProp, wantVis(focus, t, entryOf(pointer, t))),
231
+ requested' = if (visNow == wantVis(focus, t, entryOf(pointer, t))) requested else wantVis(focus, t, entryOf(pointer, t)),
232
+ outcome' = if (visNow == wantVis(focus, t, entryOf(pointer, t))) outcome else verdict,
233
+ base' = if (visNow == wantVis(focus, t, entryOf(pointer, t))) base else visNow
234
+ }
235
+ }
236
+
237
+ action pointerLeave = all {
238
+ focus' = focus,
239
+ pointer' = ptrNext("none", focus, "none"),
240
+ shownA' = if (holds) shownA else (wantVis(focus, "none", "none") == 1),
241
+ shownB' = if (holds) shownB else (wantVis(focus, "none", "none") == 2),
242
+ pendA' = pendNext("a", focus, "none", "none"),
243
+ pendB' = pendNext("b", focus, "none", "none"),
244
+ dismA' = dismNext("a", focus, "none"),
245
+ dismB' = dismNext("b", focus, "none"),
246
+ warm' = warm,
247
+ warmEnabled' = warmEnabled,
248
+ describedA' = if (holds) describedA else (wantVis(focus, "none", "none") == 1),
249
+ describedB' = if (holds) describedB else (wantVis(focus, "none", "none") == 2),
250
+ requests' = requests + moved(visNow, wantVis(focus, "none", "none")),
251
+ controlled' = controlled,
252
+ accepting' = accepting,
253
+ valueProp' = propAfter(follows, valueProp, wantVis(focus, "none", "none")),
254
+ requested' = if (visNow == wantVis(focus, "none", "none")) requested else wantVis(focus, "none", "none"),
255
+ outcome' = if (visNow == wantVis(focus, "none", "none")) outcome else verdict,
256
+ base' = if (visNow == wantVis(focus, "none", "none")) base else visNow
257
+ }
258
+
259
+ // the pointer moves onto the visible tooltip: hoverable content (no commit)
260
+ action pointerEnterTip = all {
261
+ focus' = focus,
262
+ pointer' = if (visibleT != "none") "tip" else pointer,
263
+ shownA' = shownA,
264
+ shownB' = shownB,
265
+ pendA' = pendNext("a", focus, if (visibleT != "none") "tip" else pointer, "none"),
266
+ pendB' = pendNext("b", focus, if (visibleT != "none") "tip" else pointer, "none"),
267
+ dismA' = dismNext("a", focus, if (visibleT != "none") "tip" else pointer),
268
+ dismB' = dismNext("b", focus, if (visibleT != "none") "tip" else pointer),
269
+ warm' = warm,
270
+ warmEnabled' = warmEnabled,
271
+ describedA' = describedA,
272
+ describedB' = describedB,
273
+ requests' = requests,
274
+ controlled' = controlled,
275
+ accepting' = accepting,
276
+ valueProp' = valueProp,
277
+ requested' = requested,
278
+ outcome' = outcome,
279
+ base' = base
280
+ }
281
+
282
+ // request: the show delay for t elapsed; it replaces any other visible tooltip
283
+ action showTimer = {
284
+ nondet t = Set("a", "b").oneOf()
285
+ all {
286
+ focus' = focus,
287
+ pointer' = if (and(isPend(t), pointer == "tip")) "none" else pointer,
288
+ shownA' = if (holds) shownA
289
+ else if (isPend(t)) t == "a"
290
+ else shownA,
291
+ shownB' = if (holds) shownB
292
+ else if (isPend(t)) t == "b"
293
+ else shownB,
294
+ pendA' = if (and(isPend(t), t == "a")) false else pendA,
295
+ pendB' = if (and(isPend(t), t == "b")) false else pendB,
296
+ dismA' = dismA,
297
+ dismB' = dismB,
298
+ warm' = warm,
299
+ warmEnabled' = warmEnabled,
300
+ describedA' = if (holds) describedA
301
+ else if (isPend(t)) t == "a"
302
+ else describedA,
303
+ describedB' = if (holds) describedB
304
+ else if (isPend(t)) t == "b"
305
+ else describedB,
306
+ requests' = requests + moved(visNow, if (isPend(t)) (if (t == "a") 1 else 2) else visNow),
307
+ controlled' = controlled,
308
+ accepting' = accepting,
309
+ valueProp' = propAfter(follows, valueProp, if (isPend(t)) (if (t == "a") 1 else 2) else visNow),
310
+ requested' = if (visNow == (if (isPend(t)) (if (t == "a") 1 else 2) else visNow)) requested
311
+ else if (isPend(t)) (if (t == "a") 1 else 2) else visNow,
312
+ outcome' = if (visNow == (if (isPend(t)) (if (t == "a") 1 else 2) else visNow)) outcome else verdict,
313
+ base' = if (visNow == (if (isPend(t)) (if (t == "a") 1 else 2) else visNow)) base else visNow
314
+ }
315
+ }
316
+
317
+ // request: the grace period elapsed with nothing engaged
318
+ action hideTimer = all {
319
+ focus' = focus,
320
+ pointer' = pointer,
321
+ shownA' = if (holds) shownA else if (hidable(focus, pointer)) false else shownA,
322
+ shownB' = if (holds) shownB else if (hidable(focus, pointer)) false else shownB,
323
+ pendA' = pendA,
324
+ pendB' = pendB,
325
+ dismA' = dismA,
326
+ dismB' = dismB,
327
+ warm' = if (and(holds, hidable(focus, pointer))) warm
328
+ else if (hidable(focus, pointer)) true else warm,
329
+ warmEnabled' = warmEnabled,
330
+ describedA' = if (holds) describedA
331
+ else if (hidable(focus, pointer)) false else describedA,
332
+ describedB' = if (holds) describedB
333
+ else if (hidable(focus, pointer)) false else describedB,
334
+ requests' = requests + moved(visNow, if (hidable(focus, pointer)) 0 else visNow),
335
+ controlled' = controlled,
336
+ accepting' = accepting,
337
+ valueProp' = propAfter(follows, valueProp, if (hidable(focus, pointer)) 0 else visNow),
338
+ requested' = if (visNow == (if (hidable(focus, pointer)) 0 else visNow)) requested
339
+ else if (hidable(focus, pointer)) 0 else visNow,
340
+ outcome' = if (visNow == (if (hidable(focus, pointer)) 0 else visNow)) outcome else verdict,
341
+ base' = if (visNow == (if (hidable(focus, pointer)) 0 else visNow)) base else visNow
342
+ }
343
+
344
+ // request: WCAG dismissable — hides the tooltip (and cancels armed show
345
+ // timers) without moving focus or pointer; the engaged trigger stays dismissed
346
+ action escape = all {
347
+ focus' = focus,
348
+ pointer' = if (pointer == "tip") "none" else pointer,
349
+ shownA' = if (holds) shownA else false,
350
+ shownB' = if (holds) shownB else false,
351
+ pendA' = false,
352
+ pendB' = false,
353
+ dismA' = if (rejectHide) dismA
354
+ else or(dismA, and(trigEng("a", focus, pointer), or(shownA, pendA))),
355
+ dismB' = if (rejectHide) dismB
356
+ else or(dismB, and(trigEng("b", focus, pointer), or(shownB, pendB))),
357
+ warm' = if (rejectHide) warm else or(warm, or(shownA, shownB)),
358
+ warmEnabled' = warmEnabled,
359
+ describedA' = if (holds) describedA else false,
360
+ describedB' = if (holds) describedB else false,
361
+ requests' = requests + moved(visNow, 0),
362
+ controlled' = controlled,
363
+ accepting' = accepting,
364
+ valueProp' = propAfter(follows, valueProp, 0),
365
+ requested' = if (visNow == 0) requested else 0,
366
+ outcome' = if (visNow == 0) outcome else verdict,
367
+ base' = if (visNow == 0) base else visNow
368
+ }
369
+
370
+ // commit: the host writes the controlled visible tooltip (0 none, 1 a, 2 b)
371
+ action setValue = {
372
+ nondet v = Set(0, 1, 2).oneOf()
373
+ all {
374
+ focus' = focus,
375
+ pointer' = if (and(controlled, and(v == 0, pointer == "tip"))) "none" else pointer,
376
+ shownA' = if (controlled) v == 1 else shownA,
377
+ shownB' = if (controlled) v == 2 else shownB,
378
+ pendA' = if (and(controlled, v == 1)) false else pendA,
379
+ pendB' = if (and(controlled, v == 2)) false else pendB,
380
+ dismA' = if (and(controlled, v == 1)) false else dismA,
381
+ dismB' = if (and(controlled, v == 2)) false else dismB,
382
+ warm' = warm,
383
+ warmEnabled' = warmEnabled,
384
+ describedA' = if (controlled) v == 1 else describedA,
385
+ describedB' = if (controlled) v == 2 else describedB,
386
+ requests' = requests,
387
+ controlled' = controlled,
388
+ accepting' = accepting,
389
+ valueProp' = if (controlled) v else valueProp,
390
+ requested' = requested,
391
+ outcome' = if (controlled) "none" else outcome,
392
+ base' = base
393
+ }
394
+ }
395
+
396
+ // commit: the host flips its own copy between none and tooltip a
397
+ action hostToggle = all {
398
+ focus' = focus,
399
+ pointer' = if (and(controlled, and(toggleVis == 0, pointer == "tip"))) "none" else pointer,
400
+ shownA' = if (controlled) toggleVis == 1 else shownA,
401
+ shownB' = if (controlled) false else shownB,
402
+ pendA' = if (and(controlled, toggleVis == 1)) false else pendA,
403
+ pendB' = if (controlled) false else pendB,
404
+ dismA' = if (and(controlled, toggleVis == 1)) false else dismA,
405
+ dismB' = if (controlled) false else dismB,
406
+ warm' = warm,
407
+ warmEnabled' = warmEnabled,
408
+ describedA' = if (controlled) toggleVis == 1 else describedA,
409
+ describedB' = if (controlled) false else describedB,
410
+ requests' = requests,
411
+ controlled' = controlled,
412
+ accepting' = accepting,
413
+ valueProp' = if (controlled) toggleVis else valueProp,
414
+ requested' = requested,
415
+ outcome' = if (controlled) "none" else outcome,
416
+ base' = base
417
+ }
418
+
419
+ // configuration: the host starts owning the visible tooltip, adopting it
420
+ action control = all {
421
+ focus' = focus,
422
+ pointer' = pointer,
423
+ shownA' = shownA,
424
+ shownB' = shownB,
425
+ pendA' = pendA,
426
+ pendB' = pendB,
427
+ dismA' = dismA,
428
+ dismB' = dismB,
429
+ warm' = warm,
430
+ warmEnabled' = warmEnabled,
431
+ describedA' = describedA,
432
+ describedB' = describedB,
433
+ requests' = requests,
434
+ controlled' = true,
435
+ accepting' = accepting,
436
+ valueProp' = visNow,
437
+ requested' = requested,
438
+ outcome' = outcome,
439
+ base' = base
440
+ }
441
+
442
+ // configuration: the controlled host answers every request with its intent
443
+ action acceptRequests = all {
444
+ focus' = focus,
445
+ pointer' = pointer,
446
+ shownA' = shownA,
447
+ shownB' = shownB,
448
+ pendA' = pendA,
449
+ pendB' = pendB,
450
+ dismA' = dismA,
451
+ dismB' = dismB,
452
+ warm' = warm,
453
+ warmEnabled' = warmEnabled,
454
+ describedA' = describedA,
455
+ describedB' = describedB,
456
+ requests' = requests,
457
+ controlled' = controlled,
458
+ accepting' = true,
459
+ valueProp' = valueProp,
460
+ requested' = requested,
461
+ outcome' = outcome,
462
+ base' = base
463
+ }
464
+
465
+ action warmExpire = all {
466
+ focus' = focus,
467
+ pointer' = pointer,
468
+ shownA' = shownA,
469
+ shownB' = shownB,
470
+ pendA' = pendA,
471
+ pendB' = pendB,
472
+ dismA' = dismA,
473
+ dismB' = dismB,
474
+ warm' = false,
475
+ warmEnabled' = warmEnabled,
476
+ describedA' = describedA,
477
+ describedB' = describedB,
478
+ requests' = requests,
479
+ controlled' = controlled,
480
+ accepting' = accepting,
481
+ valueProp' = valueProp,
482
+ requested' = requested,
483
+ outcome' = outcome,
484
+ base' = base
485
+ }
486
+
487
+ action setWarmup = {
488
+ nondet on = Set(true, false).oneOf()
489
+ all {
490
+ focus' = focus,
491
+ pointer' = pointer,
492
+ shownA' = shownA,
493
+ shownB' = shownB,
494
+ pendA' = pendA,
495
+ pendB' = pendB,
496
+ dismA' = dismA,
497
+ dismB' = dismB,
498
+ warm' = warm,
499
+ warmEnabled' = on,
500
+ describedA' = describedA,
501
+ describedB' = describedB,
502
+ requests' = requests,
503
+ controlled' = controlled,
504
+ accepting' = accepting,
505
+ valueProp' = valueProp,
506
+ requested' = requested,
507
+ outcome' = outcome,
508
+ base' = base
509
+ }
510
+ }
511
+
512
+ action step = any {
513
+ init, focusIn, focusOut, pointerEnter, pointerLeave, pointerEnterTip,
514
+ showTimer, hideTimer, escape, warmExpire, setWarmup, setValue, hostToggle,
515
+ control, acceptRequests
516
+ }
517
+
518
+ // at most one tooltip is visible at a time
519
+ val AtMostOneVisible: bool = not(and(shownA, shownB))
520
+ // aria-describedby is present on a trigger exactly while its tooltip exists
521
+ val DescribedByIffShown: bool = and(describedA == shownA, describedB == shownB)
522
+ // an Escape-dismissed tooltip stays hidden (and is never queued to show)
523
+ val DismissedStaysHidden: bool =
524
+ and(
525
+ and(not(and(dismA, shownA)), not(and(dismB, shownB))),
526
+ and(not(and(dismA, pendA)), not(and(dismB, pendB)))
527
+ )
528
+ // the dismissal memory is dropped once the trigger is left
529
+ val DismissedImpliesEngaged: bool =
530
+ and(or(not(dismA), trigEng("a", focus, pointer)), or(not(dismB), trigEng("b", focus, pointer)))
531
+ // a show timer only runs for an engaged trigger whose tooltip is hidden
532
+ val PendingImpliesEngagedHidden: bool =
533
+ and(
534
+ or(not(pendA), and(trigEng("a", focus, pointer), not(shownA))),
535
+ or(not(pendB), and(trigEng("b", focus, pointer), not(shownB)))
536
+ )
537
+ // hoverable: the pointer only rests on a tooltip that is visible
538
+ val TipHoverNeedsTooltip: bool = or(pointer != "tip", visibleT != "none")
539
+ // positions are one of the modelled values
540
+ val PositionsKnown: bool =
541
+ and(
542
+ or(focus == "none", or(focus == "a", focus == "b")),
543
+ or(pointer == "none", or(pointer == "a", or(pointer == "b", pointer == "tip")))
544
+ )
545
+ // callbacks only ever count up from zero
546
+ val RequestsNonNegative: bool = requests >= 0
547
+ // controlled: only the value prop sets the visible tooltip, so no event commits
548
+ val ControlledFollowsProp: bool = or(not(controlled), visNow == valueProp)
549
+ // a rejected request leaves the visible tooltip as it found it
550
+ val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), visNow == base)
551
+ // an accepted request committed exactly its intent
552
+ val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), visNow == requested)
553
+ }