@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,627 @@
1
+ // APG combobox — the date picker combobox: an editable date input whose popup is
2
+ // a modal dialog containing a calendar grid (aria-haspopup="dialog").
3
+ //
4
+ // Sources (W3C WAI-ARIA Authoring Practices Guide):
5
+ // https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
6
+ // - "Keyboard Interaction" (Dialog Popup Keyboard Interaction and its note:
7
+ // "dialogs do not support aria-activedescendant so DOM focus moves into
8
+ // the dialog from the combobox"), "WAI-ARIA Roles, States, and Properties"
9
+ // (aria-haspopup="dialog", aria-expanded)
10
+ // https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-datepicker/
11
+ // - "Keyboard Support" (Combobox, Date Picker Dialog, Calendar Buttons,
12
+ // Date Grid, OK and Cancel Buttons), "Role, Property, State, and Tabindex
13
+ // Attributes" (aria-autocomplete="none", aria-modal, roving tabindex,
14
+ // aria-selected on the cell of the current value)
15
+ //
16
+ // Modelled behaviour
17
+ // - Unlike list / grid / tree popups, DOM focus MOVES into the dialog
18
+ // (`domFocus`): while the dialog is open focus is on one of its seven tab
19
+ // stops (Previous Year, Previous Month, Next Month, Next Year, the calendar
20
+ // grid's single roving cell, Cancel, OK) and never on the combobox; while it
21
+ // is closed focus is on the combobox or outside. aria-activedescendant is
22
+ // never set.
23
+ // - Down / Alt+Down on the combobox open the dialog and focus the grid on the
24
+ // combobox's date if it holds a valid one, otherwise on today.
25
+ // - Tab / Shift+Tab cycle through the dialog tab stops and wrap around (focus
26
+ // trap); in the combobox they leave it.
27
+ // - Date grid: Up / Down +-1 week, Left / Right +-1 day (crossing months),
28
+ // Home / End first / last day of the week, PageUp / PageDown +-1 month and
29
+ // Shift+PageUp / Shift+PageDown +-1 year keeping the day number or, if that
30
+ // day does not exist, the last day of the month. Space updates the combobox
31
+ // value (dialog stays open); Enter updates it and closes the dialog.
32
+ // - The calendar buttons change the displayed month / year (same clamping as
33
+ // the Page keys). Cancel and Escape close the dialog, return DOM focus to the
34
+ // combobox and never change the value; OK closes and returns focus too.
35
+ // - Typing in the combobox replaces its text (valid date or not); the Choose
36
+ // Date button (pointer) opens the dialog; `set_value` is the prop change.
37
+ //
38
+ // Calendar model: dates are absolute day numbers 0..266 over nine months of
39
+ // three "years" (31, 28, 30 days each), day 0 = weekday 0; `TODAY` = day 15 of
40
+ // month 4. The displayed month is the month containing the grid's focused date,
41
+ // so the Page keys, buttons and week keys all reduce to integer arithmetic.
42
+ //
43
+ // Deliberate omissions
44
+ // - Real calendar arithmetic (leap years, 31-day months, locales, week start),
45
+ // the date text format / aria-describedby, aria-live announcements, the
46
+ // keyboard-help panel, min / max dates.
47
+ // - OK vs Cancel staging: the example's table updates the value on Space /
48
+ // Enter of a day, so OK and Cancel differ only by wording here (neither
49
+ // changes `value` itself).
50
+ // - Pointer selection of a day, clicking the dialog backdrop, and the combobox
51
+ // input's own click behaviour.
52
+ //
53
+ // Generator notes: no action preconditions (key actions guard their assignments
54
+ // with `atCombo` / `inDialog` / `inGrid`), only str/int/bool vars, a single
55
+ // nondet per action.
56
+ module combobox_dialog {
57
+ var open: bool // dialog displayed == aria-expanded
58
+ var domFocus: int // 0 outside, 1 combobox, 2 prev year, 3 prev month, 4 next month, 5 next year, 6 grid, 7 cancel, 8 ok
59
+ var focusAbs: int // the grid's roving-tabindex date (absolute day number)
60
+ var value: int // combobox value: date, NONE = no valid date
61
+ var lastEvent: str // name of the last event, for transition invariants
62
+ var valueChanged: bool // the last event changed `value`
63
+ var wasOpen: bool // the dialog was open before the last event
64
+ var controlled: bool // the host owns the date value (`value` prop present)
65
+ var accepting: bool // controlled host that answers every request with its intent
66
+ var valueProp: int // the controlled date value
67
+ var requested: int // intent of the last request
68
+ var outcome: str // "none" | "accepted" | "rejected"
69
+ var base: int // value when the last request was received
70
+ var requests: int // number of requests that fired a change callback
71
+
72
+ // --- configuration ------------------------------------------------------
73
+ def NONE: int = -1
74
+ def LAST: int = 266
75
+ def TODAY: int = 134
76
+ def INVALID_TEXT: int = 999
77
+
78
+ // --- calendar -------------------------------------------------------------
79
+ def minI(a: int, b: int): int = if (a < b) a else b
80
+ def mstart(k: int): int =
81
+ if (k == 0) 0 else if (k == 1) 31 else if (k == 2) 59 else if (k == 3) 89
82
+ else if (k == 4) 120 else if (k == 5) 148 else if (k == 6) 178
83
+ else if (k == 7) 209 else if (k == 8) 237 else 267
84
+ def monthOf(a: int): int =
85
+ if (a < 31) 0 else if (a < 59) 1 else if (a < 89) 2 else if (a < 120) 3
86
+ else if (a < 148) 4 else if (a < 178) 5 else if (a < 209) 6
87
+ else if (a < 237) 7 else 8
88
+ def mlen(k: int): int = mstart(k + 1) - mstart(k)
89
+ def dayOf(a: int): int = a - mstart(monthOf(a))
90
+ // +-d months keeping the day number, or the month's last day; the ends of
91
+ // the calendar do not move
92
+ def shiftMonths(a: int, d: int): int =
93
+ if (or(monthOf(a) + d < 0, monthOf(a) + d > 8)) a
94
+ else mstart(monthOf(a) + d) + minI(dayOf(a), mlen(monthOf(a) + d) - 1)
95
+ def weekStart(a: int): int = a - a % 7
96
+ def weekEnd(a: int): int = minI(weekStart(a) + 6, LAST)
97
+
98
+ // --- where DOM focus is ---------------------------------------------------
99
+ def atCombo: bool = and(not(open), domFocus == 1)
100
+ def inDialog: bool = and(open, domFocus >= 2)
101
+ def inGrid: bool = and(open, domFocus == 6)
102
+ def onMonthButton: bool = and(open, domFocus >= 2, domFocus <= 5)
103
+ def onCancelOrOk: bool = and(open, domFocus >= 7)
104
+ def nextStop(f: int): int = if (f == 8) 2 else f + 1
105
+ def prevStop(f: int): int = if (f == 2) 8 else f - 1
106
+ // Previous Year, Previous Month, Next Month, Next Year
107
+ def buttonDelta: int = if (domFocus == 2) 0 - 3 else if (domFocus == 3) 0 - 1 else if (domFocus == 4) 1 else 3
108
+ // the grid opens on the combobox's date, or today
109
+ def openTarget: int = if (value >= 0) value else TODAY
110
+ def buttonTarget: int = shiftMonths(focusAbs, buttonDelta)
111
+ def closesOnEnter: bool = or(inGrid, onCancelOrOk)
112
+
113
+ // --- derived ARIA attribute contract -----------------------------------
114
+ def ariaHasPopup: str = "dialog"
115
+ def ariaAutocomplete: str = "none"
116
+ def ariaExpanded: bool = open
117
+ // dialogs do not use aria-activedescendant: DOM focus moves instead
118
+ def ariaActivedescendant: int = NONE
119
+ def ariaModal: bool = open
120
+ def isNavigation(e: str): bool =
121
+ or(e == "arrow_down", e == "arrow_up", e == "arrow_left", e == "arrow_right",
122
+ e == "home", e == "end", e == "page_up", e == "page_down",
123
+ e == "shift_page_up", e == "shift_page_down", e == "tab", e == "shift_tab")
124
+ def opensDialog(e: str): bool =
125
+ or(e == "arrow_down", e == "alt_arrow_down", e == "click_button")
126
+ def mayChangeValue(e: str): bool =
127
+ or(e == "enter", e == "space", e == "type_text", e == "set_value")
128
+
129
+ // --- request / commit (map ticket rt1p) --------------------------------
130
+ // controlled host that keeps the old value: the request is rejected
131
+ def holds: bool = and(controlled, not(accepting))
132
+ // controlled host that writes the intent back through the value prop
133
+ def follows: bool = and(controlled, accepting)
134
+ def verdict: str = if (holds) "rejected" else "accepted"
135
+ def landedInt(h: bool, current: int, want: int): int = if (h) current else want
136
+ def propAfter(f: bool, p: int, want: int): int = if (f) want else p
137
+
138
+ action init = all {
139
+ open' = false,
140
+ domFocus' = 0,
141
+ focusAbs' = TODAY,
142
+ value' = NONE,
143
+ lastEvent' = "init",
144
+ valueChanged' = false,
145
+ wasOpen' = false,
146
+ controlled' = false,
147
+ accepting' = false,
148
+ valueProp' = NONE,
149
+ requested' = NONE,
150
+ outcome' = "none",
151
+ base' = NONE,
152
+ requests' = 0
153
+ }
154
+
155
+ // --- focus ------------------------------------------------------------------
156
+ action focus = all {
157
+ domFocus' = if (domFocus == 0) 1 else domFocus,
158
+ lastEvent' = "focus",
159
+ valueChanged' = false,
160
+ wasOpen' = open,
161
+ open' = open,
162
+ focusAbs' = focusAbs,
163
+ value' = landedInt(holds, value, value),
164
+ controlled' = controlled,
165
+ accepting' = accepting,
166
+ valueProp' = propAfter(follows, valueProp, value),
167
+ requested' = if ((value) != value) value else requested,
168
+ outcome' = if ((value) != value) verdict else outcome,
169
+ base' = if ((value) != value) value else base,
170
+ requests' = if ((value) != value) requests + 1 else requests,
171
+ }
172
+
173
+ action blur = all {
174
+ domFocus' = if (atCombo) 0 else domFocus,
175
+ lastEvent' = "blur",
176
+ valueChanged' = false,
177
+ wasOpen' = open,
178
+ open' = open,
179
+ focusAbs' = focusAbs,
180
+ value' = landedInt(holds, value, value),
181
+ controlled' = controlled,
182
+ accepting' = accepting,
183
+ valueProp' = propAfter(follows, valueProp, value),
184
+ requested' = if ((value) != value) value else requested,
185
+ outcome' = if ((value) != value) verdict else outcome,
186
+ base' = if ((value) != value) value else base,
187
+ requests' = if ((value) != value) requests + 1 else requests,
188
+ }
189
+
190
+ // Tab: leaves the combobox; inside the dialog it cycles the tab stops
191
+ action tab = all {
192
+ domFocus' = if (atCombo) 0 else if (inDialog) nextStop(domFocus) else domFocus,
193
+ lastEvent' = "tab",
194
+ valueChanged' = false,
195
+ wasOpen' = open,
196
+ open' = open,
197
+ focusAbs' = focusAbs,
198
+ value' = landedInt(holds, value, value),
199
+ controlled' = controlled,
200
+ accepting' = accepting,
201
+ valueProp' = propAfter(follows, valueProp, value),
202
+ requested' = if ((value) != value) value else requested,
203
+ outcome' = if ((value) != value) verdict else outcome,
204
+ base' = if ((value) != value) value else base,
205
+ requests' = if ((value) != value) requests + 1 else requests,
206
+ }
207
+
208
+ action shift_tab = all {
209
+ domFocus' = if (atCombo) 0 else if (inDialog) prevStop(domFocus) else domFocus,
210
+ lastEvent' = "shift_tab",
211
+ valueChanged' = false,
212
+ wasOpen' = open,
213
+ open' = open,
214
+ focusAbs' = focusAbs,
215
+ value' = landedInt(holds, value, value),
216
+ controlled' = controlled,
217
+ accepting' = accepting,
218
+ valueProp' = propAfter(follows, valueProp, value),
219
+ requested' = if ((value) != value) value else requested,
220
+ outcome' = if ((value) != value) verdict else outcome,
221
+ base' = if ((value) != value) value else base,
222
+ requests' = if ((value) != value) requests + 1 else requests,
223
+ }
224
+
225
+ // --- open ---------------------------------------------------------------------
226
+ // ArrowDown on the combobox opens the dialog; in the grid it moves a week
227
+ action arrow_down = all {
228
+ open' = if (atCombo) true else open,
229
+ domFocus' = if (atCombo) 6 else domFocus,
230
+ focusAbs' = if (atCombo) openTarget else if (and(inGrid, focusAbs + 7 <= LAST)) focusAbs + 7 else focusAbs,
231
+ lastEvent' = "arrow_down",
232
+ valueChanged' = false,
233
+ wasOpen' = open,
234
+ value' = landedInt(holds, value, value),
235
+ controlled' = controlled,
236
+ accepting' = accepting,
237
+ valueProp' = propAfter(follows, valueProp, value),
238
+ requested' = if ((value) != value) value else requested,
239
+ outcome' = if ((value) != value) verdict else outcome,
240
+ base' = if ((value) != value) value else base,
241
+ requests' = if ((value) != value) requests + 1 else requests,
242
+ }
243
+
244
+ action alt_arrow_down = all {
245
+ open' = if (atCombo) true else open,
246
+ domFocus' = if (atCombo) 6 else domFocus,
247
+ focusAbs' = if (atCombo) openTarget else focusAbs,
248
+ lastEvent' = "alt_arrow_down",
249
+ valueChanged' = false,
250
+ wasOpen' = open,
251
+ value' = landedInt(holds, value, value),
252
+ controlled' = controlled,
253
+ accepting' = accepting,
254
+ valueProp' = propAfter(follows, valueProp, value),
255
+ requested' = if ((value) != value) value else requested,
256
+ outcome' = if ((value) != value) verdict else outcome,
257
+ base' = if ((value) != value) value else base,
258
+ requests' = if ((value) != value) requests + 1 else requests,
259
+ }
260
+
261
+ // the Choose Date button (tabindex -1) opens the dialog by pointer
262
+ action click_button = all {
263
+ open' = true,
264
+ domFocus' = if (open) domFocus else 6,
265
+ focusAbs' = if (open) focusAbs else openTarget,
266
+ lastEvent' = "click_button",
267
+ valueChanged' = false,
268
+ wasOpen' = open,
269
+ value' = landedInt(holds, value, value),
270
+ controlled' = controlled,
271
+ accepting' = accepting,
272
+ valueProp' = propAfter(follows, valueProp, value),
273
+ requested' = if ((value) != value) value else requested,
274
+ outcome' = if ((value) != value) verdict else outcome,
275
+ base' = if ((value) != value) value else base,
276
+ requests' = if ((value) != value) requests + 1 else requests,
277
+ }
278
+
279
+ // --- date grid navigation ---------------------------------------------------
280
+ action arrow_up = all {
281
+ focusAbs' = if (and(inGrid, focusAbs >= 7)) focusAbs - 7 else focusAbs,
282
+ lastEvent' = "arrow_up",
283
+ valueChanged' = false,
284
+ wasOpen' = open,
285
+ open' = open,
286
+ domFocus' = domFocus,
287
+ value' = landedInt(holds, value, value),
288
+ controlled' = controlled,
289
+ accepting' = accepting,
290
+ valueProp' = propAfter(follows, valueProp, value),
291
+ requested' = if ((value) != value) value else requested,
292
+ outcome' = if ((value) != value) verdict else outcome,
293
+ base' = if ((value) != value) value else base,
294
+ requests' = if ((value) != value) requests + 1 else requests,
295
+ }
296
+
297
+ action arrow_right = all {
298
+ focusAbs' = if (and(inGrid, focusAbs < LAST)) focusAbs + 1 else focusAbs,
299
+ lastEvent' = "arrow_right",
300
+ valueChanged' = false,
301
+ wasOpen' = open,
302
+ open' = open,
303
+ domFocus' = domFocus,
304
+ value' = landedInt(holds, value, value),
305
+ controlled' = controlled,
306
+ accepting' = accepting,
307
+ valueProp' = propAfter(follows, valueProp, value),
308
+ requested' = if ((value) != value) value else requested,
309
+ outcome' = if ((value) != value) verdict else outcome,
310
+ base' = if ((value) != value) value else base,
311
+ requests' = if ((value) != value) requests + 1 else requests,
312
+ }
313
+
314
+ action arrow_left = all {
315
+ focusAbs' = if (and(inGrid, focusAbs > 0)) focusAbs - 1 else focusAbs,
316
+ lastEvent' = "arrow_left",
317
+ valueChanged' = false,
318
+ wasOpen' = open,
319
+ open' = open,
320
+ domFocus' = domFocus,
321
+ value' = landedInt(holds, value, value),
322
+ controlled' = controlled,
323
+ accepting' = accepting,
324
+ valueProp' = propAfter(follows, valueProp, value),
325
+ requested' = if ((value) != value) value else requested,
326
+ outcome' = if ((value) != value) verdict else outcome,
327
+ base' = if ((value) != value) value else base,
328
+ requests' = if ((value) != value) requests + 1 else requests,
329
+ }
330
+
331
+ action home = all {
332
+ focusAbs' = if (inGrid) weekStart(focusAbs) else focusAbs,
333
+ lastEvent' = "home",
334
+ valueChanged' = false,
335
+ wasOpen' = open,
336
+ open' = open,
337
+ domFocus' = domFocus,
338
+ value' = landedInt(holds, value, value),
339
+ controlled' = controlled,
340
+ accepting' = accepting,
341
+ valueProp' = propAfter(follows, valueProp, value),
342
+ requested' = if ((value) != value) value else requested,
343
+ outcome' = if ((value) != value) verdict else outcome,
344
+ base' = if ((value) != value) value else base,
345
+ requests' = if ((value) != value) requests + 1 else requests,
346
+ }
347
+
348
+ action end = all {
349
+ focusAbs' = if (inGrid) weekEnd(focusAbs) else focusAbs,
350
+ lastEvent' = "end",
351
+ valueChanged' = false,
352
+ wasOpen' = open,
353
+ open' = open,
354
+ domFocus' = domFocus,
355
+ value' = landedInt(holds, value, value),
356
+ controlled' = controlled,
357
+ accepting' = accepting,
358
+ valueProp' = propAfter(follows, valueProp, value),
359
+ requested' = if ((value) != value) value else requested,
360
+ outcome' = if ((value) != value) verdict else outcome,
361
+ base' = if ((value) != value) value else base,
362
+ requests' = if ((value) != value) requests + 1 else requests,
363
+ }
364
+
365
+ action page_up = all {
366
+ focusAbs' = if (inGrid) shiftMonths(focusAbs, 0 - 1) else focusAbs,
367
+ lastEvent' = "page_up",
368
+ valueChanged' = false,
369
+ wasOpen' = open,
370
+ open' = open,
371
+ domFocus' = domFocus,
372
+ value' = landedInt(holds, value, value),
373
+ controlled' = controlled,
374
+ accepting' = accepting,
375
+ valueProp' = propAfter(follows, valueProp, value),
376
+ requested' = if ((value) != value) value else requested,
377
+ outcome' = if ((value) != value) verdict else outcome,
378
+ base' = if ((value) != value) value else base,
379
+ requests' = if ((value) != value) requests + 1 else requests,
380
+ }
381
+
382
+ action page_down = all {
383
+ focusAbs' = if (inGrid) shiftMonths(focusAbs, 1) else focusAbs,
384
+ lastEvent' = "page_down",
385
+ valueChanged' = false,
386
+ wasOpen' = open,
387
+ open' = open,
388
+ domFocus' = domFocus,
389
+ value' = landedInt(holds, value, value),
390
+ controlled' = controlled,
391
+ accepting' = accepting,
392
+ valueProp' = propAfter(follows, valueProp, value),
393
+ requested' = if ((value) != value) value else requested,
394
+ outcome' = if ((value) != value) verdict else outcome,
395
+ base' = if ((value) != value) value else base,
396
+ requests' = if ((value) != value) requests + 1 else requests,
397
+ }
398
+
399
+ action shift_page_up = all {
400
+ focusAbs' = if (inGrid) shiftMonths(focusAbs, 0 - 3) else focusAbs,
401
+ lastEvent' = "shift_page_up",
402
+ valueChanged' = false,
403
+ wasOpen' = open,
404
+ open' = open,
405
+ domFocus' = domFocus,
406
+ value' = landedInt(holds, value, value),
407
+ controlled' = controlled,
408
+ accepting' = accepting,
409
+ valueProp' = propAfter(follows, valueProp, value),
410
+ requested' = if ((value) != value) value else requested,
411
+ outcome' = if ((value) != value) verdict else outcome,
412
+ base' = if ((value) != value) value else base,
413
+ requests' = if ((value) != value) requests + 1 else requests,
414
+ }
415
+
416
+ action shift_page_down = all {
417
+ focusAbs' = if (inGrid) shiftMonths(focusAbs, 3) else focusAbs,
418
+ lastEvent' = "shift_page_down",
419
+ valueChanged' = false,
420
+ wasOpen' = open,
421
+ open' = open,
422
+ domFocus' = domFocus,
423
+ value' = landedInt(holds, value, value),
424
+ controlled' = controlled,
425
+ accepting' = accepting,
426
+ valueProp' = propAfter(follows, valueProp, value),
427
+ requested' = if ((value) != value) value else requested,
428
+ outcome' = if ((value) != value) verdict else outcome,
429
+ base' = if ((value) != value) value else base,
430
+ requests' = if ((value) != value) requests + 1 else requests,
431
+ }
432
+
433
+ // --- activation -----------------------------------------------------------------
434
+ // grid: select + close; month / year buttons: change the view; Cancel / OK: close
435
+ action enter = all {
436
+ open' = if (closesOnEnter) false else open,
437
+ domFocus' = if (closesOnEnter) 1 else domFocus,
438
+ focusAbs' = if (onMonthButton) buttonTarget else focusAbs,
439
+ value' = landedInt(holds, value, if (inGrid) focusAbs else value),
440
+ controlled' = controlled,
441
+ accepting' = accepting,
442
+ valueProp' = propAfter(follows, valueProp, if (inGrid) focusAbs else value),
443
+ requested' = if ((if (inGrid) focusAbs else value) != value) if (inGrid) focusAbs else value else requested,
444
+ outcome' = if ((if (inGrid) focusAbs else value) != value) verdict else outcome,
445
+ base' = if ((if (inGrid) focusAbs else value) != value) value else base,
446
+ requests' = if ((if (inGrid) focusAbs else value) != value) requests + 1 else requests,
447
+ lastEvent' = "enter",
448
+ valueChanged' = and(inGrid, value != focusAbs),
449
+ wasOpen' = open
450
+ }
451
+
452
+ // grid: select (the dialog stays open); buttons as for Enter
453
+ action space = all {
454
+ open' = if (onCancelOrOk) false else open,
455
+ domFocus' = if (onCancelOrOk) 1 else domFocus,
456
+ focusAbs' = if (onMonthButton) buttonTarget else focusAbs,
457
+ value' = landedInt(holds, value, if (inGrid) focusAbs else value),
458
+ controlled' = controlled,
459
+ accepting' = accepting,
460
+ valueProp' = propAfter(follows, valueProp, if (inGrid) focusAbs else value),
461
+ requested' = if ((if (inGrid) focusAbs else value) != value) if (inGrid) focusAbs else value else requested,
462
+ outcome' = if ((if (inGrid) focusAbs else value) != value) verdict else outcome,
463
+ base' = if ((if (inGrid) focusAbs else value) != value) value else base,
464
+ requests' = if ((if (inGrid) focusAbs else value) != value) requests + 1 else requests,
465
+ lastEvent' = "space",
466
+ valueChanged' = and(inGrid, value != focusAbs),
467
+ wasOpen' = open
468
+ }
469
+
470
+ // Escape: close the dialog, focus back on the combobox, value untouched
471
+ action escape = all {
472
+ open' = if (inDialog) false else open,
473
+ domFocus' = if (inDialog) 1 else domFocus,
474
+ lastEvent' = "escape",
475
+ valueChanged' = false,
476
+ wasOpen' = open,
477
+ focusAbs' = focusAbs,
478
+ value' = landedInt(holds, value, value),
479
+ controlled' = controlled,
480
+ accepting' = accepting,
481
+ valueProp' = propAfter(follows, valueProp, value),
482
+ requested' = if ((value) != value) value else requested,
483
+ outcome' = if ((value) != value) verdict else outcome,
484
+ base' = if ((value) != value) value else base,
485
+ requests' = if ((value) != value) requests + 1 else requests,
486
+ }
487
+
488
+ // --- text input / props -----------------------------------------------------
489
+ // typing replaces the combobox text: a valid date or not
490
+ action type_text = {
491
+ nondet k = Set(0, 59, 134, 999).oneOf()
492
+ all {
493
+ value' = landedInt(holds, value, if (atCombo) (if (k == INVALID_TEXT) NONE else k) else value),
494
+ controlled' = controlled,
495
+ accepting' = accepting,
496
+ valueProp' = propAfter(follows, valueProp, if (atCombo) (if (k == INVALID_TEXT) NONE else k) else value),
497
+ requested' = if ((if (atCombo) (if (k == INVALID_TEXT) NONE else k) else value) != value) if (atCombo) (if (k == INVALID_TEXT) NONE else k) else value else requested,
498
+ outcome' = if ((if (atCombo) (if (k == INVALID_TEXT) NONE else k) else value) != value) verdict else outcome,
499
+ base' = if ((if (atCombo) (if (k == INVALID_TEXT) NONE else k) else value) != value) value else base,
500
+ requests' = if ((if (atCombo) (if (k == INVALID_TEXT) NONE else k) else value) != value) requests + 1 else requests,
501
+ lastEvent' = "type_text",
502
+ valueChanged' = and(atCombo, value != (if (k == INVALID_TEXT) NONE else k)),
503
+ wasOpen' = open,
504
+ open' = open,
505
+ domFocus' = domFocus,
506
+ focusAbs' = focusAbs
507
+ }
508
+ }
509
+
510
+ // the value is replaced from outside; an open dialog is left as it is
511
+ action set_value = {
512
+ nondet k = Set(0, 59, 134, 266).oneOf()
513
+ all {
514
+ value' = k,
515
+ lastEvent' = "set_value",
516
+ valueChanged' = value != k,
517
+ wasOpen' = open,
518
+ open' = open,
519
+ domFocus' = domFocus,
520
+ focusAbs' = focusAbs,
521
+ controlled' = controlled,
522
+ accepting' = accepting,
523
+ valueProp' = if (controlled) k else valueProp,
524
+ requested' = requested,
525
+ outcome' = "none",
526
+ base' = base,
527
+ requests' = requests
528
+ }
529
+ }
530
+
531
+ // commit: the host flips its own copy of the date value
532
+ action hostToggle = all {
533
+ open' = open,
534
+ domFocus' = domFocus,
535
+ focusAbs' = focusAbs,
536
+ value' = if (controlled) (if (value >= 0) NONE else TODAY) else value,
537
+ lastEvent' = "hostToggle",
538
+ valueChanged' = false,
539
+ wasOpen' = open,
540
+ controlled' = controlled,
541
+ accepting' = accepting,
542
+ valueProp' = if (controlled) (if (value >= 0) NONE else TODAY) else valueProp,
543
+ requested' = requested,
544
+ outcome' = if (controlled) "none" else outcome,
545
+ base' = base,
546
+ requests' = requests
547
+ }
548
+
549
+ // configuration: the host starts owning the date value, adopting it
550
+ action control = all {
551
+ open' = open,
552
+ domFocus' = domFocus,
553
+ focusAbs' = focusAbs,
554
+ value' = value,
555
+ lastEvent' = lastEvent,
556
+ valueChanged' = valueChanged,
557
+ wasOpen' = open,
558
+ controlled' = true,
559
+ accepting' = accepting,
560
+ valueProp' = value,
561
+ requested' = requested,
562
+ outcome' = outcome,
563
+ base' = base,
564
+ requests' = requests
565
+ }
566
+
567
+ // configuration: the controlled host answers every request with its intent
568
+ action acceptRequests = all {
569
+ open' = open,
570
+ domFocus' = domFocus,
571
+ focusAbs' = focusAbs,
572
+ value' = value,
573
+ lastEvent' = lastEvent,
574
+ valueChanged' = valueChanged,
575
+ wasOpen' = open,
576
+ controlled' = controlled,
577
+ accepting' = true,
578
+ valueProp' = valueProp,
579
+ requested' = requested,
580
+ outcome' = outcome,
581
+ base' = base,
582
+ requests' = requests
583
+ }
584
+
585
+ action step = any {
586
+ init, focus, blur, tab, shift_tab, arrow_down, alt_arrow_down, click_button,
587
+ arrow_up, arrow_right, arrow_left, home, end, page_up, page_down,
588
+ shift_page_up, shift_page_down, enter, space, escape, type_text, set_value,
589
+ hostToggle, control, acceptRequests
590
+ }
591
+
592
+ // --- invariants (APG guarantees) ----------------------------------------
593
+ // DOM focus moves INTO an open dialog (one of its seven tab stops) ...
594
+ val DomFocusInsideDialogWhenOpen: bool = or(not(open), and(domFocus >= 2, domFocus <= 8))
595
+ // ... and is on the combobox (or outside) whenever it is closed
596
+ val DomFocusOutsideDialogWhenClosed: bool = or(open, domFocus <= 1)
597
+ // the dialog popup never uses aria-activedescendant; haspopup / autocomplete /
598
+ // modal attributes follow the state
599
+ val AriaContractConsistent: bool = and(
600
+ ariaHasPopup == "dialog",
601
+ ariaAutocomplete == "none",
602
+ ariaExpanded == open,
603
+ ariaActivedescendant == NONE,
604
+ ariaModal == open
605
+ )
606
+ val DatesInRange: bool = and(
607
+ focusAbs >= 0, focusAbs <= LAST, value >= NONE, value <= LAST
608
+ )
609
+ // Escape never changes the value and returns DOM focus to the combobox
610
+ val EscapeKeepsValue: bool = or(not(lastEvent == "escape"), not(valueChanged))
611
+ val EscapeReturnsFocusToCombobox: bool =
612
+ or(not(lastEvent == "escape"), not(wasOpen), and(not(open), domFocus == 1))
613
+ // moving through the dialog never changes the value
614
+ val NavigationKeepsValue: bool = or(not(isNavigation(lastEvent)), not(valueChanged))
615
+ val ValueChangesOnlyOnCommitOrEdit: bool = or(not(valueChanged), mayChangeValue(lastEvent))
616
+ // a freshly opened dialog starts on the combobox's date, or today
617
+ val OpensOnValueOrToday: bool =
618
+ or(not(opensDialog(lastEvent)), not(open), wasOpen, focusAbs == openTarget)
619
+ // callbacks only ever count up from zero
620
+ val RequestsNonNegative: bool = requests >= 0
621
+ // controlled: only the value prop sets the date, so no event commits
622
+ val ControlledFollowsProp: bool = or(not(controlled), value == valueProp)
623
+ // a rejected request leaves the date value as it found it
624
+ val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), value == base)
625
+ // an accepted request committed exactly its intent
626
+ val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), value == requested)
627
+ }