@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,633 @@
|
|
|
1
|
+
// APG combobox — the editable combobox with a tree popup (aria-haspopup="tree").
|
|
2
|
+
//
|
|
3
|
+
// Sources (W3C WAI-ARIA Authoring Practices Guide):
|
|
4
|
+
// https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
|
|
5
|
+
// - "Keyboard Interaction" (Combobox + Tree Popup Keyboard Interaction and
|
|
6
|
+
// its note on parent nodes that are category labels), "WAI-ARIA Roles,
|
|
7
|
+
// States, and Properties" (aria-haspopup, aria-activedescendant,
|
|
8
|
+
// aria-selected on treeitem)
|
|
9
|
+
// https://www.w3.org/WAI/ARIA/apg/patterns/treeview/ (popup roles / states
|
|
10
|
+
// are defined there; only the combobox-relevant subset is modelled)
|
|
11
|
+
//
|
|
12
|
+
// Modelled behaviour
|
|
13
|
+
// - DOM focus stays on the input; the visually focused tree node is exposed
|
|
14
|
+
// through aria-activedescendant (`active`, NONE = visual focus on the
|
|
15
|
+
// textbox). A node can only be focused while the popup is open and the node
|
|
16
|
+
// is currently visible (all its ancestors expanded).
|
|
17
|
+
// - Tree: 0 Fruits (parent) {1 Apple, 2 Banana}, 3 Vegetables (parent)
|
|
18
|
+
// {4 Carrot, 5 Dill}, 6 Water (root leaf); parents 0 and 3 expand with
|
|
19
|
+
// Right / collapse with Left (`e0`, `e3`).
|
|
20
|
+
// - Variants of the "selection" note (`labelMode`, prop `set_label_mode`):
|
|
21
|
+
// 0 every node, parents included, is a selectable value, so selection
|
|
22
|
+
// follows focus everywhere;
|
|
23
|
+
// 1 parents are category labels; focusing one leaves the previously
|
|
24
|
+
// selected node selected ("sticky");
|
|
25
|
+
// 2 parents are category labels; focusing one leaves nothing selected.
|
|
26
|
+
// Focus is distinct from selection: `active` vs `selected` (aria-selected).
|
|
27
|
+
// - Down / Up move to the next / previous visible node without opening or
|
|
28
|
+
// closing nodes (no wrap); on a closed popup they open it on the first /
|
|
29
|
+
// last visible node. Right: closed parent -> expand (focus and selection do
|
|
30
|
+
// not move), open parent -> first child, leaf -> nothing. Left: open parent
|
|
31
|
+
// -> collapse, child that is a leaf / closed -> its parent, root -> nothing.
|
|
32
|
+
// Home / End focus the first / last visible node. Left / Right only act on
|
|
33
|
+
// tree nodes while one is focused.
|
|
34
|
+
// - Enter accepts the *selected* node (so a focused non-selectable parent in
|
|
35
|
+
// labelMode 2 accepts nothing) and closes; Escape closes an open popup keeping
|
|
36
|
+
// the value and clears the textbox when already closed; typing returns the
|
|
37
|
+
// visual focus to the textbox without closing the popup; blur / Tab close
|
|
38
|
+
// the popup committing nothing.
|
|
39
|
+
// - Pointer: click on the input toggles the popup; clicking a selectable node
|
|
40
|
+
// commits it, clicking a category label toggles it and focuses it.
|
|
41
|
+
// - Prop changes: `set_label_mode`, `set_value` (non-selectable values ignored).
|
|
42
|
+
//
|
|
43
|
+
// Deliberate omissions
|
|
44
|
+
// - Typed text and filtering (see combobox-autocomplete), horizontal
|
|
45
|
+
// orientation (Down/Right swap), more than two tree levels, typeahead for
|
|
46
|
+
// non-editable trees, aria-level / aria-setsize / aria-posinset, scrolling.
|
|
47
|
+
// - Programmatic expand/collapse of nodes by props and `*` (expand all).
|
|
48
|
+
//
|
|
49
|
+
// Generator notes: no action preconditions (key actions guard their assignments
|
|
50
|
+
// with `focused` / `open`), only str/int/bool vars, a single nondet per action.
|
|
51
|
+
module combobox_tree {
|
|
52
|
+
var open: bool // tree popup displayed == aria-expanded
|
|
53
|
+
var active: int // aria-activedescendant node, NONE = textbox
|
|
54
|
+
var selected: int // node with aria-selected=true, NONE = none
|
|
55
|
+
var e0: bool // node 0 (Fruits) expanded
|
|
56
|
+
var e3: bool // node 3 (Vegetables) expanded
|
|
57
|
+
var focused: bool // DOM focus is on the input
|
|
58
|
+
var labelMode: int // 0 all selectable, 1 labels + sticky selection, 2 labels + cleared selection
|
|
59
|
+
var picked: int // committed node, NONE = none
|
|
60
|
+
var lastEvent: str // name of the last event, for transition invariants
|
|
61
|
+
var valueChanged: bool // the last event changed the committed value
|
|
62
|
+
var wasOpen: bool // the popup was open before the last event
|
|
63
|
+
var controlled: bool // the host owns the picked node (`value` prop present)
|
|
64
|
+
var accepting: bool // controlled host that answers every request with its intent
|
|
65
|
+
var valueProp: int // the controlled picked node
|
|
66
|
+
var requested: int // intent of the last request
|
|
67
|
+
var outcome: str // "none" | "accepted" | "rejected"
|
|
68
|
+
var base: int // picked when the last request was received
|
|
69
|
+
var requests: int // number of requests that fired a change callback
|
|
70
|
+
|
|
71
|
+
// --- configuration ------------------------------------------------------
|
|
72
|
+
def NONE: int = -1
|
|
73
|
+
def NODES: int = 7
|
|
74
|
+
|
|
75
|
+
// --- tree structure -------------------------------------------------------
|
|
76
|
+
def isParent(n: int): bool = or(n == 0, n == 3)
|
|
77
|
+
def selectable(v: int, n: int): bool = or(v == 0, not(isParent(n)))
|
|
78
|
+
def parentOf(n: int): int =
|
|
79
|
+
if (or(n == 1, n == 2)) 0 else if (or(n == 4, n == 5)) 3 else NONE
|
|
80
|
+
def firstChild(n: int): int = if (n == 0) 1 else if (n == 3) 4 else NONE
|
|
81
|
+
def expandedNode(n: int): bool = if (n == 0) e0 else if (n == 3) e3 else false
|
|
82
|
+
def vis(n: int): bool =
|
|
83
|
+
if (n < 0) false
|
|
84
|
+
else if (or(n == 0, n == 3)) true
|
|
85
|
+
else if (or(n == 1, n == 2)) e0
|
|
86
|
+
else if (or(n == 4, n == 5)) e3
|
|
87
|
+
else n == 6
|
|
88
|
+
def nextVis(n: int): int =
|
|
89
|
+
if (vis(n + 1)) n + 1 else if (vis(n + 2)) n + 2 else if (vis(n + 3)) n + 3
|
|
90
|
+
else if (vis(n + 4)) n + 4 else if (vis(n + 5)) n + 5 else if (vis(n + 6)) n + 6
|
|
91
|
+
else n
|
|
92
|
+
def prevVis(n: int): int =
|
|
93
|
+
if (vis(n - 1)) n - 1 else if (vis(n - 2)) n - 2 else if (vis(n - 3)) n - 3
|
|
94
|
+
else if (vis(n - 4)) n - 4 else if (vis(n - 5)) n - 5 else if (vis(n - 6)) n - 6
|
|
95
|
+
else n
|
|
96
|
+
|
|
97
|
+
// --- helpers ------------------------------------------------------------
|
|
98
|
+
// focus moved onto node m: selection follows focus when m is selectable,
|
|
99
|
+
// otherwise it is kept (labelMode 1) or cleared (labelMode 2)
|
|
100
|
+
def selectedFor(m: int): int =
|
|
101
|
+
if (selectable(labelMode, m)) m else (if (labelMode == 1) selected else NONE)
|
|
102
|
+
def nav: bool = and(focused, open)
|
|
103
|
+
def onNode: bool = and(nav, active >= 0)
|
|
104
|
+
def downTarget: int = if (and(open, active >= 0)) nextVis(active) else 0
|
|
105
|
+
def upTarget: int = if (and(open, active >= 0)) prevVis(active) else NODES - 1
|
|
106
|
+
def parentOpen: bool = and(isParent(active), expandedNode(active))
|
|
107
|
+
def rightTarget: int = if (and(onNode, parentOpen)) firstChild(active) else active
|
|
108
|
+
def leftTarget: int =
|
|
109
|
+
if (and(onNode, and(not(parentOpen), parentOf(active) >= 0))) parentOf(active) else active
|
|
110
|
+
def commitsSelected: bool = and(nav, selected >= 0)
|
|
111
|
+
def escapeClears: bool = and(and(focused, not(open)), picked >= 0)
|
|
112
|
+
|
|
113
|
+
// --- derived ARIA attribute contract -----------------------------------
|
|
114
|
+
def ariaHasPopup: str = "tree"
|
|
115
|
+
def ariaExpanded: bool = open
|
|
116
|
+
def ariaActivedescendant: int = if (open) active else NONE
|
|
117
|
+
def ariaSelected(n: int): bool = and(open, n == selected)
|
|
118
|
+
def selectedCount: int =
|
|
119
|
+
(if (ariaSelected(0)) 1 else 0) + (if (ariaSelected(1)) 1 else 0) +
|
|
120
|
+
(if (ariaSelected(2)) 1 else 0) + (if (ariaSelected(3)) 1 else 0) +
|
|
121
|
+
(if (ariaSelected(4)) 1 else 0) + (if (ariaSelected(5)) 1 else 0) +
|
|
122
|
+
(if (ariaSelected(6)) 1 else 0)
|
|
123
|
+
def isNavigation(e: str): bool =
|
|
124
|
+
or(e == "arrow_down", e == "arrow_up", e == "arrow_left", e == "arrow_right",
|
|
125
|
+
e == "home", e == "end")
|
|
126
|
+
def mayChangeValue(e: str): bool =
|
|
127
|
+
or(e == "enter", e == "click_node", e == "set_value", e == "set_label_mode",
|
|
128
|
+
e == "type_char", e == "escape")
|
|
129
|
+
|
|
130
|
+
// --- request / commit (map ticket rt1p) --------------------------------
|
|
131
|
+
// controlled host that keeps the old value: the request is rejected
|
|
132
|
+
def holds: bool = and(controlled, not(accepting))
|
|
133
|
+
// controlled host that writes the intent back through the value prop
|
|
134
|
+
def follows: bool = and(controlled, accepting)
|
|
135
|
+
def verdict: str = if (holds) "rejected" else "accepted"
|
|
136
|
+
def landedInt(h: bool, current: int, want: int): int = if (h) current else want
|
|
137
|
+
def propAfter(f: bool, p: int, want: int): int = if (f) want else p
|
|
138
|
+
|
|
139
|
+
action init = all {
|
|
140
|
+
open' = false,
|
|
141
|
+
active' = NONE,
|
|
142
|
+
selected' = NONE,
|
|
143
|
+
e0' = false,
|
|
144
|
+
e3' = false,
|
|
145
|
+
focused' = false,
|
|
146
|
+
labelMode' = 0,
|
|
147
|
+
picked' = NONE,
|
|
148
|
+
lastEvent' = "init",
|
|
149
|
+
valueChanged' = false,
|
|
150
|
+
wasOpen' = false,
|
|
151
|
+
controlled' = false,
|
|
152
|
+
accepting' = false,
|
|
153
|
+
valueProp' = NONE,
|
|
154
|
+
requested' = NONE,
|
|
155
|
+
outcome' = "none",
|
|
156
|
+
base' = NONE,
|
|
157
|
+
requests' = 0
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// --- focus / blur --------------------------------------------------------
|
|
161
|
+
action focus = all {
|
|
162
|
+
focused' = true,
|
|
163
|
+
lastEvent' = "focus",
|
|
164
|
+
valueChanged' = false,
|
|
165
|
+
wasOpen' = open,
|
|
166
|
+
open' = open,
|
|
167
|
+
active' = active,
|
|
168
|
+
selected' = selected,
|
|
169
|
+
e0' = e0,
|
|
170
|
+
e3' = e3,
|
|
171
|
+
labelMode' = labelMode,
|
|
172
|
+
picked' = landedInt(holds, picked, picked),
|
|
173
|
+
controlled' = controlled,
|
|
174
|
+
accepting' = accepting,
|
|
175
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
176
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
177
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
178
|
+
base' = if ((picked) != picked) picked else base,
|
|
179
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
action blur = all {
|
|
183
|
+
open' = false,
|
|
184
|
+
active' = NONE,
|
|
185
|
+
selected' = NONE,
|
|
186
|
+
focused' = false,
|
|
187
|
+
lastEvent' = "blur",
|
|
188
|
+
valueChanged' = false,
|
|
189
|
+
wasOpen' = open,
|
|
190
|
+
e0' = e0,
|
|
191
|
+
e3' = e3,
|
|
192
|
+
labelMode' = labelMode,
|
|
193
|
+
picked' = landedInt(holds, picked, picked),
|
|
194
|
+
controlled' = controlled,
|
|
195
|
+
accepting' = accepting,
|
|
196
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
197
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
198
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
199
|
+
base' = if ((picked) != picked) picked else base,
|
|
200
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Tab: the popup and its nodes are out of the Tab sequence
|
|
204
|
+
action tab = all {
|
|
205
|
+
open' = if (focused) false else open,
|
|
206
|
+
active' = if (focused) NONE else active,
|
|
207
|
+
selected' = if (focused) NONE else selected,
|
|
208
|
+
focused' = false,
|
|
209
|
+
lastEvent' = "tab",
|
|
210
|
+
valueChanged' = false,
|
|
211
|
+
wasOpen' = open,
|
|
212
|
+
e0' = e0,
|
|
213
|
+
e3' = e3,
|
|
214
|
+
labelMode' = labelMode,
|
|
215
|
+
picked' = landedInt(holds, picked, picked),
|
|
216
|
+
controlled' = controlled,
|
|
217
|
+
accepting' = accepting,
|
|
218
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
219
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
220
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
221
|
+
base' = if ((picked) != picked) picked else base,
|
|
222
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// typing: visual focus back to the textbox, the popup opens / stays open
|
|
226
|
+
action type_char = all {
|
|
227
|
+
open' = if (focused) true else open,
|
|
228
|
+
active' = if (focused) NONE else active,
|
|
229
|
+
selected' = if (focused) NONE else selected,
|
|
230
|
+
picked' = landedInt(holds, picked, if (focused) NONE else picked),
|
|
231
|
+
controlled' = controlled,
|
|
232
|
+
accepting' = accepting,
|
|
233
|
+
valueProp' = propAfter(follows, valueProp, if (focused) NONE else picked),
|
|
234
|
+
requested' = if ((if (focused) NONE else picked) != picked) if (focused) NONE else picked else requested,
|
|
235
|
+
outcome' = if ((if (focused) NONE else picked) != picked) verdict else outcome,
|
|
236
|
+
base' = if ((if (focused) NONE else picked) != picked) picked else base,
|
|
237
|
+
requests' = if ((if (focused) NONE else picked) != picked) requests + 1 else requests,
|
|
238
|
+
lastEvent' = "type_char",
|
|
239
|
+
valueChanged' = and(focused, picked >= 0),
|
|
240
|
+
wasOpen' = open,
|
|
241
|
+
e0' = e0,
|
|
242
|
+
e3' = e3,
|
|
243
|
+
focused' = focused,
|
|
244
|
+
labelMode' = labelMode
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// --- navigation keys --------------------------------------------------------
|
|
248
|
+
action arrow_down = all {
|
|
249
|
+
open' = if (focused) true else open,
|
|
250
|
+
active' = if (focused) downTarget else active,
|
|
251
|
+
selected' = if (focused) selectedFor(downTarget) else selected,
|
|
252
|
+
lastEvent' = "arrow_down",
|
|
253
|
+
valueChanged' = false,
|
|
254
|
+
wasOpen' = open,
|
|
255
|
+
e0' = e0,
|
|
256
|
+
e3' = e3,
|
|
257
|
+
focused' = focused,
|
|
258
|
+
labelMode' = labelMode,
|
|
259
|
+
picked' = landedInt(holds, picked, picked),
|
|
260
|
+
controlled' = controlled,
|
|
261
|
+
accepting' = accepting,
|
|
262
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
263
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
264
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
265
|
+
base' = if ((picked) != picked) picked else base,
|
|
266
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
action arrow_up = all {
|
|
270
|
+
open' = if (focused) true else open,
|
|
271
|
+
active' = if (focused) upTarget else active,
|
|
272
|
+
selected' = if (focused) selectedFor(upTarget) else selected,
|
|
273
|
+
lastEvent' = "arrow_up",
|
|
274
|
+
valueChanged' = false,
|
|
275
|
+
wasOpen' = open,
|
|
276
|
+
e0' = e0,
|
|
277
|
+
e3' = e3,
|
|
278
|
+
focused' = focused,
|
|
279
|
+
labelMode' = labelMode,
|
|
280
|
+
picked' = landedInt(holds, picked, picked),
|
|
281
|
+
controlled' = controlled,
|
|
282
|
+
accepting' = accepting,
|
|
283
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
284
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
285
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
286
|
+
base' = if ((picked) != picked) picked else base,
|
|
287
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// closed parent: expand in place; open parent: first child; leaf: nothing
|
|
291
|
+
action arrow_right = all {
|
|
292
|
+
e0' = if (and(onNode, and(active == 0, not(e0)))) true else e0,
|
|
293
|
+
e3' = if (and(onNode, and(active == 3, not(e3)))) true else e3,
|
|
294
|
+
active' = rightTarget,
|
|
295
|
+
selected' = if (rightTarget != active) selectedFor(rightTarget) else selected,
|
|
296
|
+
lastEvent' = "arrow_right",
|
|
297
|
+
valueChanged' = false,
|
|
298
|
+
wasOpen' = open,
|
|
299
|
+
open' = open,
|
|
300
|
+
focused' = focused,
|
|
301
|
+
labelMode' = labelMode,
|
|
302
|
+
picked' = landedInt(holds, picked, picked),
|
|
303
|
+
controlled' = controlled,
|
|
304
|
+
accepting' = accepting,
|
|
305
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
306
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
307
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
308
|
+
base' = if ((picked) != picked) picked else base,
|
|
309
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// open parent: collapse; other child: its parent; root: nothing
|
|
313
|
+
action arrow_left = all {
|
|
314
|
+
e0' = if (and(onNode, and(active == 0, e0))) false else e0,
|
|
315
|
+
e3' = if (and(onNode, and(active == 3, e3))) false else e3,
|
|
316
|
+
active' = leftTarget,
|
|
317
|
+
selected' = if (leftTarget != active) selectedFor(leftTarget) else selected,
|
|
318
|
+
lastEvent' = "arrow_left",
|
|
319
|
+
valueChanged' = false,
|
|
320
|
+
wasOpen' = open,
|
|
321
|
+
open' = open,
|
|
322
|
+
focused' = focused,
|
|
323
|
+
labelMode' = labelMode,
|
|
324
|
+
picked' = landedInt(holds, picked, picked),
|
|
325
|
+
controlled' = controlled,
|
|
326
|
+
accepting' = accepting,
|
|
327
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
328
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
329
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
330
|
+
base' = if ((picked) != picked) picked else base,
|
|
331
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
action home = all {
|
|
335
|
+
active' = if (nav) 0 else active,
|
|
336
|
+
selected' = if (nav) selectedFor(0) else selected,
|
|
337
|
+
lastEvent' = "home",
|
|
338
|
+
valueChanged' = false,
|
|
339
|
+
wasOpen' = open,
|
|
340
|
+
open' = open,
|
|
341
|
+
e0' = e0,
|
|
342
|
+
e3' = e3,
|
|
343
|
+
focused' = focused,
|
|
344
|
+
labelMode' = labelMode,
|
|
345
|
+
picked' = landedInt(holds, picked, picked),
|
|
346
|
+
controlled' = controlled,
|
|
347
|
+
accepting' = accepting,
|
|
348
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
349
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
350
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
351
|
+
base' = if ((picked) != picked) picked else base,
|
|
352
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
action end = all {
|
|
356
|
+
active' = if (nav) NODES - 1 else active,
|
|
357
|
+
selected' = if (nav) selectedFor(NODES - 1) else selected,
|
|
358
|
+
lastEvent' = "end",
|
|
359
|
+
valueChanged' = false,
|
|
360
|
+
wasOpen' = open,
|
|
361
|
+
open' = open,
|
|
362
|
+
e0' = e0,
|
|
363
|
+
e3' = e3,
|
|
364
|
+
focused' = focused,
|
|
365
|
+
labelMode' = labelMode,
|
|
366
|
+
picked' = landedInt(holds, picked, picked),
|
|
367
|
+
controlled' = controlled,
|
|
368
|
+
accepting' = accepting,
|
|
369
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
370
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
371
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
372
|
+
base' = if ((picked) != picked) picked else base,
|
|
373
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// --- commit / dismiss ---------------------------------------------------
|
|
377
|
+
// Enter accepts the selected node; with nothing selected it does nothing
|
|
378
|
+
action enter = all {
|
|
379
|
+
open' = if (commitsSelected) false else open,
|
|
380
|
+
picked' = landedInt(holds, picked, if (commitsSelected) selected else picked),
|
|
381
|
+
controlled' = controlled,
|
|
382
|
+
accepting' = accepting,
|
|
383
|
+
valueProp' = propAfter(follows, valueProp, if (commitsSelected) selected else picked),
|
|
384
|
+
requested' = if ((if (commitsSelected) selected else picked) != picked) if (commitsSelected) selected else picked else requested,
|
|
385
|
+
outcome' = if ((if (commitsSelected) selected else picked) != picked) verdict else outcome,
|
|
386
|
+
base' = if ((if (commitsSelected) selected else picked) != picked) picked else base,
|
|
387
|
+
requests' = if ((if (commitsSelected) selected else picked) != picked) requests + 1 else requests,
|
|
388
|
+
active' = if (commitsSelected) NONE else active,
|
|
389
|
+
selected' = if (commitsSelected) NONE else selected,
|
|
390
|
+
lastEvent' = "enter",
|
|
391
|
+
valueChanged' = and(commitsSelected, picked != selected),
|
|
392
|
+
wasOpen' = open,
|
|
393
|
+
e0' = e0,
|
|
394
|
+
e3' = e3,
|
|
395
|
+
focused' = focused,
|
|
396
|
+
labelMode' = labelMode
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// open: close, keep the value; closed: clear the textbox
|
|
400
|
+
action escape = all {
|
|
401
|
+
open' = if (focused) false else open,
|
|
402
|
+
active' = if (focused) NONE else active,
|
|
403
|
+
selected' = if (focused) NONE else selected,
|
|
404
|
+
picked' = landedInt(holds, picked, if (escapeClears) NONE else picked),
|
|
405
|
+
controlled' = controlled,
|
|
406
|
+
accepting' = accepting,
|
|
407
|
+
valueProp' = propAfter(follows, valueProp, if (escapeClears) NONE else picked),
|
|
408
|
+
requested' = if ((if (escapeClears) NONE else picked) != picked) if (escapeClears) NONE else picked else requested,
|
|
409
|
+
outcome' = if ((if (escapeClears) NONE else picked) != picked) verdict else outcome,
|
|
410
|
+
base' = if ((if (escapeClears) NONE else picked) != picked) picked else base,
|
|
411
|
+
requests' = if ((if (escapeClears) NONE else picked) != picked) requests + 1 else requests,
|
|
412
|
+
lastEvent' = "escape",
|
|
413
|
+
valueChanged' = escapeClears,
|
|
414
|
+
wasOpen' = open,
|
|
415
|
+
e0' = e0,
|
|
416
|
+
e3' = e3,
|
|
417
|
+
focused' = focused,
|
|
418
|
+
labelMode' = labelMode
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// --- pointer -------------------------------------------------------------
|
|
422
|
+
action click_input = all {
|
|
423
|
+
focused' = true,
|
|
424
|
+
open' = not(open),
|
|
425
|
+
active' = NONE,
|
|
426
|
+
selected' = NONE,
|
|
427
|
+
lastEvent' = "click_input",
|
|
428
|
+
valueChanged' = false,
|
|
429
|
+
wasOpen' = open,
|
|
430
|
+
e0' = e0,
|
|
431
|
+
e3' = e3,
|
|
432
|
+
labelMode' = labelMode,
|
|
433
|
+
picked' = landedInt(holds, picked, picked),
|
|
434
|
+
controlled' = controlled,
|
|
435
|
+
accepting' = accepting,
|
|
436
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
437
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
438
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
439
|
+
base' = if ((picked) != picked) picked else base,
|
|
440
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// a selectable node is committed; a category label is toggled and focused
|
|
444
|
+
action click_node = {
|
|
445
|
+
nondet n = Set(0, 1, 2, 3, 4, 5, 6).oneOf()
|
|
446
|
+
all {
|
|
447
|
+
picked' = landedInt(holds, picked, if (and(and(open, vis(n)), selectable(labelMode, n))) n else picked),
|
|
448
|
+
controlled' = controlled,
|
|
449
|
+
accepting' = accepting,
|
|
450
|
+
valueProp' = propAfter(follows, valueProp, if (and(and(open, vis(n)), selectable(labelMode, n))) n else picked),
|
|
451
|
+
requested' = if ((if (and(and(open, vis(n)), selectable(labelMode, n))) n else picked) != picked) if (and(and(open, vis(n)), selectable(labelMode, n))) n else picked else requested,
|
|
452
|
+
outcome' = if ((if (and(and(open, vis(n)), selectable(labelMode, n))) n else picked) != picked) verdict else outcome,
|
|
453
|
+
base' = if ((if (and(and(open, vis(n)), selectable(labelMode, n))) n else picked) != picked) picked else base,
|
|
454
|
+
requests' = if ((if (and(and(open, vis(n)), selectable(labelMode, n))) n else picked) != picked) requests + 1 else requests,
|
|
455
|
+
open' = if (and(and(open, vis(n)), selectable(labelMode, n))) false else open,
|
|
456
|
+
active' = if (and(open, vis(n))) (if (selectable(labelMode, n)) NONE else n) else active,
|
|
457
|
+
selected' = if (and(open, vis(n))) (if (selectable(labelMode, n)) NONE else selectedFor(n)) else selected,
|
|
458
|
+
e0' = if (and(and(open, vis(n)), and(n == 0, not(selectable(labelMode, n))))) not(e0) else e0,
|
|
459
|
+
e3' = if (and(and(open, vis(n)), and(n == 3, not(selectable(labelMode, n))))) not(e3) else e3,
|
|
460
|
+
lastEvent' = "click_node",
|
|
461
|
+
valueChanged' = and(and(and(open, vis(n)), selectable(labelMode, n)), picked != n),
|
|
462
|
+
wasOpen' = open,
|
|
463
|
+
focused' = focused,
|
|
464
|
+
labelMode' = labelMode
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// --- prop changes ---------------------------------------------------------
|
|
469
|
+
// the labelMode changes which nodes are values: popup closes, a no-longer
|
|
470
|
+
// selectable committed value is dropped
|
|
471
|
+
action set_label_mode = {
|
|
472
|
+
nondet v = Set(0, 1, 2).oneOf()
|
|
473
|
+
all {
|
|
474
|
+
labelMode' = v,
|
|
475
|
+
open' = false,
|
|
476
|
+
active' = NONE,
|
|
477
|
+
selected' = NONE,
|
|
478
|
+
picked' = if (selectable(v, picked)) picked else NONE,
|
|
479
|
+
lastEvent' = "set_label_mode",
|
|
480
|
+
valueChanged' = and(picked >= 0, not(selectable(v, picked))),
|
|
481
|
+
wasOpen' = open,
|
|
482
|
+
e0' = e0,
|
|
483
|
+
e3' = e3,
|
|
484
|
+
focused' = focused,
|
|
485
|
+
controlled' = controlled,
|
|
486
|
+
accepting' = accepting,
|
|
487
|
+
valueProp' = if (controlled) (if (selectable(v, picked)) picked else NONE) else valueProp,
|
|
488
|
+
requested' = requested,
|
|
489
|
+
outcome' = "none",
|
|
490
|
+
base' = base,
|
|
491
|
+
requests' = requests
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// a non-selectable value is ignored
|
|
496
|
+
action set_value = {
|
|
497
|
+
nondet n = Set(0, 1, 2, 3, 4, 5, 6).oneOf()
|
|
498
|
+
all {
|
|
499
|
+
picked' = if (selectable(labelMode, n)) n else picked,
|
|
500
|
+
open' = false,
|
|
501
|
+
active' = NONE,
|
|
502
|
+
selected' = NONE,
|
|
503
|
+
lastEvent' = "set_value",
|
|
504
|
+
valueChanged' = and(selectable(labelMode, n), picked != n),
|
|
505
|
+
wasOpen' = open,
|
|
506
|
+
e0' = e0,
|
|
507
|
+
e3' = e3,
|
|
508
|
+
focused' = focused,
|
|
509
|
+
labelMode' = labelMode,
|
|
510
|
+
controlled' = controlled,
|
|
511
|
+
accepting' = accepting,
|
|
512
|
+
valueProp' = if (controlled) (if (selectable(labelMode, n)) n else picked) else valueProp,
|
|
513
|
+
requested' = requested,
|
|
514
|
+
outcome' = "none",
|
|
515
|
+
base' = base,
|
|
516
|
+
requests' = requests
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// commit: the host flips its own copy of the picked node
|
|
521
|
+
action hostToggle = all {
|
|
522
|
+
open' = open,
|
|
523
|
+
active' = active,
|
|
524
|
+
selected' = selected,
|
|
525
|
+
e0' = e0,
|
|
526
|
+
e3' = e3,
|
|
527
|
+
focused' = focused,
|
|
528
|
+
labelMode' = labelMode,
|
|
529
|
+
picked' = if (controlled) (if (picked >= 0) NONE else 1) else picked,
|
|
530
|
+
lastEvent' = "hostToggle",
|
|
531
|
+
valueChanged' = false,
|
|
532
|
+
wasOpen' = open,
|
|
533
|
+
controlled' = controlled,
|
|
534
|
+
accepting' = accepting,
|
|
535
|
+
valueProp' = if (controlled) (if (picked >= 0) NONE else 1) else valueProp,
|
|
536
|
+
requested' = requested,
|
|
537
|
+
outcome' = if (controlled) "none" else outcome,
|
|
538
|
+
base' = base,
|
|
539
|
+
requests' = requests
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// configuration: the host starts owning the picked node, adopting it
|
|
543
|
+
action control = all {
|
|
544
|
+
open' = open,
|
|
545
|
+
active' = active,
|
|
546
|
+
selected' = selected,
|
|
547
|
+
e0' = e0,
|
|
548
|
+
e3' = e3,
|
|
549
|
+
focused' = focused,
|
|
550
|
+
labelMode' = labelMode,
|
|
551
|
+
picked' = picked,
|
|
552
|
+
lastEvent' = lastEvent,
|
|
553
|
+
valueChanged' = valueChanged,
|
|
554
|
+
wasOpen' = open,
|
|
555
|
+
controlled' = true,
|
|
556
|
+
accepting' = accepting,
|
|
557
|
+
valueProp' = picked,
|
|
558
|
+
requested' = requested,
|
|
559
|
+
outcome' = outcome,
|
|
560
|
+
base' = base,
|
|
561
|
+
requests' = requests
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// configuration: the controlled host answers every request with its intent
|
|
565
|
+
action acceptRequests = all {
|
|
566
|
+
open' = open,
|
|
567
|
+
active' = active,
|
|
568
|
+
selected' = selected,
|
|
569
|
+
e0' = e0,
|
|
570
|
+
e3' = e3,
|
|
571
|
+
focused' = focused,
|
|
572
|
+
labelMode' = labelMode,
|
|
573
|
+
picked' = picked,
|
|
574
|
+
lastEvent' = lastEvent,
|
|
575
|
+
valueChanged' = valueChanged,
|
|
576
|
+
wasOpen' = open,
|
|
577
|
+
controlled' = controlled,
|
|
578
|
+
accepting' = true,
|
|
579
|
+
valueProp' = valueProp,
|
|
580
|
+
requested' = requested,
|
|
581
|
+
outcome' = outcome,
|
|
582
|
+
base' = base,
|
|
583
|
+
requests' = requests
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
action step = any {
|
|
587
|
+
init, focus, blur, tab, type_char, arrow_down, arrow_up, arrow_right,
|
|
588
|
+
arrow_left, home, end, enter, escape, click_input, click_node, set_label_mode,
|
|
589
|
+
set_value, hostToggle, control, acceptRequests
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// --- invariants (APG guarantees) ----------------------------------------
|
|
593
|
+
// aria-activedescendant refers to a visible node of the open popup
|
|
594
|
+
val ActiveNodeIsVisibleInOpenPopup: bool = or(active == NONE, and(open, vis(active)))
|
|
595
|
+
// DOM focus stays on the combobox while its popup is open
|
|
596
|
+
val DomFocusStaysOnCombobox: bool = or(not(open), focused)
|
|
597
|
+
// at most one aria-selected node, only in an open popup, always a selectable value
|
|
598
|
+
val SelectionIsSingleAndSelectable: bool = and(
|
|
599
|
+
selectedCount == (if (selected >= 0) 1 else 0),
|
|
600
|
+
or(selected == NONE, and(open, selectable(labelMode, selected)))
|
|
601
|
+
)
|
|
602
|
+
// selection follows focus on selectable nodes; on a label it is kept
|
|
603
|
+
// (labelMode 1) or cleared (labelMode 2); nothing is selected without focus
|
|
604
|
+
val SelectionFollowsFocus: bool = and(
|
|
605
|
+
or(active != NONE, selected == NONE),
|
|
606
|
+
or(active == NONE, not(selectable(labelMode, active)), selected == active),
|
|
607
|
+
or(active == NONE, selectable(labelMode, active), labelMode == 1, selected == NONE)
|
|
608
|
+
)
|
|
609
|
+
// the rendered attributes agree with the state; non-listbox popups declare
|
|
610
|
+
// their type through aria-haspopup
|
|
611
|
+
val AriaContractConsistent: bool = and(
|
|
612
|
+
ariaActivedescendant == active,
|
|
613
|
+
ariaHasPopup == "tree",
|
|
614
|
+
ariaExpanded == open
|
|
615
|
+
)
|
|
616
|
+
val EscapeOnOpenPopupKeepsValue: bool =
|
|
617
|
+
or(not(lastEvent == "escape"), not(wasOpen), not(valueChanged))
|
|
618
|
+
val NavigationKeepsValue: bool = or(not(isNavigation(lastEvent)), not(valueChanged))
|
|
619
|
+
val ValueChangesOnlyOnCommitOrEdit: bool = or(not(valueChanged), mayChangeValue(lastEvent))
|
|
620
|
+
// the committed value is always a selectable node
|
|
621
|
+
val PickedIsSelectable: bool =
|
|
622
|
+
and(picked >= NONE, picked < NODES, selectable(labelMode, picked))
|
|
623
|
+
val FocusLossClosesPopup: bool =
|
|
624
|
+
or(not(or(lastEvent == "blur", lastEvent == "tab")), and(not(open), not(focused)))
|
|
625
|
+
// callbacks only ever count up from zero
|
|
626
|
+
val RequestsNonNegative: bool = requests >= 0
|
|
627
|
+
// controlled: only the value prop sets the picked node, so no event commits
|
|
628
|
+
val ControlledFollowsProp: bool = or(not(controlled), picked == valueProp)
|
|
629
|
+
// a rejected request leaves the picked node as it found it
|
|
630
|
+
val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), picked == base)
|
|
631
|
+
// an accepted request committed exactly its intent
|
|
632
|
+
val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), picked == requested)
|
|
633
|
+
}
|