@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,626 @@
|
|
|
1
|
+
// APG combobox — the select-only combobox (no text input, listbox popup).
|
|
2
|
+
//
|
|
3
|
+
// Sources (W3C WAI-ARIA Authoring Practices Guide):
|
|
4
|
+
// https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
|
|
5
|
+
// - "About This Pattern", "Keyboard Interaction" (Combobox + Listbox Popup),
|
|
6
|
+
// "WAI-ARIA Roles, States, and Properties"
|
|
7
|
+
// https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/
|
|
8
|
+
// - "Accessibility Features", "Keyboard Support" (Closed Combobox, Listbox
|
|
9
|
+
// Popup), "Role, Property, State, and Tabindex Attributes"
|
|
10
|
+
//
|
|
11
|
+
// Modelled behaviour
|
|
12
|
+
// - DOM focus never leaves the combobox element: the popup's options are
|
|
13
|
+
// visually focused through aria-activedescendant (`active`), never by
|
|
14
|
+
// moving DOM focus. A popup can only be open while the combobox has DOM
|
|
15
|
+
// focus; blur / Tab commit the active option and close it.
|
|
16
|
+
// - Navigating the options (arrows, Home/End, PageUp/PageDown, typeahead)
|
|
17
|
+
// never changes the value. The value is committed by Enter, Space, Tab,
|
|
18
|
+
// Alt+ArrowUp, blur, an option click, or an external `set_value` prop
|
|
19
|
+
// change. Escape and a click on the closed-again input keep the value.
|
|
20
|
+
// - Closed combobox: ArrowDown / Alt+ArrowDown / Enter / Space open the popup
|
|
21
|
+
// without moving the visual focus off the current option (the selected
|
|
22
|
+
// option, or the first when none is selected); ArrowUp opens on the first
|
|
23
|
+
// option; Home / End open on the first / last option; a printable character
|
|
24
|
+
// opens the popup on the first option starting with it.
|
|
25
|
+
// - Typeahead: a character matches the first option starting with it; the
|
|
26
|
+
// same character typed in succession cycles among options with that letter.
|
|
27
|
+
// - The ARIA attribute contract is derived: aria-expanded,
|
|
28
|
+
// aria-activedescendant (present iff open), aria-selected (exactly the
|
|
29
|
+
// active option, only while open).
|
|
30
|
+
//
|
|
31
|
+
// Request vs commit (map ticket rt1p). The widget's observable state is the pair
|
|
32
|
+
// (selected value, open): both are user-changeable and host-ownable, so they are
|
|
33
|
+
// packed into one controlled value with `stateOf` (injective for value -1..5 and
|
|
34
|
+
// the open bit). Every event a user sees as a change — blur, the arrow /
|
|
35
|
+
// Home / End / typing open keys, Enter / Space / Tab / Alt+ArrowUp, the pointer
|
|
36
|
+
// clicks — is a *request* carrying its intent (`requested`); it fires
|
|
37
|
+
// onValueChange / onOpenChange (`requests` counts the calls) when the intent
|
|
38
|
+
// differs from the current state, and then:
|
|
39
|
+
// uncontrolled the request is accepted: the state commits the intent
|
|
40
|
+
// controlled, holds the host ignores it ("rejected"): the state is untouched
|
|
41
|
+
// controlled, accepts the host answers with the same value (`accepting`): the
|
|
42
|
+
// state commits through the controlled value, not the event
|
|
43
|
+
// `valueProp` is the only thing that changes the state while `controlled`;
|
|
44
|
+
// `set_value` / `setOpen` / `hostToggle` are the host writing it. `control` and
|
|
45
|
+
// `acceptRequests` are configuration events (init is literal-only).
|
|
46
|
+
//
|
|
47
|
+
// Invariants for the model: ControlledFollowsProp, RejectedRequestUntouched,
|
|
48
|
+
// AcceptedRequestCommits.
|
|
49
|
+
//
|
|
50
|
+
// `outcome` is "none" | "accepted" | "rejected" for the last request; a host
|
|
51
|
+
// write settles it back to "none". `base` is the state when it was received.
|
|
52
|
+
//
|
|
53
|
+
// Configuration variants are zero-argument defs / nondet pools: the option
|
|
54
|
+
// count and PageUp/PageDown stride (`OPTION_COUNT`, `PAGE_SIZE`), the option
|
|
55
|
+
// first letters (`letterOf`), and the typed-character pool (`type_char`).
|
|
56
|
+
//
|
|
57
|
+
// Deliberate omissions
|
|
58
|
+
// - Typeahead of multi-character strings typed in quick succession (the
|
|
59
|
+
// timeout-based buffer); only single-character matching and same-letter
|
|
60
|
+
// cycling are modelled (`typed` is the last character, reset by any other
|
|
61
|
+
// event, standing in for the timeout).
|
|
62
|
+
// - Scrolling the active option into view, hover styling, the label,
|
|
63
|
+
// aria-controls / aria-labelledby wiring (static ids).
|
|
64
|
+
// - Disabled / required props, multi-select, option groups.
|
|
65
|
+
// - Pointer hover over an option does not move the active option.
|
|
66
|
+
//
|
|
67
|
+
// Generator notes: the generator supports no action preconditions, so every
|
|
68
|
+
// key action guards its assignments with `focused`; vars hold int sentinels
|
|
69
|
+
// (`NONE = -1`) because only str/int/bool vars are supported.
|
|
70
|
+
module combobox_select_only {
|
|
71
|
+
import aria.* from "../../shared/aria"
|
|
72
|
+
|
|
73
|
+
var open: bool // popup (listbox) displayed == aria-expanded
|
|
74
|
+
var active: int // aria-activedescendant option index (NONE when closed)
|
|
75
|
+
var value: int // committed value: option index, NONE = placeholder
|
|
76
|
+
var focused: bool // DOM focus is on the combobox element
|
|
77
|
+
var typed: int // last typeahead character, NONE when the buffer is empty
|
|
78
|
+
var lastEvent: str // name of the last event, for transition invariants
|
|
79
|
+
var valueChanged: bool // the last event changed `value`
|
|
80
|
+
var controlled: bool // the host owns (value, open) (`value`/`open` props present)
|
|
81
|
+
var accepting: bool // controlled host that answers every request with its intent
|
|
82
|
+
var valueProp: int // the controlled state, packed with `stateOf`
|
|
83
|
+
var requested: int // intent of the last request, packed
|
|
84
|
+
var outcome: str // "none" | "accepted" | "rejected"
|
|
85
|
+
var base: int // state when the last request was received, packed
|
|
86
|
+
var requests: int // number of requests that fired a change callback
|
|
87
|
+
|
|
88
|
+
// --- configuration ------------------------------------------------------
|
|
89
|
+
def NONE: int = -1
|
|
90
|
+
def OPTION_COUNT: int = 6
|
|
91
|
+
def PAGE_SIZE: int = 4
|
|
92
|
+
// options: 0 Apple, 1 Banana, 2 Blueberry, 3 Cherry, 4 Cranberry, 5 Durian
|
|
93
|
+
def letterOf(i: int): int =
|
|
94
|
+
if (i == 0) 0 else if (i == 1) 1 else if (i == 2) 1 else if (i == 3) 2 else if (i == 4) 2 else 3
|
|
95
|
+
|
|
96
|
+
// --- helpers ------------------------------------------------------------
|
|
97
|
+
def minI(a: int, b: int): int = if (a < b) a else b
|
|
98
|
+
def maxI(a: int, b: int): int = if (a > b) a else b
|
|
99
|
+
def refersToOption(i: int): bool = and(i >= 0, i < OPTION_COUNT)
|
|
100
|
+
|
|
101
|
+
// the listbox opens on the selected option, or the first when none
|
|
102
|
+
def openActive: int = maxI(value, 0)
|
|
103
|
+
def openIfFocused: bool = if (focused) true else open
|
|
104
|
+
// a commit happens only while the key reaches an open popup with a focused option
|
|
105
|
+
def commits: bool = and(focused, open)
|
|
106
|
+
def commitsActive: bool = and(commits, active >= 0)
|
|
107
|
+
def committedValue: int = if (commitsActive) active else value
|
|
108
|
+
def commitChanged: bool = and(commitsActive, active != value)
|
|
109
|
+
|
|
110
|
+
// the first option with letter ch among OPTION_COUNT candidates from start
|
|
111
|
+
// (candidates and the start rule are shared/aria.qnt: ariaCycle0, ariaTaRepeatStart0)
|
|
112
|
+
def candidate(start: int, k: int): int = ariaCycle0(OPTION_COUNT, start, k)
|
|
113
|
+
def matchFrom(start: int, ch: int): int =
|
|
114
|
+
if (letterOf(candidate(start, 0)) == ch) candidate(start, 0)
|
|
115
|
+
else if (letterOf(candidate(start, 1)) == ch) candidate(start, 1)
|
|
116
|
+
else if (letterOf(candidate(start, 2)) == ch) candidate(start, 2)
|
|
117
|
+
else if (letterOf(candidate(start, 3)) == ch) candidate(start, 3)
|
|
118
|
+
else if (letterOf(candidate(start, 4)) == ch) candidate(start, 4)
|
|
119
|
+
else if (letterOf(candidate(start, 5)) == ch) candidate(start, 5)
|
|
120
|
+
else NONE
|
|
121
|
+
// a repeated character cycles on from the option after base, a new one starts at the top
|
|
122
|
+
def found(ch: int, base: int): int = matchFrom(ariaTaRepeatStart0(OPTION_COUNT, base, typed, ch), ch)
|
|
123
|
+
def typeActive(ch: int, base: int): int =
|
|
124
|
+
if (found(ch, base) == NONE) base else found(ch, base)
|
|
125
|
+
|
|
126
|
+
// --- request / commit (map ticket rt1p) --------------------------------
|
|
127
|
+
def b2i(b: bool): int = if (b) 1 else 0
|
|
128
|
+
// injective for value -1..5 and the open bit: (value + 1) * 2 + open
|
|
129
|
+
def stateOf(o: bool, v: int): int = (v + 1) * 2 + b2i(o)
|
|
130
|
+
def openOf(s: int): bool = s % 2 == 1
|
|
131
|
+
def valueOf(s: int): int = s / 2 - 1
|
|
132
|
+
def nowState: int = stateOf(open, value)
|
|
133
|
+
// controlled host that keeps the old value: the request is rejected
|
|
134
|
+
def holds: bool = and(controlled, not(accepting))
|
|
135
|
+
// controlled host that writes the intent back through the value prop
|
|
136
|
+
def follows: bool = and(controlled, accepting)
|
|
137
|
+
def verdict: str = if (holds) "rejected" else "accepted"
|
|
138
|
+
def landed(h: bool, current: bool, want: bool): bool = if (h) current else want
|
|
139
|
+
def landedInt(h: bool, current: int, want: int): int = if (h) current else want
|
|
140
|
+
def propAfter(f: bool, p: int, want: int): int = if (f) want else p
|
|
141
|
+
// the state / prop / ledger after a request for the packed `want`
|
|
142
|
+
def openAt(w: int): bool = landed(holds, open, openOf(w))
|
|
143
|
+
def valueAt(w: int): int = landedInt(holds, value, valueOf(w))
|
|
144
|
+
def propAt(w: int): int = propAfter(follows, valueProp, w)
|
|
145
|
+
def reqState(w: int): int = if (w != nowState) w else requested
|
|
146
|
+
def reqOutcome(w: int): str = if (w != nowState) verdict else outcome
|
|
147
|
+
def reqBase(w: int): int = if (w != nowState) nowState else base
|
|
148
|
+
def reqCount(w: int): int = if (w != nowState) requests + 1 else requests
|
|
149
|
+
// the packed intent of each change event
|
|
150
|
+
def blurW: int = stateOf(false, if (and(open, active >= 0)) active else value)
|
|
151
|
+
def openW: int = stateOf(openIfFocused, value)
|
|
152
|
+
def enterW: int = stateOf(if (focused) not(open) else open, committedValue)
|
|
153
|
+
def tabW: int = stateOf(if (focused) false else open, committedValue)
|
|
154
|
+
def escapeW: int = stateOf(if (focused) false else open, value)
|
|
155
|
+
def altUpW: int = stateOf(if (focused) false else open, committedValue)
|
|
156
|
+
def clickComboW: int = stateOf(not(open), value)
|
|
157
|
+
def clickOptionW(i: int): int = stateOf(false, if (open) i else value)
|
|
158
|
+
|
|
159
|
+
// --- derived ARIA attribute contract -----------------------------------
|
|
160
|
+
def ariaExpanded: bool = open
|
|
161
|
+
def ariaActivedescendant: int = if (open) active else NONE
|
|
162
|
+
def ariaSelected(i: int): bool = and(open, i == active)
|
|
163
|
+
def selectedCount: int =
|
|
164
|
+
(if (ariaSelected(0)) 1 else 0) + (if (ariaSelected(1)) 1 else 0) +
|
|
165
|
+
(if (ariaSelected(2)) 1 else 0) + (if (ariaSelected(3)) 1 else 0) +
|
|
166
|
+
(if (ariaSelected(4)) 1 else 0) + (if (ariaSelected(5)) 1 else 0)
|
|
167
|
+
def isCommitEvent(e: str): bool =
|
|
168
|
+
or(e == "enter", e == "space", e == "tab", e == "alt_arrow_up", e == "blur", e == "click_option", e == "set_value")
|
|
169
|
+
|
|
170
|
+
action init = all {
|
|
171
|
+
open' = false,
|
|
172
|
+
active' = NONE,
|
|
173
|
+
value' = NONE,
|
|
174
|
+
focused' = false,
|
|
175
|
+
typed' = NONE,
|
|
176
|
+
lastEvent' = "init",
|
|
177
|
+
valueChanged' = false,
|
|
178
|
+
controlled' = false,
|
|
179
|
+
accepting' = false,
|
|
180
|
+
valueProp' = stateOf(false, NONE),
|
|
181
|
+
requested' = stateOf(false, NONE),
|
|
182
|
+
outcome' = "none",
|
|
183
|
+
base' = stateOf(false, NONE),
|
|
184
|
+
requests' = 0
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// --- focus / blur --------------------------------------------------------
|
|
188
|
+
action focus = all {
|
|
189
|
+
focused' = true,
|
|
190
|
+
typed' = NONE,
|
|
191
|
+
lastEvent' = "focus",
|
|
192
|
+
valueChanged' = false,
|
|
193
|
+
open' = open,
|
|
194
|
+
active' = active,
|
|
195
|
+
value' = value,
|
|
196
|
+
controlled' = controlled,
|
|
197
|
+
accepting' = accepting,
|
|
198
|
+
valueProp' = valueProp,
|
|
199
|
+
requested' = requested,
|
|
200
|
+
outcome' = outcome,
|
|
201
|
+
base' = base,
|
|
202
|
+
requests' = requests
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// focus leaves the combobox: an open popup commits its active option
|
|
206
|
+
action blur = all {
|
|
207
|
+
focused' = false,
|
|
208
|
+
open' = openAt(blurW),
|
|
209
|
+
active' = NONE,
|
|
210
|
+
value' = valueAt(blurW),
|
|
211
|
+
typed' = NONE,
|
|
212
|
+
lastEvent' = "blur",
|
|
213
|
+
valueChanged' = valueAt(blurW) != value,
|
|
214
|
+
controlled' = controlled,
|
|
215
|
+
accepting' = accepting,
|
|
216
|
+
valueProp' = propAt(blurW),
|
|
217
|
+
requested' = reqState(blurW),
|
|
218
|
+
outcome' = reqOutcome(blurW),
|
|
219
|
+
base' = reqBase(blurW),
|
|
220
|
+
requests' = reqCount(blurW)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// --- closed + open arrow keys -------------------------------------------
|
|
224
|
+
action arrow_down = all {
|
|
225
|
+
open' = openAt(openW),
|
|
226
|
+
active' = if (focused) (if (open) minI(active + 1, OPTION_COUNT - 1) else openActive) else active,
|
|
227
|
+
typed' = NONE,
|
|
228
|
+
lastEvent' = "arrow_down",
|
|
229
|
+
valueChanged' = false,
|
|
230
|
+
value' = valueAt(openW),
|
|
231
|
+
focused' = focused,
|
|
232
|
+
controlled' = controlled,
|
|
233
|
+
accepting' = accepting,
|
|
234
|
+
valueProp' = propAt(openW),
|
|
235
|
+
requested' = reqState(openW),
|
|
236
|
+
outcome' = reqOutcome(openW),
|
|
237
|
+
base' = reqBase(openW),
|
|
238
|
+
requests' = reqCount(openW)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
action alt_arrow_down = all {
|
|
242
|
+
open' = openAt(openW),
|
|
243
|
+
active' = if (focused) (if (open) active else openActive) else active,
|
|
244
|
+
typed' = NONE,
|
|
245
|
+
lastEvent' = "alt_arrow_down",
|
|
246
|
+
valueChanged' = false,
|
|
247
|
+
value' = valueAt(openW),
|
|
248
|
+
focused' = focused,
|
|
249
|
+
controlled' = controlled,
|
|
250
|
+
accepting' = accepting,
|
|
251
|
+
valueProp' = propAt(openW),
|
|
252
|
+
requested' = reqState(openW),
|
|
253
|
+
outcome' = reqOutcome(openW),
|
|
254
|
+
base' = reqBase(openW),
|
|
255
|
+
requests' = reqCount(openW)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
action arrow_up = all {
|
|
259
|
+
open' = openAt(openW),
|
|
260
|
+
active' = if (focused) (if (open) maxI(active - 1, 0) else 0) else active,
|
|
261
|
+
typed' = NONE,
|
|
262
|
+
lastEvent' = "arrow_up",
|
|
263
|
+
valueChanged' = false,
|
|
264
|
+
value' = valueAt(openW),
|
|
265
|
+
focused' = focused,
|
|
266
|
+
controlled' = controlled,
|
|
267
|
+
accepting' = accepting,
|
|
268
|
+
valueProp' = propAt(openW),
|
|
269
|
+
requested' = reqState(openW),
|
|
270
|
+
outcome' = reqOutcome(openW),
|
|
271
|
+
base' = reqBase(openW),
|
|
272
|
+
requests' = reqCount(openW)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
action home = all {
|
|
276
|
+
open' = openAt(openW),
|
|
277
|
+
active' = if (focused) 0 else active,
|
|
278
|
+
typed' = NONE,
|
|
279
|
+
lastEvent' = "home",
|
|
280
|
+
valueChanged' = false,
|
|
281
|
+
value' = valueAt(openW),
|
|
282
|
+
focused' = focused,
|
|
283
|
+
controlled' = controlled,
|
|
284
|
+
accepting' = accepting,
|
|
285
|
+
valueProp' = propAt(openW),
|
|
286
|
+
requested' = reqState(openW),
|
|
287
|
+
outcome' = reqOutcome(openW),
|
|
288
|
+
base' = reqBase(openW),
|
|
289
|
+
requests' = reqCount(openW)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
action end = all {
|
|
293
|
+
open' = openAt(openW),
|
|
294
|
+
active' = if (focused) OPTION_COUNT - 1 else active,
|
|
295
|
+
typed' = NONE,
|
|
296
|
+
lastEvent' = "end",
|
|
297
|
+
valueChanged' = false,
|
|
298
|
+
value' = valueAt(openW),
|
|
299
|
+
focused' = focused,
|
|
300
|
+
controlled' = controlled,
|
|
301
|
+
accepting' = accepting,
|
|
302
|
+
valueProp' = propAt(openW),
|
|
303
|
+
requested' = reqState(openW),
|
|
304
|
+
outcome' = reqOutcome(openW),
|
|
305
|
+
base' = reqBase(openW),
|
|
306
|
+
requests' = reqCount(openW)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
action page_up = all {
|
|
310
|
+
active' = if (commits) maxI(active - PAGE_SIZE, 0) else active,
|
|
311
|
+
typed' = NONE,
|
|
312
|
+
lastEvent' = "page_up",
|
|
313
|
+
valueChanged' = false,
|
|
314
|
+
open' = open,
|
|
315
|
+
value' = value,
|
|
316
|
+
focused' = focused,
|
|
317
|
+
controlled' = controlled,
|
|
318
|
+
accepting' = accepting,
|
|
319
|
+
valueProp' = valueProp,
|
|
320
|
+
requested' = requested,
|
|
321
|
+
outcome' = outcome,
|
|
322
|
+
base' = base,
|
|
323
|
+
requests' = requests
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
action page_down = all {
|
|
327
|
+
active' = if (commits) minI(active + PAGE_SIZE, OPTION_COUNT - 1) else active,
|
|
328
|
+
typed' = NONE,
|
|
329
|
+
lastEvent' = "page_down",
|
|
330
|
+
valueChanged' = false,
|
|
331
|
+
open' = open,
|
|
332
|
+
value' = value,
|
|
333
|
+
focused' = focused,
|
|
334
|
+
controlled' = controlled,
|
|
335
|
+
accepting' = accepting,
|
|
336
|
+
valueProp' = valueProp,
|
|
337
|
+
requested' = requested,
|
|
338
|
+
outcome' = outcome,
|
|
339
|
+
base' = base,
|
|
340
|
+
requests' = requests
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// --- activation keys -----------------------------------------------------
|
|
344
|
+
// closed: open without moving; open: commit the active option and close
|
|
345
|
+
action enter = all {
|
|
346
|
+
open' = openAt(enterW),
|
|
347
|
+
active' = if (focused) (if (open) NONE else openActive) else active,
|
|
348
|
+
value' = valueAt(enterW),
|
|
349
|
+
typed' = NONE,
|
|
350
|
+
lastEvent' = "enter",
|
|
351
|
+
valueChanged' = valueAt(enterW) != value,
|
|
352
|
+
focused' = focused,
|
|
353
|
+
controlled' = controlled,
|
|
354
|
+
accepting' = accepting,
|
|
355
|
+
valueProp' = propAt(enterW),
|
|
356
|
+
requested' = reqState(enterW),
|
|
357
|
+
outcome' = reqOutcome(enterW),
|
|
358
|
+
base' = reqBase(enterW),
|
|
359
|
+
requests' = reqCount(enterW)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
action space = all {
|
|
363
|
+
open' = openAt(enterW),
|
|
364
|
+
active' = if (focused) (if (open) NONE else openActive) else active,
|
|
365
|
+
value' = valueAt(enterW),
|
|
366
|
+
typed' = NONE,
|
|
367
|
+
lastEvent' = "space",
|
|
368
|
+
valueChanged' = valueAt(enterW) != value,
|
|
369
|
+
focused' = focused,
|
|
370
|
+
controlled' = controlled,
|
|
371
|
+
accepting' = accepting,
|
|
372
|
+
valueProp' = propAt(enterW),
|
|
373
|
+
requested' = reqState(enterW),
|
|
374
|
+
outcome' = reqOutcome(enterW),
|
|
375
|
+
base' = reqBase(enterW),
|
|
376
|
+
requests' = reqCount(enterW)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Tab commits an open popup and moves DOM focus to the next page element
|
|
380
|
+
action tab = all {
|
|
381
|
+
open' = openAt(tabW),
|
|
382
|
+
active' = if (focused) NONE else active,
|
|
383
|
+
value' = valueAt(tabW),
|
|
384
|
+
focused' = false,
|
|
385
|
+
typed' = NONE,
|
|
386
|
+
lastEvent' = "tab",
|
|
387
|
+
valueChanged' = valueAt(tabW) != value,
|
|
388
|
+
controlled' = controlled,
|
|
389
|
+
accepting' = accepting,
|
|
390
|
+
valueProp' = propAt(tabW),
|
|
391
|
+
requested' = reqState(tabW),
|
|
392
|
+
outcome' = reqOutcome(tabW),
|
|
393
|
+
base' = reqBase(tabW),
|
|
394
|
+
requests' = reqCount(tabW)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Escape closes the popup and keeps the previous value
|
|
398
|
+
action escape = all {
|
|
399
|
+
open' = openAt(escapeW),
|
|
400
|
+
active' = if (focused) NONE else active,
|
|
401
|
+
typed' = NONE,
|
|
402
|
+
lastEvent' = "escape",
|
|
403
|
+
valueChanged' = false,
|
|
404
|
+
value' = valueAt(escapeW),
|
|
405
|
+
focused' = focused,
|
|
406
|
+
controlled' = controlled,
|
|
407
|
+
accepting' = accepting,
|
|
408
|
+
valueProp' = propAt(escapeW),
|
|
409
|
+
requested' = reqState(escapeW),
|
|
410
|
+
outcome' = reqOutcome(escapeW),
|
|
411
|
+
base' = reqBase(escapeW),
|
|
412
|
+
requests' = reqCount(escapeW)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Alt+ArrowUp on an open popup commits the active option and closes
|
|
416
|
+
action alt_arrow_up = all {
|
|
417
|
+
open' = openAt(altUpW),
|
|
418
|
+
active' = if (focused) NONE else active,
|
|
419
|
+
value' = valueAt(altUpW),
|
|
420
|
+
typed' = NONE,
|
|
421
|
+
lastEvent' = "alt_arrow_up",
|
|
422
|
+
valueChanged' = valueAt(altUpW) != value,
|
|
423
|
+
focused' = focused,
|
|
424
|
+
controlled' = controlled,
|
|
425
|
+
accepting' = accepting,
|
|
426
|
+
valueProp' = propAt(altUpW),
|
|
427
|
+
requested' = reqState(altUpW),
|
|
428
|
+
outcome' = reqOutcome(altUpW),
|
|
429
|
+
base' = reqBase(altUpW),
|
|
430
|
+
requests' = reqCount(altUpW)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// --- typeahead -----------------------------------------------------------
|
|
434
|
+
action type_char = {
|
|
435
|
+
nondet ch = Set(0, 1, 2, 3, 9).oneOf()
|
|
436
|
+
all {
|
|
437
|
+
open' = openAt(openW),
|
|
438
|
+
active' = if (focused) typeActive(ch, if (open) active else openActive) else active,
|
|
439
|
+
typed' = if (focused) ch else typed,
|
|
440
|
+
lastEvent' = "type_char",
|
|
441
|
+
valueChanged' = false,
|
|
442
|
+
value' = valueAt(openW),
|
|
443
|
+
focused' = focused,
|
|
444
|
+
controlled' = controlled,
|
|
445
|
+
accepting' = accepting,
|
|
446
|
+
valueProp' = propAt(openW),
|
|
447
|
+
requested' = reqState(openW),
|
|
448
|
+
outcome' = reqOutcome(openW),
|
|
449
|
+
base' = reqBase(openW),
|
|
450
|
+
requests' = reqCount(openW)
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// --- pointer -------------------------------------------------------------
|
|
455
|
+
// clicking the input focuses it and toggles the popup; the value is kept
|
|
456
|
+
action click_combobox = all {
|
|
457
|
+
focused' = true,
|
|
458
|
+
open' = openAt(clickComboW),
|
|
459
|
+
active' = if (open) NONE else openActive,
|
|
460
|
+
typed' = NONE,
|
|
461
|
+
lastEvent' = "click_combobox",
|
|
462
|
+
valueChanged' = false,
|
|
463
|
+
value' = valueAt(clickComboW),
|
|
464
|
+
controlled' = controlled,
|
|
465
|
+
accepting' = accepting,
|
|
466
|
+
valueProp' = propAt(clickComboW),
|
|
467
|
+
requested' = reqState(clickComboW),
|
|
468
|
+
outcome' = reqOutcome(clickComboW),
|
|
469
|
+
base' = reqBase(clickComboW),
|
|
470
|
+
requests' = reqCount(clickComboW)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// clicking an option of the open popup commits it and returns visual focus
|
|
474
|
+
action click_option = {
|
|
475
|
+
nondet i = Set(0, 1, 2, 3, 4, 5).oneOf()
|
|
476
|
+
all {
|
|
477
|
+
open' = openAt(clickOptionW(i)),
|
|
478
|
+
active' = NONE,
|
|
479
|
+
value' = valueAt(clickOptionW(i)),
|
|
480
|
+
focused' = if (open) true else focused,
|
|
481
|
+
typed' = NONE,
|
|
482
|
+
lastEvent' = "click_option",
|
|
483
|
+
valueChanged' = valueAt(clickOptionW(i)) != value,
|
|
484
|
+
controlled' = controlled,
|
|
485
|
+
accepting' = accepting,
|
|
486
|
+
valueProp' = propAt(clickOptionW(i)),
|
|
487
|
+
requested' = reqState(clickOptionW(i)),
|
|
488
|
+
outcome' = reqOutcome(clickOptionW(i)),
|
|
489
|
+
base' = reqBase(clickOptionW(i)),
|
|
490
|
+
requests' = reqCount(clickOptionW(i))
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// --- prop change ---------------------------------------------------------
|
|
495
|
+
// the external value replaces the committed one; popup state is untouched
|
|
496
|
+
action set_value = {
|
|
497
|
+
nondet i = Set(0, 1, 2, 3, 4, 5).oneOf()
|
|
498
|
+
all {
|
|
499
|
+
value' = i,
|
|
500
|
+
typed' = NONE,
|
|
501
|
+
lastEvent' = "set_value",
|
|
502
|
+
valueChanged' = i != value,
|
|
503
|
+
open' = open,
|
|
504
|
+
active' = active,
|
|
505
|
+
focused' = focused,
|
|
506
|
+
controlled' = controlled,
|
|
507
|
+
accepting' = accepting,
|
|
508
|
+
valueProp' = if (controlled) stateOf(open, i) else valueProp,
|
|
509
|
+
requested' = requested,
|
|
510
|
+
outcome' = "none",
|
|
511
|
+
base' = base,
|
|
512
|
+
requests' = requests
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// commit: the host writes the open state (a no-op while uncontrolled)
|
|
517
|
+
action setOpen = {
|
|
518
|
+
nondet o = Set(true, false).oneOf()
|
|
519
|
+
all {
|
|
520
|
+
open' = if (controlled) o else open,
|
|
521
|
+
active' = if (controlled) active else active,
|
|
522
|
+
value' = value,
|
|
523
|
+
focused' = focused,
|
|
524
|
+
typed' = NONE,
|
|
525
|
+
lastEvent' = "setOpen",
|
|
526
|
+
valueChanged' = false,
|
|
527
|
+
controlled' = controlled,
|
|
528
|
+
accepting' = accepting,
|
|
529
|
+
valueProp' = if (controlled) stateOf(o, value) else valueProp,
|
|
530
|
+
requested' = requested,
|
|
531
|
+
outcome' = if (controlled) "none" else outcome,
|
|
532
|
+
base' = base,
|
|
533
|
+
requests' = requests
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// commit: the host flips the open state of its own copy (an outside button)
|
|
538
|
+
action hostToggle = all {
|
|
539
|
+
open' = if (controlled) not(open) else open,
|
|
540
|
+
active' = active,
|
|
541
|
+
value' = value,
|
|
542
|
+
focused' = focused,
|
|
543
|
+
typed' = NONE,
|
|
544
|
+
lastEvent' = "hostToggle",
|
|
545
|
+
valueChanged' = false,
|
|
546
|
+
controlled' = controlled,
|
|
547
|
+
accepting' = accepting,
|
|
548
|
+
valueProp' = if (controlled) stateOf(not(open), value) else valueProp,
|
|
549
|
+
requested' = requested,
|
|
550
|
+
outcome' = if (controlled) "none" else outcome,
|
|
551
|
+
base' = base,
|
|
552
|
+
requests' = requests
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// configuration: the host starts owning (value, open), adopting the current
|
|
556
|
+
action control = all {
|
|
557
|
+
open' = open,
|
|
558
|
+
active' = active,
|
|
559
|
+
value' = value,
|
|
560
|
+
focused' = focused,
|
|
561
|
+
typed' = typed,
|
|
562
|
+
lastEvent' = lastEvent,
|
|
563
|
+
valueChanged' = valueChanged,
|
|
564
|
+
controlled' = true,
|
|
565
|
+
accepting' = accepting,
|
|
566
|
+
valueProp' = nowState,
|
|
567
|
+
requested' = requested,
|
|
568
|
+
outcome' = outcome,
|
|
569
|
+
base' = base,
|
|
570
|
+
requests' = requests
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// configuration: the controlled host answers every request with its intent
|
|
574
|
+
action acceptRequests = all {
|
|
575
|
+
open' = open,
|
|
576
|
+
active' = active,
|
|
577
|
+
value' = value,
|
|
578
|
+
focused' = focused,
|
|
579
|
+
typed' = typed,
|
|
580
|
+
lastEvent' = lastEvent,
|
|
581
|
+
valueChanged' = valueChanged,
|
|
582
|
+
controlled' = controlled,
|
|
583
|
+
accepting' = true,
|
|
584
|
+
valueProp' = valueProp,
|
|
585
|
+
requested' = requested,
|
|
586
|
+
outcome' = outcome,
|
|
587
|
+
base' = base,
|
|
588
|
+
requests' = requests
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
action step = any {
|
|
592
|
+
init, focus, blur, arrow_down, alt_arrow_down, arrow_up, home, end,
|
|
593
|
+
page_up, page_down, enter, space, tab, escape, alt_arrow_up, type_char,
|
|
594
|
+
click_combobox, click_option, set_value, setOpen, hostToggle, control,
|
|
595
|
+
acceptRequests
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// --- invariants (APG guarantees) ----------------------------------------
|
|
599
|
+
// aria-activedescendant refers to a real option iff the popup is open; a
|
|
600
|
+
// controlled host may keep the popup shut while the visual focus is moved,
|
|
601
|
+
// so this holds for the uncontrolled path only
|
|
602
|
+
val ActiveDescendantIffOpen: bool = or(controlled, refersToOption(active) == open)
|
|
603
|
+
// the rendered attributes agree with the state: no stale activedescendant,
|
|
604
|
+
// exactly one aria-selected option while open and none while closed
|
|
605
|
+
val AriaContractConsistent: bool = or(controlled,
|
|
606
|
+
and(ariaActivedescendant == active, selectedCount == (if (ariaExpanded) 1 else 0)))
|
|
607
|
+
val ActiveInRange: bool = and(active >= NONE, active < OPTION_COUNT)
|
|
608
|
+
val ValueInRange: bool = and(value >= NONE, value < OPTION_COUNT)
|
|
609
|
+
// DOM focus stays on the combobox while its popup is open (uncontrolled: a
|
|
610
|
+
// controlled host may reject the close and leave focus elsewhere)
|
|
611
|
+
val DomFocusStaysOnCombobox: bool = or(controlled, or(not(open), focused))
|
|
612
|
+
// Escape never changes the value
|
|
613
|
+
val EscapeKeepsValue: bool = or(not(lastEvent == "escape"), not(valueChanged))
|
|
614
|
+
// the value changes only on a commit event (never by navigating)
|
|
615
|
+
val CommitOnlyViaCommitEvents: bool = or(not(valueChanged), isCommitEvent(lastEvent))
|
|
616
|
+
// the typeahead buffer lives only between consecutive typed characters
|
|
617
|
+
val TypeaheadBufferIsTransient: bool = or(typed == NONE, lastEvent == "type_char")
|
|
618
|
+
// callbacks only ever count up from zero
|
|
619
|
+
val RequestsNonNegative: bool = requests >= 0
|
|
620
|
+
// controlled: only the value prop sets the state, so no event commits
|
|
621
|
+
val ControlledFollowsProp: bool = or(not(controlled), nowState == valueProp)
|
|
622
|
+
// a rejected request leaves the state as it found it
|
|
623
|
+
val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), nowState == base)
|
|
624
|
+
// an accepted request committed exactly its intent
|
|
625
|
+
val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), nowState == requested)
|
|
626
|
+
}
|