@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,396 @@
|
|
|
1
|
+
// APG Listbox, typeahead — https://www.w3.org/WAI/ARIA/apg/patterns/listbox/
|
|
2
|
+
//
|
|
3
|
+
// Sections modelled:
|
|
4
|
+
// - "Keyboard Interaction": "Type-ahead is recommended for all listboxes":
|
|
5
|
+
// type a character focus moves to the next option whose name starts
|
|
6
|
+
// with that character
|
|
7
|
+
// type several quickly focus moves to the next option whose name starts
|
|
8
|
+
// with the string typed so far
|
|
9
|
+
// and the "Printable Characters" row of every example's keyboard table.
|
|
10
|
+
//
|
|
11
|
+
// The pool is a three-letter alphabet (a=1, b=2, c=3) and six options named
|
|
12
|
+
// 0 "aa" 1 "ab" 2 "b" 3 "ba" 4 "bc" 5 "c"
|
|
13
|
+
// Names are digit strings in base 4 (first char most significant, 0 is never a
|
|
14
|
+
// char), so a prefix test is one integer division. The type-ahead buffer is the
|
|
15
|
+
// same encoding plus its length. The inactivity timer is an event: type_char
|
|
16
|
+
// arms it, `timeout` clears the buffer.
|
|
17
|
+
//
|
|
18
|
+
// Search rule (the pattern text, made precise):
|
|
19
|
+
// - empty buffer: the first option AFTER the active one that starts with the
|
|
20
|
+
// typed char, wrapping; the active option itself is the last candidate, so
|
|
21
|
+
// no other match means focus stays
|
|
22
|
+
// - non-empty buffer: the first option from the active one (inclusive) that
|
|
23
|
+
// starts with the whole buffer, wrapping — so continuing a word keeps focus
|
|
24
|
+
// on an option that still matches
|
|
25
|
+
// - no match: focus does not move (the buffer still grows)
|
|
26
|
+
//
|
|
27
|
+
// Request vs commit (map ticket rt1p). Type-ahead is a *focus move*, and this
|
|
28
|
+
// machine's controllable value is the active option: type_char and the plain
|
|
29
|
+
// moves (arrow_down/up, home, end) are *requests* carrying the intended active
|
|
30
|
+
// index. A request fires onValueChange (`requests` counts the calls) when the
|
|
31
|
+
// intent differs from the current active option, and the active option commits
|
|
32
|
+
// through `landed(holds, active, intent)`:
|
|
33
|
+
// uncontrolled accepted: focus commits the intent
|
|
34
|
+
// controlled, holds rejected: focus is untouched (the type-ahead buffer still
|
|
35
|
+
// grows, exactly as the primitive's internal buffer does)
|
|
36
|
+
// controlled, accepts accepted through the controlled value (`hostWrites`)
|
|
37
|
+
// The controlled value is `valueProp` (the active index); `setValue` /
|
|
38
|
+
// `hostToggle` are the host moving focus. `control` / `acceptRequests` are
|
|
39
|
+
// configuration events (init is literal-only). Toggling a listbox with
|
|
40
|
+
// selection-follows-focus composes this focus move with the selection requests
|
|
41
|
+
// of listbox-single.qnt; this machine models only the focus move.
|
|
42
|
+
//
|
|
43
|
+
// The prefix invariant is scoped to the uncontrolled path: a controlled host
|
|
44
|
+
// owns the active option, so a rejected request may leave focus on an option
|
|
45
|
+
// that no longer matches the (grown) buffer.
|
|
46
|
+
//
|
|
47
|
+
// Deliberate omissions / choices (ponytail):
|
|
48
|
+
// - the buffer saturates at three chars: every name has at most two, so a
|
|
49
|
+
// longer buffer can never match and its exact content is unobservable; this
|
|
50
|
+
// keeps the state space finite
|
|
51
|
+
// - navigation keys and blur clear the buffer (the pattern is silent; the
|
|
52
|
+
// example script keeps the buffer until its timeout fires)
|
|
53
|
+
// - the example script never matches option 0 on a multi-char search; the
|
|
54
|
+
// pattern text does not call for that quirk, so it is not modelled
|
|
55
|
+
// - repeating the same char to cycle ("aaa"), case folding, non-letter chars,
|
|
56
|
+
// the real 500ms duration, and IME composition
|
|
57
|
+
//
|
|
58
|
+
// Generator notes: no parameterized actions and no `let`, so a request spells
|
|
59
|
+
// `landed(...)` out per assignment and every action assigns every var.
|
|
60
|
+
module apg_listbox_typeahead {
|
|
61
|
+
import aria.* from "../../shared/aria"
|
|
62
|
+
|
|
63
|
+
var hasFocus: bool // the listbox is the focused element
|
|
64
|
+
var active: int // the active option (aria-activedescendant target)
|
|
65
|
+
var buf: int // typed chars so far, base-4 digit string
|
|
66
|
+
var bufLen: int // number of chars in the buffer (saturates at maxBuffer)
|
|
67
|
+
var armed: bool // the clear-buffer timer is running
|
|
68
|
+
var matched: bool // the last typed char focused a matching option
|
|
69
|
+
var controlled: bool // the host owns the value (`value` prop present)
|
|
70
|
+
var accepting: bool // controlled host that answers every request with its intent
|
|
71
|
+
var valueProp: int // the controlled value prop (the active index)
|
|
72
|
+
var requested: int // intent of the last request (active index)
|
|
73
|
+
var outcome: str // "none" | "accepted" | "rejected"
|
|
74
|
+
var base: int // active index when the last request was received
|
|
75
|
+
var requests: int // number of requests that fired onValueChange
|
|
76
|
+
|
|
77
|
+
def optionCount: int = 6
|
|
78
|
+
def lastIndex: int = 5
|
|
79
|
+
def noOption: int = 6
|
|
80
|
+
def maxBuffer: int = 3
|
|
81
|
+
// base-4 digit strings: a=1 b=2 c=3
|
|
82
|
+
def nameCode(i: int): int =
|
|
83
|
+
if (i == 0) 5
|
|
84
|
+
else if (i == 1) 6
|
|
85
|
+
else if (i == 2) 2
|
|
86
|
+
else if (i == 3) 9
|
|
87
|
+
else if (i == 4) 11
|
|
88
|
+
else 3
|
|
89
|
+
def nameLen(i: int): int = if (or(i == 2, i == 5)) 1 else 2
|
|
90
|
+
def pow4(n: int): int =
|
|
91
|
+
if (n == 0) 1
|
|
92
|
+
else if (n == 1) 4
|
|
93
|
+
else if (n == 2) 16
|
|
94
|
+
else 64
|
|
95
|
+
def nextIndex(i: int): int = if (i < lastIndex) i + 1 else i
|
|
96
|
+
def prevIndex(i: int): int = if (i > 0) i - 1 else i
|
|
97
|
+
|
|
98
|
+
// option i's name starts with the buffer (len chars, base-4 code)
|
|
99
|
+
def startsWith(i: int, buffer: int, len: int): bool =
|
|
100
|
+
and(len >= 1, len <= nameLen(i), nameCode(i) / pow4(nameLen(i) - len) == buffer)
|
|
101
|
+
// the first matching option, scanning optionCount candidates from start
|
|
102
|
+
// (candidates and the start rule are shared/aria.qnt: ariaCycle0, ariaTaStart0)
|
|
103
|
+
def candidate(start: int, k: int): int = ariaCycle0(optionCount, start, k)
|
|
104
|
+
def matchFrom(start: int, buffer: int, len: int): int =
|
|
105
|
+
if (startsWith(candidate(start, 0), buffer, len)) candidate(start, 0)
|
|
106
|
+
else if (startsWith(candidate(start, 1), buffer, len)) candidate(start, 1)
|
|
107
|
+
else if (startsWith(candidate(start, 2), buffer, len)) candidate(start, 2)
|
|
108
|
+
else if (startsWith(candidate(start, 3), buffer, len)) candidate(start, 3)
|
|
109
|
+
else if (startsWith(candidate(start, 4), buffer, len)) candidate(start, 4)
|
|
110
|
+
else if (startsWith(candidate(start, 5), buffer, len)) candidate(start, 5)
|
|
111
|
+
else noOption
|
|
112
|
+
def nextBuffer(buffer: int, len: int, ch: int): int =
|
|
113
|
+
if (len < maxBuffer) buffer * 4 + ch else buffer
|
|
114
|
+
def nextLength(len: int): int = if (len < maxBuffer) len + 1 else len
|
|
115
|
+
// the option typing ch would focus (noOption = no match)
|
|
116
|
+
def typeTarget(act: int, buffer: int, len: int, ch: int): int =
|
|
117
|
+
matchFrom(
|
|
118
|
+
ariaTaStart0(optionCount, act, len == 0),
|
|
119
|
+
nextBuffer(buffer, len, ch),
|
|
120
|
+
nextLength(len)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
// --- request / commit (map ticket rt1p) ---------------------------------
|
|
124
|
+
// controlled host that keeps the old value: the request is rejected
|
|
125
|
+
def holds: bool = and(controlled, not(accepting))
|
|
126
|
+
// controlled host that writes the intent back through the value prop
|
|
127
|
+
def hostWrites: bool = and(controlled, accepting)
|
|
128
|
+
def verdict: str = if (holds) "rejected" else "accepted"
|
|
129
|
+
// the active option after a request for `want`
|
|
130
|
+
def landed(h: bool, current: int, want: int): int = if (h) current else want
|
|
131
|
+
// the intended active option of each request
|
|
132
|
+
def typedIntent(ch: int): int =
|
|
133
|
+
if (hasFocus) (if (typeTarget(active, buf, bufLen, ch) == noOption) active else typeTarget(active, buf, bufLen, ch)) else active
|
|
134
|
+
def downIntent: int = if (hasFocus) nextIndex(active) else active
|
|
135
|
+
def upIntent: int = if (hasFocus) prevIndex(active) else active
|
|
136
|
+
def homeIntent: int = if (hasFocus) 0 else active
|
|
137
|
+
def endIntent: int = if (hasFocus) lastIndex else active
|
|
138
|
+
|
|
139
|
+
// --- ARIA contract, derived from the state ------------------------------
|
|
140
|
+
def ariaActiveDescendant(focused: bool, idx: int): int =
|
|
141
|
+
if (focused) idx else noOption
|
|
142
|
+
|
|
143
|
+
action init = all {
|
|
144
|
+
hasFocus' = false,
|
|
145
|
+
active' = 0,
|
|
146
|
+
buf' = 0,
|
|
147
|
+
bufLen' = 0,
|
|
148
|
+
armed' = false,
|
|
149
|
+
matched' = false,
|
|
150
|
+
controlled' = false,
|
|
151
|
+
accepting' = false,
|
|
152
|
+
valueProp' = 0,
|
|
153
|
+
requested' = 0,
|
|
154
|
+
outcome' = "none",
|
|
155
|
+
base' = 0,
|
|
156
|
+
requests' = 0
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Tab into the listbox; focus returns to the remembered active option.
|
|
160
|
+
action focus_in = all {
|
|
161
|
+
hasFocus' = true,
|
|
162
|
+
active' = active,
|
|
163
|
+
buf' = buf,
|
|
164
|
+
bufLen' = bufLen,
|
|
165
|
+
armed' = armed,
|
|
166
|
+
matched' = matched,
|
|
167
|
+
controlled' = controlled,
|
|
168
|
+
accepting' = accepting,
|
|
169
|
+
valueProp' = valueProp,
|
|
170
|
+
requested' = requested,
|
|
171
|
+
outcome' = outcome,
|
|
172
|
+
base' = base,
|
|
173
|
+
requests' = requests
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Tab out: the buffer is dropped.
|
|
177
|
+
action blur = all {
|
|
178
|
+
hasFocus' = false,
|
|
179
|
+
active' = active,
|
|
180
|
+
buf' = 0,
|
|
181
|
+
bufLen' = 0,
|
|
182
|
+
armed' = false,
|
|
183
|
+
matched' = false,
|
|
184
|
+
controlled' = controlled,
|
|
185
|
+
accepting' = accepting,
|
|
186
|
+
valueProp' = valueProp,
|
|
187
|
+
requested' = requested,
|
|
188
|
+
outcome' = outcome,
|
|
189
|
+
base' = base,
|
|
190
|
+
requests' = requests
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// A printable char: extend the buffer, re-arm the timer, request a focus move
|
|
194
|
+
// to the search result (stay put when nothing matches).
|
|
195
|
+
action type_char = {
|
|
196
|
+
nondet ch = Set(1, 2, 3).oneOf()
|
|
197
|
+
all {
|
|
198
|
+
hasFocus' = hasFocus,
|
|
199
|
+
active' = landed(holds, active, typedIntent(ch)),
|
|
200
|
+
buf' = if (hasFocus) nextBuffer(buf, bufLen, ch) else buf,
|
|
201
|
+
bufLen' = if (hasFocus) nextLength(bufLen) else bufLen,
|
|
202
|
+
armed' = if (hasFocus) true else armed,
|
|
203
|
+
matched' = if (hasFocus) (and(typeTarget(active, buf, bufLen, ch) != noOption, not(holds))) else matched,
|
|
204
|
+
controlled' = controlled,
|
|
205
|
+
accepting' = accepting,
|
|
206
|
+
valueProp' = if (hostWrites) typedIntent(ch) else valueProp,
|
|
207
|
+
requested' = if (typedIntent(ch) != active) typedIntent(ch) else requested,
|
|
208
|
+
outcome' = if (typedIntent(ch) != active) verdict else outcome,
|
|
209
|
+
base' = if (typedIntent(ch) != active) active else base,
|
|
210
|
+
requests' = if (typedIntent(ch) != active) requests + 1 else requests
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// The inactivity timer fires: the buffer is cleared.
|
|
215
|
+
action timeout = all {
|
|
216
|
+
hasFocus' = hasFocus,
|
|
217
|
+
active' = active,
|
|
218
|
+
buf' = 0,
|
|
219
|
+
bufLen' = 0,
|
|
220
|
+
armed' = false,
|
|
221
|
+
matched' = false,
|
|
222
|
+
controlled' = controlled,
|
|
223
|
+
accepting' = accepting,
|
|
224
|
+
valueProp' = valueProp,
|
|
225
|
+
requested' = requested,
|
|
226
|
+
outcome' = outcome,
|
|
227
|
+
base' = base,
|
|
228
|
+
requests' = requests
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Navigation keys request a focus move and drop the buffer.
|
|
232
|
+
action arrow_down = all {
|
|
233
|
+
hasFocus' = hasFocus,
|
|
234
|
+
active' = landed(holds, active, downIntent),
|
|
235
|
+
buf' = 0,
|
|
236
|
+
bufLen' = 0,
|
|
237
|
+
armed' = false,
|
|
238
|
+
matched' = false,
|
|
239
|
+
controlled' = controlled,
|
|
240
|
+
accepting' = accepting,
|
|
241
|
+
valueProp' = if (hostWrites) downIntent else valueProp,
|
|
242
|
+
requested' = if (downIntent != active) downIntent else requested,
|
|
243
|
+
outcome' = if (downIntent != active) verdict else outcome,
|
|
244
|
+
base' = if (downIntent != active) active else base,
|
|
245
|
+
requests' = if (downIntent != active) requests + 1 else requests
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
action arrow_up = all {
|
|
249
|
+
hasFocus' = hasFocus,
|
|
250
|
+
active' = landed(holds, active, upIntent),
|
|
251
|
+
buf' = 0,
|
|
252
|
+
bufLen' = 0,
|
|
253
|
+
armed' = false,
|
|
254
|
+
matched' = false,
|
|
255
|
+
controlled' = controlled,
|
|
256
|
+
accepting' = accepting,
|
|
257
|
+
valueProp' = if (hostWrites) upIntent else valueProp,
|
|
258
|
+
requested' = if (upIntent != active) upIntent else requested,
|
|
259
|
+
outcome' = if (upIntent != active) verdict else outcome,
|
|
260
|
+
base' = if (upIntent != active) active else base,
|
|
261
|
+
requests' = if (upIntent != active) requests + 1 else requests
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
action home = all {
|
|
265
|
+
hasFocus' = hasFocus,
|
|
266
|
+
active' = landed(holds, active, homeIntent),
|
|
267
|
+
buf' = 0,
|
|
268
|
+
bufLen' = 0,
|
|
269
|
+
armed' = false,
|
|
270
|
+
matched' = false,
|
|
271
|
+
controlled' = controlled,
|
|
272
|
+
accepting' = accepting,
|
|
273
|
+
valueProp' = if (hostWrites) homeIntent else valueProp,
|
|
274
|
+
requested' = if (homeIntent != active) homeIntent else requested,
|
|
275
|
+
outcome' = if (homeIntent != active) verdict else outcome,
|
|
276
|
+
base' = if (homeIntent != active) active else base,
|
|
277
|
+
requests' = if (homeIntent != active) requests + 1 else requests
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
action end = all {
|
|
281
|
+
hasFocus' = hasFocus,
|
|
282
|
+
active' = landed(holds, active, endIntent),
|
|
283
|
+
buf' = 0,
|
|
284
|
+
bufLen' = 0,
|
|
285
|
+
armed' = false,
|
|
286
|
+
matched' = false,
|
|
287
|
+
controlled' = controlled,
|
|
288
|
+
accepting' = accepting,
|
|
289
|
+
valueProp' = if (hostWrites) endIntent else valueProp,
|
|
290
|
+
requested' = if (endIntent != active) endIntent else requested,
|
|
291
|
+
outcome' = if (endIntent != active) verdict else outcome,
|
|
292
|
+
base' = if (endIntent != active) active else base,
|
|
293
|
+
requests' = if (endIntent != active) requests + 1 else requests
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// commit: the host writes the controlled value (a no-op while uncontrolled)
|
|
297
|
+
action setValue = {
|
|
298
|
+
nondet v = Set(0, 1, 2, 3, 4, 5).oneOf()
|
|
299
|
+
all {
|
|
300
|
+
hasFocus' = hasFocus,
|
|
301
|
+
active' = if (controlled) v else active,
|
|
302
|
+
buf' = buf,
|
|
303
|
+
bufLen' = bufLen,
|
|
304
|
+
armed' = armed,
|
|
305
|
+
matched' = if (controlled) false else matched,
|
|
306
|
+
controlled' = controlled,
|
|
307
|
+
accepting' = accepting,
|
|
308
|
+
valueProp' = if (controlled) v else valueProp,
|
|
309
|
+
requested' = requested,
|
|
310
|
+
outcome' = if (controlled) "none" else outcome,
|
|
311
|
+
base' = base,
|
|
312
|
+
requests' = requests
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// commit: the host advances its own copy of the value (an outside "next")
|
|
317
|
+
action hostToggle = all {
|
|
318
|
+
hasFocus' = hasFocus,
|
|
319
|
+
active' = if (controlled) (if (active < lastIndex) active + 1 else 0) else active,
|
|
320
|
+
buf' = buf,
|
|
321
|
+
bufLen' = bufLen,
|
|
322
|
+
armed' = armed,
|
|
323
|
+
matched' = if (controlled) false else matched,
|
|
324
|
+
controlled' = controlled,
|
|
325
|
+
accepting' = accepting,
|
|
326
|
+
valueProp' = if (controlled) (if (active < lastIndex) active + 1 else 0) else valueProp,
|
|
327
|
+
requested' = requested,
|
|
328
|
+
outcome' = if (controlled) "none" else outcome,
|
|
329
|
+
base' = base,
|
|
330
|
+
requests' = requests
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// configuration: the host starts owning the value, adopting the current one
|
|
334
|
+
action control = all {
|
|
335
|
+
hasFocus' = hasFocus,
|
|
336
|
+
active' = active,
|
|
337
|
+
buf' = buf,
|
|
338
|
+
bufLen' = bufLen,
|
|
339
|
+
armed' = armed,
|
|
340
|
+
matched' = matched,
|
|
341
|
+
controlled' = true,
|
|
342
|
+
accepting' = accepting,
|
|
343
|
+
valueProp' = active,
|
|
344
|
+
requested' = requested,
|
|
345
|
+
outcome' = outcome,
|
|
346
|
+
base' = base,
|
|
347
|
+
requests' = requests
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// configuration: the controlled host answers every request with its intent
|
|
351
|
+
action acceptRequests = all {
|
|
352
|
+
hasFocus' = hasFocus,
|
|
353
|
+
active' = active,
|
|
354
|
+
buf' = buf,
|
|
355
|
+
bufLen' = bufLen,
|
|
356
|
+
armed' = armed,
|
|
357
|
+
matched' = matched,
|
|
358
|
+
controlled' = controlled,
|
|
359
|
+
accepting' = true,
|
|
360
|
+
valueProp' = valueProp,
|
|
361
|
+
requested' = requested,
|
|
362
|
+
outcome' = outcome,
|
|
363
|
+
base' = base,
|
|
364
|
+
requests' = requests
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
action step = any {
|
|
368
|
+
init, focus_in, blur, type_char, timeout, arrow_down, arrow_up, home, end,
|
|
369
|
+
setValue, hostToggle, control, acceptRequests
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// the active option is always a real option
|
|
373
|
+
val ActiveInRange: bool = and(active >= 0, active <= lastIndex)
|
|
374
|
+
// aria-activedescendant is set exactly while the listbox holds focus
|
|
375
|
+
val ActiveDescendantTracksFocus: bool =
|
|
376
|
+
(ariaActiveDescendant(hasFocus, active) == noOption) == not(hasFocus)
|
|
377
|
+
// the buffer is a well-formed base-4 string of bufLen chars
|
|
378
|
+
val BufferWellFormed: bool =
|
|
379
|
+
and(bufLen >= 0, bufLen <= maxBuffer, buf >= 0, buf < pow4(bufLen))
|
|
380
|
+
// the timer runs exactly while there is something to clear
|
|
381
|
+
val TimerArmedIffBuffered: bool = armed == (bufLen > 0)
|
|
382
|
+
// keys only reach a focused listbox, so only it has a buffer
|
|
383
|
+
val BufferOnlyWhileFocused: bool = or(bufLen == 0, hasFocus)
|
|
384
|
+
// when typing moved (or kept) focus on a match, the focused name starts with
|
|
385
|
+
// the buffer (a rejected request never counts as matched)
|
|
386
|
+
val MatchedImpliesPrefix: bool =
|
|
387
|
+
or(not(matched), and(bufLen > 0, startsWith(active, buf, bufLen)))
|
|
388
|
+
// callbacks only ever count up from zero
|
|
389
|
+
val RequestsNonNegative: bool = requests >= 0
|
|
390
|
+
// controlled: only the value prop sets the state, so no event commits
|
|
391
|
+
val ControlledFollowsProp: bool = or(not(controlled), active == valueProp)
|
|
392
|
+
// a rejected request leaves the state as it found it
|
|
393
|
+
val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), active == base)
|
|
394
|
+
// an accepted request committed exactly its intent
|
|
395
|
+
val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), active == requested)
|
|
396
|
+
}
|