@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,578 @@
|
|
|
1
|
+
// WAI-ARIA APG — Accordion pattern (sections with show/hide functionality).
|
|
2
|
+
//
|
|
3
|
+
// Modelled from https://www.w3.org/WAI/ARIA/apg/patterns/accordion/
|
|
4
|
+
// - "About This Pattern" headers are controls that reveal/hide panels
|
|
5
|
+
// - "Keyboard Interaction" Enter/Space toggle, Tab/Shift+Tab walk every
|
|
6
|
+
// focusable element (no roving tabindex)
|
|
7
|
+
// - "WAI-ARIA Roles, States, and Properties"
|
|
8
|
+
// aria-expanded mirrors panel visibility,
|
|
9
|
+
// aria-controls / region + aria-labelledby,
|
|
10
|
+
// aria-disabled on the sole expanded header
|
|
11
|
+
// when collapsing is not permitted
|
|
12
|
+
// and the worked example
|
|
13
|
+
// https://www.w3.org/WAI/ARIA/apg/patterns/accordion/examples/accordion/
|
|
14
|
+
//
|
|
15
|
+
// State: three panels (expanded0..2), the focused element, and the
|
|
16
|
+
// configuration variants as props that can change at any time:
|
|
17
|
+
// multiple false = at most one panel expanded ("only one panel" impl.)
|
|
18
|
+
// collapsible false = one panel must stay expanded (aria-disabled on it)
|
|
19
|
+
// headingLevel the aria-level of the heading wrapping each header button
|
|
20
|
+
//
|
|
21
|
+
// Focus is a "slot" in DOM order (-1 = outside the accordion):
|
|
22
|
+
// 0 header0, 1 content0, 2 header1, 3 content1, 4 header2, 5 content2
|
|
23
|
+
// A content slot stands for the panel's focusable element (e.g. a form field)
|
|
24
|
+
// and is in the Tab sequence only while its panel is expanded.
|
|
25
|
+
//
|
|
26
|
+
// Request vs commit (map ticket rt1p). Everything a user sees as a change —
|
|
27
|
+
// click_header, key_enter, key_space — is a *request* carrying an intent (the
|
|
28
|
+
// toggled vector, packed into `requested`). It fires onExpandedChange
|
|
29
|
+
// (`requests` counts the calls) when the intent differs from the current value,
|
|
30
|
+
// and then:
|
|
31
|
+
// uncontrolled the request is accepted: the panels commit the intent
|
|
32
|
+
// controlled, holds the host ignores it ("rejected"): the panels are untouched
|
|
33
|
+
// controlled, accepts the host answers with the same value (`accepting`): the
|
|
34
|
+
// panels commit through the controlled value, not the event
|
|
35
|
+
// The controlled value (`valueProp`, the three expanded bits packed with
|
|
36
|
+
// `packed`) is the only thing that changes the panels while `controlled`;
|
|
37
|
+
// `setValue` / `hostToggle` are the host writing it. `control` and
|
|
38
|
+
// `acceptRequests` are configuration events (init is literal-only), like
|
|
39
|
+
// `disable` would be.
|
|
40
|
+
//
|
|
41
|
+
// The generator has one nondet pool per action and no collections, so the
|
|
42
|
+
// controlled value is a packed int and every action assigns every var; the
|
|
43
|
+
// per-panel request path is spelled out per member. The mode invariants
|
|
44
|
+
// (`SingleModeAtMostOneExpanded`, `NonCollapsibleKeepsOneExpanded`) are scoped
|
|
45
|
+
// to the uncontrolled path: a controlled host owns the value, so the widget
|
|
46
|
+
// renders whatever set it is given.
|
|
47
|
+
//
|
|
48
|
+
// Deliberate omissions:
|
|
49
|
+
// - the optional Down/Up/Home/End header navigation (not on the current page)
|
|
50
|
+
// - the optional region landmark and its ">6 panels" caution
|
|
51
|
+
// - static structure (role=button inside role=heading, the heading holding
|
|
52
|
+
// only the button, id wiring) beyond the id round-trip defs below
|
|
53
|
+
// - persistent adjacent controls (menubutton) and per-panel disabled headers
|
|
54
|
+
// - Enter vs Space keydown/keyup timing: both toggle identically here
|
|
55
|
+
// - Tab from outside enters at the first slot, Shift+Tab at the last: the
|
|
56
|
+
// page around the accordion is a single "outside" state
|
|
57
|
+
// - panel count is fixed at 3 (the generator has no collection state)
|
|
58
|
+
//
|
|
59
|
+
// Generator notes: constants/records are unsupported, so config is state, and
|
|
60
|
+
// every action assigns every var (guards live in the RHS).
|
|
61
|
+
module apg_accordion {
|
|
62
|
+
import aria.* from "../../shared/aria"
|
|
63
|
+
|
|
64
|
+
var expanded0: bool
|
|
65
|
+
var expanded1: bool
|
|
66
|
+
var expanded2: bool
|
|
67
|
+
var focus: int
|
|
68
|
+
var multiple: bool
|
|
69
|
+
var collapsible: bool
|
|
70
|
+
var headingLevel: int
|
|
71
|
+
var controlled: bool // the host owns the value (`value` prop present)
|
|
72
|
+
var accepting: bool // controlled host that answers every request with its intent
|
|
73
|
+
var valueProp: int // the controlled value prop, packed with `packed`
|
|
74
|
+
var requested: int // intent of the last request, packed
|
|
75
|
+
var outcome: str // "none" | "accepted" | "rejected"
|
|
76
|
+
var base: int // value when the last request was received, packed
|
|
77
|
+
var requests: int // number of requests that fired onExpandedChange
|
|
78
|
+
|
|
79
|
+
// ---- pure helpers ----------------------------------------------------
|
|
80
|
+
|
|
81
|
+
def isExpanded(i: int, a: bool, b: bool, c: bool): bool =
|
|
82
|
+
if (i == 0) a else if (i == 1) b else c
|
|
83
|
+
|
|
84
|
+
def expandedCount(a: bool, b: bool, c: bool): int =
|
|
85
|
+
(if (a) 1 else 0) + (if (b) 1 else 0) + (if (c) 1 else 0)
|
|
86
|
+
|
|
87
|
+
// a collapse is honoured when collapsing is allowed, or when another panel
|
|
88
|
+
// stays expanded
|
|
89
|
+
def canCollapse(a: bool, b: bool, c: bool, coll: bool): bool =
|
|
90
|
+
or(coll, expandedCount(a, b, c) >= 2)
|
|
91
|
+
|
|
92
|
+
// Enter/Space/click on header `i` (-1 = nothing activated): the new
|
|
93
|
+
// expanded flag of panel `j`
|
|
94
|
+
def toggledExpanded(j: int, i: int, a: bool, b: bool, c: bool, multi: bool, coll: bool): bool =
|
|
95
|
+
if (i < 0) isExpanded(j, a, b, c)
|
|
96
|
+
else if (isExpanded(i, a, b, c))
|
|
97
|
+
(if (j == i) not(canCollapse(a, b, c, coll)) else isExpanded(j, a, b, c))
|
|
98
|
+
else
|
|
99
|
+
(if (j == i) true else (if (multi) isExpanded(j, a, b, c) else false))
|
|
100
|
+
|
|
101
|
+
def lowerExpanded(j: int, a: bool, b: bool, c: bool): bool =
|
|
102
|
+
or(and(j > 0, a), and(j > 1, b))
|
|
103
|
+
|
|
104
|
+
// a prop change re-establishes the mode's constraints: single keeps the
|
|
105
|
+
// first expanded panel; non-collapsible expands panel 0 when none is open
|
|
106
|
+
def normExpanded(j: int, a: bool, b: bool, c: bool, multi: bool, coll: bool): bool =
|
|
107
|
+
if (and(not(coll), expandedCount(a, b, c) == 0)) (j == 0)
|
|
108
|
+
else if (multi) isExpanded(j, a, b, c)
|
|
109
|
+
else and(isExpanded(j, a, b, c), not(lowerExpanded(j, a, b, c)))
|
|
110
|
+
|
|
111
|
+
def onHeader(f: int): bool = and(f >= 0, f % 2 == 0)
|
|
112
|
+
def onContent(f: int): bool = and(f >= 0, f % 2 == 1)
|
|
113
|
+
def headerTarget(f: int): int = if (onHeader(f)) f / 2 else -1
|
|
114
|
+
|
|
115
|
+
// headers are always focusable; content only while its panel is expanded
|
|
116
|
+
def slotShown(s: int, a: bool, b: bool, c: bool): bool =
|
|
117
|
+
or(s % 2 == 0, isExpanded(s / 2, a, b, c))
|
|
118
|
+
|
|
119
|
+
// Tab: next focusable slot, or out of the accordion after the last
|
|
120
|
+
def nextSlot(f: int, a: bool, b: bool, c: bool): int =
|
|
121
|
+
if (f >= 5) -1
|
|
122
|
+
else if (slotShown(f + 1, a, b, c)) f + 1
|
|
123
|
+
else if (f + 2 > 5) -1
|
|
124
|
+
else f + 2
|
|
125
|
+
|
|
126
|
+
def prevFrom(s: int, a: bool, b: bool, c: bool): int =
|
|
127
|
+
if (slotShown(s, a, b, c)) s else s - 1
|
|
128
|
+
|
|
129
|
+
// Shift+Tab: previous focusable slot, or out before the first
|
|
130
|
+
def prevSlot(f: int, a: bool, b: bool, c: bool): int =
|
|
131
|
+
if (f == 0) -1
|
|
132
|
+
else prevFrom(if (f < 0) 5 else f - 1, a, b, c)
|
|
133
|
+
|
|
134
|
+
// focus inside a panel that just got hidden is lost
|
|
135
|
+
def focusAfter(f: int, a: bool, b: bool, c: bool): int =
|
|
136
|
+
if (onContent(f)) (if (isExpanded(f / 2, a, b, c)) f else -1) else f
|
|
137
|
+
|
|
138
|
+
// ---- request / commit (map ticket rt1p) -------------------------------
|
|
139
|
+
|
|
140
|
+
def b2i(b: bool): int = if (b) 1 else 0
|
|
141
|
+
// injective for three bools: bit 0 = panel 0, bit 1 = panel 1, bit 2 = panel 2
|
|
142
|
+
def packed(a: bool, b: bool, c: bool): int =
|
|
143
|
+
b2i(a) + 2 * b2i(b) + 4 * b2i(c)
|
|
144
|
+
def bitAt(i: int, n: int): bool =
|
|
145
|
+
if (i == 0) n % 2 == 1 else if (i == 1) (n / 2) % 2 == 1 else (n / 4) % 2 == 1
|
|
146
|
+
// controlled host that keeps the old value: the request is rejected
|
|
147
|
+
def holds: bool = and(controlled, not(accepting))
|
|
148
|
+
// controlled host that writes the intent back through the value prop
|
|
149
|
+
def follows: bool = and(controlled, accepting)
|
|
150
|
+
def verdict: str = if (holds) "rejected" else "accepted"
|
|
151
|
+
// the value after a request for `want`
|
|
152
|
+
def landed(h: bool, current: bool, want: bool): bool = if (h) current else want
|
|
153
|
+
// the value prop after a request for `want`
|
|
154
|
+
def propAfter(f: bool, p: int, want: int): int = if (f) want else p
|
|
155
|
+
// the intent of a request on header `i`, per member and packed
|
|
156
|
+
def intent(j: int, i: int): bool =
|
|
157
|
+
toggledExpanded(j, i, expanded0, expanded1, expanded2, multiple, collapsible)
|
|
158
|
+
def wantPacked(i: int): int = packed(intent(0, i), intent(1, i), intent(2, i))
|
|
159
|
+
def nowPacked: int = packed(expanded0, expanded1, expanded2)
|
|
160
|
+
// the value a mode prop change re-establishes
|
|
161
|
+
def normAt(j: int, multi: bool, coll: bool): bool =
|
|
162
|
+
normExpanded(j, expanded0, expanded1, expanded2, multi, coll)
|
|
163
|
+
def normPacked(multi: bool, coll: bool): int =
|
|
164
|
+
packed(normAt(0, multi, coll), normAt(1, multi, coll), normAt(2, multi, coll))
|
|
165
|
+
// a request is real only when its intent differs from the current value
|
|
166
|
+
def moves(i: int): bool = wantPacked(i) != nowPacked
|
|
167
|
+
// the host flips one bit of its own copy of the value
|
|
168
|
+
def flipAt(n: int, i: int): int =
|
|
169
|
+
if (i == 0) (if (bitAt(0, n)) n - 1 else n + 1)
|
|
170
|
+
else if (i == 1) (if (bitAt(1, n)) n - 2 else n + 2)
|
|
171
|
+
else (if (bitAt(2, n)) n - 4 else n + 4)
|
|
172
|
+
|
|
173
|
+
// ---- the ARIA contract, derived from state ----------------------------
|
|
174
|
+
|
|
175
|
+
def expandedAt(i: int): bool = isExpanded(i, expanded0, expanded1, expanded2)
|
|
176
|
+
def ariaExpanded(i: int): str = ariaBoolToken(expandedAt(i))
|
|
177
|
+
def ariaDisabled(i: int): bool =
|
|
178
|
+
and(expandedAt(i), not(canCollapse(expanded0, expanded1, expanded2, collapsible)))
|
|
179
|
+
def ariaControls(i: int): int = i
|
|
180
|
+
def regionLabelledBy(i: int): int = i
|
|
181
|
+
def tabbable(s: int): bool = slotShown(s, expanded0, expanded1, expanded2)
|
|
182
|
+
def toggledAt(j: int, i: int): bool =
|
|
183
|
+
toggledExpanded(j, i, expanded0, expanded1, expanded2, multiple, collapsible)
|
|
184
|
+
|
|
185
|
+
// aria-disabled marks exactly the header whose activation is refused;
|
|
186
|
+
// an allow expanded header collapses; a collapsed header expands
|
|
187
|
+
def toggleContractHolds(i: int): bool = and(
|
|
188
|
+
or(not(ariaDisabled(i)), toggledAt(i, i)),
|
|
189
|
+
or(not(and(expandedAt(i), not(ariaDisabled(i)))), not(toggledAt(i, i))),
|
|
190
|
+
or(expandedAt(i), toggledAt(i, i))
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
// expanding a header collapses every other panel unless multiple
|
|
194
|
+
def expandRespectsMode(i: int): bool =
|
|
195
|
+
or(
|
|
196
|
+
expandedAt(i),
|
|
197
|
+
and(
|
|
198
|
+
toggledAt(i, i),
|
|
199
|
+
or(multiple, and(not(toggledAt((i + 1) % 3, i)), not(toggledAt((i + 2) % 3, i))))
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
// aria-expanded mirrors visibility: the header is always in the Tab
|
|
204
|
+
// sequence, the panel's content is in it iff aria-expanded is "true"
|
|
205
|
+
def expandedMirrorsVisibility(i: int): bool = and(
|
|
206
|
+
tabbable(2 * i),
|
|
207
|
+
(ariaExpanded(i) == "true") == tabbable(2 * i + 1)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
// ---- actions -----------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
action init = all {
|
|
213
|
+
expanded0' = false,
|
|
214
|
+
expanded1' = false,
|
|
215
|
+
expanded2' = false,
|
|
216
|
+
focus' = -1,
|
|
217
|
+
multiple' = false,
|
|
218
|
+
collapsible' = true,
|
|
219
|
+
headingLevel' = 3,
|
|
220
|
+
controlled' = false,
|
|
221
|
+
accepting' = false,
|
|
222
|
+
valueProp' = 0,
|
|
223
|
+
requested' = 0,
|
|
224
|
+
outcome' = "none",
|
|
225
|
+
base' = 0,
|
|
226
|
+
requests' = 0
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// pointer: click moves focus to the header and requests a toggle
|
|
230
|
+
action click_header = {
|
|
231
|
+
nondet panel = Set(0, 1, 2).oneOf()
|
|
232
|
+
all {
|
|
233
|
+
expanded0' = landed(holds, expanded0, intent(0, panel)),
|
|
234
|
+
expanded1' = landed(holds, expanded1, intent(1, panel)),
|
|
235
|
+
expanded2' = landed(holds, expanded2, intent(2, panel)),
|
|
236
|
+
focus' = 2 * panel,
|
|
237
|
+
multiple' = multiple,
|
|
238
|
+
collapsible' = collapsible,
|
|
239
|
+
headingLevel' = headingLevel,
|
|
240
|
+
controlled' = controlled,
|
|
241
|
+
accepting' = accepting,
|
|
242
|
+
valueProp' = propAfter(follows, valueProp, wantPacked(panel)),
|
|
243
|
+
requested' = if (moves(panel)) wantPacked(panel) else requested,
|
|
244
|
+
outcome' = if (moves(panel)) verdict else outcome,
|
|
245
|
+
base' = if (moves(panel)) nowPacked else base,
|
|
246
|
+
requests' = if (moves(panel)) requests + 1 else requests
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// pointer: click on a panel's focusable content (only reachable if shown)
|
|
251
|
+
action click_content = {
|
|
252
|
+
nondet panel = Set(0, 1, 2).oneOf()
|
|
253
|
+
all {
|
|
254
|
+
expanded0' = expanded0,
|
|
255
|
+
expanded1' = expanded1,
|
|
256
|
+
expanded2' = expanded2,
|
|
257
|
+
focus' = if (expandedAt(panel)) 2 * panel + 1 else focus,
|
|
258
|
+
multiple' = multiple,
|
|
259
|
+
collapsible' = collapsible,
|
|
260
|
+
headingLevel' = headingLevel,
|
|
261
|
+
controlled' = controlled,
|
|
262
|
+
accepting' = accepting,
|
|
263
|
+
valueProp' = valueProp,
|
|
264
|
+
requested' = requested,
|
|
265
|
+
outcome' = outcome,
|
|
266
|
+
base' = base,
|
|
267
|
+
requests' = requests
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// request: Enter on the focused header
|
|
272
|
+
action key_enter = all {
|
|
273
|
+
expanded0' = landed(holds, expanded0, intent(0, headerTarget(focus))),
|
|
274
|
+
expanded1' = landed(holds, expanded1, intent(1, headerTarget(focus))),
|
|
275
|
+
expanded2' = landed(holds, expanded2, intent(2, headerTarget(focus))),
|
|
276
|
+
focus' = focus,
|
|
277
|
+
multiple' = multiple,
|
|
278
|
+
collapsible' = collapsible,
|
|
279
|
+
headingLevel' = headingLevel,
|
|
280
|
+
controlled' = controlled,
|
|
281
|
+
accepting' = accepting,
|
|
282
|
+
valueProp' = propAfter(follows, valueProp, wantPacked(headerTarget(focus))),
|
|
283
|
+
requested' = if (moves(headerTarget(focus))) wantPacked(headerTarget(focus)) else requested,
|
|
284
|
+
outcome' = if (moves(headerTarget(focus))) verdict else outcome,
|
|
285
|
+
base' = if (moves(headerTarget(focus))) nowPacked else base,
|
|
286
|
+
requests' = if (moves(headerTarget(focus))) requests + 1 else requests
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// request: Space on the focused header (same effect as Enter)
|
|
290
|
+
action key_space = all {
|
|
291
|
+
expanded0' = landed(holds, expanded0, intent(0, headerTarget(focus))),
|
|
292
|
+
expanded1' = landed(holds, expanded1, intent(1, headerTarget(focus))),
|
|
293
|
+
expanded2' = landed(holds, expanded2, intent(2, headerTarget(focus))),
|
|
294
|
+
focus' = focus,
|
|
295
|
+
multiple' = multiple,
|
|
296
|
+
collapsible' = collapsible,
|
|
297
|
+
headingLevel' = headingLevel,
|
|
298
|
+
controlled' = controlled,
|
|
299
|
+
accepting' = accepting,
|
|
300
|
+
valueProp' = propAfter(follows, valueProp, wantPacked(headerTarget(focus))),
|
|
301
|
+
requested' = if (moves(headerTarget(focus))) wantPacked(headerTarget(focus)) else requested,
|
|
302
|
+
outcome' = if (moves(headerTarget(focus))) verdict else outcome,
|
|
303
|
+
base' = if (moves(headerTarget(focus))) nowPacked else base,
|
|
304
|
+
requests' = if (moves(headerTarget(focus))) requests + 1 else requests
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
action key_tab = all {
|
|
308
|
+
expanded0' = expanded0,
|
|
309
|
+
expanded1' = expanded1,
|
|
310
|
+
expanded2' = expanded2,
|
|
311
|
+
focus' = nextSlot(focus, expanded0, expanded1, expanded2),
|
|
312
|
+
multiple' = multiple,
|
|
313
|
+
collapsible' = collapsible,
|
|
314
|
+
headingLevel' = headingLevel,
|
|
315
|
+
controlled' = controlled,
|
|
316
|
+
accepting' = accepting,
|
|
317
|
+
valueProp' = valueProp,
|
|
318
|
+
requested' = requested,
|
|
319
|
+
outcome' = outcome,
|
|
320
|
+
base' = base,
|
|
321
|
+
requests' = requests
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
action key_shift_tab = all {
|
|
325
|
+
expanded0' = expanded0,
|
|
326
|
+
expanded1' = expanded1,
|
|
327
|
+
expanded2' = expanded2,
|
|
328
|
+
focus' = prevSlot(focus, expanded0, expanded1, expanded2),
|
|
329
|
+
multiple' = multiple,
|
|
330
|
+
collapsible' = collapsible,
|
|
331
|
+
headingLevel' = headingLevel,
|
|
332
|
+
controlled' = controlled,
|
|
333
|
+
accepting' = accepting,
|
|
334
|
+
valueProp' = valueProp,
|
|
335
|
+
requested' = requested,
|
|
336
|
+
outcome' = outcome,
|
|
337
|
+
base' = base,
|
|
338
|
+
requests' = requests
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// focus leaves the accordion (click elsewhere, window blur)
|
|
342
|
+
action blur = all {
|
|
343
|
+
expanded0' = expanded0,
|
|
344
|
+
expanded1' = expanded1,
|
|
345
|
+
expanded2' = expanded2,
|
|
346
|
+
focus' = -1,
|
|
347
|
+
multiple' = multiple,
|
|
348
|
+
collapsible' = collapsible,
|
|
349
|
+
headingLevel' = headingLevel,
|
|
350
|
+
controlled' = controlled,
|
|
351
|
+
accepting' = accepting,
|
|
352
|
+
valueProp' = valueProp,
|
|
353
|
+
requested' = requested,
|
|
354
|
+
outcome' = outcome,
|
|
355
|
+
base' = base,
|
|
356
|
+
requests' = requests
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// prop change: single vs multiple expansion (uncontrolled only re-normalizes;
|
|
360
|
+
// a controlled host owns the value, so its panels stay put)
|
|
361
|
+
action set_multiple = {
|
|
362
|
+
nondet allow = Set(true, false).oneOf()
|
|
363
|
+
all {
|
|
364
|
+
expanded0' = if (controlled) expanded0 else normAt(0, allow, collapsible),
|
|
365
|
+
expanded1' = if (controlled) expanded1 else normAt(1, allow, collapsible),
|
|
366
|
+
expanded2' = if (controlled) expanded2 else normAt(2, allow, collapsible),
|
|
367
|
+
focus' = if (controlled) focus else focusAfter(
|
|
368
|
+
focus,
|
|
369
|
+
normAt(0, allow, collapsible),
|
|
370
|
+
normAt(1, allow, collapsible),
|
|
371
|
+
normAt(2, allow, collapsible)
|
|
372
|
+
),
|
|
373
|
+
multiple' = allow,
|
|
374
|
+
collapsible' = collapsible,
|
|
375
|
+
headingLevel' = headingLevel,
|
|
376
|
+
controlled' = controlled,
|
|
377
|
+
accepting' = accepting,
|
|
378
|
+
valueProp' = valueProp,
|
|
379
|
+
requested' = if (controlled) requested else (if (normPacked(allow, collapsible) != nowPacked) normPacked(allow, collapsible) else requested),
|
|
380
|
+
outcome' = if (controlled) outcome else (if (normPacked(allow, collapsible) != nowPacked) "accepted" else outcome),
|
|
381
|
+
base' = if (controlled) base else (if (normPacked(allow, collapsible) != nowPacked) nowPacked else base),
|
|
382
|
+
requests' = if (controlled) requests else (if (normPacked(allow, collapsible) != nowPacked) requests + 1 else requests)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// prop change: collapsing the last expanded panel permitted or not
|
|
387
|
+
action set_collapsible = {
|
|
388
|
+
nondet allow = Set(true, false).oneOf()
|
|
389
|
+
all {
|
|
390
|
+
expanded0' = if (controlled) expanded0 else normAt(0, multiple, allow),
|
|
391
|
+
expanded1' = if (controlled) expanded1 else normAt(1, multiple, allow),
|
|
392
|
+
expanded2' = if (controlled) expanded2 else normAt(2, multiple, allow),
|
|
393
|
+
focus' = if (controlled) focus else focusAfter(
|
|
394
|
+
focus,
|
|
395
|
+
normAt(0, multiple, allow),
|
|
396
|
+
normAt(1, multiple, allow),
|
|
397
|
+
normAt(2, multiple, allow)
|
|
398
|
+
),
|
|
399
|
+
multiple' = multiple,
|
|
400
|
+
collapsible' = allow,
|
|
401
|
+
headingLevel' = headingLevel,
|
|
402
|
+
controlled' = controlled,
|
|
403
|
+
accepting' = accepting,
|
|
404
|
+
valueProp' = valueProp,
|
|
405
|
+
requested' = if (controlled) requested else (if (normPacked(multiple, allow) != nowPacked) normPacked(multiple, allow) else requested),
|
|
406
|
+
outcome' = if (controlled) outcome else (if (normPacked(multiple, allow) != nowPacked) "accepted" else outcome),
|
|
407
|
+
base' = if (controlled) base else (if (normPacked(multiple, allow) != nowPacked) nowPacked else base),
|
|
408
|
+
requests' = if (controlled) requests else (if (normPacked(multiple, allow) != nowPacked) requests + 1 else requests)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// prop change: aria-level of the wrapping headings
|
|
413
|
+
action set_heading_level = {
|
|
414
|
+
nondet level = Set(1, 2, 3, 4, 5, 6).oneOf()
|
|
415
|
+
all {
|
|
416
|
+
expanded0' = expanded0,
|
|
417
|
+
expanded1' = expanded1,
|
|
418
|
+
expanded2' = expanded2,
|
|
419
|
+
focus' = focus,
|
|
420
|
+
multiple' = multiple,
|
|
421
|
+
collapsible' = collapsible,
|
|
422
|
+
headingLevel' = level,
|
|
423
|
+
controlled' = controlled,
|
|
424
|
+
accepting' = accepting,
|
|
425
|
+
valueProp' = valueProp,
|
|
426
|
+
requested' = requested,
|
|
427
|
+
outcome' = outcome,
|
|
428
|
+
base' = base,
|
|
429
|
+
requests' = requests
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// commit: the host writes the controlled value (a no-op while uncontrolled)
|
|
434
|
+
action setValue = {
|
|
435
|
+
nondet v = Set(0, 1, 2, 3, 4, 5, 6, 7).oneOf()
|
|
436
|
+
all {
|
|
437
|
+
expanded0' = if (controlled) bitAt(0, v) else expanded0,
|
|
438
|
+
expanded1' = if (controlled) bitAt(1, v) else expanded1,
|
|
439
|
+
expanded2' = if (controlled) bitAt(2, v) else expanded2,
|
|
440
|
+
focus' = if (controlled) focusAfter(focus, bitAt(0, v), bitAt(1, v), bitAt(2, v)) else focus,
|
|
441
|
+
multiple' = multiple,
|
|
442
|
+
collapsible' = collapsible,
|
|
443
|
+
headingLevel' = headingLevel,
|
|
444
|
+
controlled' = controlled,
|
|
445
|
+
accepting' = accepting,
|
|
446
|
+
valueProp' = if (controlled) v else valueProp,
|
|
447
|
+
requested' = requested,
|
|
448
|
+
outcome' = if (controlled) "none" else outcome,
|
|
449
|
+
base' = base,
|
|
450
|
+
requests' = requests
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// commit: the host flips one bit of its own copy of the value
|
|
455
|
+
action hostToggle = {
|
|
456
|
+
nondet panel = Set(0, 1, 2).oneOf()
|
|
457
|
+
all {
|
|
458
|
+
expanded0' = if (controlled) bitAt(0, flipAt(valueProp, panel)) else expanded0,
|
|
459
|
+
expanded1' = if (controlled) bitAt(1, flipAt(valueProp, panel)) else expanded1,
|
|
460
|
+
expanded2' = if (controlled) bitAt(2, flipAt(valueProp, panel)) else expanded2,
|
|
461
|
+
focus' = if (controlled) focusAfter(focus, bitAt(0, flipAt(valueProp, panel)), bitAt(1, flipAt(valueProp, panel)), bitAt(2, flipAt(valueProp, panel))) else focus,
|
|
462
|
+
multiple' = multiple,
|
|
463
|
+
collapsible' = collapsible,
|
|
464
|
+
headingLevel' = headingLevel,
|
|
465
|
+
controlled' = controlled,
|
|
466
|
+
accepting' = accepting,
|
|
467
|
+
valueProp' = if (controlled) flipAt(valueProp, panel) else valueProp,
|
|
468
|
+
requested' = requested,
|
|
469
|
+
outcome' = if (controlled) "none" else outcome,
|
|
470
|
+
base' = base,
|
|
471
|
+
requests' = requests
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// configuration: the host starts owning the value, adopting the current one
|
|
476
|
+
action control = all {
|
|
477
|
+
expanded0' = expanded0,
|
|
478
|
+
expanded1' = expanded1,
|
|
479
|
+
expanded2' = expanded2,
|
|
480
|
+
focus' = focus,
|
|
481
|
+
multiple' = multiple,
|
|
482
|
+
collapsible' = collapsible,
|
|
483
|
+
headingLevel' = headingLevel,
|
|
484
|
+
controlled' = true,
|
|
485
|
+
accepting' = accepting,
|
|
486
|
+
valueProp' = nowPacked,
|
|
487
|
+
requested' = requested,
|
|
488
|
+
outcome' = outcome,
|
|
489
|
+
base' = base,
|
|
490
|
+
requests' = requests
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// configuration: the controlled host answers every request with its intent
|
|
494
|
+
action acceptRequests = all {
|
|
495
|
+
expanded0' = expanded0,
|
|
496
|
+
expanded1' = expanded1,
|
|
497
|
+
expanded2' = expanded2,
|
|
498
|
+
focus' = focus,
|
|
499
|
+
multiple' = multiple,
|
|
500
|
+
collapsible' = collapsible,
|
|
501
|
+
headingLevel' = headingLevel,
|
|
502
|
+
controlled' = controlled,
|
|
503
|
+
accepting' = true,
|
|
504
|
+
valueProp' = valueProp,
|
|
505
|
+
requested' = requested,
|
|
506
|
+
outcome' = outcome,
|
|
507
|
+
base' = base,
|
|
508
|
+
requests' = requests
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
action step = any {
|
|
512
|
+
init,
|
|
513
|
+
click_header,
|
|
514
|
+
click_content,
|
|
515
|
+
key_enter,
|
|
516
|
+
key_space,
|
|
517
|
+
key_tab,
|
|
518
|
+
key_shift_tab,
|
|
519
|
+
blur,
|
|
520
|
+
set_multiple,
|
|
521
|
+
set_collapsible,
|
|
522
|
+
set_heading_level,
|
|
523
|
+
setValue,
|
|
524
|
+
hostToggle,
|
|
525
|
+
control,
|
|
526
|
+
acceptRequests
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ---- invariants ----------------------------------------------------------
|
|
530
|
+
|
|
531
|
+
val FocusInRange: bool = and(focus >= -1, focus <= 5)
|
|
532
|
+
// content of a collapsed (hidden) panel never holds focus
|
|
533
|
+
val FocusNotInCollapsedPanel: bool =
|
|
534
|
+
or(not(onContent(focus)), expandedAt(focus / 2))
|
|
535
|
+
// "only one panel expanded" implementations (uncontrolled: a controlled host
|
|
536
|
+
// owns the value and may hand us any set)
|
|
537
|
+
val SingleModeAtMostOneExpanded: bool =
|
|
538
|
+
ariaSingleHoldsAtMostOne(controlled, multiple, expandedCount(expanded0, expanded1, expanded2))
|
|
539
|
+
// "one panel must stay expanded" implementations (uncontrolled)
|
|
540
|
+
val NonCollapsibleKeepsOneExpanded: bool =
|
|
541
|
+
or(controlled, or(collapsible, expandedCount(expanded0, expanded1, expanded2) >= 1))
|
|
542
|
+
// aria-disabled only ever sits on an expanded header that cannot collapse
|
|
543
|
+
val AriaDisabledOnlyOnExpanded: bool = and(
|
|
544
|
+
or(not(ariaDisabled(0)), expandedAt(0)),
|
|
545
|
+
or(not(ariaDisabled(1)), expandedAt(1)),
|
|
546
|
+
or(not(ariaDisabled(2)), expandedAt(2))
|
|
547
|
+
)
|
|
548
|
+
val AriaDisabledMatchesToggle: bool = and(
|
|
549
|
+
toggleContractHolds(0),
|
|
550
|
+
toggleContractHolds(1),
|
|
551
|
+
toggleContractHolds(2)
|
|
552
|
+
)
|
|
553
|
+
val ExpandRespectsMode: bool = and(
|
|
554
|
+
expandRespectsMode(0),
|
|
555
|
+
expandRespectsMode(1),
|
|
556
|
+
expandRespectsMode(2)
|
|
557
|
+
)
|
|
558
|
+
val AriaExpandedMirrorsVisibility: bool = and(
|
|
559
|
+
expandedMirrorsVisibility(0),
|
|
560
|
+
expandedMirrorsVisibility(1),
|
|
561
|
+
expandedMirrorsVisibility(2)
|
|
562
|
+
)
|
|
563
|
+
// aria-controls on header i and aria-labelledby on its region round-trip
|
|
564
|
+
val ControlsLabelledByRoundTrip: bool = and(
|
|
565
|
+
regionLabelledBy(ariaControls(0)) == 0,
|
|
566
|
+
regionLabelledBy(ariaControls(1)) == 1,
|
|
567
|
+
regionLabelledBy(ariaControls(2)) == 2
|
|
568
|
+
)
|
|
569
|
+
val HeadingLevelValid: bool = and(headingLevel >= 1, headingLevel <= 6)
|
|
570
|
+
// callbacks only ever count up from zero
|
|
571
|
+
val RequestsNonNegative: bool = requests >= 0
|
|
572
|
+
// controlled: only the value prop sets the state, so no event commits
|
|
573
|
+
val ControlledFollowsProp: bool = or(not(controlled), nowPacked == valueProp)
|
|
574
|
+
// a rejected request leaves the state as it found it
|
|
575
|
+
val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), nowPacked == base)
|
|
576
|
+
// an accepted request committed exactly its intent
|
|
577
|
+
val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), nowPacked == requested)
|
|
578
|
+
}
|