@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.
- package/LICENSE +21 -0
- package/README.md +65 -2
- package/dist/apg/accordion.bridge.d.mts +35 -0
- package/dist/apg/accordion.bridge.mjs +326 -0
- package/dist/apg/accordion.model.json +8397 -0
- package/dist/apg/alert.bridge.d.mts +22 -0
- package/dist/apg/alert.bridge.mjs +86 -0
- package/dist/apg/alert.model.json +871 -0
- package/dist/apg/alertdialog.bridge.d.mts +21 -0
- package/dist/apg/alertdialog.bridge.mjs +221 -0
- package/dist/apg/alertdialog.model.json +5232 -0
- package/dist/apg/breadcrumb.bridge.d.mts +26 -0
- package/dist/apg/breadcrumb.bridge.mjs +120 -0
- package/dist/apg/breadcrumb.model.json +574 -0
- package/dist/apg/button.bridge.d.mts +35 -0
- package/dist/apg/button.bridge.mjs +208 -0
- package/dist/apg/button.model.json +5814 -0
- package/dist/apg/carousel.bridge.d.mts +44 -0
- package/dist/apg/carousel.bridge.mjs +262 -0
- package/dist/apg/carousel.model.json +8963 -0
- package/dist/apg/checkbox-group.bridge.d.mts +25 -0
- package/dist/apg/checkbox-group.bridge.mjs +276 -0
- package/dist/apg/checkbox-group.model.json +3863 -0
- package/dist/apg/checkbox.bridge.d.mts +34 -0
- package/dist/apg/checkbox.bridge.mjs +253 -0
- package/dist/apg/checkbox.model.json +3158 -0
- package/dist/apg/combobox-autocomplete.bridge.d.mts +14 -0
- package/dist/apg/combobox-autocomplete.bridge.mjs +326 -0
- package/dist/apg/combobox-autocomplete.model.json +11586 -0
- package/dist/apg/combobox-dialog.bridge.d.mts +17 -0
- package/dist/apg/combobox-dialog.bridge.mjs +250 -0
- package/dist/apg/combobox-dialog.model.json +8981 -0
- package/dist/apg/combobox-grid.bridge.d.mts +17 -0
- package/dist/apg/combobox-grid.bridge.mjs +227 -0
- package/dist/apg/combobox-grid.model.json +8554 -0
- package/dist/apg/combobox-select-only.bridge.d.mts +24 -0
- package/dist/apg/combobox-select-only.bridge.mjs +304 -0
- package/dist/apg/combobox-select-only.model.json +7846 -0
- package/dist/apg/combobox-tree.bridge.d.mts +17 -0
- package/dist/apg/combobox-tree.bridge.mjs +211 -0
- package/dist/apg/combobox-tree.model.json +9345 -0
- package/dist/apg/dialog-modal.bridge.d.mts +35 -0
- package/dist/apg/dialog-modal.bridge.mjs +186 -0
- package/dist/apg/dialog-modal.model.json +7580 -0
- package/dist/apg/disclosure.bridge.d.mts +25 -0
- package/dist/apg/disclosure.bridge.mjs +202 -0
- package/dist/apg/disclosure.model.json +4703 -0
- package/dist/apg/feed.bridge.d.mts +31 -0
- package/dist/apg/feed.bridge.mjs +246 -0
- package/dist/apg/feed.model.json +4523 -0
- package/dist/apg/grid-selection.bridge.d.mts +13 -0
- package/dist/apg/grid-selection.bridge.mjs +160 -0
- package/dist/apg/grid-selection.model.json +6974 -0
- package/dist/apg/grid-sort.bridge.d.mts +12 -0
- package/dist/apg/grid-sort.bridge.mjs +138 -0
- package/dist/apg/grid-sort.model.json +3799 -0
- package/dist/apg/grid.bridge.d.mts +54 -0
- package/dist/apg/grid.bridge.mjs +441 -0
- package/dist/apg/grid.model.json +3802 -0
- package/dist/apg/link.bridge.d.mts +24 -0
- package/dist/apg/link.bridge.mjs +126 -0
- package/dist/apg/link.model.json +694 -0
- package/dist/apg/listbox-multi.bridge.d.mts +10 -0
- package/dist/apg/listbox-multi.bridge.mjs +261 -0
- package/dist/apg/listbox-multi.model.json +10893 -0
- package/dist/apg/listbox-rearrangeable.bridge.d.mts +20 -0
- package/dist/apg/listbox-rearrangeable.bridge.mjs +237 -0
- package/dist/apg/listbox-rearrangeable.model.json +17592 -0
- package/dist/apg/listbox-shared.d.mts +58 -0
- package/dist/apg/listbox-shared.mjs +171 -0
- package/dist/apg/listbox-single.bridge.d.mts +10 -0
- package/dist/apg/listbox-single.bridge.mjs +196 -0
- package/dist/apg/listbox-single.model.json +8024 -0
- package/dist/apg/listbox-typeahead.bridge.d.mts +10 -0
- package/dist/apg/listbox-typeahead.bridge.mjs +153 -0
- package/dist/apg/listbox-typeahead.model.json +5615 -0
- package/dist/apg/menu-button.bridge.d.mts +29 -0
- package/dist/apg/menu-button.bridge.mjs +259 -0
- package/dist/apg/menu-button.model.json +5706 -0
- package/dist/apg/menu-checkable.bridge.d.mts +25 -0
- package/dist/apg/menu-checkable.bridge.mjs +226 -0
- package/dist/apg/menu-checkable.model.json +4277 -0
- package/dist/apg/menu-menubar.bridge.d.mts +27 -0
- package/dist/apg/menu-menubar.bridge.mjs +273 -0
- package/dist/apg/menu-menubar.model.json +9540 -0
- package/dist/apg/meter.bridge.d.mts +28 -0
- package/dist/apg/meter.bridge.mjs +159 -0
- package/dist/apg/meter.model.json +1185 -0
- package/dist/apg/player.bridge.d.mts +24 -0
- package/dist/apg/player.bridge.mjs +342 -0
- package/dist/apg/player.model.json +11013 -0
- package/dist/apg/radio.bridge.d.mts +35 -0
- package/dist/apg/radio.bridge.mjs +371 -0
- package/dist/apg/radio.model.json +5011 -0
- package/dist/apg/slider-multithumb.bridge.d.mts +34 -0
- package/dist/apg/slider-multithumb.bridge.mjs +365 -0
- package/dist/apg/slider-multithumb.model.json +5774 -0
- package/dist/apg/slider.bridge.d.mts +54 -0
- package/dist/apg/slider.bridge.mjs +359 -0
- package/dist/apg/slider.model.json +4881 -0
- package/dist/apg/spinbutton.bridge.d.mts +41 -0
- package/dist/apg/spinbutton.bridge.mjs +327 -0
- package/dist/apg/spinbutton.model.json +7571 -0
- package/dist/apg/switch.bridge.d.mts +30 -0
- package/dist/apg/switch.bridge.mjs +243 -0
- package/dist/apg/switch.model.json +3159 -0
- package/dist/apg/tabs.bridge.d.mts +41 -0
- package/dist/apg/tabs.bridge.mjs +294 -0
- package/dist/apg/tabs.model.json +7659 -0
- package/dist/apg/timeline.bridge.d.mts +36 -0
- package/dist/apg/timeline.bridge.mjs +265 -0
- package/dist/apg/timeline.model.json +6189 -0
- package/dist/apg/toolbar.bridge.d.mts +25 -0
- package/dist/apg/toolbar.bridge.mjs +307 -0
- package/dist/apg/toolbar.model.json +6616 -0
- package/dist/apg/tooltip.bridge.d.mts +36 -0
- package/dist/apg/tooltip.bridge.mjs +208 -0
- package/dist/apg/tooltip.model.json +8295 -0
- package/dist/apg/treegrid.bridge.d.mts +28 -0
- package/dist/apg/treegrid.bridge.mjs +311 -0
- package/dist/apg/treegrid.model.json +17358 -0
- package/dist/apg/treeview.bridge.d.mts +26 -0
- package/dist/apg/treeview.bridge.mjs +396 -0
- package/dist/apg/treeview.model.json +13745 -0
- package/dist/apg/windowsplitter.bridge.d.mts +36 -0
- package/dist/apg/windowsplitter.bridge.mjs +242 -0
- package/dist/apg/windowsplitter.model.json +6253 -0
- package/dist/canvas/camera.model.json +6568 -0
- package/dist/canvas/canvas.bridge.d.mts +190 -0
- package/dist/canvas/canvas.bridge.mjs +729 -0
- package/dist/canvas/pointer.model.json +17095 -0
- package/dist/scenario.d.mts +233 -0
- package/dist/scenario.mjs +313 -0
- package/package.json +57 -4
- package/scenarios/apg/README.md +221 -0
- package/scenarios/apg/accordion.base-ui.json +1005 -0
- package/scenarios/apg/accordion.react-aria.json +929 -0
- package/scenarios/apg/alert.base-ui.json +81 -0
- package/scenarios/apg/alert.react-aria.json +98 -0
- package/scenarios/apg/alertdialog.base-ui.json +331 -0
- package/scenarios/apg/alertdialog.react-aria.json +225 -0
- package/scenarios/apg/breadcrumb.react-aria.json +312 -0
- package/scenarios/apg/button.ariakit.json +136 -0
- package/scenarios/apg/button.base-ui.json +747 -0
- package/scenarios/apg/button.react-aria.json +443 -0
- package/scenarios/apg/carousel.apg.json +909 -0
- package/scenarios/apg/checkbox.ariakit.json +299 -0
- package/scenarios/apg/checkbox.base-ui.json +1126 -0
- package/scenarios/apg/checkbox.react-aria.json +518 -0
- package/scenarios/apg/combobox.ariakit.json +945 -0
- package/scenarios/apg/combobox.base-ui.json +2444 -0
- package/scenarios/apg/combobox.react-aria.json +845 -0
- package/scenarios/apg/dialog-modal.ariakit.json +1102 -0
- package/scenarios/apg/dialog-modal.base-ui.json +775 -0
- package/scenarios/apg/dialog-modal.react-aria.json +638 -0
- package/scenarios/apg/disclosure.apg.json +349 -0
- package/scenarios/apg/disclosure.ariakit.json +163 -0
- package/scenarios/apg/disclosure.base-ui.json +451 -0
- package/scenarios/apg/disclosure.react-aria.json +643 -0
- package/scenarios/apg/feed.apg.json +442 -0
- package/scenarios/apg/grid.ariakit.json +197 -0
- package/scenarios/apg/grid.base-ui.json +720 -0
- package/scenarios/apg/grid.react-aria.json +969 -0
- package/scenarios/apg/landmarks.ariakit.json +93 -0
- package/scenarios/apg/landmarks.react-aria.json +330 -0
- package/scenarios/apg/link.ariakit.json +83 -0
- package/scenarios/apg/link.base-ui.json +88 -0
- package/scenarios/apg/link.react-aria.json +186 -0
- package/scenarios/apg/listbox.ariakit.json +78 -0
- package/scenarios/apg/listbox.base-ui.json +163 -0
- package/scenarios/apg/listbox.react-aria.json +1118 -0
- package/scenarios/apg/menu-button.ariakit.json +924 -0
- package/scenarios/apg/menu-button.base-ui.json +1155 -0
- package/scenarios/apg/menu-button.react-aria.json +1080 -0
- package/scenarios/apg/menubar.ariakit.json +758 -0
- package/scenarios/apg/menubar.base-ui.json +673 -0
- package/scenarios/apg/meter.base-ui.json +243 -0
- package/scenarios/apg/meter.react-aria.json +201 -0
- package/scenarios/apg/radio.ariakit.json +459 -0
- package/scenarios/apg/radio.base-ui.json +581 -0
- package/scenarios/apg/radio.react-aria.json +576 -0
- package/scenarios/apg/slider-multithumb.base-ui.json +316 -0
- package/scenarios/apg/slider-multithumb.react-aria.json +363 -0
- package/scenarios/apg/slider.base-ui.json +544 -0
- package/scenarios/apg/slider.react-aria.json +456 -0
- package/scenarios/apg/spinbutton.base-ui.json +1688 -0
- package/scenarios/apg/spinbutton.react-aria.json +426 -0
- package/scenarios/apg/switch.base-ui.json +456 -0
- package/scenarios/apg/switch.react-aria.json +303 -0
- package/scenarios/apg/table.ariakit.json +65 -0
- package/scenarios/apg/table.react-aria.json +221 -0
- package/scenarios/apg/tabs.ariakit.json +329 -0
- package/scenarios/apg/tabs.base-ui.json +875 -0
- package/scenarios/apg/tabs.react-aria.json +644 -0
- package/scenarios/apg/toolbar.ariakit.json +595 -0
- package/scenarios/apg/toolbar.base-ui.json +1189 -0
- package/scenarios/apg/toolbar.react-aria.json +383 -0
- package/scenarios/apg/tooltip.ariakit.json +441 -0
- package/scenarios/apg/tooltip.base-ui.json +411 -0
- package/scenarios/apg/tooltip.react-aria.json +454 -0
- package/scenarios/apg/treegrid.apg.json +2558 -0
- package/scenarios/apg/treegrid.react-aria.json +5094 -0
- package/scenarios/apg/treeview.apg.json +928 -0
- package/scenarios/apg/windowsplitter.apg.json +557 -0
- package/scenarios/canvas/README.md +69 -0
- package/scenarios/canvas/camera.contract.json +237 -0
- package/scenarios/canvas/camera.excalidraw.json +57 -0
- package/scenarios/canvas/marquee.excalidraw.json +122 -0
- package/scenarios/canvas/pan.excalidraw.json +46 -0
- package/scenarios/canvas/pointer.contract.json +528 -0
- package/scenarios/parts.json +291 -0
- package/shared/aria.qnt +186 -0
- package/shared/details-group.qnt +50 -0
- package/shared/focusgroup.qnt +152 -0
- package/shared/invokers.qnt +81 -0
- package/shared/popover.qnt +71 -0
- package/specs/apg/STATIC.md +103 -0
- package/specs/apg/accordion.qnt +578 -0
- package/specs/apg/alert.qnt +131 -0
- package/specs/apg/alertdialog.qnt +455 -0
- package/specs/apg/breadcrumb.qnt +88 -0
- package/specs/apg/button.qnt +445 -0
- package/specs/apg/carousel.qnt +732 -0
- package/specs/apg/checkbox-group.qnt +388 -0
- package/specs/apg/checkbox.qnt +394 -0
- package/specs/apg/combobox-autocomplete.qnt +764 -0
- package/specs/apg/combobox-dialog.qnt +627 -0
- package/specs/apg/combobox-grid.qnt +632 -0
- package/specs/apg/combobox-select-only.qnt +626 -0
- package/specs/apg/combobox-tree.qnt +633 -0
- package/specs/apg/dialog-modal.qnt +499 -0
- package/specs/apg/disclosure.qnt +309 -0
- package/specs/apg/feed.qnt +485 -0
- package/specs/apg/grid-selection.qnt +512 -0
- package/specs/apg/grid-sort.qnt +330 -0
- package/specs/apg/grid.qnt +360 -0
- package/specs/apg/link.qnt +119 -0
- package/specs/apg/listbox-multi.qnt +799 -0
- package/specs/apg/listbox-rearrangeable.qnt +801 -0
- package/specs/apg/listbox-single.qnt +511 -0
- package/specs/apg/listbox-typeahead.qnt +396 -0
- package/specs/apg/menu-button.qnt +363 -0
- package/specs/apg/menu-checkable.qnt +304 -0
- package/specs/apg/menu-menubar.qnt +531 -0
- package/specs/apg/meter.qnt +129 -0
- package/specs/apg/player.qnt +1081 -0
- package/specs/apg/radio.qnt +394 -0
- package/specs/apg/slider-multithumb.qnt +441 -0
- package/specs/apg/slider.qnt +396 -0
- package/specs/apg/spinbutton.qnt +494 -0
- package/specs/apg/switch.qnt +389 -0
- package/specs/apg/tabs.qnt +412 -0
- package/specs/apg/timeline.qnt +612 -0
- package/specs/apg/toolbar.qnt +364 -0
- package/specs/apg/tooltip.qnt +553 -0
- package/specs/apg/treegrid.qnt +983 -0
- package/specs/apg/treeview.qnt +1004 -0
- package/specs/apg/windowsplitter.qnt +487 -0
- package/specs/canvas/camera.qnt +407 -0
- 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
|
+
}
|