@cssints/apg-specs 0.0.0-stage → 0.5.2
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,330 @@
|
|
|
1
|
+
// WAI-ARIA APG — grid: sortable column headers (data grid example 2).
|
|
2
|
+
//
|
|
3
|
+
// Modelled from https://www.w3.org/WAI/ARIA/apg/patterns/grid/ ("WAI-ARIA Roles,
|
|
4
|
+
// States, and Properties": aria-sort on the header cell of the sorted column; "if
|
|
5
|
+
// column or row header cells do not provide functions, such as sort or filter,
|
|
6
|
+
// they do not need to be focusable") and from the "Sortable Data Grid With Editable
|
|
7
|
+
// Cells" example in https://www.w3.org/WAI/ARIA/apg/patterns/grid/examples/data-grids/
|
|
8
|
+
// (aria-sort ascending / descending / none; headers focusable because Date and
|
|
9
|
+
// Amount sort). Also the Grid and Table Properties Practice referenced there
|
|
10
|
+
// (one sorted column at a time).
|
|
11
|
+
//
|
|
12
|
+
// The machine is the header row only: roving focus among the COLS column headers,
|
|
13
|
+
// Enter / Space / click activate the sort button of a header. Columns 0 and 2 are
|
|
14
|
+
// sortable, column 1 is not. aria-sort per header is derived:
|
|
15
|
+
// absent - the column is not sortable (or sorting is disabled);
|
|
16
|
+
// "none" - sortable but not the sorted column;
|
|
17
|
+
// "ascending" / "descending" - the sorted column.
|
|
18
|
+
//
|
|
19
|
+
// Deliberate decisions where APG is silent:
|
|
20
|
+
// - activating an unsorted sortable header sorts it ascending and resets the
|
|
21
|
+
// previously sorted header to "none"; activating the sorted header toggles
|
|
22
|
+
// ascending <-> descending (it never returns to "none");
|
|
23
|
+
// - activating a non-sortable header only moves focus;
|
|
24
|
+
// - header focus does not wrap; keys are ignored without DOM focus;
|
|
25
|
+
// - disabling sorting clears the sort and removes aria-sort everywhere.
|
|
26
|
+
//
|
|
27
|
+
// Request vs commit (map ticket rt1p). The sort — a (column, direction) pair — is
|
|
28
|
+
// the value, packed into `sortPacked` (-1 = unsorted). Every event a user sees as
|
|
29
|
+
// a sort — `key` (Enter/Space on a sortable header) and `click` — is a *request*
|
|
30
|
+
// carrying an intent (`requested`); it fires onValueChange (`requests` counts the
|
|
31
|
+
// calls) when the intent differs from the current value, and then:
|
|
32
|
+
// uncontrolled the request is accepted: the sort commits the intent
|
|
33
|
+
// controlled, holds the host ignores it ("rejected"): the sort is untouched
|
|
34
|
+
// controlled, accepts the host answers with the same value (`accepting`): the
|
|
35
|
+
// sort commits through the controlled value, not the event
|
|
36
|
+
// `valueProp` is the only thing that changes the sort while `controlled`;
|
|
37
|
+
// `setValue` / `hostToggle` are the host writing it. `control` / `acceptRequests`
|
|
38
|
+
// are configuration events (init is literal-only) and `set_sort_enabled` is an
|
|
39
|
+
// ordinary prop change (a request when it clears an existing sort). Invariants for
|
|
40
|
+
// the model: ControlledFollowsProp, RejectedRequestUntouched,
|
|
41
|
+
// AcceptedRequestCommits; `NoSortWhenDisabled` is scoped to the uncontrolled path
|
|
42
|
+
// (a controlled host owns the value and may hold a sort while sorting is off).
|
|
43
|
+
//
|
|
44
|
+
// Deliberate omissions: Up/Down between the header row and the body cells (compose
|
|
45
|
+
// with grid.qnt by treating the header as row -1), the data rows' order itself,
|
|
46
|
+
// aria-live sort announcements, column hiding (aria-colindex gaps), the
|
|
47
|
+
// "headers are not focusable if they have no function" variant (here every header
|
|
48
|
+
// is focusable whenever sorting is enabled), filter buttons.
|
|
49
|
+
//
|
|
50
|
+
// Generator gaps hit: no sets/lists/maps (the sorted column is an int, -1 = none);
|
|
51
|
+
// no `const` (sizes are zero-argument `val`s); one nondet per action.
|
|
52
|
+
module apg_grid_sort {
|
|
53
|
+
var headerCol: int // focused header column, 0-based (the roving tab stop)
|
|
54
|
+
var hasFocus: bool // DOM focus is on a header cell
|
|
55
|
+
var sortEnabled: bool // the grid offers sorting at all
|
|
56
|
+
var sortCol: int // sorted column, -1 = unsorted
|
|
57
|
+
var sortDir: str // "none" | "ascending" | "descending"
|
|
58
|
+
var controlled: bool // the host owns the sort (`value` prop present)
|
|
59
|
+
var accepting: bool // controlled host that answers every request with its intent
|
|
60
|
+
var valueProp: int // the controlled value prop (packed, -1 = unsorted)
|
|
61
|
+
var requested: int // intent of the last request (packed)
|
|
62
|
+
var outcome: str // "none" | "accepted" | "rejected"
|
|
63
|
+
var base: int // value when the last request was received (packed)
|
|
64
|
+
var requests: int // number of requests that fired onValueChange
|
|
65
|
+
|
|
66
|
+
val COLS: int = 3
|
|
67
|
+
val LAST_COL: int = COLS - 1
|
|
68
|
+
|
|
69
|
+
// columns 0 (Date) and 2 (Amount) sort; column 1 (Type) does not
|
|
70
|
+
def sortable(c: int): bool = c != 1
|
|
71
|
+
|
|
72
|
+
// --- ARIA contract (derived) ---
|
|
73
|
+
// "absent" means the aria-sort attribute is omitted
|
|
74
|
+
def ariaSort(c: int): str =
|
|
75
|
+
if (not(and(sortEnabled, sortable(c)))) "absent"
|
|
76
|
+
else if (c == sortCol) sortDir
|
|
77
|
+
else "none"
|
|
78
|
+
def headerTabindex(c: int): int = if (c == headerCol) 0 else -1
|
|
79
|
+
def isSortedHeader(c: int): bool = or(ariaSort(c) == "ascending", ariaSort(c) == "descending")
|
|
80
|
+
def sortedHeaders: int =
|
|
81
|
+
(if (isSortedHeader(0)) 1 else 0) + (if (isSortedHeader(1)) 1 else 0) + (if (isSortedHeader(2)) 1 else 0)
|
|
82
|
+
def headerTabStops: int =
|
|
83
|
+
(if (headerTabindex(0) == 0) 1 else 0) + (if (headerTabindex(1) == 0) 1 else 0) + (if (headerTabindex(2) == 0) 1 else 0)
|
|
84
|
+
|
|
85
|
+
// sorting by column c (a no-op for unsortable columns / disabled sorting)
|
|
86
|
+
def sorts(c: int): bool = and(sortEnabled, sortable(c))
|
|
87
|
+
def nextDir(c: int): str =
|
|
88
|
+
if (and(c == sortCol, sortDir == "ascending")) "descending" else "ascending"
|
|
89
|
+
|
|
90
|
+
def keyCol(k: str): int =
|
|
91
|
+
if (k == "ArrowRight") (if (headerCol < LAST_COL) headerCol + 1 else headerCol)
|
|
92
|
+
else if (k == "ArrowLeft") (if (headerCol > 0) headerCol - 1 else headerCol)
|
|
93
|
+
else if (k == "Home") 0
|
|
94
|
+
else if (k == "End") LAST_COL
|
|
95
|
+
else headerCol
|
|
96
|
+
def activates(k: str): bool = and(hasFocus, or(k == "Enter", k == "Space"), sorts(headerCol))
|
|
97
|
+
|
|
98
|
+
// --- request / commit (map ticket rt1p) ----------------------------------
|
|
99
|
+
def moved(before: int, after: int): int = if (before == after) 0 else 1
|
|
100
|
+
// injective: -1 unsorted; else 2*column + (descending ? 1 : 0)
|
|
101
|
+
def sortPacked(c: int, d: str): int =
|
|
102
|
+
if (c == -1) -1 else 2 * c + (if (d == "descending") 1 else 0)
|
|
103
|
+
def unpackCol(v: int): int = if (v == -1) -1 else v / 2
|
|
104
|
+
def unpackDir(v: int): str =
|
|
105
|
+
if (v == -1) "none" else if (v % 2 == 1) "descending" else "ascending"
|
|
106
|
+
def nowPacked: int = sortPacked(sortCol, sortDir)
|
|
107
|
+
// controlled host that keeps the old value: the request is rejected
|
|
108
|
+
def holds: bool = and(controlled, not(accepting))
|
|
109
|
+
// controlled host that writes the intent back through the value prop
|
|
110
|
+
def follows: bool = and(controlled, accepting)
|
|
111
|
+
def verdict: str = if (holds) "rejected" else "accepted"
|
|
112
|
+
// the value after a request for `want`
|
|
113
|
+
def landedI(h: bool, current: int, want: int): int = if (h) current else want
|
|
114
|
+
def landedS(h: bool, current: str, want: str): str = if (h) current else want
|
|
115
|
+
// the value prop after a request for `want`
|
|
116
|
+
def propAfter(f: bool, p: int, want: int): int = if (f) want else p
|
|
117
|
+
// the request intent of a key / a click, per member and packed
|
|
118
|
+
def keyWantCol(k: str): int = if (activates(k)) headerCol else sortCol
|
|
119
|
+
def keyWantDir(k: str): str = if (activates(k)) nextDir(headerCol) else sortDir
|
|
120
|
+
def keyWantPacked(k: str): int = sortPacked(keyWantCol(k), keyWantDir(k))
|
|
121
|
+
def keyMoves(k: str): bool = keyWantPacked(k) != nowPacked
|
|
122
|
+
def clickWantCol(c: int): int = if (sorts(c)) c else sortCol
|
|
123
|
+
def clickWantDir(c: int): str = if (sorts(c)) nextDir(c) else sortDir
|
|
124
|
+
def clickWantPacked(c: int): int = sortPacked(clickWantCol(c), clickWantDir(c))
|
|
125
|
+
def clickMoves(c: int): bool = clickWantPacked(c) != nowPacked
|
|
126
|
+
// disabling sorting clears an existing sort: a real request only then
|
|
127
|
+
def setEnabledMoves(f: bool): bool = and(not(f), nowPacked != -1)
|
|
128
|
+
// the host flips its own copy: None -> column 0 ascending, else flip the direction
|
|
129
|
+
def flipDir(d: str): str = if (d == "ascending") "descending" else "ascending"
|
|
130
|
+
def togglePacked: int =
|
|
131
|
+
if (sortCol == -1) sortPacked(0, "ascending") else sortPacked(sortCol, flipDir(sortDir))
|
|
132
|
+
|
|
133
|
+
action init = all {
|
|
134
|
+
headerCol' = 0,
|
|
135
|
+
hasFocus' = false,
|
|
136
|
+
sortEnabled' = true,
|
|
137
|
+
sortCol' = -1,
|
|
138
|
+
sortDir' = "none",
|
|
139
|
+
controlled' = false,
|
|
140
|
+
accepting' = false,
|
|
141
|
+
valueProp' = -1,
|
|
142
|
+
requested' = -1,
|
|
143
|
+
outcome' = "none",
|
|
144
|
+
base' = -1,
|
|
145
|
+
requests' = 0
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// request: a keydown on the header row (Enter / Space activate a sortable column)
|
|
149
|
+
action key = {
|
|
150
|
+
nondet key = Set("ArrowRight", "ArrowLeft", "Home", "End", "Enter", "Space").oneOf()
|
|
151
|
+
all {
|
|
152
|
+
headerCol' = if (hasFocus) keyCol(key) else headerCol,
|
|
153
|
+
hasFocus' = hasFocus,
|
|
154
|
+
sortEnabled' = sortEnabled,
|
|
155
|
+
sortCol' = landedI(holds, sortCol, keyWantCol(key)),
|
|
156
|
+
sortDir' = landedS(holds, sortDir, keyWantDir(key)),
|
|
157
|
+
controlled' = controlled,
|
|
158
|
+
accepting' = accepting,
|
|
159
|
+
valueProp' = if (keyMoves(key)) propAfter(follows, valueProp, keyWantPacked(key)) else valueProp,
|
|
160
|
+
requested' = if (keyMoves(key)) keyWantPacked(key) else requested,
|
|
161
|
+
outcome' = if (keyMoves(key)) verdict else outcome,
|
|
162
|
+
base' = if (keyMoves(key)) nowPacked else base,
|
|
163
|
+
requests' = if (keyMoves(key)) requests + 1 else requests
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// request: pointer on a header: focuses it and activates its sort button
|
|
168
|
+
action click = {
|
|
169
|
+
nondet column = Set(0, 1, 2).oneOf()
|
|
170
|
+
all {
|
|
171
|
+
headerCol' = column,
|
|
172
|
+
hasFocus' = true,
|
|
173
|
+
sortEnabled' = sortEnabled,
|
|
174
|
+
sortCol' = landedI(holds, sortCol, clickWantCol(column)),
|
|
175
|
+
sortDir' = landedS(holds, sortDir, clickWantDir(column)),
|
|
176
|
+
controlled' = controlled,
|
|
177
|
+
accepting' = accepting,
|
|
178
|
+
valueProp' = if (clickMoves(column)) propAfter(follows, valueProp, clickWantPacked(column)) else valueProp,
|
|
179
|
+
requested' = if (clickMoves(column)) clickWantPacked(column) else requested,
|
|
180
|
+
outcome' = if (clickMoves(column)) verdict else outcome,
|
|
181
|
+
base' = if (clickMoves(column)) nowPacked else base,
|
|
182
|
+
requests' = if (clickMoves(column)) requests + 1 else requests
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
action focus_in = all {
|
|
187
|
+
headerCol' = headerCol,
|
|
188
|
+
hasFocus' = true,
|
|
189
|
+
sortEnabled' = sortEnabled,
|
|
190
|
+
sortCol' = sortCol,
|
|
191
|
+
sortDir' = sortDir,
|
|
192
|
+
controlled' = controlled,
|
|
193
|
+
accepting' = accepting,
|
|
194
|
+
valueProp' = valueProp,
|
|
195
|
+
requested' = requested,
|
|
196
|
+
outcome' = outcome,
|
|
197
|
+
base' = base,
|
|
198
|
+
requests' = requests
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
action focus_out = all {
|
|
202
|
+
headerCol' = headerCol,
|
|
203
|
+
hasFocus' = false,
|
|
204
|
+
sortEnabled' = sortEnabled,
|
|
205
|
+
sortCol' = sortCol,
|
|
206
|
+
sortDir' = sortDir,
|
|
207
|
+
controlled' = controlled,
|
|
208
|
+
accepting' = accepting,
|
|
209
|
+
valueProp' = valueProp,
|
|
210
|
+
requested' = requested,
|
|
211
|
+
outcome' = outcome,
|
|
212
|
+
base' = base,
|
|
213
|
+
requests' = requests
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// prop change: disabling sorting requests clearing the sort
|
|
217
|
+
action set_sort_enabled = {
|
|
218
|
+
nondet flag = Set(true, false).oneOf()
|
|
219
|
+
all {
|
|
220
|
+
headerCol' = headerCol,
|
|
221
|
+
hasFocus' = hasFocus,
|
|
222
|
+
sortEnabled' = flag,
|
|
223
|
+
sortCol' = if (flag) sortCol else landedI(holds, sortCol, -1),
|
|
224
|
+
sortDir' = if (flag) sortDir else landedS(holds, sortDir, "none"),
|
|
225
|
+
controlled' = controlled,
|
|
226
|
+
accepting' = accepting,
|
|
227
|
+
valueProp' = if (setEnabledMoves(flag)) propAfter(follows, valueProp, -1) else valueProp,
|
|
228
|
+
requested' = if (setEnabledMoves(flag)) -1 else requested,
|
|
229
|
+
outcome' = if (setEnabledMoves(flag)) verdict else outcome,
|
|
230
|
+
base' = if (setEnabledMoves(flag)) nowPacked else base,
|
|
231
|
+
requests' = if (setEnabledMoves(flag)) requests + 1 else requests
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// commit: the host writes the controlled sort (a no-op while uncontrolled)
|
|
236
|
+
action setValue = {
|
|
237
|
+
nondet v = Set(-1, 0, 1, 4, 5).oneOf()
|
|
238
|
+
all {
|
|
239
|
+
headerCol' = headerCol,
|
|
240
|
+
hasFocus' = hasFocus,
|
|
241
|
+
sortEnabled' = sortEnabled,
|
|
242
|
+
sortCol' = if (controlled) unpackCol(v) else sortCol,
|
|
243
|
+
sortDir' = if (controlled) unpackDir(v) else sortDir,
|
|
244
|
+
controlled' = controlled,
|
|
245
|
+
accepting' = accepting,
|
|
246
|
+
valueProp' = if (controlled) v else valueProp,
|
|
247
|
+
requested' = requested,
|
|
248
|
+
outcome' = if (controlled) "none" else outcome,
|
|
249
|
+
base' = base,
|
|
250
|
+
requests' = requests
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// commit: the host flips its own copy of the sort (an outside button)
|
|
255
|
+
action hostToggle = all {
|
|
256
|
+
headerCol' = headerCol,
|
|
257
|
+
hasFocus' = hasFocus,
|
|
258
|
+
sortEnabled' = sortEnabled,
|
|
259
|
+
sortCol' = if (controlled) unpackCol(togglePacked) else sortCol,
|
|
260
|
+
sortDir' = if (controlled) unpackDir(togglePacked) else sortDir,
|
|
261
|
+
controlled' = controlled,
|
|
262
|
+
accepting' = accepting,
|
|
263
|
+
valueProp' = if (controlled) togglePacked else valueProp,
|
|
264
|
+
requested' = requested,
|
|
265
|
+
outcome' = if (controlled) "none" else outcome,
|
|
266
|
+
base' = base,
|
|
267
|
+
requests' = requests
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// configuration: the host starts owning the sort, adopting the current one
|
|
271
|
+
action control = all {
|
|
272
|
+
headerCol' = headerCol,
|
|
273
|
+
hasFocus' = hasFocus,
|
|
274
|
+
sortEnabled' = sortEnabled,
|
|
275
|
+
sortCol' = sortCol,
|
|
276
|
+
sortDir' = sortDir,
|
|
277
|
+
controlled' = true,
|
|
278
|
+
accepting' = accepting,
|
|
279
|
+
valueProp' = nowPacked,
|
|
280
|
+
requested' = requested,
|
|
281
|
+
outcome' = outcome,
|
|
282
|
+
base' = base,
|
|
283
|
+
requests' = requests
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// configuration: the controlled host answers every request with its intent
|
|
287
|
+
action acceptRequests = all {
|
|
288
|
+
headerCol' = headerCol,
|
|
289
|
+
hasFocus' = hasFocus,
|
|
290
|
+
sortEnabled' = sortEnabled,
|
|
291
|
+
sortCol' = sortCol,
|
|
292
|
+
sortDir' = sortDir,
|
|
293
|
+
controlled' = controlled,
|
|
294
|
+
accepting' = true,
|
|
295
|
+
valueProp' = valueProp,
|
|
296
|
+
requested' = requested,
|
|
297
|
+
outcome' = outcome,
|
|
298
|
+
base' = base,
|
|
299
|
+
requests' = requests
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
action step = any {
|
|
303
|
+
init, key, click, focus_in, focus_out, set_sort_enabled,
|
|
304
|
+
setValue, hostToggle, control, acceptRequests
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
val HeaderFocusInBounds: bool = and(headerCol >= 0, headerCol < COLS)
|
|
308
|
+
// roving tabindex across the header row: exactly one header has tabindex=0
|
|
309
|
+
val ExactlyOneHeaderTabStop: bool = headerTabStops == 1
|
|
310
|
+
// at most one header carries ascending/descending
|
|
311
|
+
val AtMostOneSortedHeader: bool = sortedHeaders <= 1
|
|
312
|
+
// a sorted column is a sortable column; unsorted means direction "none"
|
|
313
|
+
val SortedColumnIsSortable: bool = or(sortCol == -1, and(sortCol >= 0, sortCol < COLS, sortable(sortCol)))
|
|
314
|
+
val UnsortedMeansNoDirection: bool = or(sortCol != -1, sortDir == "none")
|
|
315
|
+
val SortedMeansDirection: bool = or(sortCol == -1, or(sortDir == "ascending", sortDir == "descending"))
|
|
316
|
+
// no sort state (and so no aria-sort) when sorting is disabled; a controlled
|
|
317
|
+
// host owns the value and may hold a sort while sorting is turned off
|
|
318
|
+
val NoSortWhenDisabled: bool = or(controlled, or(sortEnabled, and(sortCol == -1, sortDir == "none")))
|
|
319
|
+
// a non-sortable header never exposes aria-sort
|
|
320
|
+
val NonSortableHeaderHasNoAriaSort: bool = ariaSort(1) == "absent"
|
|
321
|
+
val SortDirValid: bool = or(sortDir == "none", sortDir == "ascending", sortDir == "descending")
|
|
322
|
+
// callbacks only ever count up from zero
|
|
323
|
+
val RequestsNonNegative: bool = requests >= 0
|
|
324
|
+
// controlled: only the value prop sets the state, so no event commits
|
|
325
|
+
val ControlledFollowsProp: bool = or(not(controlled), nowPacked == valueProp)
|
|
326
|
+
// a rejected request leaves the state as it found it
|
|
327
|
+
val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), nowPacked == base)
|
|
328
|
+
// an accepted request committed exactly its intent
|
|
329
|
+
val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), nowPacked == requested)
|
|
330
|
+
}
|