@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,512 @@
|
|
|
1
|
+
// WAI-ARIA APG — grid: optional cell / row / column selection.
|
|
2
|
+
//
|
|
3
|
+
// Modelled from https://www.w3.org/WAI/ARIA/apg/patterns/grid/ :
|
|
4
|
+
// - "Keyboard Interaction For Data Grids" (selection keys: Control+Space selects
|
|
5
|
+
// the column, Shift+Space the row, Control+A all cells, Shift+Arrow extends
|
|
6
|
+
// the selection by one cell) and the same list under "Keyboard Interaction
|
|
7
|
+
// For Layout Grids";
|
|
8
|
+
// - "WAI-ARIA Roles, States, and Properties" (aria-selected=true on selected
|
|
9
|
+
// cells or rows; on every cell of a selected column; the attribute exists only
|
|
10
|
+
// if the grid supports selection);
|
|
11
|
+
// and the selection behaviour of https://www.w3.org/WAI/ARIA/apg/patterns/grid/examples/advanced-data-grid/
|
|
12
|
+
//
|
|
13
|
+
// The selection is one rectangle [selR0..selR1] x [selC0..selC1] plus an anchor
|
|
14
|
+
// cell. A row selection is a full-width rectangle, a column selection a
|
|
15
|
+
// full-height one, Control+A the whole grid, Shift+Arrow the bounding box of the
|
|
16
|
+
// anchor and the moving focus cell. aria-selected is derived from the rectangle.
|
|
17
|
+
// Focus (row, col) is kept here only because Shift+Arrow needs it; full
|
|
18
|
+
// navigation (Home/End/Page keys, wrap, edit mode) is grid.qnt.
|
|
19
|
+
//
|
|
20
|
+
// Deliberate decisions where APG is silent:
|
|
21
|
+
// - a plain arrow (or click) collapses the selection and re-anchors on the new
|
|
22
|
+
// focus cell; Escape clears the selection;
|
|
23
|
+
// - Shift+Space / Control+Space / Control+A re-anchor on the focus cell;
|
|
24
|
+
// - Shift+Arrow at the grid edge still selects the focus cell (no movement);
|
|
25
|
+
// - on a grid that is not selectable, Shift+Arrow is a plain move and the
|
|
26
|
+
// selection keys do nothing; turning selectability off clears the selection;
|
|
27
|
+
// - while the grid has no focus, keys are ignored; blur keeps the selection.
|
|
28
|
+
//
|
|
29
|
+
// Request vs commit (map ticket rt1p). The selection — "nothing" or the active
|
|
30
|
+
// rectangle — is the value, packed into `selectionPacked` (0 = nothing selected).
|
|
31
|
+
// Every event a user sees as a selection change — `key` (plain / Shift arrows,
|
|
32
|
+
// Shift+Space, Control+Space, Control+A, Escape), `click`, `shift_click` and the
|
|
33
|
+
// `set_selectable` prop clearing an active selection — is a *request* carrying an
|
|
34
|
+
// intent (`requested`); it fires onValueChange (`requests` counts the calls) when
|
|
35
|
+
// the intent differs from the current value, and then:
|
|
36
|
+
// uncontrolled the request is accepted: the selection commits the intent
|
|
37
|
+
// controlled, holds the host ignores it ("rejected"): the selection is untouched
|
|
38
|
+
// controlled, accepts the host answers with the same value (`accepting`): the
|
|
39
|
+
// selection commits through the controlled value, not the event
|
|
40
|
+
// `valueProp` is the only thing that changes the selection while `controlled`;
|
|
41
|
+
// `setValue` / `hostToggle` are the host writing it. `control` / `acceptRequests`
|
|
42
|
+
// are configuration events (init is literal-only). Focus, the anchor and the
|
|
43
|
+
// collapsed rectangle still move with the event even when the request is rejected
|
|
44
|
+
// (roving focus), which is why the four shape/containment invariants are scoped to
|
|
45
|
+
// the uncontrolled path: a controlled host owns the value and may hand the widget
|
|
46
|
+
// any rectangle. Invariants for the model: ControlledFollowsProp,
|
|
47
|
+
// RejectedRequestUntouched, AcceptedRequestCommits.
|
|
48
|
+
//
|
|
49
|
+
// Deliberate omissions: Tab / Shift+Tab (focus leaves via focus_out), Home/End,
|
|
50
|
+
// PageUp/PageDown and wrapping (grid.qnt), non-rectangular multi-selection
|
|
51
|
+
// (Control+click toggling), the row-selection checkbox column, cut/copy/paste,
|
|
52
|
+
// aria-multiselectable, header cells.
|
|
53
|
+
//
|
|
54
|
+
// Generator gaps hit: no sets/lists/maps, so the selection is a rectangle of
|
|
55
|
+
// scalar bounds rather than a set of cells; no `const` (sizes are zero-argument
|
|
56
|
+
// `val`s); one nondet per action (cell index / key name are single pools).
|
|
57
|
+
module apg_grid_selection {
|
|
58
|
+
var row: int // focused row, 0-based
|
|
59
|
+
var col: int // focused column, 0-based
|
|
60
|
+
var anchorRow: int // selection anchor
|
|
61
|
+
var anchorCol: int
|
|
62
|
+
var selActive: bool // a selection exists
|
|
63
|
+
var selR0: int // selection rectangle (inclusive); the focus cell when inactive
|
|
64
|
+
var selR1: int
|
|
65
|
+
var selC0: int
|
|
66
|
+
var selC1: int
|
|
67
|
+
var hasFocus: bool // DOM focus is inside the grid
|
|
68
|
+
var selectable: bool // the grid supports selection (aria-selected exists)
|
|
69
|
+
var controlled: bool // the host owns the selection (`value` prop present)
|
|
70
|
+
var accepting: bool // controlled host that answers every request with its intent
|
|
71
|
+
var valueProp: int // the controlled value prop (packed; 0 = nothing selected)
|
|
72
|
+
var requested: int // intent of the last request (packed)
|
|
73
|
+
var outcome: str // "none" | "accepted" | "rejected"
|
|
74
|
+
var base: int // value when the last request was received (packed)
|
|
75
|
+
var requests: int // number of requests that fired onValueChange
|
|
76
|
+
|
|
77
|
+
// bounded grid: keep CELL pool of click / shift_click in step
|
|
78
|
+
val ROWS: int = 4
|
|
79
|
+
val COLS: int = 3
|
|
80
|
+
val LAST_ROW: int = ROWS - 1
|
|
81
|
+
val LAST_COL: int = COLS - 1
|
|
82
|
+
|
|
83
|
+
def lowerOf(a: int, b: int): int = if (a < b) a else b
|
|
84
|
+
def upperOf(a: int, b: int): int = if (a > b) a else b
|
|
85
|
+
def clampRow(r: int): int = if (r < 0) 0 else if (r > LAST_ROW) LAST_ROW else r
|
|
86
|
+
def clampCol(c: int): int = if (c < 0) 0 else if (c > LAST_COL) LAST_COL else c
|
|
87
|
+
|
|
88
|
+
def dRow(k: str): int =
|
|
89
|
+
if (or(k == "ArrowDown", k == "Shift+ArrowDown")) 1
|
|
90
|
+
else if (or(k == "ArrowUp", k == "Shift+ArrowUp")) -1
|
|
91
|
+
else 0
|
|
92
|
+
def dCol(k: str): int =
|
|
93
|
+
if (or(k == "ArrowRight", k == "Shift+ArrowRight")) 1
|
|
94
|
+
else if (or(k == "ArrowLeft", k == "Shift+ArrowLeft")) -1
|
|
95
|
+
else 0
|
|
96
|
+
def newRow(k: str): int = clampRow(row + dRow(k))
|
|
97
|
+
def newCol(k: str): int = clampCol(col + dCol(k))
|
|
98
|
+
|
|
99
|
+
def isPlainArrow(k: str): bool =
|
|
100
|
+
or(k == "ArrowRight", k == "ArrowLeft", k == "ArrowDown", k == "ArrowUp")
|
|
101
|
+
def isShiftArrow(k: str): bool =
|
|
102
|
+
or(k == "Shift+ArrowRight", k == "Shift+ArrowLeft", k == "Shift+ArrowDown", k == "Shift+ArrowUp")
|
|
103
|
+
|
|
104
|
+
// key classes (all gated on DOM focus inside the grid)
|
|
105
|
+
def collapses(k: str): bool =
|
|
106
|
+
and(hasFocus, or(isPlainArrow(k), and(isShiftArrow(k), not(selectable))))
|
|
107
|
+
def extendsSel(k: str): bool = and(hasFocus, selectable, isShiftArrow(k))
|
|
108
|
+
def selectsRow(k: str): bool = and(hasFocus, selectable, k == "Shift+Space")
|
|
109
|
+
def selectsCol(k: str): bool = and(hasFocus, selectable, k == "Control+Space")
|
|
110
|
+
def selectsAll(k: str): bool = and(hasFocus, selectable, k == "Control+a")
|
|
111
|
+
def clears(k: str): bool = and(hasFocus, k == "Escape")
|
|
112
|
+
def selects(k: str): bool = or(extendsSel(k), selectsRow(k), selectsCol(k), selectsAll(k))
|
|
113
|
+
def moves(k: str): bool = or(collapses(k), extendsSel(k))
|
|
114
|
+
// re-anchors on the focus cell (selection keys and Escape)
|
|
115
|
+
def reanchors(k: str): bool = or(selectsRow(k), selectsCol(k), selectsAll(k), clears(k))
|
|
116
|
+
|
|
117
|
+
// --- ARIA contract (derived) ---
|
|
118
|
+
def cellSelected(r: int, c: int): bool =
|
|
119
|
+
and(selActive, r >= selR0, r <= selR1, c >= selC0, c <= selC1)
|
|
120
|
+
def rowSelected(r: int): bool =
|
|
121
|
+
and(selActive, r >= selR0, r <= selR1, selC0 == 0, selC1 == LAST_COL)
|
|
122
|
+
def colSelected(c: int): bool =
|
|
123
|
+
and(selActive, c >= selC0, c <= selC1, selR0 == 0, selR1 == LAST_ROW)
|
|
124
|
+
// aria-selected is only present on a grid that supports selection
|
|
125
|
+
def ariaSelectedPresent: bool = selectable
|
|
126
|
+
def cellAriaSelected(r: int, c: int): bool = and(selectable, cellSelected(r, c))
|
|
127
|
+
def selectedCellCount: int =
|
|
128
|
+
if (selActive) (selR1 - selR0 + 1) * (selC1 - selC0 + 1) else 0
|
|
129
|
+
|
|
130
|
+
// --- request / commit (map ticket rt1p) ----------------------------------
|
|
131
|
+
def moved(before: int, after: int): int = if (before == after) 0 else 1
|
|
132
|
+
// injective for the 60 ordered rectangles: 0 = none, else 1 + r0 + 4*r1 + 16*c0 + 48*c1
|
|
133
|
+
def selectionPacked(active: bool, r0: int, r1: int, c0: int, c1: int): int =
|
|
134
|
+
if (active) 1 + r0 + 4 * r1 + 16 * c0 + 48 * c1 else 0
|
|
135
|
+
def unpackR0(v: int): int = (v - 1) % 4
|
|
136
|
+
def unpackR1(v: int): int = ((v - 1) / 4) % 4
|
|
137
|
+
def unpackC0(v: int): int = ((v - 1) / 16) % 3
|
|
138
|
+
def unpackC1(v: int): int = ((v - 1) / 48) % 3
|
|
139
|
+
def nowPacked: int = selectionPacked(selActive, selR0, selR1, selC0, selC1)
|
|
140
|
+
// controlled host that keeps the old value: the request is rejected
|
|
141
|
+
def holds: bool = and(controlled, not(accepting))
|
|
142
|
+
// controlled host that writes the intent back through the value prop
|
|
143
|
+
def follows: bool = and(controlled, accepting)
|
|
144
|
+
def verdict: str = if (holds) "rejected" else "accepted"
|
|
145
|
+
// the value after a request for `want`
|
|
146
|
+
def landed(h: bool, current: bool, want: bool): bool = if (h) current else want
|
|
147
|
+
def landedI(h: bool, current: int, want: int): int = if (h) current else want
|
|
148
|
+
// the value prop after a request for `want`
|
|
149
|
+
def propAfter(f: bool, p: int, want: int): int = if (f) want else p
|
|
150
|
+
// the selection intent of a key, per member (mirrors the old action RHS)
|
|
151
|
+
def keyWantActive(k: str): bool =
|
|
152
|
+
if (selects(k)) true else if (or(collapses(k), clears(k))) false else selActive
|
|
153
|
+
def keyWantR0(k: str): int =
|
|
154
|
+
if (extendsSel(k)) lowerOf(anchorRow, newRow(k))
|
|
155
|
+
else if (selectsRow(k)) row
|
|
156
|
+
else if (or(selectsCol(k), selectsAll(k))) 0
|
|
157
|
+
else if (collapses(k)) newRow(k)
|
|
158
|
+
else if (clears(k)) row
|
|
159
|
+
else selR0
|
|
160
|
+
def keyWantR1(k: str): int =
|
|
161
|
+
if (extendsSel(k)) upperOf(anchorRow, newRow(k))
|
|
162
|
+
else if (selectsRow(k)) row
|
|
163
|
+
else if (or(selectsCol(k), selectsAll(k))) LAST_ROW
|
|
164
|
+
else if (collapses(k)) newRow(k)
|
|
165
|
+
else if (clears(k)) row
|
|
166
|
+
else selR1
|
|
167
|
+
def keyWantC0(k: str): int =
|
|
168
|
+
if (extendsSel(k)) lowerOf(anchorCol, newCol(k))
|
|
169
|
+
else if (selectsCol(k)) col
|
|
170
|
+
else if (or(selectsRow(k), selectsAll(k))) 0
|
|
171
|
+
else if (collapses(k)) newCol(k)
|
|
172
|
+
else if (clears(k)) col
|
|
173
|
+
else selC0
|
|
174
|
+
def keyWantC1(k: str): int =
|
|
175
|
+
if (extendsSel(k)) upperOf(anchorCol, newCol(k))
|
|
176
|
+
else if (selectsCol(k)) col
|
|
177
|
+
else if (or(selectsRow(k), selectsAll(k))) LAST_COL
|
|
178
|
+
else if (collapses(k)) newCol(k)
|
|
179
|
+
else if (clears(k)) col
|
|
180
|
+
else selC1
|
|
181
|
+
def keyWantPacked(k: str): int =
|
|
182
|
+
selectionPacked(keyWantActive(k), keyWantR0(k), keyWantR1(k), keyWantC0(k), keyWantC1(k))
|
|
183
|
+
def keyMoves(k: str): bool = keyWantPacked(k) != nowPacked
|
|
184
|
+
// a click collapses the selection: the intent is "nothing selected"
|
|
185
|
+
def clickMoves: bool = nowPacked != 0
|
|
186
|
+
// Shift+click extends from the anchor; on a non-selectable grid it degrades to a click
|
|
187
|
+
def shiftWantR0(cell: int): int = if (selectable) lowerOf(anchorRow, cell / COLS) else cell / COLS
|
|
188
|
+
def shiftWantR1(cell: int): int = if (selectable) upperOf(anchorRow, cell / COLS) else cell / COLS
|
|
189
|
+
def shiftWantC0(cell: int): int = if (selectable) lowerOf(anchorCol, cell % COLS) else cell % COLS
|
|
190
|
+
def shiftWantC1(cell: int): int = if (selectable) upperOf(anchorCol, cell % COLS) else cell % COLS
|
|
191
|
+
def shiftWantPacked(cell: int): int =
|
|
192
|
+
selectionPacked(selectable, shiftWantR0(cell), shiftWantR1(cell), shiftWantC0(cell), shiftWantC1(cell))
|
|
193
|
+
def shiftMoves(cell: int): bool = shiftWantPacked(cell) != nowPacked
|
|
194
|
+
// turning selection support off clears an existing selection: a real request only then
|
|
195
|
+
def setSelMoves(flag: bool): bool = and(not(flag), nowPacked != 0)
|
|
196
|
+
// the host flips its own copy: nothing -> the focus cell, else nothing
|
|
197
|
+
def togglePacked: int =
|
|
198
|
+
if (selActive) 0 else selectionPacked(true, row, row, col, col)
|
|
199
|
+
|
|
200
|
+
action init = all {
|
|
201
|
+
row' = 0,
|
|
202
|
+
col' = 0,
|
|
203
|
+
anchorRow' = 0,
|
|
204
|
+
anchorCol' = 0,
|
|
205
|
+
selActive' = false,
|
|
206
|
+
selR0' = 0,
|
|
207
|
+
selR1' = 0,
|
|
208
|
+
selC0' = 0,
|
|
209
|
+
selC1' = 0,
|
|
210
|
+
hasFocus' = false,
|
|
211
|
+
selectable' = true,
|
|
212
|
+
controlled' = false,
|
|
213
|
+
accepting' = false,
|
|
214
|
+
valueProp' = 0,
|
|
215
|
+
requested' = 0,
|
|
216
|
+
outcome' = "none",
|
|
217
|
+
base' = 0,
|
|
218
|
+
requests' = 0
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// request: a keydown while the grid has DOM focus
|
|
222
|
+
action key = {
|
|
223
|
+
nondet key = Set(
|
|
224
|
+
"ArrowRight", "ArrowLeft", "ArrowDown", "ArrowUp",
|
|
225
|
+
"Shift+ArrowRight", "Shift+ArrowLeft", "Shift+ArrowDown", "Shift+ArrowUp",
|
|
226
|
+
"Shift+Space", "Control+Space", "Control+a", "Escape"
|
|
227
|
+
).oneOf()
|
|
228
|
+
all {
|
|
229
|
+
row' = if (moves(key)) newRow(key) else row,
|
|
230
|
+
col' = if (moves(key)) newCol(key) else col,
|
|
231
|
+
anchorRow' =
|
|
232
|
+
if (collapses(key)) newRow(key)
|
|
233
|
+
else if (reanchors(key)) row
|
|
234
|
+
else anchorRow,
|
|
235
|
+
anchorCol' =
|
|
236
|
+
if (collapses(key)) newCol(key)
|
|
237
|
+
else if (reanchors(key)) col
|
|
238
|
+
else anchorCol,
|
|
239
|
+
selActive' = landed(holds, selActive, keyWantActive(key)),
|
|
240
|
+
selR0' = landedI(holds, selR0, keyWantR0(key)),
|
|
241
|
+
selR1' = landedI(holds, selR1, keyWantR1(key)),
|
|
242
|
+
selC0' = landedI(holds, selC0, keyWantC0(key)),
|
|
243
|
+
selC1' = landedI(holds, selC1, keyWantC1(key)),
|
|
244
|
+
hasFocus' = hasFocus,
|
|
245
|
+
selectable' = selectable,
|
|
246
|
+
controlled' = controlled,
|
|
247
|
+
accepting' = accepting,
|
|
248
|
+
valueProp' = if (keyMoves(key)) propAfter(follows, valueProp, keyWantPacked(key)) else valueProp,
|
|
249
|
+
requested' = if (keyMoves(key)) keyWantPacked(key) else requested,
|
|
250
|
+
outcome' = if (keyMoves(key)) verdict else outcome,
|
|
251
|
+
base' = if (keyMoves(key)) nowPacked else base,
|
|
252
|
+
requests' = if (keyMoves(key)) requests + 1 else requests
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// request: pointer on a cell: focuses it, collapses the selection onto it
|
|
257
|
+
action click = {
|
|
258
|
+
nondet cell = Set(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11).oneOf()
|
|
259
|
+
all {
|
|
260
|
+
row' = cell / COLS,
|
|
261
|
+
col' = cell % COLS,
|
|
262
|
+
anchorRow' = cell / COLS,
|
|
263
|
+
anchorCol' = cell % COLS,
|
|
264
|
+
selActive' = landed(holds, selActive, false),
|
|
265
|
+
selR0' = landedI(holds, selR0, cell / COLS),
|
|
266
|
+
selR1' = landedI(holds, selR1, cell / COLS),
|
|
267
|
+
selC0' = landedI(holds, selC0, cell % COLS),
|
|
268
|
+
selC1' = landedI(holds, selC1, cell % COLS),
|
|
269
|
+
hasFocus' = true,
|
|
270
|
+
selectable' = selectable,
|
|
271
|
+
controlled' = controlled,
|
|
272
|
+
accepting' = accepting,
|
|
273
|
+
valueProp' = if (clickMoves) propAfter(follows, valueProp, 0) else valueProp,
|
|
274
|
+
requested' = if (clickMoves) 0 else requested,
|
|
275
|
+
outcome' = if (clickMoves) verdict else outcome,
|
|
276
|
+
base' = if (clickMoves) nowPacked else base,
|
|
277
|
+
requests' = if (clickMoves) requests + 1 else requests
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// request: Shift+click extends from the anchor to the clicked cell
|
|
282
|
+
action shift_click = {
|
|
283
|
+
nondet cell = Set(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11).oneOf()
|
|
284
|
+
all {
|
|
285
|
+
row' = cell / COLS,
|
|
286
|
+
col' = cell % COLS,
|
|
287
|
+
anchorRow' = if (selectable) anchorRow else cell / COLS,
|
|
288
|
+
anchorCol' = if (selectable) anchorCol else cell % COLS,
|
|
289
|
+
selActive' = landed(holds, selActive, selectable),
|
|
290
|
+
selR0' = landedI(holds, selR0, shiftWantR0(cell)),
|
|
291
|
+
selR1' = landedI(holds, selR1, shiftWantR1(cell)),
|
|
292
|
+
selC0' = landedI(holds, selC0, shiftWantC0(cell)),
|
|
293
|
+
selC1' = landedI(holds, selC1, shiftWantC1(cell)),
|
|
294
|
+
hasFocus' = true,
|
|
295
|
+
selectable' = selectable,
|
|
296
|
+
controlled' = controlled,
|
|
297
|
+
accepting' = accepting,
|
|
298
|
+
valueProp' = if (shiftMoves(cell)) propAfter(follows, valueProp, shiftWantPacked(cell)) else valueProp,
|
|
299
|
+
requested' = if (shiftMoves(cell)) shiftWantPacked(cell) else requested,
|
|
300
|
+
outcome' = if (shiftMoves(cell)) verdict else outcome,
|
|
301
|
+
base' = if (shiftMoves(cell)) nowPacked else base,
|
|
302
|
+
requests' = if (shiftMoves(cell)) requests + 1 else requests
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
action focus_in = all {
|
|
307
|
+
row' = row,
|
|
308
|
+
col' = col,
|
|
309
|
+
anchorRow' = anchorRow,
|
|
310
|
+
anchorCol' = anchorCol,
|
|
311
|
+
selActive' = selActive,
|
|
312
|
+
selR0' = selR0,
|
|
313
|
+
selR1' = selR1,
|
|
314
|
+
selC0' = selC0,
|
|
315
|
+
selC1' = selC1,
|
|
316
|
+
hasFocus' = true,
|
|
317
|
+
selectable' = selectable,
|
|
318
|
+
controlled' = controlled,
|
|
319
|
+
accepting' = accepting,
|
|
320
|
+
valueProp' = valueProp,
|
|
321
|
+
requested' = requested,
|
|
322
|
+
outcome' = outcome,
|
|
323
|
+
base' = base,
|
|
324
|
+
requests' = requests
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// blur keeps the selection
|
|
328
|
+
action focus_out = all {
|
|
329
|
+
row' = row,
|
|
330
|
+
col' = col,
|
|
331
|
+
anchorRow' = anchorRow,
|
|
332
|
+
anchorCol' = anchorCol,
|
|
333
|
+
selActive' = selActive,
|
|
334
|
+
selR0' = selR0,
|
|
335
|
+
selR1' = selR1,
|
|
336
|
+
selC0' = selC0,
|
|
337
|
+
selC1' = selC1,
|
|
338
|
+
hasFocus' = false,
|
|
339
|
+
selectable' = selectable,
|
|
340
|
+
controlled' = controlled,
|
|
341
|
+
accepting' = accepting,
|
|
342
|
+
valueProp' = valueProp,
|
|
343
|
+
requested' = requested,
|
|
344
|
+
outcome' = outcome,
|
|
345
|
+
base' = base,
|
|
346
|
+
requests' = requests
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// request: turning selection support off clears the selection
|
|
350
|
+
action set_selectable = {
|
|
351
|
+
nondet flag = Set(true, false).oneOf()
|
|
352
|
+
all {
|
|
353
|
+
row' = row,
|
|
354
|
+
col' = col,
|
|
355
|
+
anchorRow' = if (flag) anchorRow else row,
|
|
356
|
+
anchorCol' = if (flag) anchorCol else col,
|
|
357
|
+
selActive' = if (flag) selActive else landed(holds, selActive, false),
|
|
358
|
+
selR0' = if (flag) selR0 else landedI(holds, selR0, row),
|
|
359
|
+
selR1' = if (flag) selR1 else landedI(holds, selR1, row),
|
|
360
|
+
selC0' = if (flag) selC0 else landedI(holds, selC0, col),
|
|
361
|
+
selC1' = if (flag) selC1 else landedI(holds, selC1, col),
|
|
362
|
+
hasFocus' = hasFocus,
|
|
363
|
+
selectable' = flag,
|
|
364
|
+
controlled' = controlled,
|
|
365
|
+
accepting' = accepting,
|
|
366
|
+
valueProp' = if (setSelMoves(flag)) propAfter(follows, valueProp, 0) else valueProp,
|
|
367
|
+
requested' = if (setSelMoves(flag)) 0 else requested,
|
|
368
|
+
outcome' = if (setSelMoves(flag)) verdict else outcome,
|
|
369
|
+
base' = if (setSelMoves(flag)) nowPacked else base,
|
|
370
|
+
requests' = if (setSelMoves(flag)) requests + 1 else requests
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// commit: the host writes the controlled selection (a no-op while uncontrolled)
|
|
375
|
+
action setValue = {
|
|
376
|
+
// A representative spread (none, a cell, a rectangle, the full grid) keeps
|
|
377
|
+
// the commit path covered without exploding the bounded exploration.
|
|
378
|
+
nondet v = Set(0, 1, 6, 11, 49, 65, 97, 144).oneOf()
|
|
379
|
+
all {
|
|
380
|
+
row' = row,
|
|
381
|
+
col' = col,
|
|
382
|
+
anchorRow' = anchorRow,
|
|
383
|
+
anchorCol' = anchorCol,
|
|
384
|
+
selActive' = if (controlled) v != 0 else selActive,
|
|
385
|
+
selR0' = if (controlled) (if (v == 0) row else unpackR0(v)) else selR0,
|
|
386
|
+
selR1' = if (controlled) (if (v == 0) row else unpackR1(v)) else selR1,
|
|
387
|
+
selC0' = if (controlled) (if (v == 0) col else unpackC0(v)) else selC0,
|
|
388
|
+
selC1' = if (controlled) (if (v == 0) col else unpackC1(v)) else selC1,
|
|
389
|
+
hasFocus' = hasFocus,
|
|
390
|
+
selectable' = selectable,
|
|
391
|
+
controlled' = controlled,
|
|
392
|
+
accepting' = accepting,
|
|
393
|
+
valueProp' = if (controlled) v else valueProp,
|
|
394
|
+
requested' = requested,
|
|
395
|
+
outcome' = if (controlled) "none" else outcome,
|
|
396
|
+
base' = base,
|
|
397
|
+
requests' = requests
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// commit: the host flips its own copy (nothing -> the focus cell, else nothing)
|
|
402
|
+
action hostToggle = all {
|
|
403
|
+
row' = row,
|
|
404
|
+
col' = col,
|
|
405
|
+
anchorRow' = anchorRow,
|
|
406
|
+
anchorCol' = anchorCol,
|
|
407
|
+
selActive' = if (controlled) togglePacked != 0 else selActive,
|
|
408
|
+
selR0' = if (controlled) (if (togglePacked == 0) row else unpackR0(togglePacked)) else selR0,
|
|
409
|
+
selR1' = if (controlled) (if (togglePacked == 0) row else unpackR1(togglePacked)) else selR1,
|
|
410
|
+
selC0' = if (controlled) (if (togglePacked == 0) col else unpackC0(togglePacked)) else selC0,
|
|
411
|
+
selC1' = if (controlled) (if (togglePacked == 0) col else unpackC1(togglePacked)) else selC1,
|
|
412
|
+
hasFocus' = hasFocus,
|
|
413
|
+
selectable' = selectable,
|
|
414
|
+
controlled' = controlled,
|
|
415
|
+
accepting' = accepting,
|
|
416
|
+
valueProp' = if (controlled) togglePacked else valueProp,
|
|
417
|
+
requested' = requested,
|
|
418
|
+
outcome' = if (controlled) "none" else outcome,
|
|
419
|
+
base' = base,
|
|
420
|
+
requests' = requests
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// configuration: the host starts owning the selection, adopting the current one
|
|
424
|
+
action control = all {
|
|
425
|
+
row' = row,
|
|
426
|
+
col' = col,
|
|
427
|
+
anchorRow' = anchorRow,
|
|
428
|
+
anchorCol' = anchorCol,
|
|
429
|
+
selActive' = selActive,
|
|
430
|
+
selR0' = selR0,
|
|
431
|
+
selR1' = selR1,
|
|
432
|
+
selC0' = selC0,
|
|
433
|
+
selC1' = selC1,
|
|
434
|
+
hasFocus' = hasFocus,
|
|
435
|
+
selectable' = selectable,
|
|
436
|
+
controlled' = true,
|
|
437
|
+
accepting' = accepting,
|
|
438
|
+
valueProp' = nowPacked,
|
|
439
|
+
requested' = requested,
|
|
440
|
+
outcome' = outcome,
|
|
441
|
+
base' = base,
|
|
442
|
+
requests' = requests
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// configuration: the controlled host answers every request with its intent
|
|
446
|
+
action acceptRequests = all {
|
|
447
|
+
row' = row,
|
|
448
|
+
col' = col,
|
|
449
|
+
anchorRow' = anchorRow,
|
|
450
|
+
anchorCol' = anchorCol,
|
|
451
|
+
selActive' = selActive,
|
|
452
|
+
selR0' = selR0,
|
|
453
|
+
selR1' = selR1,
|
|
454
|
+
selC0' = selC0,
|
|
455
|
+
selC1' = selC1,
|
|
456
|
+
hasFocus' = hasFocus,
|
|
457
|
+
selectable' = selectable,
|
|
458
|
+
controlled' = controlled,
|
|
459
|
+
accepting' = true,
|
|
460
|
+
valueProp' = valueProp,
|
|
461
|
+
requested' = requested,
|
|
462
|
+
outcome' = outcome,
|
|
463
|
+
base' = base,
|
|
464
|
+
requests' = requests
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
action step = any {
|
|
468
|
+
init, key, click, shift_click, focus_in, focus_out, set_selectable,
|
|
469
|
+
setValue, hostToggle, control, acceptRequests
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
val FocusInBounds: bool = and(row >= 0, row < ROWS, col >= 0, col < COLS)
|
|
473
|
+
val AnchorInBounds: bool = and(anchorRow >= 0, anchorRow < ROWS, anchorCol >= 0, anchorCol < COLS)
|
|
474
|
+
// the rectangle is well-formed and inside the grid, active or not
|
|
475
|
+
val SelectionRectOrdered: bool = and(
|
|
476
|
+
selR0 >= 0, selR0 <= selR1, selR1 < ROWS,
|
|
477
|
+
selC0 >= 0, selC0 <= selC1, selC1 < COLS
|
|
478
|
+
)
|
|
479
|
+
// the focused cell is always part of an active selection (so it has aria-selected=true);
|
|
480
|
+
// a controlled host owns the value, so focus may leave the held rectangle
|
|
481
|
+
val ActiveSelectionContainsFocus: bool = or(controlled, or(not(selActive), cellSelected(row, col)))
|
|
482
|
+
// ... and so is the anchor (uncontrolled)
|
|
483
|
+
val ActiveSelectionContainsAnchor: bool = or(controlled, or(not(selActive), cellSelected(anchorRow, anchorCol)))
|
|
484
|
+
// no selection: the rectangle and anchor collapse onto the focus cell (uncontrolled)
|
|
485
|
+
val InactiveSelectionCollapsed: bool = or(
|
|
486
|
+
controlled,
|
|
487
|
+
or(
|
|
488
|
+
selActive,
|
|
489
|
+
and(selR0 == row, selR1 == row, selC0 == col, selC1 == col, anchorRow == row, anchorCol == col)
|
|
490
|
+
)
|
|
491
|
+
)
|
|
492
|
+
// a grid without selection support has no selection (uncontrolled)
|
|
493
|
+
val SelectionOnlyWhenSelectable: bool = or(controlled, or(not(selActive), selectable))
|
|
494
|
+
// a full-width selection marks every cell of the focused row selected
|
|
495
|
+
val FullWidthSelectionIsRowSelection: bool = or(
|
|
496
|
+
not(rowSelected(row)),
|
|
497
|
+
and(cellSelected(row, 0), cellSelected(row, 1), cellSelected(row, 2))
|
|
498
|
+
)
|
|
499
|
+
// a full-height selection marks every cell of the focused column selected
|
|
500
|
+
val FullHeightSelectionIsColumnSelection: bool = or(
|
|
501
|
+
not(colSelected(col)),
|
|
502
|
+
and(cellSelected(0, col), cellSelected(1, col), cellSelected(2, col), cellSelected(3, col))
|
|
503
|
+
)
|
|
504
|
+
// callbacks only ever count up from zero
|
|
505
|
+
val RequestsNonNegative: bool = requests >= 0
|
|
506
|
+
// controlled: only the value prop sets the state, so no event commits
|
|
507
|
+
val ControlledFollowsProp: bool = or(not(controlled), nowPacked == valueProp)
|
|
508
|
+
// a rejected request leaves the state as it found it
|
|
509
|
+
val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), nowPacked == base)
|
|
510
|
+
// an accepted request committed exactly its intent
|
|
511
|
+
val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), nowPacked == requested)
|
|
512
|
+
}
|