@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,360 @@
|
|
|
1
|
+
// WAI-ARIA APG — grid (data grid + layout grid): roving focus, navigation, wrap, edit mode.
|
|
2
|
+
//
|
|
3
|
+
// Modelled from https://www.w3.org/WAI/ARIA/apg/patterns/grid/ :
|
|
4
|
+
// - "Keyboard Interaction For Data Grids" (no wrapping, arrows, Home/End,
|
|
5
|
+
// Ctrl+Home/End, PageUp/PageDown)
|
|
6
|
+
// - "Keyboard Interaction For Layout Grids" (optional wrapping across row /
|
|
7
|
+
// column boundaries)
|
|
8
|
+
// - "Whether to Focus on a Cell Or an Element Inside It"
|
|
9
|
+
// - "Editing and Navigating Inside a Cell" (Enter / F2 / Escape / alphanumeric)
|
|
10
|
+
// - "WAI-ARIA Roles, States, and Properties" (aria-rowindex/colindex/rowcount/
|
|
11
|
+
// colcount/readonly, roving tabindex)
|
|
12
|
+
// and the examples https://www.w3.org/WAI/ARIA/apg/patterns/grid/examples/data-grids/
|
|
13
|
+
// and https://www.w3.org/WAI/ARIA/apg/patterns/grid/examples/layout-grids/
|
|
14
|
+
// (data-wrap-cols / data-wrap-rows, PageDown/PageUp by a fixed row count, tabindex
|
|
15
|
+
// -1/0 on the cell or on the widget inside it).
|
|
16
|
+
//
|
|
17
|
+
// The grid is a bounded ROWS x COLS body (header row: see grid-sort.qnt;
|
|
18
|
+
// selection: see grid-selection.qnt). Focus is the pair (row, col); a cell is
|
|
19
|
+
// addressed by the linear index row * COLS + col, which makes every wrap rule
|
|
20
|
+
// "+1 / -1 / +COLS" arithmetic.
|
|
21
|
+
//
|
|
22
|
+
// Events: `key` (the keydown alphabet below), `click` (pointer on a cell),
|
|
23
|
+
// `focus_in` / `focus_out` (focus enters / leaves the grid), and prop changes
|
|
24
|
+
// `set_kind`, `set_wrap_cols`, `set_wrap_rows`, `set_focus_target`,
|
|
25
|
+
// `set_readonly`. The ARIA contract is the derived defs (tabindex, aria-*), not
|
|
26
|
+
// state.
|
|
27
|
+
//
|
|
28
|
+
// Deliberate decisions where APG says "optional" / "may":
|
|
29
|
+
// - a data grid never wraps (APG: wrapping "would be disorienting" in a data
|
|
30
|
+
// grid), so switching to a data grid clears both wrap flags;
|
|
31
|
+
// - Enter or F2 in edit mode commits and restores grid navigation; Escape
|
|
32
|
+
// cancels (discards the draft) and restores navigation;
|
|
33
|
+
// - leaving the grid (blur, click elsewhere in the grid) commits an edit;
|
|
34
|
+
// - a read-only cell (last column) and a read-only grid never enter edit mode;
|
|
35
|
+
// turning the grid read-only mid-edit cancels the edit;
|
|
36
|
+
// - while editing, every other key belongs to the input (focus never moves).
|
|
37
|
+
//
|
|
38
|
+
// Deliberate omissions (no state / no events for them):
|
|
39
|
+
// - header rows/cells, sort and aria-sort (grid-sort.qnt);
|
|
40
|
+
// - cell/row/column selection and aria-selected (grid-selection.qnt);
|
|
41
|
+
// - aria-rowindex/aria-colindex gaps from virtualization or hidden columns
|
|
42
|
+
// (here the indices are always the dense 1-based position);
|
|
43
|
+
// - cells with several widgets (Tab / Arrow between widgets inside a cell) and
|
|
44
|
+
// Tab / Shift+Tab inside edit mode (modelled as a no-op);
|
|
45
|
+
// - layout-grid optional rule "Home/End go to the first/last cell of the grid
|
|
46
|
+
// when a row has fewer than three cells";
|
|
47
|
+
// - Enter moving the edit to the neighbouring cell, cut/copy/paste, spans,
|
|
48
|
+
// grid labelling (aria-labelledby/describedby), dynamic rows/columns,
|
|
49
|
+
// author-configurable page size (PAGE is a constant);
|
|
50
|
+
// - DOM `focus()` calls: focus is the abstract bit `hasFocus`.
|
|
51
|
+
//
|
|
52
|
+
// Controlled state (map ticket rt1p): `grid` is NAVIGATION-ONLY and stays
|
|
53
|
+
// uncontrolled. Focus is roving internal navigation state (`row`, `col`,
|
|
54
|
+
// `hasFocus`, `mode`), not a host-owned value the consumer can pass down; the
|
|
55
|
+
// user-changeable values live in the sibling machines — the selected rectangle
|
|
56
|
+
// (grid-selection.qnt) and the sort (grid-sort.qnt). Those two carry the
|
|
57
|
+
// `controlled`/`accepting`/`valueProp` request/commit shape; this machine has no
|
|
58
|
+
// `value` prop to express, so it deliberately omits it.
|
|
59
|
+
//
|
|
60
|
+
// Generator gaps hit: no `const` declarations (grid sizes are zero-argument
|
|
61
|
+
// `val`s of type int); one nondet per action (cell index / key name are single
|
|
62
|
+
// pools); no sets/lists/maps (cell state is derived from scalar vars; the
|
|
63
|
+
// "exactly one tab stop" invariant enumerates the ROWS x COLS cells).
|
|
64
|
+
module apg_grid {
|
|
65
|
+
var row: int // focused row, 0-based
|
|
66
|
+
var col: int // focused column, 0-based
|
|
67
|
+
var hasFocus: bool // DOM focus is inside the grid
|
|
68
|
+
var mode: str // "navigate" | "edit"
|
|
69
|
+
var dirty: bool // the edit draft differs from the committed value
|
|
70
|
+
var lastEdit: str // "none" | "committed" | "cancelled"
|
|
71
|
+
var gridKind: str // "data" | "layout"
|
|
72
|
+
var wrapCols: bool // Left/Right wrap across row boundaries (data-wrap-cols)
|
|
73
|
+
var wrapRows: bool // Up/Down wrap across column boundaries (data-wrap-rows)
|
|
74
|
+
var focusTarget: str // "cell" | "widget": what carries tabindex and DOM focus
|
|
75
|
+
var gridReadonly: bool // aria-readonly on the grid element
|
|
76
|
+
|
|
77
|
+
// bounded grid: change here, then keep CELL_POOL (click) and the
|
|
78
|
+
// tabStops enumeration in step
|
|
79
|
+
val ROWS: int = 4
|
|
80
|
+
val COLS: int = 3
|
|
81
|
+
val PAGE: int = 2
|
|
82
|
+
val LAST_ROW: int = ROWS - 1
|
|
83
|
+
val LAST_COL: int = COLS - 1
|
|
84
|
+
val CELLS: int = ROWS * COLS
|
|
85
|
+
|
|
86
|
+
// the last column is computed data (e.g. a balance): never editable
|
|
87
|
+
def colReadonly(c: int): bool = c == LAST_COL
|
|
88
|
+
|
|
89
|
+
def cur: int = row * COLS + col
|
|
90
|
+
def navigating: bool = and(hasFocus, mode == "navigate")
|
|
91
|
+
def editing: bool = mode == "edit"
|
|
92
|
+
def canEdit: bool = and(not(gridReadonly), not(colReadonly(col)))
|
|
93
|
+
|
|
94
|
+
// --- navigation targets (linear cell index); a blocked move returns `cur` ---
|
|
95
|
+
def rightIdx: int =
|
|
96
|
+
if (col < LAST_COL) cur + 1 else if (and(wrapCols, row < LAST_ROW)) cur + 1 else cur
|
|
97
|
+
def leftIdx: int =
|
|
98
|
+
if (col > 0) cur - 1 else if (and(wrapCols, row > 0)) cur - 1 else cur
|
|
99
|
+
def downIdx: int =
|
|
100
|
+
if (row < LAST_ROW) cur + COLS else if (and(wrapRows, col < LAST_COL)) col + 1 else cur
|
|
101
|
+
def upIdx: int =
|
|
102
|
+
if (row > 0) cur - COLS else if (and(wrapRows, col > 0)) LAST_ROW * COLS + col - 1 else cur
|
|
103
|
+
def pageDownIdx: int =
|
|
104
|
+
if (row + PAGE > LAST_ROW) LAST_ROW * COLS + col else cur + PAGE * COLS
|
|
105
|
+
def pageUpIdx: int =
|
|
106
|
+
if (row < PAGE) col else cur - PAGE * COLS
|
|
107
|
+
|
|
108
|
+
def keyTarget(k: str): int =
|
|
109
|
+
if (k == "ArrowRight") rightIdx
|
|
110
|
+
else if (k == "ArrowLeft") leftIdx
|
|
111
|
+
else if (k == "ArrowDown") downIdx
|
|
112
|
+
else if (k == "ArrowUp") upIdx
|
|
113
|
+
else if (k == "PageDown") pageDownIdx
|
|
114
|
+
else if (k == "PageUp") pageUpIdx
|
|
115
|
+
else if (k == "Home") row * COLS
|
|
116
|
+
else if (k == "End") row * COLS + LAST_COL
|
|
117
|
+
else if (k == "Control+Home") 0
|
|
118
|
+
else if (k == "Control+End") CELLS - 1
|
|
119
|
+
else cur
|
|
120
|
+
|
|
121
|
+
// --- edit mode ---
|
|
122
|
+
// Enter / F2 / a printable key start editing a focused editable cell
|
|
123
|
+
def startsEdit(k: str): bool =
|
|
124
|
+
and(navigating, canEdit, or(k == "Enter", k == "F2", k == "a"))
|
|
125
|
+
def commitsEdit(k: str): bool = and(editing, or(k == "Enter", k == "F2"))
|
|
126
|
+
def cancelsEdit(k: str): bool = and(editing, k == "Escape")
|
|
127
|
+
def endsEdit(k: str): bool = or(commitsEdit(k), cancelsEdit(k))
|
|
128
|
+
def leavesGrid(k: str): bool = and(navigating, or(k == "Tab", k == "Shift+Tab"))
|
|
129
|
+
|
|
130
|
+
// --- ARIA contract (derived) ---
|
|
131
|
+
val ARIA_ROWCOUNT: int = ROWS
|
|
132
|
+
val ARIA_COLCOUNT: int = COLS
|
|
133
|
+
def ariaRowindex(r: int): int = r + 1
|
|
134
|
+
def ariaColindex(c: int): int = c + 1
|
|
135
|
+
def cellReadonly(r: int, c: int): bool = or(gridReadonly, colReadonly(c))
|
|
136
|
+
// roving tabindex: exactly the focused cell (or its widget) is tabbable
|
|
137
|
+
def cellTabindex(r: int, c: int): int =
|
|
138
|
+
if (and(r == row, c == col, focusTarget == "cell")) 0 else -1
|
|
139
|
+
def widgetTabindex(r: int, c: int): int =
|
|
140
|
+
if (and(r == row, c == col, focusTarget == "widget")) 0 else -1
|
|
141
|
+
def cellDomFocused(r: int, c: int): bool =
|
|
142
|
+
and(navigating, focusTarget == "cell", r == row, c == col)
|
|
143
|
+
def widgetDomFocused(r: int, c: int): bool =
|
|
144
|
+
and(navigating, focusTarget == "widget", r == row, c == col)
|
|
145
|
+
def inputDomFocused(r: int, c: int): bool = and(editing, r == row, c == col)
|
|
146
|
+
|
|
147
|
+
def tabStops(r: int, c: int): int =
|
|
148
|
+
(if (cellTabindex(r, c) == 0) 1 else 0) + (if (widgetTabindex(r, c) == 0) 1 else 0)
|
|
149
|
+
def rowTabStops(r: int): int =
|
|
150
|
+
tabStops(r, 0) + tabStops(r, 1) + tabStops(r, 2)
|
|
151
|
+
|
|
152
|
+
action init = all {
|
|
153
|
+
row' = 0,
|
|
154
|
+
col' = 0,
|
|
155
|
+
hasFocus' = false,
|
|
156
|
+
mode' = "navigate",
|
|
157
|
+
dirty' = false,
|
|
158
|
+
lastEdit' = "none",
|
|
159
|
+
gridKind' = "data",
|
|
160
|
+
wrapCols' = false,
|
|
161
|
+
wrapRows' = false,
|
|
162
|
+
focusTarget' = "cell",
|
|
163
|
+
gridReadonly' = false
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// keydown while the grid has focus; ignored otherwise
|
|
167
|
+
action key = {
|
|
168
|
+
nondet key = Set(
|
|
169
|
+
"ArrowRight", "ArrowLeft", "ArrowDown", "ArrowUp",
|
|
170
|
+
"Home", "End", "Control+Home", "Control+End",
|
|
171
|
+
"PageUp", "PageDown",
|
|
172
|
+
"Enter", "F2", "Escape", "a",
|
|
173
|
+
"Tab", "Shift+Tab"
|
|
174
|
+
).oneOf()
|
|
175
|
+
all {
|
|
176
|
+
row' = if (navigating) keyTarget(key) / COLS else row,
|
|
177
|
+
col' = if (navigating) keyTarget(key) % COLS else col,
|
|
178
|
+
hasFocus' = if (leavesGrid(key)) false else hasFocus,
|
|
179
|
+
mode' = if (startsEdit(key)) "edit" else if (endsEdit(key)) "navigate" else mode,
|
|
180
|
+
dirty' =
|
|
181
|
+
if (startsEdit(key)) (key == "a")
|
|
182
|
+
else if (and(editing, key == "a")) true
|
|
183
|
+
else if (endsEdit(key)) false
|
|
184
|
+
else dirty,
|
|
185
|
+
lastEdit' =
|
|
186
|
+
if (commitsEdit(key)) "committed"
|
|
187
|
+
else if (cancelsEdit(key)) "cancelled"
|
|
188
|
+
else lastEdit,
|
|
189
|
+
gridKind' = gridKind,
|
|
190
|
+
wrapCols' = wrapCols,
|
|
191
|
+
wrapRows' = wrapRows,
|
|
192
|
+
focusTarget' = focusTarget,
|
|
193
|
+
gridReadonly' = gridReadonly
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// pointer on a cell: focuses it; clicking another cell commits an edit
|
|
198
|
+
action click = {
|
|
199
|
+
nondet cell = Set(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11).oneOf()
|
|
200
|
+
all {
|
|
201
|
+
row' = cell / COLS,
|
|
202
|
+
col' = cell % COLS,
|
|
203
|
+
hasFocus' = true,
|
|
204
|
+
mode' = if (and(editing, cell == cur)) "edit" else "navigate",
|
|
205
|
+
dirty' = if (and(editing, cell == cur)) dirty else false,
|
|
206
|
+
lastEdit' = if (and(editing, cell != cur)) "committed" else lastEdit,
|
|
207
|
+
gridKind' = gridKind,
|
|
208
|
+
wrapCols' = wrapCols,
|
|
209
|
+
wrapRows' = wrapRows,
|
|
210
|
+
focusTarget' = focusTarget,
|
|
211
|
+
gridReadonly' = gridReadonly
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// focus enters the grid (Tab): lands on the roving tab stop, position kept
|
|
216
|
+
action focus_in = all {
|
|
217
|
+
row' = row,
|
|
218
|
+
col' = col,
|
|
219
|
+
hasFocus' = true,
|
|
220
|
+
mode' = mode,
|
|
221
|
+
dirty' = dirty,
|
|
222
|
+
lastEdit' = lastEdit,
|
|
223
|
+
gridKind' = gridKind,
|
|
224
|
+
wrapCols' = wrapCols,
|
|
225
|
+
wrapRows' = wrapRows,
|
|
226
|
+
focusTarget' = focusTarget,
|
|
227
|
+
gridReadonly' = gridReadonly
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// focus leaves the grid: an open edit is committed, the roving tab stop stays
|
|
231
|
+
action focus_out = all {
|
|
232
|
+
row' = row,
|
|
233
|
+
col' = col,
|
|
234
|
+
hasFocus' = false,
|
|
235
|
+
mode' = "navigate",
|
|
236
|
+
dirty' = false,
|
|
237
|
+
lastEdit' = if (editing) "committed" else lastEdit,
|
|
238
|
+
gridKind' = gridKind,
|
|
239
|
+
wrapCols' = wrapCols,
|
|
240
|
+
wrapRows' = wrapRows,
|
|
241
|
+
focusTarget' = focusTarget,
|
|
242
|
+
gridReadonly' = gridReadonly
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// data grids never wrap
|
|
246
|
+
action set_kind = {
|
|
247
|
+
nondet choice = Set("data", "layout").oneOf()
|
|
248
|
+
all {
|
|
249
|
+
row' = row,
|
|
250
|
+
col' = col,
|
|
251
|
+
hasFocus' = hasFocus,
|
|
252
|
+
mode' = mode,
|
|
253
|
+
dirty' = dirty,
|
|
254
|
+
lastEdit' = lastEdit,
|
|
255
|
+
gridKind' = choice,
|
|
256
|
+
wrapCols' = if (choice == "data") false else wrapCols,
|
|
257
|
+
wrapRows' = if (choice == "data") false else wrapRows,
|
|
258
|
+
focusTarget' = focusTarget,
|
|
259
|
+
gridReadonly' = gridReadonly
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// wrapping is only available on layout grids
|
|
264
|
+
action set_wrap_cols = {
|
|
265
|
+
nondet flag = Set(true, false).oneOf()
|
|
266
|
+
all {
|
|
267
|
+
row' = row,
|
|
268
|
+
col' = col,
|
|
269
|
+
hasFocus' = hasFocus,
|
|
270
|
+
mode' = mode,
|
|
271
|
+
dirty' = dirty,
|
|
272
|
+
lastEdit' = lastEdit,
|
|
273
|
+
gridKind' = gridKind,
|
|
274
|
+
wrapCols' = if (gridKind == "layout") flag else false,
|
|
275
|
+
wrapRows' = wrapRows,
|
|
276
|
+
focusTarget' = focusTarget,
|
|
277
|
+
gridReadonly' = gridReadonly
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
action set_wrap_rows = {
|
|
282
|
+
nondet flag = Set(true, false).oneOf()
|
|
283
|
+
all {
|
|
284
|
+
row' = row,
|
|
285
|
+
col' = col,
|
|
286
|
+
hasFocus' = hasFocus,
|
|
287
|
+
mode' = mode,
|
|
288
|
+
dirty' = dirty,
|
|
289
|
+
lastEdit' = lastEdit,
|
|
290
|
+
gridKind' = gridKind,
|
|
291
|
+
wrapCols' = wrapCols,
|
|
292
|
+
wrapRows' = if (gridKind == "layout") flag else false,
|
|
293
|
+
focusTarget' = focusTarget,
|
|
294
|
+
gridReadonly' = gridReadonly
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
action set_focus_target = {
|
|
299
|
+
nondet choice = Set("cell", "widget").oneOf()
|
|
300
|
+
all {
|
|
301
|
+
row' = row,
|
|
302
|
+
col' = col,
|
|
303
|
+
hasFocus' = hasFocus,
|
|
304
|
+
mode' = mode,
|
|
305
|
+
dirty' = dirty,
|
|
306
|
+
lastEdit' = lastEdit,
|
|
307
|
+
gridKind' = gridKind,
|
|
308
|
+
wrapCols' = wrapCols,
|
|
309
|
+
wrapRows' = wrapRows,
|
|
310
|
+
focusTarget' = choice,
|
|
311
|
+
gridReadonly' = gridReadonly
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// read-only grid: a running edit is cancelled
|
|
316
|
+
action set_readonly = {
|
|
317
|
+
nondet flag = Set(true, false).oneOf()
|
|
318
|
+
all {
|
|
319
|
+
row' = row,
|
|
320
|
+
col' = col,
|
|
321
|
+
hasFocus' = hasFocus,
|
|
322
|
+
mode' = if (and(flag, editing)) "navigate" else mode,
|
|
323
|
+
dirty' = if (and(flag, editing)) false else dirty,
|
|
324
|
+
lastEdit' = if (and(flag, editing)) "cancelled" else lastEdit,
|
|
325
|
+
gridKind' = gridKind,
|
|
326
|
+
wrapCols' = wrapCols,
|
|
327
|
+
wrapRows' = wrapRows,
|
|
328
|
+
focusTarget' = focusTarget,
|
|
329
|
+
gridReadonly' = flag
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
action step = any {
|
|
334
|
+
init, key, click, focus_in, focus_out,
|
|
335
|
+
set_kind, set_wrap_cols, set_wrap_rows, set_focus_target, set_readonly
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// focus coordinates stay inside the bounded grid
|
|
339
|
+
val FocusInBounds: bool = and(row >= 0, row < ROWS, col >= 0, col < COLS)
|
|
340
|
+
// roving tabindex: exactly one element (cell or widget) in the whole grid has tabindex=0
|
|
341
|
+
val ExactlyOneTabStop: bool =
|
|
342
|
+
rowTabStops(0) + rowTabStops(1) + rowTabStops(2) + rowTabStops(3) == 1
|
|
343
|
+
// aria-rowindex / aria-colindex of the focused cell are 1-based and within the counts
|
|
344
|
+
val AriaIndicesInRange: bool = and(
|
|
345
|
+
ariaRowindex(row) >= 1, ariaRowindex(row) <= ARIA_ROWCOUNT,
|
|
346
|
+
ariaColindex(col) >= 1, ariaColindex(col) <= ARIA_COLCOUNT
|
|
347
|
+
)
|
|
348
|
+
val ModeValid: bool = or(mode == "navigate", mode == "edit")
|
|
349
|
+
val KindValid: bool = or(gridKind == "data", gridKind == "layout")
|
|
350
|
+
val FocusTargetValid: bool = or(focusTarget == "cell", focusTarget == "widget")
|
|
351
|
+
val LastEditValid: bool = or(lastEdit == "none", lastEdit == "committed", lastEdit == "cancelled")
|
|
352
|
+
// an input can only be focused while DOM focus is inside the grid
|
|
353
|
+
val EditImpliesFocus: bool = or(not(editing), hasFocus)
|
|
354
|
+
// only an editable cell of an editable grid is ever in edit mode
|
|
355
|
+
val EditOnEditableCell: bool = or(not(editing), canEdit)
|
|
356
|
+
// a draft exists only inside edit mode
|
|
357
|
+
val DirtyImpliesEdit: bool = or(not(dirty), editing)
|
|
358
|
+
// wrapping is a layout-grid-only behaviour
|
|
359
|
+
val DataGridNeverWraps: bool = or(gridKind == "layout", and(not(wrapCols), not(wrapRows)))
|
|
360
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// APG Link — https://www.w3.org/WAI/ARIA/apg/patterns/link/
|
|
2
|
+
//
|
|
3
|
+
// A link is an interactive reference to a resource. A native <a href> brings
|
|
4
|
+
// focusability and activation for free; a non-native element with role=link
|
|
5
|
+
// must supply both itself (tabindex=0 and an Enter handler), because "applying
|
|
6
|
+
// the link role will not cause browsers to enhance the element".
|
|
7
|
+
//
|
|
8
|
+
// Keyboard (APG): Enter executes the link and moves focus to the link target.
|
|
9
|
+
// Space does NOT activate a link.
|
|
10
|
+
//
|
|
11
|
+
// Events:
|
|
12
|
+
// focusLink / blur the link gains / loses focus (a link that is not
|
|
13
|
+
// focusable ignores focusLink)
|
|
14
|
+
// press Enter on the focused link
|
|
15
|
+
// pressSpace Space on the focused link (no effect)
|
|
16
|
+
// click pointer activation (focuses, then executes)
|
|
17
|
+
// back the user navigates back to the origin
|
|
18
|
+
// setNative / setCustom the element is a native <a href> / a role=link span
|
|
19
|
+
//
|
|
20
|
+
// location: 0 = origin, 1 = link target. focus: "none" | "link" | "target".
|
|
21
|
+
//
|
|
22
|
+
// ponytail: the optional Shift+F10 context menu is not modelled; add a
|
|
23
|
+
// `menuOpen` var when the widget implements it.
|
|
24
|
+
module apg_link {
|
|
25
|
+
var native: bool // native <a href> (no role needed)
|
|
26
|
+
var tabindex: bool // tabindex=0 present (custom role=link only)
|
|
27
|
+
var focus: str // "none" | "link" | "target"
|
|
28
|
+
var location: int // 0 = origin, 1 = target
|
|
29
|
+
var activations: int // times the link executed
|
|
30
|
+
|
|
31
|
+
def focusable: bool = or(native, tabindex)
|
|
32
|
+
|
|
33
|
+
action init = all {
|
|
34
|
+
native' = true,
|
|
35
|
+
tabindex' = false,
|
|
36
|
+
focus' = "none",
|
|
37
|
+
location' = 0,
|
|
38
|
+
activations' = 0
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
action focusLink = all {
|
|
42
|
+
native' = native,
|
|
43
|
+
tabindex' = tabindex,
|
|
44
|
+
focus' = if (focusable) "link" else focus,
|
|
45
|
+
location' = location,
|
|
46
|
+
activations' = activations
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
action blur = all {
|
|
50
|
+
native' = native,
|
|
51
|
+
tabindex' = tabindex,
|
|
52
|
+
focus' = "none",
|
|
53
|
+
location' = location,
|
|
54
|
+
activations' = activations
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
action press = all {
|
|
58
|
+
native' = native,
|
|
59
|
+
tabindex' = tabindex,
|
|
60
|
+
focus' = if (focus == "link") "target" else focus,
|
|
61
|
+
location' = if (focus == "link") 1 else location,
|
|
62
|
+
activations' = if (focus == "link") activations + 1 else activations
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
action pressSpace = all {
|
|
66
|
+
native' = native,
|
|
67
|
+
tabindex' = tabindex,
|
|
68
|
+
focus' = focus,
|
|
69
|
+
location' = location,
|
|
70
|
+
activations' = activations
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
action click = all {
|
|
74
|
+
native' = native,
|
|
75
|
+
tabindex' = tabindex,
|
|
76
|
+
focus' = "target",
|
|
77
|
+
location' = 1,
|
|
78
|
+
activations' = activations + 1
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
action back = all {
|
|
82
|
+
native' = native,
|
|
83
|
+
tabindex' = tabindex,
|
|
84
|
+
focus' = "none",
|
|
85
|
+
location' = 0,
|
|
86
|
+
activations' = activations
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
action setNative = all {
|
|
90
|
+
native' = true,
|
|
91
|
+
tabindex' = false,
|
|
92
|
+
focus' = focus,
|
|
93
|
+
location' = location,
|
|
94
|
+
activations' = activations
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
action setCustom = all {
|
|
98
|
+
native' = false,
|
|
99
|
+
tabindex' = true,
|
|
100
|
+
focus' = focus,
|
|
101
|
+
location' = location,
|
|
102
|
+
activations' = activations
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
action step = any { init, focusLink, blur, press, pressSpace, click, back, setNative, setCustom }
|
|
106
|
+
|
|
107
|
+
// APG: a non-native link role must be keyboard reachable (tabindex=0)
|
|
108
|
+
val KeyboardReachable: bool = focusable
|
|
109
|
+
// only a focusable element can hold focus on the link
|
|
110
|
+
val FocusOnLinkImpliesFocusable: bool = or(focus != "link", focusable)
|
|
111
|
+
// focus lands on the target only after arriving there
|
|
112
|
+
val TargetFocusImpliesArrived: bool = or(focus != "target", location == 1)
|
|
113
|
+
// being at the target means the link ran at least once
|
|
114
|
+
val ArrivedImpliesActivated: bool = or(location == 0, activations >= 1)
|
|
115
|
+
// only the origin and the target exist
|
|
116
|
+
val LocationKnown: bool = or(location == 0, location == 1)
|
|
117
|
+
// the activation count only grows from zero
|
|
118
|
+
val ActivationsNonNegative: bool = activations >= 0
|
|
119
|
+
}
|