@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,632 @@
|
|
|
1
|
+
// APG combobox — the editable combobox with a grid popup (aria-haspopup="grid").
|
|
2
|
+
//
|
|
3
|
+
// Sources (W3C WAI-ARIA Authoring Practices Guide):
|
|
4
|
+
// https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
|
|
5
|
+
// - "Keyboard Interaction" (Combobox + Grid Popup Keyboard Interaction and
|
|
6
|
+
// its notes on one suggestion per cell vs. per row), "WAI-ARIA Roles,
|
|
7
|
+
// States, and Properties" (aria-haspopup, aria-activedescendant,
|
|
8
|
+
// aria-selected on option / gridcell / row)
|
|
9
|
+
// https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/grid-combo/
|
|
10
|
+
// - "Keyboard Support" (Textbox, Grid Popup), "Role, Property, State, and
|
|
11
|
+
// Tabindex Attributes" (Textbox, Grid Popup)
|
|
12
|
+
//
|
|
13
|
+
// Modelled behaviour
|
|
14
|
+
// - DOM focus stays on the input; the visually focused grid cell is exposed
|
|
15
|
+
// through aria-activedescendant (`row`, `col`; both NONE = visual focus on
|
|
16
|
+
// the textbox). A grid cell can only be focused while the popup is open,
|
|
17
|
+
// and the popup can only be open while the input has DOM focus.
|
|
18
|
+
// - Layout variant (`cellValues`, prop `set_layout`): false = every row is one
|
|
19
|
+
// suggestion, so the row containing the focused cell is aria-selected and
|
|
20
|
+
// vertical arrows do not wrap across columns; true = every cell is its own
|
|
21
|
+
// suggestion, so exactly the focused cell is aria-selected and vertical
|
|
22
|
+
// arrows wrap from the bottom of one column to the top of the next.
|
|
23
|
+
// Horizontal arrows move cell by cell and wrap across rows in both layouts.
|
|
24
|
+
// - ArrowDown / ArrowUp open the grid and focus its first / last cell, or move
|
|
25
|
+
// the focus with wrap-around. Left / Right move between cells only while a
|
|
26
|
+
// cell is focused (otherwise they edit the text). Home / End / Delete return
|
|
27
|
+
// the visual focus to the textbox without closing the popup. Enter commits
|
|
28
|
+
// the suggestion under the focused cell and closes; Escape closes an open
|
|
29
|
+
// grid keeping the value, and clears the textbox when already closed.
|
|
30
|
+
// Typing / Backspace return the visual focus to the textbox. Blur / Tab
|
|
31
|
+
// close the grid (list autocomplete: nothing is committed).
|
|
32
|
+
// - Pointer: click on the input toggles the grid; clicking a cell commits it.
|
|
33
|
+
// - Prop changes: `set_layout` (the committed value is cleared because the
|
|
34
|
+
// suggestion id space changes), `set_value`.
|
|
35
|
+
//
|
|
36
|
+
// Deliberate omissions
|
|
37
|
+
// - Typed text content and filtering (see combobox-autocomplete): only whether
|
|
38
|
+
// the textbox holds typed text (`hasText`) matters for Escape-to-clear.
|
|
39
|
+
// - PageUp / PageDown / Ctrl+Home / Ctrl+End (optional in the pattern),
|
|
40
|
+
// asynchronous suggestions, scrolling, hover, grid headers / empty cells.
|
|
41
|
+
// - A fixed 3x3 grid (`ROWS`, `COLS`); sizes are constants, not pools.
|
|
42
|
+
//
|
|
43
|
+
// Generator notes: no action preconditions (key actions guard their assignments
|
|
44
|
+
// with `focused`), only str/int/bool vars, a single nondet per action.
|
|
45
|
+
module combobox_grid {
|
|
46
|
+
var open: bool // grid popup displayed == aria-expanded
|
|
47
|
+
var row: int // row of the visually focused cell, NONE = textbox
|
|
48
|
+
var col: int // column of the visually focused cell, NONE = textbox
|
|
49
|
+
var focused: bool // DOM focus is on the input
|
|
50
|
+
var hasText: bool // the textbox holds typed text
|
|
51
|
+
var picked: int // committed suggestion id, NONE = none
|
|
52
|
+
var cellValues: bool // layout: true = one suggestion per cell, false = per row
|
|
53
|
+
var lastEvent: str // name of the last event, for transition invariants
|
|
54
|
+
var valueChanged: bool // the last event changed the committed value / text
|
|
55
|
+
var wasOpen: bool // the grid was open before the last event
|
|
56
|
+
var controlled: bool // the host owns the picked suggestion (`value` prop present)
|
|
57
|
+
var accepting: bool // controlled host that answers every request with its intent
|
|
58
|
+
var valueProp: int // the controlled picked suggestion
|
|
59
|
+
var requested: int // intent of the last request
|
|
60
|
+
var outcome: str // "none" | "accepted" | "rejected"
|
|
61
|
+
var base: int // picked when the last request was received
|
|
62
|
+
var requests: int // number of requests that fired a change callback
|
|
63
|
+
|
|
64
|
+
// --- configuration ------------------------------------------------------
|
|
65
|
+
def NONE: int = -1
|
|
66
|
+
def ROWS: int = 3
|
|
67
|
+
def COLS: int = 3
|
|
68
|
+
def CELLS: int = 9
|
|
69
|
+
|
|
70
|
+
// --- helpers ------------------------------------------------------------
|
|
71
|
+
def hasActive: bool = row >= 0
|
|
72
|
+
def idx: int = row * COLS + col
|
|
73
|
+
def suggestionAt(k: int): int = if (cellValues) k else k / COLS
|
|
74
|
+
def focusedSuggestion: int = suggestionAt(idx)
|
|
75
|
+
def edits: bool = or(hasText, picked >= 0)
|
|
76
|
+
|
|
77
|
+
// vertical movement: per-row layout wraps within the column, per-cell layout
|
|
78
|
+
// wraps from the end of a column to the start of the next one
|
|
79
|
+
def stepDownRow: int =
|
|
80
|
+
if (cellValues) (if (row < ROWS - 1) row + 1 else 0) else (row + 1) % ROWS
|
|
81
|
+
def stepDownCol: int =
|
|
82
|
+
if (cellValues) (if (row < ROWS - 1) col else (col + 1) % COLS) else col
|
|
83
|
+
def stepUpRow: int =
|
|
84
|
+
if (cellValues) (if (row > 0) row - 1 else ROWS - 1) else (row + ROWS - 1) % ROWS
|
|
85
|
+
def stepUpCol: int =
|
|
86
|
+
if (cellValues) (if (row > 0) col else (col + COLS - 1) % COLS) else col
|
|
87
|
+
def downRow: int = if (hasActive) stepDownRow else 0
|
|
88
|
+
def downCol: int = if (hasActive) stepDownCol else 0
|
|
89
|
+
def upRow: int = if (hasActive) stepUpRow else ROWS - 1
|
|
90
|
+
def upCol: int = if (hasActive) stepUpCol else COLS - 1
|
|
91
|
+
// horizontal movement is linear and wraps across rows
|
|
92
|
+
def rightIdx: int = (idx + 1) % CELLS
|
|
93
|
+
def leftIdx: int = (idx + CELLS - 1) % CELLS
|
|
94
|
+
def moves: bool = and(focused, hasActive)
|
|
95
|
+
|
|
96
|
+
def escapeClears: bool = and(and(focused, not(open)), edits)
|
|
97
|
+
|
|
98
|
+
// --- derived ARIA attribute contract -----------------------------------
|
|
99
|
+
def ariaHasPopup: str = "grid"
|
|
100
|
+
def ariaExpanded: bool = open
|
|
101
|
+
def ariaActivedescendant: int = if (hasActive) idx else NONE
|
|
102
|
+
def rowSelected(r: int): bool = and(not(cellValues), and(hasActive, r == row))
|
|
103
|
+
def cellSelected(r: int, c: int): bool = and(cellValues, and(hasActive, r == row, c == col))
|
|
104
|
+
def selectedRows: int =
|
|
105
|
+
(if (rowSelected(0)) 1 else 0) + (if (rowSelected(1)) 1 else 0) + (if (rowSelected(2)) 1 else 0)
|
|
106
|
+
def selectedCells: int =
|
|
107
|
+
(if (cellSelected(0, 0)) 1 else 0) + (if (cellSelected(0, 1)) 1 else 0) + (if (cellSelected(0, 2)) 1 else 0) +
|
|
108
|
+
(if (cellSelected(1, 0)) 1 else 0) + (if (cellSelected(1, 1)) 1 else 0) + (if (cellSelected(1, 2)) 1 else 0) +
|
|
109
|
+
(if (cellSelected(2, 0)) 1 else 0) + (if (cellSelected(2, 1)) 1 else 0) + (if (cellSelected(2, 2)) 1 else 0)
|
|
110
|
+
def isNavigation(e: str): bool =
|
|
111
|
+
or(e == "arrow_down", e == "arrow_up", e == "arrow_left", e == "arrow_right",
|
|
112
|
+
e == "home", e == "end", e == "delete")
|
|
113
|
+
def mayChangeValue(e: str): bool =
|
|
114
|
+
or(e == "enter", e == "click_cell", e == "set_value", e == "set_layout",
|
|
115
|
+
e == "type_char", e == "backspace", e == "escape")
|
|
116
|
+
|
|
117
|
+
// --- request / commit (map ticket rt1p) --------------------------------
|
|
118
|
+
// controlled host that keeps the old value: the request is rejected
|
|
119
|
+
def holds: bool = and(controlled, not(accepting))
|
|
120
|
+
// controlled host that writes the intent back through the value prop
|
|
121
|
+
def follows: bool = and(controlled, accepting)
|
|
122
|
+
def verdict: str = if (holds) "rejected" else "accepted"
|
|
123
|
+
def landedInt(h: bool, current: int, want: int): int = if (h) current else want
|
|
124
|
+
def propAfter(f: bool, p: int, want: int): int = if (f) want else p
|
|
125
|
+
|
|
126
|
+
action init = all {
|
|
127
|
+
open' = false,
|
|
128
|
+
row' = NONE,
|
|
129
|
+
col' = NONE,
|
|
130
|
+
focused' = false,
|
|
131
|
+
hasText' = false,
|
|
132
|
+
picked' = NONE,
|
|
133
|
+
cellValues' = false,
|
|
134
|
+
lastEvent' = "init",
|
|
135
|
+
valueChanged' = false,
|
|
136
|
+
wasOpen' = false,
|
|
137
|
+
controlled' = false,
|
|
138
|
+
accepting' = false,
|
|
139
|
+
valueProp' = NONE,
|
|
140
|
+
requested' = NONE,
|
|
141
|
+
outcome' = "none",
|
|
142
|
+
base' = NONE,
|
|
143
|
+
requests' = 0
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- focus / blur --------------------------------------------------------
|
|
147
|
+
action focus = all {
|
|
148
|
+
focused' = true,
|
|
149
|
+
lastEvent' = "focus",
|
|
150
|
+
valueChanged' = false,
|
|
151
|
+
wasOpen' = open,
|
|
152
|
+
open' = open,
|
|
153
|
+
row' = row,
|
|
154
|
+
col' = col,
|
|
155
|
+
hasText' = hasText,
|
|
156
|
+
picked' = landedInt(holds, picked, picked),
|
|
157
|
+
controlled' = controlled,
|
|
158
|
+
accepting' = accepting,
|
|
159
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
160
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
161
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
162
|
+
base' = if ((picked) != picked) picked else base,
|
|
163
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
164
|
+
cellValues' = cellValues
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
action blur = all {
|
|
168
|
+
open' = false,
|
|
169
|
+
row' = NONE,
|
|
170
|
+
col' = NONE,
|
|
171
|
+
focused' = false,
|
|
172
|
+
lastEvent' = "blur",
|
|
173
|
+
valueChanged' = false,
|
|
174
|
+
wasOpen' = open,
|
|
175
|
+
hasText' = hasText,
|
|
176
|
+
picked' = landedInt(holds, picked, picked),
|
|
177
|
+
controlled' = controlled,
|
|
178
|
+
accepting' = accepting,
|
|
179
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
180
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
181
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
182
|
+
base' = if ((picked) != picked) picked else base,
|
|
183
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
184
|
+
cellValues' = cellValues
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Tab: the popup and its cells are out of the Tab sequence
|
|
188
|
+
action tab = all {
|
|
189
|
+
open' = if (focused) false else open,
|
|
190
|
+
row' = if (focused) NONE else row,
|
|
191
|
+
col' = if (focused) NONE else col,
|
|
192
|
+
focused' = false,
|
|
193
|
+
lastEvent' = "tab",
|
|
194
|
+
valueChanged' = false,
|
|
195
|
+
wasOpen' = open,
|
|
196
|
+
hasText' = hasText,
|
|
197
|
+
picked' = landedInt(holds, picked, picked),
|
|
198
|
+
controlled' = controlled,
|
|
199
|
+
accepting' = accepting,
|
|
200
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
201
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
202
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
203
|
+
base' = if ((picked) != picked) picked else base,
|
|
204
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
205
|
+
cellValues' = cellValues
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// --- text editing: visual focus returns to the textbox ------------------
|
|
209
|
+
action type_char = all {
|
|
210
|
+
hasText' = if (focused) true else hasText,
|
|
211
|
+
picked' = landedInt(holds, picked, if (focused) NONE else picked),
|
|
212
|
+
controlled' = controlled,
|
|
213
|
+
accepting' = accepting,
|
|
214
|
+
valueProp' = propAfter(follows, valueProp, if (focused) NONE else picked),
|
|
215
|
+
requested' = if ((if (focused) NONE else picked) != picked) if (focused) NONE else picked else requested,
|
|
216
|
+
outcome' = if ((if (focused) NONE else picked) != picked) verdict else outcome,
|
|
217
|
+
base' = if ((if (focused) NONE else picked) != picked) picked else base,
|
|
218
|
+
requests' = if ((if (focused) NONE else picked) != picked) requests + 1 else requests,
|
|
219
|
+
open' = if (focused) true else open,
|
|
220
|
+
row' = if (focused) NONE else row,
|
|
221
|
+
col' = if (focused) NONE else col,
|
|
222
|
+
lastEvent' = "type_char",
|
|
223
|
+
valueChanged' = and(focused, or(not(hasText), picked >= 0)),
|
|
224
|
+
wasOpen' = open,
|
|
225
|
+
focused' = focused,
|
|
226
|
+
cellValues' = cellValues
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
action backspace = all {
|
|
230
|
+
hasText' = if (focused) edits else hasText,
|
|
231
|
+
picked' = landedInt(holds, picked, if (focused) NONE else picked),
|
|
232
|
+
controlled' = controlled,
|
|
233
|
+
accepting' = accepting,
|
|
234
|
+
valueProp' = propAfter(follows, valueProp, if (focused) NONE else picked),
|
|
235
|
+
requested' = if ((if (focused) NONE else picked) != picked) if (focused) NONE else picked else requested,
|
|
236
|
+
outcome' = if ((if (focused) NONE else picked) != picked) verdict else outcome,
|
|
237
|
+
base' = if ((if (focused) NONE else picked) != picked) picked else base,
|
|
238
|
+
requests' = if ((if (focused) NONE else picked) != picked) requests + 1 else requests,
|
|
239
|
+
row' = if (focused) NONE else row,
|
|
240
|
+
col' = if (focused) NONE else col,
|
|
241
|
+
lastEvent' = "backspace",
|
|
242
|
+
valueChanged' = and(focused, picked >= 0),
|
|
243
|
+
wasOpen' = open,
|
|
244
|
+
open' = open,
|
|
245
|
+
focused' = focused,
|
|
246
|
+
cellValues' = cellValues
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
action delete = all {
|
|
250
|
+
row' = if (focused) NONE else row,
|
|
251
|
+
col' = if (focused) NONE else col,
|
|
252
|
+
lastEvent' = "delete",
|
|
253
|
+
valueChanged' = false,
|
|
254
|
+
wasOpen' = open,
|
|
255
|
+
open' = open,
|
|
256
|
+
focused' = focused,
|
|
257
|
+
hasText' = hasText,
|
|
258
|
+
picked' = landedInt(holds, picked, picked),
|
|
259
|
+
controlled' = controlled,
|
|
260
|
+
accepting' = accepting,
|
|
261
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
262
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
263
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
264
|
+
base' = if ((picked) != picked) picked else base,
|
|
265
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
266
|
+
cellValues' = cellValues
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// --- navigation keys --------------------------------------------------------
|
|
270
|
+
action arrow_down = all {
|
|
271
|
+
open' = if (focused) true else open,
|
|
272
|
+
row' = if (focused) downRow else row,
|
|
273
|
+
col' = if (focused) downCol else col,
|
|
274
|
+
lastEvent' = "arrow_down",
|
|
275
|
+
valueChanged' = false,
|
|
276
|
+
wasOpen' = open,
|
|
277
|
+
focused' = focused,
|
|
278
|
+
hasText' = hasText,
|
|
279
|
+
picked' = landedInt(holds, picked, picked),
|
|
280
|
+
controlled' = controlled,
|
|
281
|
+
accepting' = accepting,
|
|
282
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
283
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
284
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
285
|
+
base' = if ((picked) != picked) picked else base,
|
|
286
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
287
|
+
cellValues' = cellValues
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
action arrow_up = all {
|
|
291
|
+
open' = if (focused) true else open,
|
|
292
|
+
row' = if (focused) upRow else row,
|
|
293
|
+
col' = if (focused) upCol else col,
|
|
294
|
+
lastEvent' = "arrow_up",
|
|
295
|
+
valueChanged' = false,
|
|
296
|
+
wasOpen' = open,
|
|
297
|
+
focused' = focused,
|
|
298
|
+
hasText' = hasText,
|
|
299
|
+
picked' = landedInt(holds, picked, picked),
|
|
300
|
+
controlled' = controlled,
|
|
301
|
+
accepting' = accepting,
|
|
302
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
303
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
304
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
305
|
+
base' = if ((picked) != picked) picked else base,
|
|
306
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
307
|
+
cellValues' = cellValues
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Left / Right move between cells (wrapping across rows); with the visual
|
|
311
|
+
// focus on the textbox they edit the text instead
|
|
312
|
+
action arrow_right = all {
|
|
313
|
+
row' = if (moves) rightIdx / COLS else row,
|
|
314
|
+
col' = if (moves) rightIdx % COLS else col,
|
|
315
|
+
lastEvent' = "arrow_right",
|
|
316
|
+
valueChanged' = false,
|
|
317
|
+
wasOpen' = open,
|
|
318
|
+
open' = open,
|
|
319
|
+
focused' = focused,
|
|
320
|
+
hasText' = hasText,
|
|
321
|
+
picked' = landedInt(holds, picked, picked),
|
|
322
|
+
controlled' = controlled,
|
|
323
|
+
accepting' = accepting,
|
|
324
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
325
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
326
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
327
|
+
base' = if ((picked) != picked) picked else base,
|
|
328
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
329
|
+
cellValues' = cellValues
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
action arrow_left = all {
|
|
333
|
+
row' = if (moves) leftIdx / COLS else row,
|
|
334
|
+
col' = if (moves) leftIdx % COLS else col,
|
|
335
|
+
lastEvent' = "arrow_left",
|
|
336
|
+
valueChanged' = false,
|
|
337
|
+
wasOpen' = open,
|
|
338
|
+
open' = open,
|
|
339
|
+
focused' = focused,
|
|
340
|
+
hasText' = hasText,
|
|
341
|
+
picked' = landedInt(holds, picked, picked),
|
|
342
|
+
controlled' = controlled,
|
|
343
|
+
accepting' = accepting,
|
|
344
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
345
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
346
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
347
|
+
base' = if ((picked) != picked) picked else base,
|
|
348
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
349
|
+
cellValues' = cellValues
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Home / End: visual focus back to the textbox, grid stays open
|
|
353
|
+
action home = all {
|
|
354
|
+
row' = if (focused) NONE else row,
|
|
355
|
+
col' = if (focused) NONE else col,
|
|
356
|
+
lastEvent' = "home",
|
|
357
|
+
valueChanged' = false,
|
|
358
|
+
wasOpen' = open,
|
|
359
|
+
open' = open,
|
|
360
|
+
focused' = focused,
|
|
361
|
+
hasText' = hasText,
|
|
362
|
+
picked' = landedInt(holds, picked, picked),
|
|
363
|
+
controlled' = controlled,
|
|
364
|
+
accepting' = accepting,
|
|
365
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
366
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
367
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
368
|
+
base' = if ((picked) != picked) picked else base,
|
|
369
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
370
|
+
cellValues' = cellValues
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
action end = all {
|
|
374
|
+
row' = if (focused) NONE else row,
|
|
375
|
+
col' = if (focused) NONE else col,
|
|
376
|
+
lastEvent' = "end",
|
|
377
|
+
valueChanged' = false,
|
|
378
|
+
wasOpen' = open,
|
|
379
|
+
open' = open,
|
|
380
|
+
focused' = focused,
|
|
381
|
+
hasText' = hasText,
|
|
382
|
+
picked' = landedInt(holds, picked, picked),
|
|
383
|
+
controlled' = controlled,
|
|
384
|
+
accepting' = accepting,
|
|
385
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
386
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
387
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
388
|
+
base' = if ((picked) != picked) picked else base,
|
|
389
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
390
|
+
cellValues' = cellValues
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// --- commit / dismiss ---------------------------------------------------
|
|
394
|
+
// Enter on a focused cell commits its suggestion (cell or whole row) and closes
|
|
395
|
+
action enter = all {
|
|
396
|
+
open' = if (moves) false else open,
|
|
397
|
+
picked' = landedInt(holds, picked, if (moves) focusedSuggestion else picked),
|
|
398
|
+
controlled' = controlled,
|
|
399
|
+
accepting' = accepting,
|
|
400
|
+
valueProp' = propAfter(follows, valueProp, if (moves) focusedSuggestion else picked),
|
|
401
|
+
requested' = if ((if (moves) focusedSuggestion else picked) != picked) if (moves) focusedSuggestion else picked else requested,
|
|
402
|
+
outcome' = if ((if (moves) focusedSuggestion else picked) != picked) verdict else outcome,
|
|
403
|
+
base' = if ((if (moves) focusedSuggestion else picked) != picked) picked else base,
|
|
404
|
+
requests' = if ((if (moves) focusedSuggestion else picked) != picked) requests + 1 else requests,
|
|
405
|
+
row' = if (moves) NONE else row,
|
|
406
|
+
col' = if (moves) NONE else col,
|
|
407
|
+
lastEvent' = "enter",
|
|
408
|
+
valueChanged' = and(moves, picked != focusedSuggestion),
|
|
409
|
+
wasOpen' = open,
|
|
410
|
+
focused' = focused,
|
|
411
|
+
hasText' = hasText,
|
|
412
|
+
cellValues' = cellValues
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// open: close, keep the value; closed: clear the textbox
|
|
416
|
+
action escape = all {
|
|
417
|
+
open' = if (focused) false else open,
|
|
418
|
+
row' = if (focused) NONE else row,
|
|
419
|
+
col' = if (focused) NONE else col,
|
|
420
|
+
hasText' = if (escapeClears) false else hasText,
|
|
421
|
+
picked' = landedInt(holds, picked, if (escapeClears) NONE else picked),
|
|
422
|
+
controlled' = controlled,
|
|
423
|
+
accepting' = accepting,
|
|
424
|
+
valueProp' = propAfter(follows, valueProp, if (escapeClears) NONE else picked),
|
|
425
|
+
requested' = if ((if (escapeClears) NONE else picked) != picked) if (escapeClears) NONE else picked else requested,
|
|
426
|
+
outcome' = if ((if (escapeClears) NONE else picked) != picked) verdict else outcome,
|
|
427
|
+
base' = if ((if (escapeClears) NONE else picked) != picked) picked else base,
|
|
428
|
+
requests' = if ((if (escapeClears) NONE else picked) != picked) requests + 1 else requests,
|
|
429
|
+
lastEvent' = "escape",
|
|
430
|
+
valueChanged' = escapeClears,
|
|
431
|
+
wasOpen' = open,
|
|
432
|
+
focused' = focused,
|
|
433
|
+
cellValues' = cellValues
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// --- pointer -------------------------------------------------------------
|
|
437
|
+
action click_input = all {
|
|
438
|
+
focused' = true,
|
|
439
|
+
open' = not(open),
|
|
440
|
+
row' = NONE,
|
|
441
|
+
col' = NONE,
|
|
442
|
+
lastEvent' = "click_input",
|
|
443
|
+
valueChanged' = false,
|
|
444
|
+
wasOpen' = open,
|
|
445
|
+
hasText' = hasText,
|
|
446
|
+
picked' = landedInt(holds, picked, picked),
|
|
447
|
+
controlled' = controlled,
|
|
448
|
+
accepting' = accepting,
|
|
449
|
+
valueProp' = propAfter(follows, valueProp, picked),
|
|
450
|
+
requested' = if ((picked) != picked) picked else requested,
|
|
451
|
+
outcome' = if ((picked) != picked) verdict else outcome,
|
|
452
|
+
base' = if ((picked) != picked) picked else base,
|
|
453
|
+
requests' = if ((picked) != picked) requests + 1 else requests,
|
|
454
|
+
cellValues' = cellValues
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// clicking a cell of the open grid commits it and returns visual focus
|
|
458
|
+
action click_cell = {
|
|
459
|
+
nondet k = Set(0, 1, 2, 3, 4, 5, 6, 7, 8).oneOf()
|
|
460
|
+
all {
|
|
461
|
+
picked' = landedInt(holds, picked, if (open) suggestionAt(k) else picked),
|
|
462
|
+
controlled' = controlled,
|
|
463
|
+
accepting' = accepting,
|
|
464
|
+
valueProp' = propAfter(follows, valueProp, if (open) suggestionAt(k) else picked),
|
|
465
|
+
requested' = if ((if (open) suggestionAt(k) else picked) != picked) if (open) suggestionAt(k) else picked else requested,
|
|
466
|
+
outcome' = if ((if (open) suggestionAt(k) else picked) != picked) verdict else outcome,
|
|
467
|
+
base' = if ((if (open) suggestionAt(k) else picked) != picked) picked else base,
|
|
468
|
+
requests' = if ((if (open) suggestionAt(k) else picked) != picked) requests + 1 else requests,
|
|
469
|
+
open' = false,
|
|
470
|
+
row' = NONE,
|
|
471
|
+
col' = NONE,
|
|
472
|
+
focused' = if (open) true else focused,
|
|
473
|
+
lastEvent' = "click_cell",
|
|
474
|
+
valueChanged' = and(open, picked != suggestionAt(k)),
|
|
475
|
+
wasOpen' = open,
|
|
476
|
+
hasText' = hasText,
|
|
477
|
+
cellValues' = cellValues
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// --- prop changes ---------------------------------------------------------
|
|
482
|
+
// the suggestion id space changes with the layout: popup closes, value cleared
|
|
483
|
+
action set_layout = {
|
|
484
|
+
nondet cells = Set(true, false).oneOf()
|
|
485
|
+
all {
|
|
486
|
+
cellValues' = cells,
|
|
487
|
+
open' = false,
|
|
488
|
+
row' = NONE,
|
|
489
|
+
col' = NONE,
|
|
490
|
+
picked' = NONE,
|
|
491
|
+
lastEvent' = "set_layout",
|
|
492
|
+
valueChanged' = picked >= 0,
|
|
493
|
+
wasOpen' = open,
|
|
494
|
+
focused' = focused,
|
|
495
|
+
hasText' = hasText,
|
|
496
|
+
controlled' = controlled,
|
|
497
|
+
accepting' = accepting,
|
|
498
|
+
valueProp' = if (controlled) NONE else valueProp,
|
|
499
|
+
requested' = requested,
|
|
500
|
+
outcome' = "none",
|
|
501
|
+
base' = base,
|
|
502
|
+
requests' = requests
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
action set_value = {
|
|
507
|
+
nondet i = Set(0, 1, 2).oneOf()
|
|
508
|
+
all {
|
|
509
|
+
picked' = i,
|
|
510
|
+
open' = false,
|
|
511
|
+
row' = NONE,
|
|
512
|
+
col' = NONE,
|
|
513
|
+
lastEvent' = "set_value",
|
|
514
|
+
valueChanged' = picked != i,
|
|
515
|
+
wasOpen' = open,
|
|
516
|
+
focused' = focused,
|
|
517
|
+
hasText' = hasText,
|
|
518
|
+
cellValues' = cellValues,
|
|
519
|
+
controlled' = controlled,
|
|
520
|
+
accepting' = accepting,
|
|
521
|
+
valueProp' = if (controlled) i else valueProp,
|
|
522
|
+
requested' = requested,
|
|
523
|
+
outcome' = "none",
|
|
524
|
+
base' = base,
|
|
525
|
+
requests' = requests
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// commit: the host flips its own copy of the picked suggestion
|
|
530
|
+
action hostToggle = all {
|
|
531
|
+
open' = open,
|
|
532
|
+
row' = row,
|
|
533
|
+
col' = col,
|
|
534
|
+
focused' = focused,
|
|
535
|
+
hasText' = hasText,
|
|
536
|
+
picked' = if (controlled) (if (picked >= 0) NONE else 0) else picked,
|
|
537
|
+
cellValues' = cellValues,
|
|
538
|
+
lastEvent' = "hostToggle",
|
|
539
|
+
valueChanged' = false,
|
|
540
|
+
wasOpen' = open,
|
|
541
|
+
controlled' = controlled,
|
|
542
|
+
accepting' = accepting,
|
|
543
|
+
valueProp' = if (controlled) (if (picked >= 0) NONE else 0) else valueProp,
|
|
544
|
+
requested' = requested,
|
|
545
|
+
outcome' = if (controlled) "none" else outcome,
|
|
546
|
+
base' = base,
|
|
547
|
+
requests' = requests
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// configuration: the host starts owning the picked suggestion, adopting it
|
|
551
|
+
action control = all {
|
|
552
|
+
open' = open,
|
|
553
|
+
row' = row,
|
|
554
|
+
col' = col,
|
|
555
|
+
focused' = focused,
|
|
556
|
+
hasText' = hasText,
|
|
557
|
+
picked' = picked,
|
|
558
|
+
cellValues' = cellValues,
|
|
559
|
+
lastEvent' = lastEvent,
|
|
560
|
+
valueChanged' = valueChanged,
|
|
561
|
+
wasOpen' = open,
|
|
562
|
+
controlled' = true,
|
|
563
|
+
accepting' = accepting,
|
|
564
|
+
valueProp' = picked,
|
|
565
|
+
requested' = requested,
|
|
566
|
+
outcome' = outcome,
|
|
567
|
+
base' = base,
|
|
568
|
+
requests' = requests
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// configuration: the controlled host answers every request with its intent
|
|
572
|
+
action acceptRequests = all {
|
|
573
|
+
open' = open,
|
|
574
|
+
row' = row,
|
|
575
|
+
col' = col,
|
|
576
|
+
focused' = focused,
|
|
577
|
+
hasText' = hasText,
|
|
578
|
+
picked' = picked,
|
|
579
|
+
cellValues' = cellValues,
|
|
580
|
+
lastEvent' = lastEvent,
|
|
581
|
+
valueChanged' = valueChanged,
|
|
582
|
+
wasOpen' = open,
|
|
583
|
+
controlled' = controlled,
|
|
584
|
+
accepting' = true,
|
|
585
|
+
valueProp' = valueProp,
|
|
586
|
+
requested' = requested,
|
|
587
|
+
outcome' = outcome,
|
|
588
|
+
base' = base,
|
|
589
|
+
requests' = requests
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
action step = any {
|
|
593
|
+
init, focus, blur, tab, type_char, backspace, delete, arrow_down, arrow_up,
|
|
594
|
+
arrow_right, arrow_left, home, end, enter, escape, click_input, click_cell,
|
|
595
|
+
set_layout, set_value, hostToggle, control, acceptRequests
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// --- invariants (APG guarantees) ----------------------------------------
|
|
599
|
+
// the focused cell is a real grid cell; row and column are set together
|
|
600
|
+
val GridPositionConsistent: bool = and(
|
|
601
|
+
row >= NONE, row < ROWS, col >= NONE, col < COLS, (row == NONE) == (col == NONE)
|
|
602
|
+
)
|
|
603
|
+
// aria-activedescendant (a focused cell) exists only while the grid is open
|
|
604
|
+
val ActiveCellRequiresOpenGrid: bool = or(row == NONE, open)
|
|
605
|
+
// DOM focus stays on the combobox while its popup is open
|
|
606
|
+
val DomFocusStaysOnCombobox: bool = or(not(open), focused)
|
|
607
|
+
// aria-selected: one row (per-row layout) or one cell (per-cell layout)
|
|
608
|
+
// while a cell is focused, nothing otherwise; selection follows focus
|
|
609
|
+
val AriaContractConsistent: bool = and(
|
|
610
|
+
ariaActivedescendant == (if (hasActive) idx else NONE),
|
|
611
|
+
selectedRows + selectedCells == (if (hasActive) 1 else 0),
|
|
612
|
+
or(not(cellValues), selectedRows == 0),
|
|
613
|
+
or(cellValues, selectedCells == 0)
|
|
614
|
+
)
|
|
615
|
+
val EscapeOnOpenGridKeepsValue: bool =
|
|
616
|
+
or(not(lastEvent == "escape"), not(wasOpen), not(valueChanged))
|
|
617
|
+
val NavigationKeepsValue: bool = or(not(isNavigation(lastEvent)), not(valueChanged))
|
|
618
|
+
val ValueChangesOnlyOnCommitOrEdit: bool = or(not(valueChanged), mayChangeValue(lastEvent))
|
|
619
|
+
// a committed suggestion id exists in the current layout
|
|
620
|
+
val PickedInRange: bool =
|
|
621
|
+
or(picked == NONE, picked < (if (cellValues) CELLS else ROWS))
|
|
622
|
+
val FocusLossClosesGrid: bool =
|
|
623
|
+
or(not(or(lastEvent == "blur", lastEvent == "tab")), and(not(open), not(focused)))
|
|
624
|
+
// callbacks only ever count up from zero
|
|
625
|
+
val RequestsNonNegative: bool = requests >= 0
|
|
626
|
+
// controlled: only the value prop sets the picked suggestion, so no event commits
|
|
627
|
+
val ControlledFollowsProp: bool = or(not(controlled), picked == valueProp)
|
|
628
|
+
// a rejected request leaves the picked suggestion as it found it
|
|
629
|
+
val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), picked == base)
|
|
630
|
+
// an accepted request committed exactly its intent
|
|
631
|
+
val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), picked == requested)
|
|
632
|
+
}
|