@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,531 @@
1
+ // WAI-ARIA APG — Menu and Menubar: roving focus, nested submenus, typeahead.
2
+ //
3
+ // Pattern: https://www.w3.org/WAI/ARIA/apg/patterns/menubar/
4
+ // - "Keyboard Interaction": Tab / Shift+Tab, Enter, Space, Down/Up/Right/Left
5
+ // Arrow, Home/End, printable-character typeahead, Escape, and the rule that
6
+ // Right/Left Arrow on a menubar item with a submenu open it "without moving
7
+ // focus into the submenu" (the Recommended variant, modelled here).
8
+ // - "WAI-ARIA Roles, States, and Properties": aria-haspopup / aria-expanded on
9
+ // parent menuitems, roving tabindex (one menubar item has tabindex=0).
10
+ // Examples: https://www.w3.org/WAI/ARIA/apg/patterns/menubar/examples/menubar-editor/
11
+ // https://www.w3.org/WAI/ARIA/apg/patterns/menubar/examples/menubar-navigation/
12
+ //
13
+ // Request vs commit (map ticket rt1p). The controlled value is the open *top*
14
+ // menu (`openTop`, 0 = none): a menubar whose open menu a host owns. Every event
15
+ // that would open or close/replace that menu — tab_out, arrow_right/left/down/up,
16
+ // key_home/end, activate, escape, typeahead when it moves along the bar — is a
17
+ // *request* carrying the intended `openTop` (`requested`); it fires the change
18
+ // callback (`requests` counts the calls) when the intent differs, and then:
19
+ // uncontrolled the request is accepted: the whole transition commits
20
+ // controlled, holds the host ignores it ("rejected"): nothing moves
21
+ // controlled, accepts the host answers with the same value (`accepting`): the
22
+ // transition commits through the controlled value
23
+ // `valueProp` is the only thing that changes `openTop` while `controlled`;
24
+ // `setValue` / `hostToggle` are the host writing it. `control` / `acceptRequests`
25
+ // are configuration events (init is literal-only). A rejected request reverts the
26
+ // *whole* transition (focus and roving tab stop included), so the ARIA
27
+ // invariants still hold. `openSub` (which item's submenu is open) stays inside
28
+ // the open menu and is not part of the controlled value.
29
+ //
30
+ // Fixed fixture (the menu tree is data of the model, indices are 1-based, 0 = none):
31
+ //
32
+ // menubar: 1 "File" ▸ 2 "Edit" ▸ 3 "About" (plain link, no menu)
33
+ // menu(1): 1 plain 2 plain ▸ (submenu, 2 items) 3 plain
34
+ // menu(2): 1 plain 2 plain
35
+ // submenu(1,2): 1 plain 2 plain (depth bound: menubar + 2 levels)
36
+ //
37
+ // Focus is `(focusLevel, focusIdx)`: level 0 = outside the widget, 1 = menubar,
38
+ // 2 = a top-level menu, 3 = the submenu of the open item. The open path is
39
+ // `(openTop, openSub)`: at most one open menu per level is structural (each is one
40
+ // slot), `openSub` is the index inside menu(openTop) whose submenu is open.
41
+ //
42
+ // ARIA contract (derived defs, consumed by the invariants and by an implementation):
43
+ // tabIndexBar(i, barStop) roving tabindex: 0 on barStop, -1 elsewhere
44
+ // ariaHasPopupBar / ariaHasPopupItem parent menuitems (aria-haspopup)
45
+ // ariaExpandedBar(i, openTop) aria-expanded on a menubar item
46
+ // ariaExpandedItem(t, i, openTop, openSub) aria-expanded on a menu item
47
+ //
48
+ // Deliberate omissions (generator subset / scope): Shift+Tab is the same event as
49
+ // Tab (`tab_out`); no pointer/hover events; no disabled items and no
50
+ // menuitemcheckbox/menuitemradio (see menu-checkable.qnt); no aria-activedescendant
51
+ // variant (focus is modelled as `element.focus()`); no vertical orientation; no
52
+ // separators; Home/End are always available alongside wrapping; Enter vs Space is
53
+ // one `activate` event with a `key` pool because they only differ on checkable
54
+ // items; typeahead matches a one-letter label code (1..3) rather than a string,
55
+ // starting after the focused item and wrapping; activating a leaf closes every menu
56
+ // and returns focus to the menubar item that owns the menu (APG note 3 allows the
57
+ // invoking context instead); activations are recorded as an opaque `lastActivated`
58
+ // code (top*100 + item*10 + subitem).
59
+ module apg_menu_menubar {
60
+ import aria.* from "../../shared/aria"
61
+
62
+ var focusLevel: int // 0 outside | 1 menubar | 2 top-level menu | 3 submenu
63
+ var focusIdx: int // focused item inside its container (0 when outside)
64
+ var barStop: int // roving-tabindex holder of the menubar (1..3)
65
+ var openTop: int // menubar item whose menu is open (0 none)
66
+ var openSub: int // item of menu(openTop) whose submenu is open (0 none)
67
+ var lastActivated: int // code of the last activated leaf (0 none)
68
+ var controlled: bool // the host owns `openTop` (`value` prop present)
69
+ var accepting: bool // controlled host that answers every request with its intent
70
+ var valueProp: int // the controlled open top menu (meaningful while controlled)
71
+ var requested: int // intent of the last request
72
+ var outcome: str // "none" | "accepted" | "rejected"
73
+ var base: int // openTop when the last request was received
74
+ var requests: int // number of requests that fired the change callback
75
+
76
+ // ---- fixture ----------------------------------------------------------
77
+ def menuSize(t: int): int = if (t == 1) 3 else if (t == 2) 2 else 0
78
+ def subSize(t: int, i: int): int = if (and(t == 1, i == 2)) 2 else 0
79
+ def hasMenu(t: int): bool = menuSize(t) > 0
80
+ def hasSub(t: int, i: int): bool = subSize(t, i) > 0
81
+ def containerSize(l: int, t: int, s: int): int =
82
+ if (l == 1) 3 else if (l == 2) menuSize(t) else if (l == 3) subSize(t, s) else 0
83
+ // one-letter label codes (typeahead): bar 1,2,3 | menu(1) 1,2,1 | menu(2) 2,3 | sub 3,1
84
+ def letterOf(l: int, t: int, i: int): int =
85
+ if (l == 1) i
86
+ else if (l == 2) (if (t == 1) (if (i == 2) 2 else 1) else (if (i == 1) 2 else 3))
87
+ else (if (i == 1) 3 else 1)
88
+
89
+ // ---- movement helpers ---------------------------------------------------
90
+ def barNext(i: int): int = ariaWrapNext1(i, 3)
91
+ def barPrev(i: int): int = ariaWrapPrev1(i, 3)
92
+ // after moving along the menubar, the new item's menu is opened (focus stays on
93
+ // the bar item) only when a menu was already open and the new item has one
94
+ def follow(wasOpen: int, nb: int): int = if (and(wasOpen > 0, hasMenu(nb))) nb else 0
95
+ def cn(l: int, t: int, s: int, i: int): int = ariaWrapNext1(i, containerSize(l, t, s))
96
+ def typeTarget(l: int, t: int, s: int, i: int, ch: int): int =
97
+ if (letterOf(l, t, cn(l, t, s, i)) == ch) cn(l, t, s, i)
98
+ else if (letterOf(l, t, cn(l, t, s, cn(l, t, s, i))) == ch) cn(l, t, s, cn(l, t, s, i))
99
+ else if (letterOf(l, t, cn(l, t, s, cn(l, t, s, cn(l, t, s, i)))) == ch) cn(l, t, s, cn(l, t, s, cn(l, t, s, i)))
100
+ else i
101
+
102
+ // ---- activation decisions -----------------------------------------------
103
+ def entersSub(l: int, t: int, i: int): bool = and(l == 2, hasSub(t, i))
104
+ def opensMenu(l: int, b: int): bool = and(l == 1, hasMenu(b))
105
+ def fires(l: int, b: int, t: int, i: int): bool =
106
+ and(l > 0, not(opensMenu(l, b)), not(entersSub(l, t, i)))
107
+ def activationCode(l: int, b: int, t: int, s: int, i: int): int =
108
+ if (l == 1) b * 100 else if (l == 2) t * 100 + i * 10 else t * 100 + s * 10 + i
109
+
110
+ // ---- ARIA contract (derived) -------------------------------------------
111
+ def b2i(b: bool): int = if (b) 1 else 0
112
+ def tabIndexBar(i: int, b: int): int = if (i == b) 0 else -1
113
+ def ariaHasPopupBar(i: int): bool = hasMenu(i)
114
+ def ariaHasPopupItem(t: int, i: int): bool = hasSub(t, i)
115
+ def ariaExpandedBar(i: int, t: int): bool = and(hasMenu(i), t == i)
116
+ def ariaExpandedItem(t: int, i: int, ot: int, s: int): bool =
117
+ and(t > 0, t == ot, hasSub(t, i), s == i)
118
+
119
+ // ---- request / commit (map ticket rt1p) ---------------------------------
120
+ def holds: bool = and(controlled, not(accepting))
121
+ def follows: bool = and(controlled, accepting)
122
+ def verdict: str = if (holds) "rejected" else "accepted"
123
+ def landed(h: bool, cur: int, want: int): int = if (h) cur else want
124
+ def propAfter(f: bool, p: int, want: int): int = if (f) want else p
125
+ // the intended open top menu of each request
126
+ def rightWant: int =
127
+ if (entersSub(focusLevel, openTop, focusIdx)) openTop
128
+ else follow(openTop, barNext(barStop))
129
+ def leftWant: int =
130
+ if (or(focusLevel == 0, focusLevel == 3)) openTop
131
+ else follow(openTop, barPrev(barStop))
132
+ def downWant: int = if (opensMenu(focusLevel, barStop)) barStop else openTop
133
+ def homeWant: int = if (focusLevel == 1) follow(openTop, 1) else openTop
134
+ def endWant: int = if (focusLevel == 1) follow(openTop, 3) else openTop
135
+ def activateWant: int =
136
+ if (opensMenu(focusLevel, barStop)) barStop
137
+ else if (and(focusLevel >= 2, not(entersSub(focusLevel, openTop, focusIdx)))) 0
138
+ else openTop
139
+ def escapeWant: int = if (focusLevel == 3) openTop else 0
140
+ def typeWant(ch: int): int =
141
+ if (focusLevel == 1) follow(openTop, typeTarget(focusLevel, openTop, openSub, focusIdx, ch))
142
+ else openTop
143
+ def moves(want: int): bool = want != openTop
144
+ def rejects(want: int): bool = and(holds, moves(want))
145
+
146
+ action init = all {
147
+ focusLevel' = 0,
148
+ focusIdx' = 0,
149
+ barStop' = 1,
150
+ openTop' = 0,
151
+ openSub' = 0,
152
+ lastActivated' = 0,
153
+ controlled' = false,
154
+ accepting' = false,
155
+ valueProp' = 0,
156
+ requested' = 0,
157
+ outcome' = "none",
158
+ base' = 0,
159
+ requests' = 0
160
+ }
161
+
162
+ // Tab into the menubar: focus lands on the remembered (roving) item
163
+ action focus_in = all {
164
+ focusLevel' = if (focusLevel == 0) 1 else focusLevel,
165
+ focusIdx' = if (focusLevel == 0) barStop else focusIdx,
166
+ barStop' = barStop,
167
+ openTop' = openTop,
168
+ openSub' = openSub,
169
+ lastActivated' = lastActivated,
170
+ controlled' = controlled,
171
+ accepting' = accepting,
172
+ valueProp' = valueProp,
173
+ requested' = requested,
174
+ outcome' = outcome,
175
+ base' = base,
176
+ requests' = requests
177
+ }
178
+
179
+ // request: Tab / Shift+Tab from any item closes every menu and leaves
180
+ action tab_out = all {
181
+ focusLevel' = if (rejects(0)) focusLevel else 0,
182
+ focusIdx' = if (rejects(0)) focusIdx else 0,
183
+ barStop' = barStop,
184
+ openTop' = landed(holds, openTop, 0),
185
+ openSub' = if (rejects(0)) openSub else 0,
186
+ lastActivated' = lastActivated,
187
+ controlled' = controlled,
188
+ accepting' = accepting,
189
+ valueProp' = propAfter(follows, valueProp, 0),
190
+ requested' = if (moves(0)) 0 else requested,
191
+ outcome' = if (moves(0)) verdict else outcome,
192
+ base' = if (moves(0)) openTop else base,
193
+ requests' = if (moves(0)) requests + 1 else requests
194
+ }
195
+
196
+ // request: menubar -> next item (its menu follows); menu -> submenu; else no move
197
+ action arrow_right = all {
198
+ focusLevel' = if (rejects(rightWant)) focusLevel
199
+ else if (focusLevel == 0) 0
200
+ else if (entersSub(focusLevel, openTop, focusIdx)) 3
201
+ else 1,
202
+ focusIdx' = if (rejects(rightWant)) focusIdx
203
+ else if (focusLevel == 0) focusIdx
204
+ else if (entersSub(focusLevel, openTop, focusIdx)) 1
205
+ else barNext(barStop),
206
+ barStop' = if (rejects(rightWant)) barStop
207
+ else if (or(focusLevel == 0, entersSub(focusLevel, openTop, focusIdx))) barStop
208
+ else barNext(barStop),
209
+ openTop' = landed(holds, openTop, rightWant),
210
+ openSub' = if (rejects(rightWant)) openSub
211
+ else if (entersSub(focusLevel, openTop, focusIdx)) focusIdx
212
+ else 0,
213
+ lastActivated' = lastActivated,
214
+ controlled' = controlled,
215
+ accepting' = accepting,
216
+ valueProp' = propAfter(follows, valueProp, rightWant),
217
+ requested' = if (moves(rightWant)) rightWant else requested,
218
+ outcome' = if (moves(rightWant)) verdict else outcome,
219
+ base' = if (moves(rightWant)) openTop else base,
220
+ requests' = if (moves(rightWant)) requests + 1 else requests
221
+ }
222
+
223
+ // request: submenu -> parent item; top-level menu -> previous menubar item
224
+ action arrow_left = all {
225
+ focusLevel' = if (rejects(leftWant)) focusLevel
226
+ else if (focusLevel == 0) 0
227
+ else if (focusLevel == 3) 2
228
+ else 1,
229
+ focusIdx' = if (rejects(leftWant)) focusIdx
230
+ else if (focusLevel == 0) focusIdx
231
+ else if (focusLevel == 3) openSub
232
+ else barPrev(barStop),
233
+ barStop' = if (rejects(leftWant)) barStop
234
+ else if (or(focusLevel == 0, focusLevel == 3)) barStop
235
+ else barPrev(barStop),
236
+ openTop' = landed(holds, openTop, leftWant),
237
+ openSub' = if (rejects(leftWant)) openSub else 0,
238
+ lastActivated' = lastActivated,
239
+ controlled' = controlled,
240
+ accepting' = accepting,
241
+ valueProp' = propAfter(follows, valueProp, leftWant),
242
+ requested' = if (moves(leftWant)) leftWant else requested,
243
+ outcome' = if (moves(leftWant)) verdict else outcome,
244
+ base' = if (moves(leftWant)) openTop else base,
245
+ requests' = if (moves(leftWant)) requests + 1 else requests
246
+ }
247
+
248
+ // request: menubar -> open the menu on its first item; menu -> next item, wrapping
249
+ action arrow_down = all {
250
+ focusLevel' = if (rejects(downWant)) focusLevel
251
+ else if (opensMenu(focusLevel, barStop)) 2
252
+ else focusLevel,
253
+ focusIdx' = if (rejects(downWant)) focusIdx
254
+ else if (opensMenu(focusLevel, barStop)) 1
255
+ else if (focusLevel >= 2) ariaWrapNext1(focusIdx, containerSize(focusLevel, openTop, openSub))
256
+ else focusIdx,
257
+ barStop' = barStop,
258
+ openTop' = landed(holds, openTop, downWant),
259
+ openSub' = if (rejects(downWant)) openSub else openSub,
260
+ lastActivated' = lastActivated,
261
+ controlled' = controlled,
262
+ accepting' = accepting,
263
+ valueProp' = propAfter(follows, valueProp, downWant),
264
+ requested' = if (moves(downWant)) downWant else requested,
265
+ outcome' = if (moves(downWant)) verdict else outcome,
266
+ base' = if (moves(downWant)) openTop else base,
267
+ requests' = if (moves(downWant)) requests + 1 else requests
268
+ }
269
+
270
+ // request: menubar (optional APG behaviour) -> open the menu on its last item
271
+ action arrow_up = all {
272
+ focusLevel' = if (rejects(downWant)) focusLevel
273
+ else if (opensMenu(focusLevel, barStop)) 2
274
+ else focusLevel,
275
+ focusIdx' = if (rejects(downWant)) focusIdx
276
+ else if (opensMenu(focusLevel, barStop)) menuSize(barStop)
277
+ else if (focusLevel >= 2) ariaWrapPrev1(focusIdx, containerSize(focusLevel, openTop, openSub))
278
+ else focusIdx,
279
+ barStop' = barStop,
280
+ openTop' = landed(holds, openTop, downWant),
281
+ openSub' = if (rejects(downWant)) openSub else openSub,
282
+ lastActivated' = lastActivated,
283
+ controlled' = controlled,
284
+ accepting' = accepting,
285
+ valueProp' = propAfter(follows, valueProp, downWant),
286
+ requested' = if (moves(downWant)) downWant else requested,
287
+ outcome' = if (moves(downWant)) verdict else outcome,
288
+ base' = if (moves(downWant)) openTop else base,
289
+ requests' = if (moves(downWant)) requests + 1 else requests
290
+ }
291
+
292
+ // request: Home on the menubar; a menu re-opens when one was open
293
+ action key_home = all {
294
+ focusLevel' = focusLevel,
295
+ focusIdx' = if (rejects(homeWant)) focusIdx else if (focusLevel == 0) focusIdx else 1,
296
+ barStop' = if (rejects(homeWant)) barStop else if (focusLevel == 1) 1 else barStop,
297
+ openTop' = landed(holds, openTop, homeWant),
298
+ openSub' = openSub,
299
+ lastActivated' = lastActivated,
300
+ controlled' = controlled,
301
+ accepting' = accepting,
302
+ valueProp' = propAfter(follows, valueProp, homeWant),
303
+ requested' = if (moves(homeWant)) homeWant else requested,
304
+ outcome' = if (moves(homeWant)) verdict else outcome,
305
+ base' = if (moves(homeWant)) openTop else base,
306
+ requests' = if (moves(homeWant)) requests + 1 else requests
307
+ }
308
+
309
+ // request: End on the menubar; a menu re-opens when one was open
310
+ action key_end = all {
311
+ focusLevel' = focusLevel,
312
+ focusIdx' = if (rejects(endWant)) focusIdx else if (focusLevel == 0) focusIdx else containerSize(focusLevel, openTop, openSub),
313
+ barStop' = if (rejects(endWant)) barStop else if (focusLevel == 1) 3 else barStop,
314
+ openTop' = landed(holds, openTop, endWant),
315
+ openSub' = openSub,
316
+ lastActivated' = lastActivated,
317
+ controlled' = controlled,
318
+ accepting' = accepting,
319
+ valueProp' = propAfter(follows, valueProp, endWant),
320
+ requested' = if (moves(endWant)) endWant else requested,
321
+ outcome' = if (moves(endWant)) verdict else outcome,
322
+ base' = if (moves(endWant)) openTop else base,
323
+ requests' = if (moves(endWant)) requests + 1 else requests
324
+ }
325
+
326
+ // request: Enter / Space: a parent item opens its submenu on the first item; any
327
+ // other item is activated, every menu closes and focus returns to the menubar item
328
+ action activate = {
329
+ nondet key = Set("Enter", "Space").oneOf()
330
+ all {
331
+ focusLevel' = if (rejects(activateWant)) focusLevel
332
+ else if (opensMenu(focusLevel, barStop)) 2
333
+ else if (entersSub(focusLevel, openTop, focusIdx)) 3
334
+ else if (focusLevel == 0) 0
335
+ else 1,
336
+ focusIdx' = if (rejects(activateWant)) focusIdx
337
+ else if (or(opensMenu(focusLevel, barStop), entersSub(focusLevel, openTop, focusIdx))) 1
338
+ else if (focusLevel >= 2) openTop
339
+ else focusIdx,
340
+ barStop' = barStop,
341
+ openTop' = landed(holds, openTop, activateWant),
342
+ openSub' = if (rejects(activateWant)) openSub
343
+ else if (entersSub(focusLevel, openTop, focusIdx)) focusIdx
344
+ else 0,
345
+ lastActivated' = if (rejects(activateWant)) lastActivated
346
+ else if (fires(focusLevel, barStop, openTop, focusIdx))
347
+ activationCode(focusLevel, barStop, openTop, openSub, focusIdx)
348
+ else lastActivated,
349
+ controlled' = controlled,
350
+ accepting' = accepting,
351
+ valueProp' = propAfter(follows, valueProp, activateWant),
352
+ requested' = if (moves(activateWant)) activateWant else requested,
353
+ outcome' = if (moves(activateWant)) verdict else outcome,
354
+ base' = if (moves(activateWant)) openTop else base,
355
+ requests' = if (moves(activateWant)) requests + 1 else requests
356
+ }
357
+ }
358
+
359
+ // request: Escape: close the menu holding focus and return focus to its invoker
360
+ // (parent item for a submenu, menubar item for a top-level menu); on the
361
+ // menubar it dismisses a menu that is open without focus
362
+ action escape = all {
363
+ focusLevel' = if (rejects(escapeWant)) focusLevel
364
+ else if (focusLevel == 3) 2
365
+ else if (focusLevel == 2) 1
366
+ else focusLevel,
367
+ focusIdx' = if (rejects(escapeWant)) focusIdx
368
+ else if (focusLevel == 3) openSub
369
+ else if (focusLevel == 2) openTop
370
+ else focusIdx,
371
+ barStop' = barStop,
372
+ openTop' = landed(holds, openTop, escapeWant),
373
+ openSub' = if (rejects(escapeWant)) openSub else 0,
374
+ lastActivated' = lastActivated,
375
+ controlled' = controlled,
376
+ accepting' = accepting,
377
+ valueProp' = propAfter(follows, valueProp, escapeWant),
378
+ requested' = if (moves(escapeWant)) escapeWant else requested,
379
+ outcome' = if (moves(escapeWant)) verdict else outcome,
380
+ base' = if (moves(escapeWant)) openTop else base,
381
+ requests' = if (moves(escapeWant)) requests + 1 else requests
382
+ }
383
+
384
+ // request (on the menubar): printable character moves the roving focus (and a
385
+ // followed menu); inside a menu it only moves focus within the container
386
+ action typeahead = {
387
+ nondet ch = Set(1, 2, 3).oneOf()
388
+ all {
389
+ focusLevel' = focusLevel,
390
+ focusIdx' = if (focusLevel == 0)
391
+ focusIdx
392
+ else if (rejects(typeWant(ch))) focusIdx
393
+ else typeTarget(focusLevel, openTop, openSub, focusIdx, ch),
394
+ barStop' = if (rejects(typeWant(ch))) barStop
395
+ else if (focusLevel == 1) typeTarget(focusLevel, openTop, openSub, focusIdx, ch)
396
+ else barStop,
397
+ openTop' = landed(holds, openTop, typeWant(ch)),
398
+ openSub' = openSub,
399
+ lastActivated' = lastActivated,
400
+ controlled' = controlled,
401
+ accepting' = accepting,
402
+ valueProp' = propAfter(follows, valueProp, typeWant(ch)),
403
+ requested' = if (moves(typeWant(ch))) typeWant(ch) else requested,
404
+ outcome' = if (moves(typeWant(ch))) verdict else outcome,
405
+ base' = if (moves(typeWant(ch))) openTop else base,
406
+ requests' = if (moves(typeWant(ch))) requests + 1 else requests
407
+ }
408
+ }
409
+
410
+ // commit: the host writes the controlled open top menu (a no-op while uncontrolled)
411
+ action setValue = {
412
+ nondet t = Set(0, 1, 2).oneOf()
413
+ all {
414
+ focusLevel' = if (controlled) 1 else focusLevel,
415
+ focusIdx' = if (controlled) (if (t == 0) barStop else t) else focusIdx,
416
+ barStop' = if (controlled) (if (t == 0) barStop else t) else barStop,
417
+ openTop' = if (controlled) t else openTop,
418
+ openSub' = if (controlled) 0 else openSub,
419
+ lastActivated' = lastActivated,
420
+ controlled' = controlled,
421
+ accepting' = accepting,
422
+ valueProp' = if (controlled) t else valueProp,
423
+ requested' = requested,
424
+ outcome' = if (controlled) "none" else outcome,
425
+ base' = base,
426
+ requests' = requests
427
+ }
428
+ }
429
+
430
+ // commit: the host flips its own copy of the open top menu (open the first menu
431
+ // with one, close with none)
432
+ action hostToggle = all {
433
+ focusLevel' = if (controlled) 1 else focusLevel,
434
+ focusIdx' = if (controlled) (if (valueProp == 0) 1 else barStop) else focusIdx,
435
+ barStop' = if (controlled) (if (valueProp == 0) 1 else barStop) else barStop,
436
+ openTop' = if (controlled) (if (valueProp == 0) 1 else 0) else openTop,
437
+ openSub' = if (controlled) 0 else openSub,
438
+ lastActivated' = lastActivated,
439
+ controlled' = controlled,
440
+ accepting' = accepting,
441
+ valueProp' = if (controlled) (if (valueProp == 0) 1 else 0) else valueProp,
442
+ requested' = requested,
443
+ outcome' = if (controlled) "none" else outcome,
444
+ base' = base,
445
+ requests' = requests
446
+ }
447
+
448
+ // configuration: the host starts owning the open top menu, adopting the current one
449
+ action control = all {
450
+ focusLevel' = focusLevel,
451
+ focusIdx' = focusIdx,
452
+ barStop' = barStop,
453
+ openTop' = openTop,
454
+ openSub' = openSub,
455
+ lastActivated' = lastActivated,
456
+ controlled' = true,
457
+ accepting' = accepting,
458
+ valueProp' = openTop,
459
+ requested' = requested,
460
+ outcome' = outcome,
461
+ base' = base,
462
+ requests' = requests
463
+ }
464
+
465
+ // configuration: the controlled host answers every request with its intent
466
+ action acceptRequests = all {
467
+ focusLevel' = focusLevel,
468
+ focusIdx' = focusIdx,
469
+ barStop' = barStop,
470
+ openTop' = openTop,
471
+ openSub' = openSub,
472
+ lastActivated' = lastActivated,
473
+ controlled' = controlled,
474
+ accepting' = true,
475
+ valueProp' = valueProp,
476
+ requested' = requested,
477
+ outcome' = outcome,
478
+ base' = base,
479
+ requests' = requests
480
+ }
481
+
482
+ action step = any {
483
+ init, focus_in, tab_out, arrow_right, arrow_left, arrow_down, arrow_up,
484
+ key_home, key_end, activate, escape, typeahead,
485
+ setValue, hostToggle, control, acceptRequests
486
+ }
487
+
488
+ val LevelValid: bool = and(focusLevel >= 0, focusLevel <= 3)
489
+ // outside the widget nothing is open and nothing is focused
490
+ val FocusOutsideClosesAll: bool =
491
+ or(focusLevel != 0, and(focusIdx == 0, openTop == 0, openSub == 0))
492
+ // focus on the menubar sits on the roving tab stop
493
+ val BarFocusIsTabStop: bool =
494
+ or(focusLevel != 1, and(focusIdx == barStop, barStop >= 1, barStop <= 3))
495
+ // focus inside a menu is on an item of the open path
496
+ val FocusInsideOpenPath: bool = and(
497
+ or(focusLevel != 2, and(openTop > 0, openTop == barStop, openSub == 0,
498
+ focusIdx >= 1, focusIdx <= menuSize(openTop))),
499
+ or(focusLevel != 3, and(openTop > 0, hasSub(openTop, openSub),
500
+ focusIdx >= 1, focusIdx <= subSize(openTop, openSub)))
501
+ )
502
+ // the open path is well formed: a menu belongs to the roving item, a submenu
503
+ // to a parent item of that menu and only while focus is inside it
504
+ val OpenPathWellFormed: bool = and(
505
+ or(openTop == 0, and(hasMenu(openTop), openTop == barStop)),
506
+ or(openSub == 0, and(openTop > 0, hasSub(openTop, openSub), focusLevel == 3))
507
+ )
508
+ // exactly one menubar item has tabindex=0
509
+ val ExactlyOneBarTabStop: bool =
510
+ b2i(tabIndexBar(1, barStop) == 0) + b2i(tabIndexBar(2, barStop) == 0) + b2i(tabIndexBar(3, barStop) == 0) == 1
511
+ // at most one menubar item is aria-expanded, and it is the one with the open menu
512
+ val AtMostOneBarExpanded: bool =
513
+ and(
514
+ b2i(ariaExpandedBar(1, openTop)) + b2i(ariaExpandedBar(2, openTop)) + b2i(ariaExpandedBar(3, openTop)) <= 1,
515
+ or(openTop == 0, ariaExpandedBar(openTop, openTop))
516
+ )
517
+ // at most one menu item is aria-expanded, and it is the parent of the open submenu
518
+ val AtMostOneItemExpanded: bool =
519
+ and(
520
+ b2i(ariaExpandedItem(openTop, 1, openTop, openSub)) + b2i(ariaExpandedItem(openTop, 2, openTop, openSub)) + b2i(ariaExpandedItem(openTop, 3, openTop, openSub)) <= 1,
521
+ or(openSub == 0, ariaExpandedItem(openTop, openSub, openTop, openSub))
522
+ )
523
+ // callbacks only ever count up from zero
524
+ val RequestsNonNegative: bool = requests >= 0
525
+ // controlled: only the value prop sets the open menu, so no event commits
526
+ val ControlledFollowsProp: bool = or(not(controlled), openTop == valueProp)
527
+ // a rejected request leaves the state as it found it
528
+ val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), openTop == base)
529
+ // an accepted request committed exactly its intent
530
+ val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), openTop == requested)
531
+ }
@@ -0,0 +1,129 @@
1
+ // APG Meter — https://www.w3.org/WAI/ARIA/apg/patterns/meter/
2
+ //
3
+ // A graphical display of a numeric value within a defined range (battery level,
4
+ // fuel). NOT a progressbar (no completion semantics) and not for unbounded
5
+ // values. The spec models the rendered attributes next to the input so the APG
6
+ // contract is checkable:
7
+ // - aria-valuemin < aria-valuemax;
8
+ // - aria-valuenow lies in [min, max] (the raw value is clamped, and is
9
+ // re-clamped when the range changes);
10
+ // - aria-valuetext is present when a bare percentage would not be friendly,
11
+ // and a present valuetext describes the CURRENT value (never stale).
12
+ //
13
+ // Events:
14
+ // setValue the consumer feeds a raw value (may be out of range)
15
+ // setRange the consumer changes [min, max] (one selector `r`
16
+ // encodes the pair); an invalid pair (min >= max) is ignored
17
+ // addText/removeText render / drop aria-valuetext
18
+ // percentFriendly / percentUnfriendly
19
+ // whether a percentage alone is understandable
20
+ //
21
+ // Keyboard: not applicable. Label (aria-labelledby / aria-label) is a static DOM
22
+ // contract, see specs/apg/STATIC.md.
23
+ module apg_meter {
24
+ var min: int // aria-valuemin
25
+ var max: int // aria-valuemax
26
+ var raw: int // the consumer's unclamped value
27
+ var now: int // aria-valuenow (clamped raw)
28
+ var friendly: bool // a percentage alone is understandable
29
+ var hasText: bool // aria-valuetext is rendered
30
+ var textNow: int // the value aria-valuetext describes (0 when absent)
31
+
32
+ def clamp(v: int, lo: int, hi: int): int =
33
+ if (v < lo) lo else if (v > hi) hi else v
34
+ // range selector: 0..2 are valid ranges, 3 (min > max) and 4 (min == max) are not
35
+ def loOf(r: int): int = if (r == 0) 0 else if (r == 1) 10 else if (r == 2) 20 else if (r == 3) 20 else 10
36
+ def hiOf(r: int): int = if (r == 0) 100 else if (r == 1) 50 else if (r == 2) 200 else if (r == 3) 10 else 10
37
+ def pickMin(lo: int, hi: int): int = if (lo < hi) lo else min
38
+ def pickMax(lo: int, hi: int): int = if (lo < hi) hi else max
39
+
40
+ action init = all {
41
+ min' = 0,
42
+ max' = 100,
43
+ raw' = 50,
44
+ now' = 50,
45
+ friendly' = true,
46
+ hasText' = false,
47
+ textNow' = 0
48
+ }
49
+
50
+ action setValue = {
51
+ nondet v = Set(-5, 0, 5, 15, 25, 60, 100, 150).oneOf()
52
+ all {
53
+ min' = min,
54
+ max' = max,
55
+ raw' = v,
56
+ now' = clamp(v, min, max),
57
+ friendly' = friendly,
58
+ hasText' = hasText,
59
+ textNow' = if (hasText) clamp(v, min, max) else 0
60
+ }
61
+ }
62
+
63
+ action setRange = {
64
+ nondet r = Set(0, 1, 2, 3, 4).oneOf()
65
+ all {
66
+ min' = pickMin(loOf(r), hiOf(r)),
67
+ max' = pickMax(loOf(r), hiOf(r)),
68
+ raw' = raw,
69
+ now' = clamp(raw, pickMin(loOf(r), hiOf(r)), pickMax(loOf(r), hiOf(r))),
70
+ friendly' = friendly,
71
+ hasText' = hasText,
72
+ textNow' = if (hasText) clamp(raw, pickMin(loOf(r), hiOf(r)), pickMax(loOf(r), hiOf(r))) else 0
73
+ }
74
+ }
75
+
76
+ action addText = all {
77
+ min' = min,
78
+ max' = max,
79
+ raw' = raw,
80
+ now' = now,
81
+ friendly' = friendly,
82
+ hasText' = true,
83
+ textNow' = now
84
+ }
85
+
86
+ // a meter whose percentage is not friendly keeps its valuetext
87
+ action removeText = all {
88
+ min' = min,
89
+ max' = max,
90
+ raw' = raw,
91
+ now' = now,
92
+ friendly' = friendly,
93
+ hasText' = if (friendly) false else hasText,
94
+ textNow' = if (friendly) 0 else textNow
95
+ }
96
+
97
+ action percentFriendly = all {
98
+ min' = min,
99
+ max' = max,
100
+ raw' = raw,
101
+ now' = now,
102
+ friendly' = true,
103
+ hasText' = hasText,
104
+ textNow' = textNow
105
+ }
106
+
107
+ action percentUnfriendly = all {
108
+ min' = min,
109
+ max' = max,
110
+ raw' = raw,
111
+ now' = now,
112
+ friendly' = false,
113
+ hasText' = true,
114
+ textNow' = now
115
+ }
116
+
117
+ action step = any { init, setValue, setRange, addText, removeText, percentFriendly, percentUnfriendly }
118
+
119
+ // APG: aria-valuemin is less than aria-valuemax
120
+ val RangeValid: bool = min < max
121
+ // APG: aria-valuenow is between aria-valuemin and aria-valuemax
122
+ val NowInRange: bool = and(min <= now, now <= max)
123
+ // the rendered value is exactly the clamped raw value
124
+ val NowIsClampedRaw: bool = now == clamp(raw, min, max)
125
+ // APG: valuetext is set when the percentage alone is not user friendly
126
+ val TextWhenNotFriendly: bool = or(friendly, hasText)
127
+ // aria-valuetext never describes a stale value
128
+ val TextNeverStale: bool = if (hasText) textNow == now else textNow == 0
129
+ }