@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,445 @@
|
|
|
1
|
+
// WAI-ARIA APG — Button pattern (command button + toggle button).
|
|
2
|
+
//
|
|
3
|
+
// Modelled from https://www.w3.org/WAI/ARIA/apg/patterns/button/ :
|
|
4
|
+
// - "About This Pattern": ordinary button and toggle button (aria-pressed)
|
|
5
|
+
// - "Keyboard Interaction": Space and Enter activate the button; focus stays
|
|
6
|
+
// on the button after activation (no context change is modelled)
|
|
7
|
+
// - "WAI-ARIA Roles, States, and Properties": role=button, aria-disabled
|
|
8
|
+
// when the action is unavailable, aria-pressed true/false on a toggle
|
|
9
|
+
// and the Button Examples (https://www.w3.org/WAI/ARIA/apg/patterns/button/examples/button/):
|
|
10
|
+
// Enter activates on keydown, Space is armed on keydown and activates on keyup
|
|
11
|
+
// (the native <button> behaviour), so releasing Space after a blur does nothing.
|
|
12
|
+
//
|
|
13
|
+
// Attribute contract (derived from state, never stored):
|
|
14
|
+
// role = "button"
|
|
15
|
+
// aria-pressed = "true" | "false" on a toggle, absent on a command button
|
|
16
|
+
// aria-disabled = "true" when disabled, absent otherwise
|
|
17
|
+
// A disabled button stays focusable (aria-disabled, not the native attribute)
|
|
18
|
+
// but never activates.
|
|
19
|
+
//
|
|
20
|
+
// Request vs commit (map ticket rt1p). A toggle's activation is a *request*
|
|
21
|
+
// carrying the new pressed state (`requested`); it fires onPressedChange
|
|
22
|
+
// (`requests` counts the calls) when the intent differs from the current value,
|
|
23
|
+
// and then:
|
|
24
|
+
// uncontrolled the request is accepted: the state commits the intent
|
|
25
|
+
// controlled, holds the host ignores it ("rejected"): the state is untouched
|
|
26
|
+
// controlled, accepts the host answers with the same value (`accepting`): the
|
|
27
|
+
// state commits through the controlled value, not the event
|
|
28
|
+
// A command button's activation is not a value request: it only counts
|
|
29
|
+
// (`activations`). The controlled value (`valueProp`) is the only thing that
|
|
30
|
+
// changes state while `controlled`; `setValue` / `hostToggle` are the host
|
|
31
|
+
// writing it. `control` / `acceptRequests` are configuration events, `seed`
|
|
32
|
+
// plants an initial value without counting as a request.
|
|
33
|
+
//
|
|
34
|
+
// Invariants for the model: ControlledFollowsProp (in controlled mode no event
|
|
35
|
+
// commits), RejectedRequestUntouched, AcceptedRequestCommits.
|
|
36
|
+
//
|
|
37
|
+
// `outcome` is "none" | "accepted" | "rejected" for the last request; a host
|
|
38
|
+
// write settles it back to "none". `base` is the value when it was received.
|
|
39
|
+
//
|
|
40
|
+
// Deliberate omissions:
|
|
41
|
+
// - accessible name / aria-labelledby / aria-describedby (static wiring, no state)
|
|
42
|
+
// - post-activation focus management (dialog open/close, wizard step, shortcut keys)
|
|
43
|
+
// - menu button (aria-haspopup) — owned by the menu-button pattern
|
|
44
|
+
// - partially pressed (mixed) toggle: only the true/false values the button page lists
|
|
45
|
+
// - Enter key repeat, native `disabled`, and pointer-capture details
|
|
46
|
+
module apg_button {
|
|
47
|
+
import aria.* from "../../shared/aria"
|
|
48
|
+
|
|
49
|
+
var focused: bool // the button has keyboard focus
|
|
50
|
+
var disabled: bool // aria-disabled: focusable, never activates
|
|
51
|
+
var isToggle: bool // config: toggle button (aria-pressed) vs command button
|
|
52
|
+
var pressed: bool // toggle state; always false on a command button
|
|
53
|
+
var toggleCount: int // committed flips since it became a toggle
|
|
54
|
+
var spaceArmed: bool // Space went down on the focused button, awaiting keyup
|
|
55
|
+
var pointerArmed: bool // pointer went down on the button, awaiting release
|
|
56
|
+
var activations: int // onPress callbacks fired (what the consumer observes)
|
|
57
|
+
var requests: int // onPressedChange callbacks fired by user input
|
|
58
|
+
var controlled: bool // the host owns the pressed value (`value` prop present)
|
|
59
|
+
var accepting: bool // controlled host that answers every request with its intent
|
|
60
|
+
var valueProp: bool // the controlled value prop (meaningful while controlled)
|
|
61
|
+
var requested: bool // intent of the last request
|
|
62
|
+
var outcome: str // "none" | "accepted" | "rejected"
|
|
63
|
+
var base: bool // value when the last request was received
|
|
64
|
+
|
|
65
|
+
// guards: the generator supports no preconditions, so they live in the RHS
|
|
66
|
+
def enterFires(focused: bool, disabled: bool, key: str): bool =
|
|
67
|
+
and(focused, not(disabled), key == "Enter")
|
|
68
|
+
def spaceArms(focused: bool, disabled: bool, key: str): bool =
|
|
69
|
+
and(focused, not(disabled), key == "Space")
|
|
70
|
+
def spaceReleaseFires(armed: bool, disabled: bool, key: str): bool =
|
|
71
|
+
and(armed, not(disabled), key == "Space")
|
|
72
|
+
def pointerReleaseFires(armed: bool, disabled: bool): bool =
|
|
73
|
+
and(armed, not(disabled))
|
|
74
|
+
|
|
75
|
+
def b2i(b: bool): int = if (b) 1 else 0
|
|
76
|
+
def moved(before: bool, after: bool): int = if (before == after) 0 else 1
|
|
77
|
+
|
|
78
|
+
// ARIA attribute contract
|
|
79
|
+
def ariaPressed(isToggle: bool, pressed: bool): str =
|
|
80
|
+
if (isToggle) ariaBoolToken(pressed) else "absent"
|
|
81
|
+
def ariaDisabled(disabled: bool): str =
|
|
82
|
+
if (disabled) "true" else "absent"
|
|
83
|
+
|
|
84
|
+
// request / commit (map ticket rt1p)
|
|
85
|
+
def holds: bool = and(controlled, not(accepting))
|
|
86
|
+
def follows: bool = and(controlled, accepting)
|
|
87
|
+
def verdict: str = if (holds) "rejected" else "accepted"
|
|
88
|
+
// the pressed value after a request for `want`
|
|
89
|
+
def landed(h: bool, c: bool, want: bool): bool = if (h) c else want
|
|
90
|
+
// the value prop after a request for `want`
|
|
91
|
+
def propAfter(f: bool, p: bool, want: bool): bool = if (f) want else p
|
|
92
|
+
|
|
93
|
+
action init = all {
|
|
94
|
+
focused' = false,
|
|
95
|
+
disabled' = false,
|
|
96
|
+
isToggle' = false,
|
|
97
|
+
pressed' = false,
|
|
98
|
+
toggleCount' = 0,
|
|
99
|
+
spaceArmed' = false,
|
|
100
|
+
pointerArmed' = false,
|
|
101
|
+
activations' = 0,
|
|
102
|
+
requests' = 0,
|
|
103
|
+
controlled' = false,
|
|
104
|
+
accepting' = false,
|
|
105
|
+
valueProp' = false,
|
|
106
|
+
requested' = false,
|
|
107
|
+
outcome' = "none",
|
|
108
|
+
base' = false
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
action focus = all {
|
|
112
|
+
focused' = true,
|
|
113
|
+
disabled' = disabled,
|
|
114
|
+
isToggle' = isToggle,
|
|
115
|
+
pressed' = pressed,
|
|
116
|
+
toggleCount' = toggleCount,
|
|
117
|
+
spaceArmed' = spaceArmed,
|
|
118
|
+
pointerArmed' = pointerArmed,
|
|
119
|
+
activations' = activations,
|
|
120
|
+
requests' = requests,
|
|
121
|
+
controlled' = controlled,
|
|
122
|
+
accepting' = accepting,
|
|
123
|
+
valueProp' = valueProp,
|
|
124
|
+
requested' = requested,
|
|
125
|
+
outcome' = outcome,
|
|
126
|
+
base' = base
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// blur cancels a pending Space press (keyup would land elsewhere)
|
|
130
|
+
action blur = all {
|
|
131
|
+
focused' = false,
|
|
132
|
+
disabled' = disabled,
|
|
133
|
+
isToggle' = isToggle,
|
|
134
|
+
pressed' = pressed,
|
|
135
|
+
toggleCount' = toggleCount,
|
|
136
|
+
spaceArmed' = false,
|
|
137
|
+
pointerArmed' = pointerArmed,
|
|
138
|
+
activations' = activations,
|
|
139
|
+
requests' = requests,
|
|
140
|
+
controlled' = controlled,
|
|
141
|
+
accepting' = accepting,
|
|
142
|
+
valueProp' = valueProp,
|
|
143
|
+
requested' = requested,
|
|
144
|
+
outcome' = outcome,
|
|
145
|
+
base' = base
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Keyboard Interaction: Enter activates on keydown, Space arms until keyup.
|
|
149
|
+
// "Other" stands for any key the pattern does not handle.
|
|
150
|
+
action key_down = {
|
|
151
|
+
nondet key = Set("Enter", "Space", "Other").oneOf()
|
|
152
|
+
all {
|
|
153
|
+
focused' = focused,
|
|
154
|
+
disabled' = disabled,
|
|
155
|
+
isToggle' = isToggle,
|
|
156
|
+
pressed' = if (and(enterFires(focused, disabled, key), isToggle)) landed(holds, pressed, not(pressed)) else pressed,
|
|
157
|
+
toggleCount' = if (and(enterFires(focused, disabled, key), isToggle)) toggleCount + moved(pressed, landed(holds, pressed, not(pressed))) else toggleCount,
|
|
158
|
+
spaceArmed' = if (spaceArms(focused, disabled, key)) true else spaceArmed,
|
|
159
|
+
pointerArmed' = pointerArmed,
|
|
160
|
+
activations' = if (enterFires(focused, disabled, key)) activations + 1 else activations,
|
|
161
|
+
requests' = if (and(enterFires(focused, disabled, key), isToggle)) requests + 1 else requests,
|
|
162
|
+
controlled' = controlled,
|
|
163
|
+
accepting' = accepting,
|
|
164
|
+
valueProp' = if (and(enterFires(focused, disabled, key), isToggle)) propAfter(follows, valueProp, not(pressed)) else valueProp,
|
|
165
|
+
requested' = if (and(enterFires(focused, disabled, key), isToggle)) not(pressed) else requested,
|
|
166
|
+
outcome' = if (and(enterFires(focused, disabled, key), isToggle)) verdict else outcome,
|
|
167
|
+
base' = if (and(enterFires(focused, disabled, key), isToggle)) pressed else base
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
action key_up = {
|
|
172
|
+
nondet key = Set("Enter", "Space", "Other").oneOf()
|
|
173
|
+
all {
|
|
174
|
+
focused' = focused,
|
|
175
|
+
disabled' = disabled,
|
|
176
|
+
isToggle' = isToggle,
|
|
177
|
+
pressed' = if (and(spaceReleaseFires(spaceArmed, disabled, key), isToggle)) landed(holds, pressed, not(pressed)) else pressed,
|
|
178
|
+
toggleCount' = if (and(spaceReleaseFires(spaceArmed, disabled, key), isToggle)) toggleCount + moved(pressed, landed(holds, pressed, not(pressed))) else toggleCount,
|
|
179
|
+
spaceArmed' = if (key == "Space") false else spaceArmed,
|
|
180
|
+
pointerArmed' = pointerArmed,
|
|
181
|
+
activations' = if (spaceReleaseFires(spaceArmed, disabled, key)) activations + 1 else activations,
|
|
182
|
+
requests' = if (and(spaceReleaseFires(spaceArmed, disabled, key), isToggle)) requests + 1 else requests,
|
|
183
|
+
controlled' = controlled,
|
|
184
|
+
accepting' = accepting,
|
|
185
|
+
valueProp' = if (and(spaceReleaseFires(spaceArmed, disabled, key), isToggle)) propAfter(follows, valueProp, not(pressed)) else valueProp,
|
|
186
|
+
requested' = if (and(spaceReleaseFires(spaceArmed, disabled, key), isToggle)) not(pressed) else requested,
|
|
187
|
+
outcome' = if (and(spaceReleaseFires(spaceArmed, disabled, key), isToggle)) verdict else outcome,
|
|
188
|
+
base' = if (and(spaceReleaseFires(spaceArmed, disabled, key), isToggle)) pressed else base
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// pointer down focuses the button (also when aria-disabled) and arms a click
|
|
193
|
+
action pointer_down = all {
|
|
194
|
+
focused' = true,
|
|
195
|
+
disabled' = disabled,
|
|
196
|
+
isToggle' = isToggle,
|
|
197
|
+
pressed' = pressed,
|
|
198
|
+
toggleCount' = toggleCount,
|
|
199
|
+
spaceArmed' = spaceArmed,
|
|
200
|
+
pointerArmed' = not(disabled),
|
|
201
|
+
activations' = activations,
|
|
202
|
+
requests' = requests,
|
|
203
|
+
controlled' = controlled,
|
|
204
|
+
accepting' = accepting,
|
|
205
|
+
valueProp' = valueProp,
|
|
206
|
+
requested' = requested,
|
|
207
|
+
outcome' = outcome,
|
|
208
|
+
base' = base
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// pointer released over the button: the click activates
|
|
212
|
+
action pointer_up = all {
|
|
213
|
+
focused' = focused,
|
|
214
|
+
disabled' = disabled,
|
|
215
|
+
isToggle' = isToggle,
|
|
216
|
+
pressed' = if (and(pointerReleaseFires(pointerArmed, disabled), isToggle)) landed(holds, pressed, not(pressed)) else pressed,
|
|
217
|
+
toggleCount' = if (and(pointerReleaseFires(pointerArmed, disabled), isToggle)) toggleCount + moved(pressed, landed(holds, pressed, not(pressed))) else toggleCount,
|
|
218
|
+
spaceArmed' = spaceArmed,
|
|
219
|
+
pointerArmed' = false,
|
|
220
|
+
activations' = if (pointerReleaseFires(pointerArmed, disabled)) activations + 1 else activations,
|
|
221
|
+
requests' = if (and(pointerReleaseFires(pointerArmed, disabled), isToggle)) requests + 1 else requests,
|
|
222
|
+
controlled' = controlled,
|
|
223
|
+
accepting' = accepting,
|
|
224
|
+
valueProp' = if (and(pointerReleaseFires(pointerArmed, disabled), isToggle)) propAfter(follows, valueProp, not(pressed)) else valueProp,
|
|
225
|
+
requested' = if (and(pointerReleaseFires(pointerArmed, disabled), isToggle)) not(pressed) else requested,
|
|
226
|
+
outcome' = if (and(pointerReleaseFires(pointerArmed, disabled), isToggle)) verdict else outcome,
|
|
227
|
+
base' = if (and(pointerReleaseFires(pointerArmed, disabled), isToggle)) pressed else base
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// pointer left / cancelled before release: no click
|
|
231
|
+
action pointer_cancel = all {
|
|
232
|
+
focused' = focused,
|
|
233
|
+
disabled' = disabled,
|
|
234
|
+
isToggle' = isToggle,
|
|
235
|
+
pressed' = pressed,
|
|
236
|
+
toggleCount' = toggleCount,
|
|
237
|
+
spaceArmed' = spaceArmed,
|
|
238
|
+
pointerArmed' = false,
|
|
239
|
+
activations' = activations,
|
|
240
|
+
requests' = requests,
|
|
241
|
+
controlled' = controlled,
|
|
242
|
+
accepting' = accepting,
|
|
243
|
+
valueProp' = valueProp,
|
|
244
|
+
requested' = requested,
|
|
245
|
+
outcome' = outcome,
|
|
246
|
+
base' = base
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// commit: the host writes the controlled value (a no-op while uncontrolled)
|
|
250
|
+
action setValue = {
|
|
251
|
+
nondet v = Set(true, false).oneOf()
|
|
252
|
+
all {
|
|
253
|
+
focused' = focused,
|
|
254
|
+
disabled' = disabled,
|
|
255
|
+
isToggle' = isToggle,
|
|
256
|
+
pressed' = if (and(controlled, isToggle)) v else pressed,
|
|
257
|
+
toggleCount' = if (and(controlled, isToggle)) toggleCount + moved(pressed, v) else toggleCount,
|
|
258
|
+
spaceArmed' = spaceArmed,
|
|
259
|
+
pointerArmed' = pointerArmed,
|
|
260
|
+
activations' = activations,
|
|
261
|
+
requests' = requests,
|
|
262
|
+
controlled' = controlled,
|
|
263
|
+
accepting' = accepting,
|
|
264
|
+
valueProp' = if (and(controlled, isToggle)) v else valueProp,
|
|
265
|
+
requested' = requested,
|
|
266
|
+
outcome' = if (controlled) "none" else outcome,
|
|
267
|
+
base' = base
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// commit: the host flips its own copy of the value (an outside toggle)
|
|
272
|
+
action hostToggle = all {
|
|
273
|
+
focused' = focused,
|
|
274
|
+
disabled' = disabled,
|
|
275
|
+
isToggle' = isToggle,
|
|
276
|
+
pressed' = if (and(controlled, isToggle)) not(valueProp) else pressed,
|
|
277
|
+
toggleCount' = if (and(controlled, isToggle)) toggleCount + 1 else toggleCount,
|
|
278
|
+
spaceArmed' = spaceArmed,
|
|
279
|
+
pointerArmed' = pointerArmed,
|
|
280
|
+
activations' = activations,
|
|
281
|
+
requests' = requests,
|
|
282
|
+
controlled' = controlled,
|
|
283
|
+
accepting' = accepting,
|
|
284
|
+
valueProp' = if (and(controlled, isToggle)) not(valueProp) else valueProp,
|
|
285
|
+
requested' = requested,
|
|
286
|
+
outcome' = if (controlled) "none" else outcome,
|
|
287
|
+
base' = base
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// configuration: the host starts owning the pressed value, adopting the current one
|
|
291
|
+
action control = all {
|
|
292
|
+
focused' = focused,
|
|
293
|
+
disabled' = disabled,
|
|
294
|
+
isToggle' = isToggle,
|
|
295
|
+
pressed' = pressed,
|
|
296
|
+
toggleCount' = toggleCount,
|
|
297
|
+
spaceArmed' = spaceArmed,
|
|
298
|
+
pointerArmed' = pointerArmed,
|
|
299
|
+
activations' = activations,
|
|
300
|
+
requests' = requests,
|
|
301
|
+
controlled' = true,
|
|
302
|
+
accepting' = accepting,
|
|
303
|
+
valueProp' = pressed,
|
|
304
|
+
requested' = requested,
|
|
305
|
+
outcome' = outcome,
|
|
306
|
+
base' = base
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// configuration: the controlled host answers every request with its intent
|
|
310
|
+
action acceptRequests = all {
|
|
311
|
+
focused' = focused,
|
|
312
|
+
disabled' = disabled,
|
|
313
|
+
isToggle' = isToggle,
|
|
314
|
+
pressed' = pressed,
|
|
315
|
+
toggleCount' = toggleCount,
|
|
316
|
+
spaceArmed' = spaceArmed,
|
|
317
|
+
pointerArmed' = pointerArmed,
|
|
318
|
+
activations' = activations,
|
|
319
|
+
requests' = requests,
|
|
320
|
+
controlled' = controlled,
|
|
321
|
+
accepting' = true,
|
|
322
|
+
valueProp' = valueProp,
|
|
323
|
+
requested' = requested,
|
|
324
|
+
outcome' = outcome,
|
|
325
|
+
base' = base
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// configuration: plant an initial value without counting as a request.
|
|
329
|
+
// Inert while controlled: only the value prop may move the state there.
|
|
330
|
+
action seed = {
|
|
331
|
+
nondet v = Set(true, false).oneOf()
|
|
332
|
+
all {
|
|
333
|
+
focused' = focused,
|
|
334
|
+
disabled' = disabled,
|
|
335
|
+
isToggle' = isToggle,
|
|
336
|
+
pressed' = if (controlled) pressed else (if (isToggle) v else pressed),
|
|
337
|
+
toggleCount' = if (controlled) toggleCount else (if (isToggle) b2i(v) else toggleCount),
|
|
338
|
+
spaceArmed' = spaceArmed,
|
|
339
|
+
pointerArmed' = pointerArmed,
|
|
340
|
+
activations' = activations,
|
|
341
|
+
requests' = requests,
|
|
342
|
+
controlled' = controlled,
|
|
343
|
+
accepting' = accepting,
|
|
344
|
+
valueProp' = valueProp,
|
|
345
|
+
requested' = requested,
|
|
346
|
+
outcome' = "none",
|
|
347
|
+
base' = base
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// prop change: aria-disabled. Disabling drops any pending press.
|
|
352
|
+
action set_disabled = {
|
|
353
|
+
nondet value = Set(true, false).oneOf()
|
|
354
|
+
all {
|
|
355
|
+
focused' = focused,
|
|
356
|
+
disabled' = value,
|
|
357
|
+
isToggle' = isToggle,
|
|
358
|
+
pressed' = pressed,
|
|
359
|
+
toggleCount' = toggleCount,
|
|
360
|
+
spaceArmed' = if (value) false else spaceArmed,
|
|
361
|
+
pointerArmed' = if (value) false else pointerArmed,
|
|
362
|
+
activations' = activations,
|
|
363
|
+
requests' = requests,
|
|
364
|
+
controlled' = controlled,
|
|
365
|
+
accepting' = accepting,
|
|
366
|
+
valueProp' = valueProp,
|
|
367
|
+
requested' = requested,
|
|
368
|
+
outcome' = outcome,
|
|
369
|
+
base' = base
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// prop change: command button <-> toggle button. A command button has no
|
|
374
|
+
// pressed state, so leaving toggle mode resets it; a controlled toggle
|
|
375
|
+
// re-adopts the value prop.
|
|
376
|
+
action set_toggle = {
|
|
377
|
+
nondet value = Set(true, false).oneOf()
|
|
378
|
+
all {
|
|
379
|
+
focused' = focused,
|
|
380
|
+
disabled' = disabled,
|
|
381
|
+
isToggle' = value,
|
|
382
|
+
pressed' = if (value) (if (controlled) valueProp else pressed) else false,
|
|
383
|
+
toggleCount' = if (value) (if (controlled) b2i(valueProp) else toggleCount) else 0,
|
|
384
|
+
spaceArmed' = spaceArmed,
|
|
385
|
+
pointerArmed' = pointerArmed,
|
|
386
|
+
activations' = activations,
|
|
387
|
+
requests' = requests,
|
|
388
|
+
controlled' = controlled,
|
|
389
|
+
accepting' = accepting,
|
|
390
|
+
valueProp' = if (controlled) (if (value) valueProp else false) else valueProp,
|
|
391
|
+
requested' = requested,
|
|
392
|
+
outcome' = "none",
|
|
393
|
+
base' = base
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
action step = any {
|
|
398
|
+
init,
|
|
399
|
+
focus,
|
|
400
|
+
blur,
|
|
401
|
+
key_down,
|
|
402
|
+
key_up,
|
|
403
|
+
pointer_down,
|
|
404
|
+
pointer_up,
|
|
405
|
+
pointer_cancel,
|
|
406
|
+
setValue,
|
|
407
|
+
hostToggle,
|
|
408
|
+
control,
|
|
409
|
+
acceptRequests,
|
|
410
|
+
seed,
|
|
411
|
+
set_disabled,
|
|
412
|
+
set_toggle
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// aria-pressed is present exactly on a toggle button
|
|
416
|
+
val PressedAttrPresentIffToggle: bool =
|
|
417
|
+
(ariaPressed(isToggle, pressed) != "absent") == isToggle
|
|
418
|
+
// ...and mirrors the pressed state
|
|
419
|
+
val PressedAttrMirrorsState: bool =
|
|
420
|
+
or(not(isToggle), (ariaPressed(isToggle, pressed) == "true") == pressed)
|
|
421
|
+
// aria-disabled="true" exactly when disabled, otherwise absent
|
|
422
|
+
val DisabledAttrContract: bool = and(
|
|
423
|
+
(ariaDisabled(disabled) == "true") == disabled,
|
|
424
|
+
(ariaDisabled(disabled) == "absent") == not(disabled)
|
|
425
|
+
)
|
|
426
|
+
// a command button never carries pressed state
|
|
427
|
+
val CommandButtonNeverPressed: bool = or(isToggle, not(pressed))
|
|
428
|
+
// pressed is the parity of the committed flips since toggle mode began
|
|
429
|
+
val PressedIsToggleParity: bool = pressed == (toggleCount % 2 == 1)
|
|
430
|
+
// a request only ever rides an activation
|
|
431
|
+
val RequestsWithinActivations: bool = requests <= activations
|
|
432
|
+
// a pending Space press needs a focused, enabled button
|
|
433
|
+
val SpaceArmedImpliesFocusedEnabled: bool =
|
|
434
|
+
or(not(spaceArmed), and(focused, not(disabled)))
|
|
435
|
+
// a pending pointer press never survives disabling
|
|
436
|
+
val PointerArmedImpliesEnabled: bool =
|
|
437
|
+
or(not(pointerArmed), not(disabled))
|
|
438
|
+
// controlled: only the value prop sets the state, so no event commits
|
|
439
|
+
val ControlledFollowsProp: bool =
|
|
440
|
+
or(not(controlled), or(not(isToggle), pressed == valueProp))
|
|
441
|
+
// a rejected request leaves the state as it found it
|
|
442
|
+
val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), pressed == base)
|
|
443
|
+
// an accepted request committed exactly its intent
|
|
444
|
+
val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), pressed == requested)
|
|
445
|
+
}
|