@cssints/apg-specs 0.0.0-stage → 0.5.2
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,764 @@
|
|
|
1
|
+
// APG combobox — the editable combobox with a listbox popup and the three
|
|
2
|
+
// autocomplete behaviours: aria-autocomplete none / list / both.
|
|
3
|
+
//
|
|
4
|
+
// Sources (W3C WAI-ARIA Authoring Practices Guide):
|
|
5
|
+
// https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
|
|
6
|
+
// - "About This Pattern" (the four forms of autocomplete), "Keyboard
|
|
7
|
+
// Interaction" (Combobox + Listbox Popup), "WAI-ARIA Roles, States, and
|
|
8
|
+
// Properties"
|
|
9
|
+
// https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-none/
|
|
10
|
+
// https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-list/
|
|
11
|
+
// https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-both/
|
|
12
|
+
// - "Keyboard Support" (Textbox, Listbox Popup), "Role, Property, State, and
|
|
13
|
+
// Tabindex Attributes" (Textbox, Listbox Popup, Button)
|
|
14
|
+
//
|
|
15
|
+
// Modelled behaviour
|
|
16
|
+
// - DOM focus stays on the input (`focused`); the popup's visual focus is
|
|
17
|
+
// aria-activedescendant (`active`). A popup can only be open while the input
|
|
18
|
+
// has DOM focus; active != NONE implies the popup is open and the referenced
|
|
19
|
+
// option is currently visible. active == NONE with the popup open means
|
|
20
|
+
// visual focus is on the textbox itself.
|
|
21
|
+
// - Autocomplete mode (`mode`, aria-autocomplete): 0 none = the suggestion list
|
|
22
|
+
// is the same whatever is typed; 1 list = suggestions are filtered by the
|
|
23
|
+
// typed text, nothing is selected automatically (manual selection);
|
|
24
|
+
// 2 both = like list, plus the first match is selected automatically after an
|
|
25
|
+
// edit and its completion string is shown inline (`inlineCompletion`).
|
|
26
|
+
// - Typing / Backspace edit the text; the popup opens iff there are
|
|
27
|
+
// suggestions (and, for filtered modes, text is non-empty) and closes when
|
|
28
|
+
// the filter has no match. Editing returns visual focus to the textbox
|
|
29
|
+
// (active = NONE) except in "both", which re-selects the first match.
|
|
30
|
+
// - ArrowDown / ArrowUp open the popup (first / last visible option) or move
|
|
31
|
+
// the visual focus with wrap-around; with an auto-selected suggestion,
|
|
32
|
+
// ArrowDown goes to the following one. Alt+ArrowDown opens without moving
|
|
33
|
+
// focus; Alt+ArrowUp closes. Left / Right / Home / End / Delete return the
|
|
34
|
+
// visual focus to the textbox without closing the popup.
|
|
35
|
+
// - Enter commits the visually focused option (`picked`) and closes; without a
|
|
36
|
+
// focused option it just closes. Escape closes an open popup keeping the
|
|
37
|
+
// value (inline completion dropped); with the popup closed it clears the
|
|
38
|
+
// textbox, except in mode none (whose APG example has no such Escape).
|
|
39
|
+
// Blur / Tab close the popup; in "both" the auto-selected suggestion is
|
|
40
|
+
// accepted as the value, in none / list the typed text stays.
|
|
41
|
+
// - Pointer: click on the input / open button toggles the popup and keeps DOM
|
|
42
|
+
// focus on the input; clicking an option commits it.
|
|
43
|
+
// - Prop changes: `set_autocomplete` swaps the mode (popup closes), `set_value`
|
|
44
|
+
// replaces the committed suggestion from outside (popup closes).
|
|
45
|
+
// - The ARIA contract is derived: aria-autocomplete, aria-expanded,
|
|
46
|
+
// aria-activedescendant (only meaningful while open), aria-selected (exactly
|
|
47
|
+
// the active option).
|
|
48
|
+
//
|
|
49
|
+
// Text model: the typed text is a node of a tiny prefix trie over the corpus
|
|
50
|
+
// 0 Alabama, 1 Alaska, 2 Arizona, 3 Colorado, 4 Connecticut, 5 Delaware
|
|
51
|
+
// (node 0 "", 1 "A", 2 "Al", 3 "Ar", 4 "C", 5 "Co", 6 "Col", 7 "D", 8 any
|
|
52
|
+
// non-matching text), typed with the character pool A l r C o D Z. A committed
|
|
53
|
+
// suggestion is `picked` (the textbox then shows its full name); typing after
|
|
54
|
+
// a commit appends to a full name, which matches nothing (node 8).
|
|
55
|
+
//
|
|
56
|
+
// Deliberate omissions
|
|
57
|
+
// - Cursor position / text selection and the standard text-editing keys
|
|
58
|
+
// (Left/Right/Home/End/Delete are modelled only for their effect on visual
|
|
59
|
+
// focus); the textual content of the inline completion.
|
|
60
|
+
// - Autocomplete none's `Escape` on a closed popup is a no-op, as in the APG
|
|
61
|
+
// example; the pattern's other optional Escape / Alt behaviours are chosen,
|
|
62
|
+
// not parameterised.
|
|
63
|
+
// - Grid / tree / dialog popups (see combobox-grid / -tree / -dialog), the
|
|
64
|
+
// Open button's own focus (it is out of the Tab sequence, tabindex=-1),
|
|
65
|
+
// asynchronous suggestions, scrolling the active option into view, hover,
|
|
66
|
+
// multi-select and `aria-controls` / label wiring.
|
|
67
|
+
//
|
|
68
|
+
// Generator notes: no action preconditions (key actions guard their
|
|
69
|
+
// assignments with `focused`), only str/int/bool vars (int sentinels, `NONE`),
|
|
70
|
+
// and a single nondet per action.
|
|
71
|
+
module combobox_autocomplete {
|
|
72
|
+
var mode: int // aria-autocomplete: 0 none, 1 list, 2 both
|
|
73
|
+
var open: bool // popup displayed == aria-expanded
|
|
74
|
+
var text: int // typed text (prefix-trie node)
|
|
75
|
+
var picked: int // committed suggestion shown in the textbox, NONE = typed text
|
|
76
|
+
var active: int // aria-activedescendant option index, NONE = visual focus on textbox
|
|
77
|
+
var focused: bool // DOM focus is on the input
|
|
78
|
+
var lastEvent: str // name of the last event, for transition invariants
|
|
79
|
+
var valueChanged: bool // the last event changed the committed text / suggestion
|
|
80
|
+
var wasOpen: bool // the popup was open before the last event
|
|
81
|
+
var controlled: bool // the host owns the picked suggestion (`value` prop present)
|
|
82
|
+
var accepting: bool // controlled host that answers every request with its intent
|
|
83
|
+
var valueProp: int // the controlled picked suggestion
|
|
84
|
+
var requested: int // intent of the last request
|
|
85
|
+
var outcome: str // "none" | "accepted" | "rejected"
|
|
86
|
+
var base: int // picked when the last request was received
|
|
87
|
+
var requests: int // number of requests that fired a change callback
|
|
88
|
+
|
|
89
|
+
// --- configuration ------------------------------------------------------
|
|
90
|
+
def NONE: int = -1
|
|
91
|
+
def OPTION_COUNT: int = 6
|
|
92
|
+
|
|
93
|
+
// --- helpers ------------------------------------------------------------
|
|
94
|
+
def cyc(from: int, k: int): int = (from + k) % OPTION_COUNT
|
|
95
|
+
def cycBack(from: int, k: int): int = (from + OPTION_COUNT - k) % OPTION_COUNT
|
|
96
|
+
|
|
97
|
+
// corpus prefix trie
|
|
98
|
+
def matches(i: int, t: int): bool =
|
|
99
|
+
if (t == 0) true
|
|
100
|
+
else if (t == 1) i <= 2
|
|
101
|
+
else if (t == 2) i <= 1
|
|
102
|
+
else if (t == 3) i == 2
|
|
103
|
+
else if (t == 4) or(i == 3, i == 4)
|
|
104
|
+
else if (t == 5) or(i == 3, i == 4)
|
|
105
|
+
else if (t == 6) i == 3
|
|
106
|
+
else if (t == 7) i == 5
|
|
107
|
+
else false
|
|
108
|
+
// characters: 0 A, 1 l, 2 r, 3 C, 4 o, 5 D, 6 Z
|
|
109
|
+
def typeNext(t: int, c: int): int =
|
|
110
|
+
if (t == 0) (if (c == 0) 1 else if (c == 3) 4 else if (c == 5) 7 else 8)
|
|
111
|
+
else if (t == 1) (if (c == 1) 2 else if (c == 2) 3 else 8)
|
|
112
|
+
else if (t == 4) (if (c == 4) 5 else 8)
|
|
113
|
+
else if (t == 5) (if (c == 1) 6 else 8)
|
|
114
|
+
else 8
|
|
115
|
+
def parentNode(t: int): int =
|
|
116
|
+
if (t == 2) 1 else if (t == 3) 1 else if (t == 5) 4 else if (t == 6) 5 else 0
|
|
117
|
+
def firstLetterNode(i: int): int = if (i <= 2) 1 else if (i <= 4) 4 else 7
|
|
118
|
+
|
|
119
|
+
// visibility of an option under (mode, picked, text)
|
|
120
|
+
def matchesFor(i: int, m: int, t: int): bool = if (m == 0) true else matches(i, t)
|
|
121
|
+
def visFor(i: int, m: int, p: int, t: int): bool =
|
|
122
|
+
if (and(p >= 0, m != 0)) i == p else matchesFor(i, m, t)
|
|
123
|
+
def vis(i: int): bool = visFor(i, mode, picked, text)
|
|
124
|
+
def count(m: int, p: int, t: int): int =
|
|
125
|
+
(if (visFor(0, m, p, t)) 1 else 0) + (if (visFor(1, m, p, t)) 1 else 0) +
|
|
126
|
+
(if (visFor(2, m, p, t)) 1 else 0) + (if (visFor(3, m, p, t)) 1 else 0) +
|
|
127
|
+
(if (visFor(4, m, p, t)) 1 else 0) + (if (visFor(5, m, p, t)) 1 else 0)
|
|
128
|
+
def firstFor(m: int, p: int, t: int): int =
|
|
129
|
+
if (visFor(0, m, p, t)) 0 else if (visFor(1, m, p, t)) 1
|
|
130
|
+
else if (visFor(2, m, p, t)) 2 else if (visFor(3, m, p, t)) 3
|
|
131
|
+
else if (visFor(4, m, p, t)) 4 else if (visFor(5, m, p, t)) 5 else NONE
|
|
132
|
+
def lastFor(m: int, p: int, t: int): int =
|
|
133
|
+
if (visFor(5, m, p, t)) 5 else if (visFor(4, m, p, t)) 4
|
|
134
|
+
else if (visFor(3, m, p, t)) 3 else if (visFor(2, m, p, t)) 2
|
|
135
|
+
else if (visFor(1, m, p, t)) 1 else if (visFor(0, m, p, t)) 0 else NONE
|
|
136
|
+
def visCount: int = count(mode, picked, text)
|
|
137
|
+
def firstVis: int = firstFor(mode, picked, text)
|
|
138
|
+
def lastVis: int = lastFor(mode, picked, text)
|
|
139
|
+
def nextVis(from: int): int =
|
|
140
|
+
if (vis(cyc(from, 1))) cyc(from, 1) else if (vis(cyc(from, 2))) cyc(from, 2)
|
|
141
|
+
else if (vis(cyc(from, 3))) cyc(from, 3) else if (vis(cyc(from, 4))) cyc(from, 4)
|
|
142
|
+
else if (vis(cyc(from, 5))) cyc(from, 5) else if (vis(cyc(from, 6))) cyc(from, 6)
|
|
143
|
+
else NONE
|
|
144
|
+
def prevVis(from: int): int =
|
|
145
|
+
if (vis(cycBack(from, 1))) cycBack(from, 1) else if (vis(cycBack(from, 2))) cycBack(from, 2)
|
|
146
|
+
else if (vis(cycBack(from, 3))) cycBack(from, 3) else if (vis(cycBack(from, 4))) cycBack(from, 4)
|
|
147
|
+
else if (vis(cycBack(from, 5))) cycBack(from, 5) else if (vis(cycBack(from, 6))) cycBack(from, 6)
|
|
148
|
+
else NONE
|
|
149
|
+
|
|
150
|
+
// after an edit the textbox holds typed text (picked == NONE)
|
|
151
|
+
def editBase: int = if (picked >= 0) 8 else text
|
|
152
|
+
def typedText(c: int): int = typeNext(editBase, c)
|
|
153
|
+
def backText: int = if (picked >= 0) firstLetterNode(picked) else parentNode(text)
|
|
154
|
+
def edits: bool = or(text != 0, picked >= 0)
|
|
155
|
+
def editOpen(m: int, t: int): bool =
|
|
156
|
+
and(count(m, NONE, t) > 0, or(m == 0, t != 0))
|
|
157
|
+
// list / none return visual focus to the textbox; both re-selects the first match
|
|
158
|
+
def editActive(m: int, t: int): int =
|
|
159
|
+
if (and(editOpen(m, t), m == 2)) firstFor(m, NONE, t) else NONE
|
|
160
|
+
|
|
161
|
+
def canOpen: bool = visCount > 0
|
|
162
|
+
def downActive: int =
|
|
163
|
+
if (open) (if (active >= 0) nextVis(active) else firstVis)
|
|
164
|
+
else (if (canOpen) firstVis else NONE)
|
|
165
|
+
def upActive: int =
|
|
166
|
+
if (open) (if (active >= 0) prevVis(active) else lastVis)
|
|
167
|
+
else (if (canOpen) lastVis else NONE)
|
|
168
|
+
// Enter accepts the visually focused option
|
|
169
|
+
def accepts: bool = and(focused, active >= 0)
|
|
170
|
+
// leaving the input accepts the auto-selected suggestion in mode both only
|
|
171
|
+
def blurAccepts: bool = and(mode == 2, active >= 0)
|
|
172
|
+
def escapeClears: bool = and(and(focused, not(open)), and(mode != 0, edits))
|
|
173
|
+
|
|
174
|
+
// --- derived ARIA attribute contract -----------------------------------
|
|
175
|
+
def ariaAutocomplete: str = if (mode == 0) "none" else if (mode == 1) "list" else "both"
|
|
176
|
+
def ariaExpanded: bool = open
|
|
177
|
+
def ariaActivedescendant: int = if (open) active else NONE
|
|
178
|
+
def ariaSelected(i: int): bool = and(open, i == active)
|
|
179
|
+
def inlineCompletion: bool = and(mode == 2, active >= 0)
|
|
180
|
+
def selectedCount: int =
|
|
181
|
+
(if (ariaSelected(0)) 1 else 0) + (if (ariaSelected(1)) 1 else 0) +
|
|
182
|
+
(if (ariaSelected(2)) 1 else 0) + (if (ariaSelected(3)) 1 else 0) +
|
|
183
|
+
(if (ariaSelected(4)) 1 else 0) + (if (ariaSelected(5)) 1 else 0)
|
|
184
|
+
def isEdit(e: str): bool = or(e == "type_char", e == "backspace")
|
|
185
|
+
def isNavigation(e: str): bool =
|
|
186
|
+
or(e == "arrow_down", e == "arrow_up", e == "alt_arrow_down", e == "alt_arrow_up",
|
|
187
|
+
e == "arrow_left", e == "arrow_right", e == "home", e == "end", e == "delete")
|
|
188
|
+
def mayChangeValue(e: str): bool =
|
|
189
|
+
or(e == "enter", e == "tab", e == "blur", e == "click_option", e == "set_value",
|
|
190
|
+
e == "type_char", e == "backspace", e == "escape")
|
|
191
|
+
|
|
192
|
+
// --- request / commit (map ticket rt1p) --------------------------------
|
|
193
|
+
// controlled host that keeps the old value: the request is rejected
|
|
194
|
+
def holds: bool = and(controlled, not(accepting))
|
|
195
|
+
// controlled host that writes the intent back through the value prop
|
|
196
|
+
def follows: bool = and(controlled, accepting)
|
|
197
|
+
def verdict: str = if (holds) "rejected" else "accepted"
|
|
198
|
+
def landedInt(h: bool, current: int, want: int): int = if (h) current else want
|
|
199
|
+
def propAfter(f: bool, p: int, want: int): int = if (f) want else p
|
|
200
|
+
|
|
201
|
+
action init = all {
|
|
202
|
+
mode' = 1,
|
|
203
|
+
open' = false,
|
|
204
|
+
text' = 0,
|
|
205
|
+
picked' = NONE,
|
|
206
|
+
active' = NONE,
|
|
207
|
+
focused' = false,
|
|
208
|
+
lastEvent' = "init",
|
|
209
|
+
valueChanged' = false,
|
|
210
|
+
wasOpen' = false,
|
|
211
|
+
controlled' = false,
|
|
212
|
+
accepting' = false,
|
|
213
|
+
valueProp' = NONE,
|
|
214
|
+
requested' = NONE,
|
|
215
|
+
outcome' = "none",
|
|
216
|
+
base' = NONE,
|
|
217
|
+
requests' = 0
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// --- focus / blur --------------------------------------------------------
|
|
221
|
+
action focus = all {
|
|
222
|
+
focused' = true,
|
|
223
|
+
lastEvent' = "focus",
|
|
224
|
+
valueChanged' = false,
|
|
225
|
+
wasOpen' = open,
|
|
226
|
+
mode' = mode,
|
|
227
|
+
open' = open,
|
|
228
|
+
text' = text,
|
|
229
|
+
picked' = landedInt(holds, picked, picked),
|
|
230
|
+
controlled' = controlled,
|
|
231
|
+
accepting' = accepting,
|
|
232
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
233
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
234
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
235
|
+
base' = if ((picked) != picked) picked else base,
|
|
236
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
237
|
+
active' = active
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// focus leaves the input by pointer
|
|
241
|
+
action blur = all {
|
|
242
|
+
open' = false,
|
|
243
|
+
active' = NONE,
|
|
244
|
+
picked' = landedInt(holds, picked, if (blurAccepts) active else picked),
|
|
245
|
+
controlled' = controlled,
|
|
246
|
+
accepting' = accepting,
|
|
247
|
+
valueProp' = propAfter(follows, valueProp, if (blurAccepts) active else picked),
|
|
248
|
+
requested' = if ((if (blurAccepts) active else picked) != picked) if (blurAccepts) active else picked else requested,
|
|
249
|
+
outcome' = if ((if (blurAccepts) active else picked) != picked) verdict else outcome,
|
|
250
|
+
base' = if ((if (blurAccepts) active else picked) != picked) picked else base,
|
|
251
|
+
requests' = if ((if (blurAccepts) active else picked) != picked) requests + 1 else requests,
|
|
252
|
+
focused' = false,
|
|
253
|
+
lastEvent' = "blur",
|
|
254
|
+
valueChanged' = and(blurAccepts, picked != active),
|
|
255
|
+
wasOpen' = open,
|
|
256
|
+
mode' = mode,
|
|
257
|
+
text' = text
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Tab: same as blur; the popup is out of the Tab sequence
|
|
261
|
+
action tab = all {
|
|
262
|
+
open' = if (focused) false else open,
|
|
263
|
+
active' = if (focused) NONE else active,
|
|
264
|
+
picked' = landedInt(holds, picked, if (and(focused, blurAccepts)) active else picked),
|
|
265
|
+
controlled' = controlled,
|
|
266
|
+
accepting' = accepting,
|
|
267
|
+
valueProp' = propAfter(follows, valueProp, if (and(focused, blurAccepts)) active else picked),
|
|
268
|
+
requested' = if ((if (and(focused, blurAccepts)) active else picked) != picked) if (and(focused, blurAccepts)) active else picked else requested,
|
|
269
|
+
outcome' = if ((if (and(focused, blurAccepts)) active else picked) != picked) verdict else outcome,
|
|
270
|
+
base' = if ((if (and(focused, blurAccepts)) active else picked) != picked) picked else base,
|
|
271
|
+
requests' = if ((if (and(focused, blurAccepts)) active else picked) != picked) requests + 1 else requests,
|
|
272
|
+
focused' = false,
|
|
273
|
+
lastEvent' = "tab",
|
|
274
|
+
valueChanged' = and(and(focused, blurAccepts), picked != active),
|
|
275
|
+
wasOpen' = open,
|
|
276
|
+
mode' = mode,
|
|
277
|
+
text' = text
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// --- text editing ---------------------------------------------------------
|
|
281
|
+
action type_char = {
|
|
282
|
+
nondet ch = Set(0, 1, 2, 3, 4, 5, 6).oneOf()
|
|
283
|
+
all {
|
|
284
|
+
text' = if (focused) typedText(ch) else text,
|
|
285
|
+
picked' = landedInt(holds, picked, if (focused) NONE else picked),
|
|
286
|
+
controlled' = controlled,
|
|
287
|
+
accepting' = accepting,
|
|
288
|
+
valueProp' = propAfter(follows, valueProp, if (focused) NONE else picked),
|
|
289
|
+
requested' = if ((if (focused) NONE else picked) != picked) if (focused) NONE else picked else requested,
|
|
290
|
+
outcome' = if ((if (focused) NONE else picked) != picked) verdict else outcome,
|
|
291
|
+
base' = if ((if (focused) NONE else picked) != picked) picked else base,
|
|
292
|
+
requests' = if ((if (focused) NONE else picked) != picked) requests + 1 else requests,
|
|
293
|
+
open' = if (focused) editOpen(mode, typedText(ch)) else open,
|
|
294
|
+
active' = if (focused) editActive(mode, typedText(ch)) else active,
|
|
295
|
+
lastEvent' = "type_char",
|
|
296
|
+
valueChanged' = and(focused, or(typedText(ch) != text, picked >= 0)),
|
|
297
|
+
wasOpen' = open,
|
|
298
|
+
mode' = mode,
|
|
299
|
+
focused' = focused
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// with nothing to delete only the visual focus returns to the textbox
|
|
304
|
+
action backspace = all {
|
|
305
|
+
text' = if (and(focused, edits)) backText else text,
|
|
306
|
+
picked' = landedInt(holds, picked, if (focused) NONE else picked),
|
|
307
|
+
controlled' = controlled,
|
|
308
|
+
accepting' = accepting,
|
|
309
|
+
valueProp' = propAfter(follows, valueProp, if (focused) NONE else picked),
|
|
310
|
+
requested' = if ((if (focused) NONE else picked) != picked) if (focused) NONE else picked else requested,
|
|
311
|
+
outcome' = if ((if (focused) NONE else picked) != picked) verdict else outcome,
|
|
312
|
+
base' = if ((if (focused) NONE else picked) != picked) picked else base,
|
|
313
|
+
requests' = if ((if (focused) NONE else picked) != picked) requests + 1 else requests,
|
|
314
|
+
open' = if (and(focused, edits)) editOpen(mode, backText) else open,
|
|
315
|
+
active' = if (focused) (if (edits) editActive(mode, backText) else NONE) else active,
|
|
316
|
+
lastEvent' = "backspace",
|
|
317
|
+
valueChanged' = and(and(focused, edits), or(backText != text, picked >= 0)),
|
|
318
|
+
wasOpen' = open,
|
|
319
|
+
mode' = mode,
|
|
320
|
+
focused' = focused
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Delete drops the selected suggestion / inline completion; the popup stays
|
|
324
|
+
action delete = all {
|
|
325
|
+
active' = if (focused) NONE else active,
|
|
326
|
+
lastEvent' = "delete",
|
|
327
|
+
valueChanged' = false,
|
|
328
|
+
wasOpen' = open,
|
|
329
|
+
mode' = mode,
|
|
330
|
+
open' = open,
|
|
331
|
+
text' = text,
|
|
332
|
+
picked' = landedInt(holds, picked, picked),
|
|
333
|
+
controlled' = controlled,
|
|
334
|
+
accepting' = accepting,
|
|
335
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
336
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
337
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
338
|
+
base' = if ((picked) != picked) picked else base,
|
|
339
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
340
|
+
focused' = focused
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// --- navigation keys --------------------------------------------------------
|
|
344
|
+
action arrow_down = all {
|
|
345
|
+
open' = if (focused) or(open, canOpen) else open,
|
|
346
|
+
active' = if (focused) downActive else active,
|
|
347
|
+
lastEvent' = "arrow_down",
|
|
348
|
+
valueChanged' = false,
|
|
349
|
+
wasOpen' = open,
|
|
350
|
+
mode' = mode,
|
|
351
|
+
text' = text,
|
|
352
|
+
picked' = landedInt(holds, picked, picked),
|
|
353
|
+
controlled' = controlled,
|
|
354
|
+
accepting' = accepting,
|
|
355
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
356
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
357
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
358
|
+
base' = if ((picked) != picked) picked else base,
|
|
359
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
360
|
+
focused' = focused
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
action arrow_up = all {
|
|
364
|
+
open' = if (focused) or(open, canOpen) else open,
|
|
365
|
+
active' = if (focused) upActive else active,
|
|
366
|
+
lastEvent' = "arrow_up",
|
|
367
|
+
valueChanged' = false,
|
|
368
|
+
wasOpen' = open,
|
|
369
|
+
mode' = mode,
|
|
370
|
+
text' = text,
|
|
371
|
+
picked' = landedInt(holds, picked, picked),
|
|
372
|
+
controlled' = controlled,
|
|
373
|
+
accepting' = accepting,
|
|
374
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
375
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
376
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
377
|
+
base' = if ((picked) != picked) picked else base,
|
|
378
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
379
|
+
focused' = focused
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// opens without moving the visual focus
|
|
383
|
+
action alt_arrow_down = all {
|
|
384
|
+
open' = if (focused) or(open, canOpen) else open,
|
|
385
|
+
lastEvent' = "alt_arrow_down",
|
|
386
|
+
valueChanged' = false,
|
|
387
|
+
wasOpen' = open,
|
|
388
|
+
mode' = mode,
|
|
389
|
+
text' = text,
|
|
390
|
+
picked' = landedInt(holds, picked, picked),
|
|
391
|
+
controlled' = controlled,
|
|
392
|
+
accepting' = accepting,
|
|
393
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
394
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
395
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
396
|
+
base' = if ((picked) != picked) picked else base,
|
|
397
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
398
|
+
active' = active,
|
|
399
|
+
focused' = focused
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// closes the popup; visual focus returns to the textbox
|
|
403
|
+
action alt_arrow_up = all {
|
|
404
|
+
open' = if (focused) false else open,
|
|
405
|
+
active' = if (focused) NONE else active,
|
|
406
|
+
lastEvent' = "alt_arrow_up",
|
|
407
|
+
valueChanged' = false,
|
|
408
|
+
wasOpen' = open,
|
|
409
|
+
mode' = mode,
|
|
410
|
+
text' = text,
|
|
411
|
+
picked' = landedInt(holds, picked, picked),
|
|
412
|
+
controlled' = controlled,
|
|
413
|
+
accepting' = accepting,
|
|
414
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
415
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
416
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
417
|
+
base' = if ((picked) != picked) picked else base,
|
|
418
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
419
|
+
focused' = focused
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Left / Right / Home / End: visual focus back to the textbox, popup stays open
|
|
423
|
+
action arrow_left = all {
|
|
424
|
+
active' = if (focused) NONE else active,
|
|
425
|
+
lastEvent' = "arrow_left",
|
|
426
|
+
valueChanged' = false,
|
|
427
|
+
wasOpen' = open,
|
|
428
|
+
mode' = mode,
|
|
429
|
+
open' = open,
|
|
430
|
+
text' = text,
|
|
431
|
+
picked' = landedInt(holds, picked, picked),
|
|
432
|
+
controlled' = controlled,
|
|
433
|
+
accepting' = accepting,
|
|
434
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
435
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
436
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
437
|
+
base' = if ((picked) != picked) picked else base,
|
|
438
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
439
|
+
focused' = focused
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
action arrow_right = all {
|
|
443
|
+
active' = if (focused) NONE else active,
|
|
444
|
+
lastEvent' = "arrow_right",
|
|
445
|
+
valueChanged' = false,
|
|
446
|
+
wasOpen' = open,
|
|
447
|
+
mode' = mode,
|
|
448
|
+
open' = open,
|
|
449
|
+
text' = text,
|
|
450
|
+
picked' = landedInt(holds, picked, picked),
|
|
451
|
+
controlled' = controlled,
|
|
452
|
+
accepting' = accepting,
|
|
453
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
454
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
455
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
456
|
+
base' = if ((picked) != picked) picked else base,
|
|
457
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
458
|
+
focused' = focused
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
action home = all {
|
|
462
|
+
active' = if (focused) NONE else active,
|
|
463
|
+
lastEvent' = "home",
|
|
464
|
+
valueChanged' = false,
|
|
465
|
+
wasOpen' = open,
|
|
466
|
+
mode' = mode,
|
|
467
|
+
open' = open,
|
|
468
|
+
text' = text,
|
|
469
|
+
picked' = landedInt(holds, picked, picked),
|
|
470
|
+
controlled' = controlled,
|
|
471
|
+
accepting' = accepting,
|
|
472
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
473
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
474
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
475
|
+
base' = if ((picked) != picked) picked else base,
|
|
476
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
477
|
+
focused' = focused
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
action end = all {
|
|
481
|
+
active' = if (focused) NONE else active,
|
|
482
|
+
lastEvent' = "end",
|
|
483
|
+
valueChanged' = false,
|
|
484
|
+
wasOpen' = open,
|
|
485
|
+
mode' = mode,
|
|
486
|
+
open' = open,
|
|
487
|
+
text' = text,
|
|
488
|
+
picked' = landedInt(holds, picked, picked),
|
|
489
|
+
controlled' = controlled,
|
|
490
|
+
accepting' = accepting,
|
|
491
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
492
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
493
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
494
|
+
base' = if ((picked) != picked) picked else base,
|
|
495
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
496
|
+
focused' = focused
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// --- commit / dismiss ---------------------------------------------------
|
|
500
|
+
// accepts the focused option and closes; without one it only closes
|
|
501
|
+
action enter = all {
|
|
502
|
+
open' = if (focused) false else open,
|
|
503
|
+
picked' = landedInt(holds, picked, if (accepts) active else picked),
|
|
504
|
+
controlled' = controlled,
|
|
505
|
+
accepting' = accepting,
|
|
506
|
+
valueProp' = propAfter(follows, valueProp, if (accepts) active else picked),
|
|
507
|
+
requested' = if ((if (accepts) active else picked) != picked) if (accepts) active else picked else requested,
|
|
508
|
+
outcome' = if ((if (accepts) active else picked) != picked) verdict else outcome,
|
|
509
|
+
base' = if ((if (accepts) active else picked) != picked) picked else base,
|
|
510
|
+
requests' = if ((if (accepts) active else picked) != picked) requests + 1 else requests,
|
|
511
|
+
active' = if (focused) NONE else active,
|
|
512
|
+
lastEvent' = "enter",
|
|
513
|
+
valueChanged' = and(accepts, picked != active),
|
|
514
|
+
wasOpen' = open,
|
|
515
|
+
mode' = mode,
|
|
516
|
+
text' = text,
|
|
517
|
+
focused' = focused
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// open: close, keep the value; closed: clear the textbox (not in mode none)
|
|
521
|
+
action escape = all {
|
|
522
|
+
open' = if (focused) false else open,
|
|
523
|
+
active' = if (focused) NONE else active,
|
|
524
|
+
text' = if (escapeClears) 0 else text,
|
|
525
|
+
picked' = landedInt(holds, picked, if (escapeClears) NONE else picked),
|
|
526
|
+
controlled' = controlled,
|
|
527
|
+
accepting' = accepting,
|
|
528
|
+
valueProp' = propAfter(follows, valueProp, if (escapeClears) NONE else picked),
|
|
529
|
+
requested' = if ((if (escapeClears) NONE else picked) != picked) if (escapeClears) NONE else picked else requested,
|
|
530
|
+
outcome' = if ((if (escapeClears) NONE else picked) != picked) verdict else outcome,
|
|
531
|
+
base' = if ((if (escapeClears) NONE else picked) != picked) picked else base,
|
|
532
|
+
requests' = if ((if (escapeClears) NONE else picked) != picked) requests + 1 else requests,
|
|
533
|
+
lastEvent' = "escape",
|
|
534
|
+
valueChanged' = escapeClears,
|
|
535
|
+
wasOpen' = open,
|
|
536
|
+
mode' = mode,
|
|
537
|
+
focused' = focused
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// --- pointer -------------------------------------------------------------
|
|
541
|
+
// click on the input toggles the popup; DOM focus stays on the input
|
|
542
|
+
action click_input = all {
|
|
543
|
+
focused' = true,
|
|
544
|
+
open' = if (open) false else canOpen,
|
|
545
|
+
active' = NONE,
|
|
546
|
+
lastEvent' = "click_input",
|
|
547
|
+
valueChanged' = false,
|
|
548
|
+
wasOpen' = open,
|
|
549
|
+
mode' = mode,
|
|
550
|
+
text' = text,
|
|
551
|
+
picked' = picked,
|
|
552
|
+
controlled' = controlled,
|
|
553
|
+
accepting' = accepting,
|
|
554
|
+
valueProp' = valueProp,
|
|
555
|
+
requested' = requested,
|
|
556
|
+
outcome' = outcome,
|
|
557
|
+
base' = base,
|
|
558
|
+
requests' = requests
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// the Open button (tabindex -1) toggles the popup and focuses the input
|
|
562
|
+
action click_button = all {
|
|
563
|
+
focused' = true,
|
|
564
|
+
open' = if (open) false else canOpen,
|
|
565
|
+
active' = NONE,
|
|
566
|
+
lastEvent' = "click_button",
|
|
567
|
+
valueChanged' = false,
|
|
568
|
+
wasOpen' = open,
|
|
569
|
+
mode' = mode,
|
|
570
|
+
text' = text,
|
|
571
|
+
picked' = picked,
|
|
572
|
+
controlled' = controlled,
|
|
573
|
+
accepting' = accepting,
|
|
574
|
+
valueProp' = valueProp,
|
|
575
|
+
requested' = requested,
|
|
576
|
+
outcome' = outcome,
|
|
577
|
+
base' = base,
|
|
578
|
+
requests' = requests
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// clicking a visible option of the open popup commits it
|
|
582
|
+
action click_option = {
|
|
583
|
+
nondet i = Set(0, 1, 2, 3, 4, 5).oneOf()
|
|
584
|
+
all {
|
|
585
|
+
picked' = landedInt(holds, picked, if (and(open, vis(i))) i else picked),
|
|
586
|
+
controlled' = controlled,
|
|
587
|
+
accepting' = accepting,
|
|
588
|
+
valueProp' = propAfter(follows, valueProp, if (and(open, vis(i))) i else picked),
|
|
589
|
+
requested' = if ((if (and(open, vis(i))) i else picked) != picked) if (and(open, vis(i))) i else picked else requested,
|
|
590
|
+
outcome' = if ((if (and(open, vis(i))) i else picked) != picked) verdict else outcome,
|
|
591
|
+
base' = if ((if (and(open, vis(i))) i else picked) != picked) picked else base,
|
|
592
|
+
requests' = if ((if (and(open, vis(i))) i else picked) != picked) requests + 1 else requests,
|
|
593
|
+
open' = if (and(open, vis(i))) false else open,
|
|
594
|
+
active' = if (and(open, vis(i))) NONE else active,
|
|
595
|
+
focused' = if (and(open, vis(i))) true else focused,
|
|
596
|
+
lastEvent' = "click_option",
|
|
597
|
+
valueChanged' = and(and(open, vis(i)), picked != i),
|
|
598
|
+
wasOpen' = open,
|
|
599
|
+
mode' = mode,
|
|
600
|
+
text' = text
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// --- prop changes ---------------------------------------------------------
|
|
605
|
+
// aria-autocomplete prop swapped: the popup closes, the value is kept
|
|
606
|
+
action set_autocomplete = {
|
|
607
|
+
nondet m = Set(0, 1, 2).oneOf()
|
|
608
|
+
all {
|
|
609
|
+
mode' = m,
|
|
610
|
+
open' = false,
|
|
611
|
+
active' = NONE,
|
|
612
|
+
lastEvent' = "set_autocomplete",
|
|
613
|
+
valueChanged' = false,
|
|
614
|
+
wasOpen' = open,
|
|
615
|
+
text' = text,
|
|
616
|
+
picked' = landedInt(holds, picked, picked),
|
|
617
|
+
controlled' = controlled,
|
|
618
|
+
accepting' = accepting,
|
|
619
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
620
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
621
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
622
|
+
base' = if ((picked) != picked) picked else base,
|
|
623
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
624
|
+
focused' = focused
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// the committed suggestion is replaced from outside: the popup closes
|
|
629
|
+
action set_value = {
|
|
630
|
+
nondet i = Set(0, 1, 2, 3, 4, 5).oneOf()
|
|
631
|
+
all {
|
|
632
|
+
picked' = i,
|
|
633
|
+
open' = false,
|
|
634
|
+
active' = NONE,
|
|
635
|
+
lastEvent' = "set_value",
|
|
636
|
+
valueChanged' = picked != i,
|
|
637
|
+
wasOpen' = open,
|
|
638
|
+
mode' = mode,
|
|
639
|
+
text' = text,
|
|
640
|
+
focused' = focused,
|
|
641
|
+
controlled' = controlled,
|
|
642
|
+
accepting' = accepting,
|
|
643
|
+
valueProp' = if (controlled) i else valueProp,
|
|
644
|
+
requested' = requested,
|
|
645
|
+
outcome' = "none",
|
|
646
|
+
base' = base,
|
|
647
|
+
requests' = requests
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// commit: the host flips its own copy of the picked suggestion
|
|
652
|
+
action hostToggle = all {
|
|
653
|
+
mode' = mode,
|
|
654
|
+
open' = open,
|
|
655
|
+
text' = text,
|
|
656
|
+
picked' = if (controlled) (if (picked >= 0) NONE else 0) else picked,
|
|
657
|
+
active' = NONE,
|
|
658
|
+
focused' = focused,
|
|
659
|
+
lastEvent' = "hostToggle",
|
|
660
|
+
valueChanged' = false,
|
|
661
|
+
wasOpen' = open,
|
|
662
|
+
controlled' = controlled,
|
|
663
|
+
accepting' = accepting,
|
|
664
|
+
valueProp' = if (controlled) (if (picked >= 0) NONE else 0) else valueProp,
|
|
665
|
+
requested' = requested,
|
|
666
|
+
outcome' = if (controlled) "none" else outcome,
|
|
667
|
+
base' = base,
|
|
668
|
+
requests' = requests
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// configuration: the host starts owning the picked suggestion, adopting it
|
|
672
|
+
action control = all {
|
|
673
|
+
mode' = mode,
|
|
674
|
+
open' = open,
|
|
675
|
+
text' = text,
|
|
676
|
+
picked' = picked,
|
|
677
|
+
active' = active,
|
|
678
|
+
focused' = focused,
|
|
679
|
+
lastEvent' = lastEvent,
|
|
680
|
+
valueChanged' = valueChanged,
|
|
681
|
+
wasOpen' = open,
|
|
682
|
+
controlled' = true,
|
|
683
|
+
accepting' = accepting,
|
|
684
|
+
valueProp' = picked,
|
|
685
|
+
requested' = requested,
|
|
686
|
+
outcome' = outcome,
|
|
687
|
+
base' = base,
|
|
688
|
+
requests' = requests
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// configuration: the controlled host answers every request with its intent
|
|
692
|
+
action acceptRequests = all {
|
|
693
|
+
mode' = mode,
|
|
694
|
+
open' = open,
|
|
695
|
+
text' = text,
|
|
696
|
+
picked' = picked,
|
|
697
|
+
active' = active,
|
|
698
|
+
focused' = focused,
|
|
699
|
+
lastEvent' = lastEvent,
|
|
700
|
+
valueChanged' = valueChanged,
|
|
701
|
+
wasOpen' = open,
|
|
702
|
+
controlled' = controlled,
|
|
703
|
+
accepting' = true,
|
|
704
|
+
valueProp' = valueProp,
|
|
705
|
+
requested' = requested,
|
|
706
|
+
outcome' = outcome,
|
|
707
|
+
base' = base,
|
|
708
|
+
requests' = requests
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
action step = any {
|
|
712
|
+
init, focus, blur, tab, type_char, backspace, delete, arrow_down, arrow_up,
|
|
713
|
+
alt_arrow_down, alt_arrow_up, arrow_left, arrow_right, home, end, enter,
|
|
714
|
+
escape, click_input, click_button, click_option, set_autocomplete, set_value,
|
|
715
|
+
hostToggle, control, acceptRequests
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// --- invariants (APG guarantees) ----------------------------------------
|
|
719
|
+
// aria-activedescendant refers to a currently visible option of an open popup
|
|
720
|
+
// (a controlled host may reject an edit, so the committed picked suggestion
|
|
721
|
+
// can lag the text the visual focus was computed from)
|
|
722
|
+
val ActiveDescendantRefersToVisibleOption: bool =
|
|
723
|
+
or(controlled, or(active == NONE, and(open, vis(active))))
|
|
724
|
+
// an open popup is never empty
|
|
725
|
+
val OpenPopupHasSuggestions: bool = or(controlled, or(not(open), visCount > 0))
|
|
726
|
+
// DOM focus stays on the combobox while its popup is open
|
|
727
|
+
val DomFocusStaysOnCombobox: bool = or(not(open), focused)
|
|
728
|
+
// rendered attributes agree with the state; the inline completion is only
|
|
729
|
+
// ever exposed under aria-autocomplete="both"
|
|
730
|
+
val AriaContractConsistent: bool = and(
|
|
731
|
+
ariaActivedescendant == active,
|
|
732
|
+
selectedCount == (if (active >= 0) 1 else 0),
|
|
733
|
+
or(not(inlineCompletion), ariaAutocomplete == "both")
|
|
734
|
+
)
|
|
735
|
+
// editing returns visual focus to the textbox (none / list never select
|
|
736
|
+
// automatically) ...
|
|
737
|
+
val EditingReturnsVisualFocusToTextbox: bool =
|
|
738
|
+
or(not(isEdit(lastEvent)), mode == 2, active == NONE)
|
|
739
|
+
// ... except that mode "both" selects the first match automatically
|
|
740
|
+
val BothModeAutoSelectsFirstMatchOnEdit: bool =
|
|
741
|
+
or(controlled, or(not(isEdit(lastEvent)), mode != 2, active == NONE, active == firstVis))
|
|
742
|
+
// Escape on an open popup never changes the value
|
|
743
|
+
val EscapeOnOpenPopupKeepsValue: bool =
|
|
744
|
+
or(not(lastEvent == "escape"), not(wasOpen), not(valueChanged))
|
|
745
|
+
// navigating never changes the committed value
|
|
746
|
+
val NavigationKeepsValue: bool = or(not(isNavigation(lastEvent)), not(valueChanged))
|
|
747
|
+
// the value changes only on commit / edit / dismiss / prop events
|
|
748
|
+
val ValueChangesOnlyOnCommitOrEdit: bool = or(not(valueChanged), mayChangeValue(lastEvent))
|
|
749
|
+
// losing focus closes the popup
|
|
750
|
+
val FocusLossClosesPopup: bool =
|
|
751
|
+
or(not(or(lastEvent == "blur", lastEvent == "tab")), and(not(open), not(focused)))
|
|
752
|
+
val StateInRange: bool = and(
|
|
753
|
+
mode >= 0, mode <= 2, text >= 0, text <= 8,
|
|
754
|
+
picked >= NONE, picked < OPTION_COUNT, active >= NONE, active < OPTION_COUNT
|
|
755
|
+
)
|
|
756
|
+
// callbacks only ever count up from zero
|
|
757
|
+
val RequestsNonNegative: bool = requests >= 0
|
|
758
|
+
// controlled: only the value prop sets the picked suggestion, so no event commits
|
|
759
|
+
val ControlledFollowsProp: bool = or(not(controlled), picked == valueProp)
|
|
760
|
+
// a rejected request leaves the picked suggestion as it found it
|
|
761
|
+
val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), picked == base)
|
|
762
|
+
// an accepted request committed exactly its intent
|
|
763
|
+
val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), picked == requested)
|
|
764
|
+
}
|