@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,388 @@
|
|
|
1
|
+
// WAI-ARIA APG — Checkbox pattern: the tri-state (mixed) group controller.
|
|
2
|
+
//
|
|
3
|
+
// Modelled from https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/ ("About This
|
|
4
|
+
// Pattern": one tri-state checkbox represents and controls a group of dual-state
|
|
5
|
+
// checkboxes; "Keyboard Interaction": Space changes the state; "Roles, States,
|
|
6
|
+
// and Properties": aria-checked true / false / mixed) and the Mixed-State example
|
|
7
|
+
// https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/examples/checkbox-mixed/
|
|
8
|
+
// (Tab moves among the checkboxes; Space on the controller sets the children; the
|
|
9
|
+
// controller has aria-controls = the children).
|
|
10
|
+
//
|
|
11
|
+
// The controller (focus target "parent") owns no state: its aria-checked is
|
|
12
|
+
// DERIVED from the three children c0..c2 —
|
|
13
|
+
// all checked -> "true", none checked -> "false", otherwise -> "mixed".
|
|
14
|
+
// Activating the controller:
|
|
15
|
+
// all checked -> uncheck all
|
|
16
|
+
// mixed -> check all
|
|
17
|
+
// none checked -> check all, or — when `remember` is on and a mixed
|
|
18
|
+
// combination was seen before — restore that last
|
|
19
|
+
// mixed combination ("activating the overall
|
|
20
|
+
// checkbox a third time recreates that partially
|
|
21
|
+
// checked state"). Cycle: mixed -> true -> false -> mixed.
|
|
22
|
+
// Activating a child toggles just that child.
|
|
23
|
+
//
|
|
24
|
+
// Request vs commit (map ticket rt1p). Activating a checkbox (click / Space) is
|
|
25
|
+
// a *request* carrying the intended checked set (`requested`, the three children
|
|
26
|
+
// packed with `packed`). It fires onValueChange (`requests` counts the calls)
|
|
27
|
+
// when the intent differs from the current value, and then:
|
|
28
|
+
// uncontrolled the request is accepted: the children commit the intent
|
|
29
|
+
// controlled, holds the host ignores it ("rejected"): the children are untouched
|
|
30
|
+
// controlled, accepts the host answers with the same value (`accepting`): the
|
|
31
|
+
// children commit through the controlled value
|
|
32
|
+
// The controlled value (`valueProp`, the three checked bits packed) is the only
|
|
33
|
+
// thing that changes the children while `controlled`; `setValue` / `hostToggle`
|
|
34
|
+
// are the host writing it. `control` and `acceptRequests` are configuration
|
|
35
|
+
// events (init is literal-only). The mixed memory (m0..m2) is internal derived
|
|
36
|
+
// state, not the controlled value, and tracks the combination being left.
|
|
37
|
+
//
|
|
38
|
+
// Configuration variants: `remember` (the optional last-mixed memory) is a prop.
|
|
39
|
+
// The group size is a constant 3 (bool vars — the generator has no collections),
|
|
40
|
+
// so the controlled value is packed and every action assigns every var.
|
|
41
|
+
//
|
|
42
|
+
// Deliberate omissions:
|
|
43
|
+
// - per-child disabled / the disabled controller (see checkbox.qnt for disabling)
|
|
44
|
+
// - Space armed-on-keydown / fire-on-keyup and pointer arming (see checkbox.qnt);
|
|
45
|
+
// here Space and click activate directly (a collapsed key/pointer press)
|
|
46
|
+
// - shift+Tab, accessible names, fieldset/legend or role=group labelling
|
|
47
|
+
// - the page-load "default combination" fallback: with no mixed memory the
|
|
48
|
+
// controller checks all
|
|
49
|
+
// - a variable number of children (generator subset: no lists/sets in state)
|
|
50
|
+
module apg_checkbox_group {
|
|
51
|
+
var c0: bool // child checkbox 0 checked
|
|
52
|
+
var c1: bool // child checkbox 1 checked
|
|
53
|
+
var c2: bool // child checkbox 2 checked
|
|
54
|
+
var m0: bool // last mixed combination seen, child 0 (all false = none yet)
|
|
55
|
+
var m1: bool // ... child 1
|
|
56
|
+
var m2: bool // ... child 2
|
|
57
|
+
var focus: str // "none" | "parent" | "c0" | "c1" | "c2"
|
|
58
|
+
var remember: bool // config: the controller restores the last mixed combination
|
|
59
|
+
var controlled: bool // the host owns the value (`value` prop present)
|
|
60
|
+
var accepting: bool // controlled host that answers every request with its intent
|
|
61
|
+
var valueProp: int // the controlled value prop, packed with `packed`
|
|
62
|
+
var requested: int // intent of the last request, packed
|
|
63
|
+
var outcome: str // "none" | "accepted" | "rejected"
|
|
64
|
+
var base: int // value when the last request was received, packed
|
|
65
|
+
var requests: int // number of requests that fired onValueChange
|
|
66
|
+
|
|
67
|
+
def isAll(a: bool, b: bool, c: bool): bool = and(a, b, c)
|
|
68
|
+
def isNone(a: bool, b: bool, c: bool): bool = not(or(a, b, c))
|
|
69
|
+
def isMixedSet(a: bool, b: bool, c: bool): bool =
|
|
70
|
+
and(not(isAll(a, b, c)), not(isNone(a, b, c)))
|
|
71
|
+
|
|
72
|
+
// ARIA attribute contract of the controller (aria-controls = c0 c1 c2, constant)
|
|
73
|
+
def parentChecked(a: bool, b: bool, c: bool): str =
|
|
74
|
+
if (isAll(a, b, c)) "true" else if (isNone(a, b, c)) "false" else "mixed"
|
|
75
|
+
|
|
76
|
+
// the third activation: none checked, memory on, and a mixed combination known
|
|
77
|
+
def restores(a: bool, b: bool, c: bool, ma: bool, mb: bool, mc: bool, remember: bool): bool =
|
|
78
|
+
and(remember, isNone(a, b, c), isMixedSet(ma, mb, mc))
|
|
79
|
+
|
|
80
|
+
// one child's next value when `target` is activated
|
|
81
|
+
def nextChild(name: str, ci: bool, mi: bool, target: str, allChecked: bool, restore: bool): bool =
|
|
82
|
+
if (target == name) not(ci)
|
|
83
|
+
else if (target == "parent") (if (allChecked) false else if (restore) mi else true)
|
|
84
|
+
else ci
|
|
85
|
+
|
|
86
|
+
// the memory tracks the combination being left while the group is mixed
|
|
87
|
+
def keepMemory(mixedNow: bool, ci: bool, mi: bool): bool = if (mixedNow) ci else mi
|
|
88
|
+
|
|
89
|
+
// Tab order: outside -> controller -> c0 -> c1 -> c2 -> outside
|
|
90
|
+
def nextFocus(f: str): str =
|
|
91
|
+
if (f == "none") "parent"
|
|
92
|
+
else if (f == "parent") "c0"
|
|
93
|
+
else if (f == "c0") "c1"
|
|
94
|
+
else if (f == "c1") "c2"
|
|
95
|
+
else "none"
|
|
96
|
+
|
|
97
|
+
// ---- request / commit (map ticket rt1p) -------------------------------
|
|
98
|
+
def b2i(b: bool): int = if (b) 1 else 0
|
|
99
|
+
// injective for three bools: bit 0 = c0, bit 1 = c1, bit 2 = c2
|
|
100
|
+
def packed(a: bool, b: bool, c: bool): int =
|
|
101
|
+
b2i(a) + 2 * b2i(b) + 4 * b2i(c)
|
|
102
|
+
def bitAt(i: int, n: int): bool =
|
|
103
|
+
if (i == 0) n % 2 == 1 else if (i == 1) (n / 2) % 2 == 1 else (n / 4) % 2 == 1
|
|
104
|
+
def holds: bool = and(controlled, not(accepting))
|
|
105
|
+
def follows: bool = and(controlled, accepting)
|
|
106
|
+
def verdict: str = if (holds) "rejected" else "accepted"
|
|
107
|
+
def landed(h: bool, current: bool, want: bool): bool = if (h) current else want
|
|
108
|
+
def propAfter(f: bool, p: int, want: int): int = if (f) want else p
|
|
109
|
+
def nowPacked: int = packed(c0, c1, c2)
|
|
110
|
+
// the intended checked set of activating `target`, packed
|
|
111
|
+
def intent(name: str, ci: bool, mi: bool, target: str): bool =
|
|
112
|
+
nextChild(name, ci, mi, target, isAll(c0, c1, c2), restores(c0, c1, c2, m0, m1, m2, remember))
|
|
113
|
+
def intentPacked(target: str): int =
|
|
114
|
+
packed(
|
|
115
|
+
intent("c0", c0, m0, target),
|
|
116
|
+
intent("c1", c1, m1, target),
|
|
117
|
+
intent("c2", c2, m2, target)
|
|
118
|
+
)
|
|
119
|
+
def moves(target: str): bool = intentPacked(target) != nowPacked
|
|
120
|
+
// the host flips one bit of its own copy of the value
|
|
121
|
+
def flipAt(n: int, i: int): int =
|
|
122
|
+
if (i == 0) (if (bitAt(0, n)) n - 1 else n + 1)
|
|
123
|
+
else if (i == 1) (if (bitAt(1, n)) n - 2 else n + 2)
|
|
124
|
+
else (if (bitAt(2, n)) n - 4 else n + 4)
|
|
125
|
+
|
|
126
|
+
action init = all {
|
|
127
|
+
c0' = false,
|
|
128
|
+
c1' = false,
|
|
129
|
+
c2' = false,
|
|
130
|
+
m0' = false,
|
|
131
|
+
m1' = false,
|
|
132
|
+
m2' = false,
|
|
133
|
+
focus' = "none",
|
|
134
|
+
remember' = false,
|
|
135
|
+
controlled' = false,
|
|
136
|
+
accepting' = false,
|
|
137
|
+
valueProp' = 0,
|
|
138
|
+
requested' = 0,
|
|
139
|
+
outcome' = "none",
|
|
140
|
+
base' = 0,
|
|
141
|
+
requests' = 0
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
action focus_on = {
|
|
145
|
+
nondet target = Set("parent", "c0", "c1", "c2").oneOf()
|
|
146
|
+
all {
|
|
147
|
+
c0' = c0,
|
|
148
|
+
c1' = c1,
|
|
149
|
+
c2' = c2,
|
|
150
|
+
m0' = m0,
|
|
151
|
+
m1' = m1,
|
|
152
|
+
m2' = m2,
|
|
153
|
+
focus' = target,
|
|
154
|
+
remember' = remember,
|
|
155
|
+
controlled' = controlled,
|
|
156
|
+
accepting' = accepting,
|
|
157
|
+
valueProp' = valueProp,
|
|
158
|
+
requested' = requested,
|
|
159
|
+
outcome' = outcome,
|
|
160
|
+
base' = base,
|
|
161
|
+
requests' = requests
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
action blur = all {
|
|
166
|
+
c0' = c0,
|
|
167
|
+
c1' = c1,
|
|
168
|
+
c2' = c2,
|
|
169
|
+
m0' = m0,
|
|
170
|
+
m1' = m1,
|
|
171
|
+
m2' = m2,
|
|
172
|
+
focus' = "none",
|
|
173
|
+
remember' = remember,
|
|
174
|
+
controlled' = controlled,
|
|
175
|
+
accepting' = accepting,
|
|
176
|
+
valueProp' = valueProp,
|
|
177
|
+
requested' = requested,
|
|
178
|
+
outcome' = outcome,
|
|
179
|
+
base' = base,
|
|
180
|
+
requests' = requests
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Keyboard Interaction: Tab moves focus among the checkboxes
|
|
184
|
+
action key_tab = all {
|
|
185
|
+
c0' = c0,
|
|
186
|
+
c1' = c1,
|
|
187
|
+
c2' = c2,
|
|
188
|
+
m0' = m0,
|
|
189
|
+
m1' = m1,
|
|
190
|
+
m2' = m2,
|
|
191
|
+
focus' = nextFocus(focus),
|
|
192
|
+
remember' = remember,
|
|
193
|
+
controlled' = controlled,
|
|
194
|
+
accepting' = accepting,
|
|
195
|
+
valueProp' = valueProp,
|
|
196
|
+
requested' = requested,
|
|
197
|
+
outcome' = outcome,
|
|
198
|
+
base' = base,
|
|
199
|
+
requests' = requests
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// request: Space changes the state of the focused checkbox
|
|
203
|
+
action key_space = all {
|
|
204
|
+
c0' = landed(holds, c0, intent("c0", c0, m0, focus)),
|
|
205
|
+
c1' = landed(holds, c1, intent("c1", c1, m1, focus)),
|
|
206
|
+
c2' = landed(holds, c2, intent("c2", c2, m2, focus)),
|
|
207
|
+
m0' = keepMemory(isMixedSet(c0, c1, c2), c0, m0),
|
|
208
|
+
m1' = keepMemory(isMixedSet(c0, c1, c2), c1, m1),
|
|
209
|
+
m2' = keepMemory(isMixedSet(c0, c1, c2), c2, m2),
|
|
210
|
+
focus' = focus,
|
|
211
|
+
remember' = remember,
|
|
212
|
+
controlled' = controlled,
|
|
213
|
+
accepting' = accepting,
|
|
214
|
+
valueProp' = propAfter(follows, valueProp, intentPacked(focus)),
|
|
215
|
+
requested' = if (moves(focus)) intentPacked(focus) else requested,
|
|
216
|
+
outcome' = if (moves(focus)) verdict else outcome,
|
|
217
|
+
base' = if (moves(focus)) nowPacked else base,
|
|
218
|
+
requests' = if (moves(focus)) requests + 1 else requests
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// pointer: a click focuses its target and requests activation
|
|
222
|
+
action click = {
|
|
223
|
+
nondet target = Set("parent", "c0", "c1", "c2").oneOf()
|
|
224
|
+
all {
|
|
225
|
+
c0' = landed(holds, c0, intent("c0", c0, m0, target)),
|
|
226
|
+
c1' = landed(holds, c1, intent("c1", c1, m1, target)),
|
|
227
|
+
c2' = landed(holds, c2, intent("c2", c2, m2, target)),
|
|
228
|
+
m0' = keepMemory(isMixedSet(c0, c1, c2), c0, m0),
|
|
229
|
+
m1' = keepMemory(isMixedSet(c0, c1, c2), c1, m1),
|
|
230
|
+
m2' = keepMemory(isMixedSet(c0, c1, c2), c2, m2),
|
|
231
|
+
focus' = target,
|
|
232
|
+
remember' = remember,
|
|
233
|
+
controlled' = controlled,
|
|
234
|
+
accepting' = accepting,
|
|
235
|
+
valueProp' = propAfter(follows, valueProp, intentPacked(target)),
|
|
236
|
+
requested' = if (moves(target)) intentPacked(target) else requested,
|
|
237
|
+
outcome' = if (moves(target)) verdict else outcome,
|
|
238
|
+
base' = if (moves(target)) nowPacked else base,
|
|
239
|
+
requests' = if (moves(target)) requests + 1 else requests
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// prop change: the optional last-mixed memory
|
|
244
|
+
action set_remember = {
|
|
245
|
+
nondet value = Set(true, false).oneOf()
|
|
246
|
+
all {
|
|
247
|
+
c0' = c0,
|
|
248
|
+
c1' = c1,
|
|
249
|
+
c2' = c2,
|
|
250
|
+
m0' = m0,
|
|
251
|
+
m1' = m1,
|
|
252
|
+
m2' = m2,
|
|
253
|
+
focus' = focus,
|
|
254
|
+
remember' = value,
|
|
255
|
+
controlled' = controlled,
|
|
256
|
+
accepting' = accepting,
|
|
257
|
+
valueProp' = valueProp,
|
|
258
|
+
requested' = requested,
|
|
259
|
+
outcome' = outcome,
|
|
260
|
+
base' = base,
|
|
261
|
+
requests' = requests
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// commit: the host writes the controlled value (a no-op while uncontrolled)
|
|
266
|
+
action setValue = {
|
|
267
|
+
nondet v = Set(0, 1, 2, 3, 4, 5, 6, 7).oneOf()
|
|
268
|
+
all {
|
|
269
|
+
c0' = if (controlled) bitAt(0, v) else c0,
|
|
270
|
+
c1' = if (controlled) bitAt(1, v) else c1,
|
|
271
|
+
c2' = if (controlled) bitAt(2, v) else c2,
|
|
272
|
+
m0' = m0,
|
|
273
|
+
m1' = m1,
|
|
274
|
+
m2' = m2,
|
|
275
|
+
focus' = focus,
|
|
276
|
+
remember' = remember,
|
|
277
|
+
controlled' = controlled,
|
|
278
|
+
accepting' = accepting,
|
|
279
|
+
valueProp' = if (controlled) v else valueProp,
|
|
280
|
+
requested' = requested,
|
|
281
|
+
outcome' = if (controlled) "none" else outcome,
|
|
282
|
+
base' = base,
|
|
283
|
+
requests' = requests
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// commit: the host flips one child of its own copy of the value
|
|
288
|
+
action hostToggle = {
|
|
289
|
+
nondet panel = Set(0, 1, 2).oneOf()
|
|
290
|
+
all {
|
|
291
|
+
c0' = if (controlled) bitAt(0, flipAt(valueProp, panel)) else c0,
|
|
292
|
+
c1' = if (controlled) bitAt(1, flipAt(valueProp, panel)) else c1,
|
|
293
|
+
c2' = if (controlled) bitAt(2, flipAt(valueProp, panel)) else c2,
|
|
294
|
+
m0' = m0,
|
|
295
|
+
m1' = m1,
|
|
296
|
+
m2' = m2,
|
|
297
|
+
focus' = focus,
|
|
298
|
+
remember' = remember,
|
|
299
|
+
controlled' = controlled,
|
|
300
|
+
accepting' = accepting,
|
|
301
|
+
valueProp' = if (controlled) flipAt(valueProp, panel) else valueProp,
|
|
302
|
+
requested' = requested,
|
|
303
|
+
outcome' = if (controlled) "none" else outcome,
|
|
304
|
+
base' = base,
|
|
305
|
+
requests' = requests
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// configuration: the host starts owning the value, adopting the current one
|
|
310
|
+
action control = all {
|
|
311
|
+
c0' = c0,
|
|
312
|
+
c1' = c1,
|
|
313
|
+
c2' = c2,
|
|
314
|
+
m0' = m0,
|
|
315
|
+
m1' = m1,
|
|
316
|
+
m2' = m2,
|
|
317
|
+
focus' = focus,
|
|
318
|
+
remember' = remember,
|
|
319
|
+
controlled' = true,
|
|
320
|
+
accepting' = accepting,
|
|
321
|
+
valueProp' = nowPacked,
|
|
322
|
+
requested' = requested,
|
|
323
|
+
outcome' = outcome,
|
|
324
|
+
base' = base,
|
|
325
|
+
requests' = requests
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// configuration: the controlled host answers every request with its intent
|
|
329
|
+
action acceptRequests = all {
|
|
330
|
+
c0' = c0,
|
|
331
|
+
c1' = c1,
|
|
332
|
+
c2' = c2,
|
|
333
|
+
m0' = m0,
|
|
334
|
+
m1' = m1,
|
|
335
|
+
m2' = m2,
|
|
336
|
+
focus' = focus,
|
|
337
|
+
remember' = remember,
|
|
338
|
+
controlled' = controlled,
|
|
339
|
+
accepting' = true,
|
|
340
|
+
valueProp' = valueProp,
|
|
341
|
+
requested' = requested,
|
|
342
|
+
outcome' = outcome,
|
|
343
|
+
base' = base,
|
|
344
|
+
requests' = requests
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
action step = any {
|
|
348
|
+
init,
|
|
349
|
+
focus_on,
|
|
350
|
+
blur,
|
|
351
|
+
key_tab,
|
|
352
|
+
key_space,
|
|
353
|
+
click,
|
|
354
|
+
set_remember,
|
|
355
|
+
setValue,
|
|
356
|
+
hostToggle,
|
|
357
|
+
control,
|
|
358
|
+
acceptRequests
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// APG: a mixed controller is mixed iff the children disagree
|
|
362
|
+
val MixedIffChildrenDisagree: bool =
|
|
363
|
+
(parentChecked(c0, c1, c2) == "mixed") == or(c0 != c1, c1 != c2)
|
|
364
|
+
// checked iff every child is checked
|
|
365
|
+
val CheckedIffAllChildrenChecked: bool =
|
|
366
|
+
(parentChecked(c0, c1, c2) == "true") == and(c0, c1, c2)
|
|
367
|
+
// not checked iff no child is checked
|
|
368
|
+
val UncheckedIffNoChildChecked: bool =
|
|
369
|
+
(parentChecked(c0, c1, c2) == "false") == not(or(c0, c1, c2))
|
|
370
|
+
// the memory only ever holds a genuinely mixed combination (or nothing yet)
|
|
371
|
+
val MemoryIsMixedOrEmpty: bool =
|
|
372
|
+
or(isNone(m0, m1, m2), isMixedSet(m0, m1, m2))
|
|
373
|
+
val FocusValid: bool = or(
|
|
374
|
+
focus == "none",
|
|
375
|
+
focus == "parent",
|
|
376
|
+
focus == "c0",
|
|
377
|
+
focus == "c1",
|
|
378
|
+
focus == "c2"
|
|
379
|
+
)
|
|
380
|
+
// callbacks only ever count up from zero
|
|
381
|
+
val RequestsNonNegative: bool = requests >= 0
|
|
382
|
+
// controlled: only the value prop sets the state, so no event commits
|
|
383
|
+
val ControlledFollowsProp: bool = or(not(controlled), nowPacked == valueProp)
|
|
384
|
+
// a rejected request leaves the state as it found it
|
|
385
|
+
val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), nowPacked == base)
|
|
386
|
+
// an accepted request committed exactly its intent
|
|
387
|
+
val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), nowPacked == requested)
|
|
388
|
+
}
|