@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,441 @@
|
|
|
1
|
+
// WAI-ARIA APG — Slider (Multi-Thumb) pattern, two thumbs sharing one rail.
|
|
2
|
+
//
|
|
3
|
+
// Modelled (https://www.w3.org/WAI/ARIA/apg/patterns/slider-multithumb/ and
|
|
4
|
+
// its Horizontal Multi-Thumb Slider example, a hotel price range):
|
|
5
|
+
// - "About This Pattern": the thumbs may not pass one another, so the maximum
|
|
6
|
+
// of the lower thumb is the value of the upper thumb and the minimum of the
|
|
7
|
+
// upper thumb is the value of the lower thumb (equal values are allowed).
|
|
8
|
+
// - "Keyboard Interaction": each thumb is in the tab sequence and has the
|
|
9
|
+
// Slider pattern keys (Right/Up +1, Left/Down -1, PageUp/PageDown +/- large
|
|
10
|
+
// step, Home/End -> the first/last value the thumb may take); the tab
|
|
11
|
+
// order is constant (lo, then hi) whatever the values are.
|
|
12
|
+
// Home/End therefore stop at the neighbour: Home on the upper thumb sets it
|
|
13
|
+
// to the lower thumb's value (its current aria-valuemin), End on the lower
|
|
14
|
+
// thumb sets it to the upper thumb's value.
|
|
15
|
+
// - "WAI-ARIA Roles, States, and Properties": per thumb role=slider,
|
|
16
|
+
// aria-valuenow / valuemin / valuemax (the dependent bound follows the
|
|
17
|
+
// neighbour on every change), aria-valuetext, aria-orientation.
|
|
18
|
+
// - The RTL rule of the Slider spec (APG "Note 2"): horizontal + dir=rtl flips
|
|
19
|
+
// Left/Right; Up/Down never flip; vertical keeps Left/Right as in LTR.
|
|
20
|
+
//
|
|
21
|
+
// Request vs commit (map ticket rt1p). A key or a rail press is a *request*
|
|
22
|
+
// carrying an intent (the pair packed into `requested`); it fires onValueChange
|
|
23
|
+
// (`requests` counts the calls) when the intent differs from the current pair,
|
|
24
|
+
// and then:
|
|
25
|
+
// uncontrolled the request is accepted: the thumbs commit the intent
|
|
26
|
+
// controlled, holds the host ignores it ("rejected"): the thumbs are untouched
|
|
27
|
+
// controlled, accepts the host answers with the same value (`accepting`): the
|
|
28
|
+
// thumbs commit through the controlled value
|
|
29
|
+
// `valueProp` (the pair packed with `snap`) is the only thing that changes the
|
|
30
|
+
// thumbs while `controlled`; `setValue` is the host writing it. `control` /
|
|
31
|
+
// `acceptRequests` are configuration events (init is literal-only).
|
|
32
|
+
//
|
|
33
|
+
// Deliberate omissions (ponytail: ceilings, not bugs):
|
|
34
|
+
// - The independent-thumbs variant ("in some multi-thumb sliders, each thumb
|
|
35
|
+
// sets a value that does not depend on the other thumb values"): the spec
|
|
36
|
+
// fixes the linked variant, so the "tab order unchanged after passing"
|
|
37
|
+
// case cannot occur here. A Solid implementation of the independent variant
|
|
38
|
+
// should use the plain slider spec once per thumb.
|
|
39
|
+
// - Exactly two thumbs. The bound rules generalise to N thumbs as
|
|
40
|
+
// min = previous thumb, max = next thumb; Quint (and the generator) have
|
|
41
|
+
// no list state, so N is not expressible. The controlled pair is packed
|
|
42
|
+
// into one int for the same reason.
|
|
43
|
+
// - aria-label / aria-labelledby, non-integer values, a coarser step grid,
|
|
44
|
+
// dragging (only a discrete pointer press per thumb, clamped to its
|
|
45
|
+
// bounds), disabled, and aria-valuetext formatting (units, locale).
|
|
46
|
+
// - Picking the nearest thumb for a pointer press on the rail: the event
|
|
47
|
+
// names the thumb.
|
|
48
|
+
module apg_slider_multithumb {
|
|
49
|
+
import aria.* from "../../shared/aria"
|
|
50
|
+
|
|
51
|
+
var lo: int // lower thumb value
|
|
52
|
+
var hi: int // upper thumb value
|
|
53
|
+
var focused: str // "none" | "lo" | "hi"
|
|
54
|
+
var orientation: str // "horizontal" | "vertical"
|
|
55
|
+
var dir: str // "ltr" | "rtl"
|
|
56
|
+
var handled: bool // the last key was consumed (preventDefault)
|
|
57
|
+
var controlled: bool // the host owns the pair (`values` prop present)
|
|
58
|
+
var accepting: bool // controlled host that answers every request with its intent
|
|
59
|
+
var valueProp: int // the controlled pair, packed with `snap`
|
|
60
|
+
var requested: int // intent of the last request, packed
|
|
61
|
+
var outcome: str // "none" | "accepted" | "rejected"
|
|
62
|
+
var base: int // pair when the last request was received, packed
|
|
63
|
+
var requests: int // number of requests that fired onValueChange
|
|
64
|
+
|
|
65
|
+
// --- configuration -----------------------------------------------------
|
|
66
|
+
def rangeMin: int = 0
|
|
67
|
+
def rangeMax: int = 10
|
|
68
|
+
def smallStep: int = 1
|
|
69
|
+
def largeStep: int = 3
|
|
70
|
+
|
|
71
|
+
// --- pure behaviour ----------------------------------------------------
|
|
72
|
+
def clampTo(v: int, a: int, b: int): int =
|
|
73
|
+
if (v < a) a else if (v > b) b else v
|
|
74
|
+
|
|
75
|
+
def valueOf(t: str): int = if (t == "lo") lo else hi
|
|
76
|
+
// the dependent bounds: each thumb is limited by its neighbour
|
|
77
|
+
def minOf(t: str): int = if (t == "lo") rangeMin else lo
|
|
78
|
+
def maxOf(t: str): int = if (t == "lo") hi else rangeMax
|
|
79
|
+
|
|
80
|
+
// arrows and RTL mirroring: shared/aria.qnt ariaValueArrow
|
|
81
|
+
def keyDelta(k: str, o: str, d: str): int =
|
|
82
|
+
if (k == "PageUp") largeStep
|
|
83
|
+
else if (k == "PageDown") -largeStep
|
|
84
|
+
else smallStep * ariaValueArrow(k, o, d)
|
|
85
|
+
|
|
86
|
+
def isHandledKey(k: str): bool =
|
|
87
|
+
or(k == "Home", k == "End", keyDelta(k, "horizontal", "ltr") != 0)
|
|
88
|
+
|
|
89
|
+
// the new value of thumb t after key k (t's own bounds, not the full range)
|
|
90
|
+
def nextOf(t: str, k: str, o: str, d: str): int =
|
|
91
|
+
if (k == "Home") minOf(t)
|
|
92
|
+
else if (k == "End") maxOf(t)
|
|
93
|
+
else clampTo(valueOf(t) + keyDelta(k, o, d), minOf(t), maxOf(t))
|
|
94
|
+
|
|
95
|
+
def valueTextFor(v: int): str =
|
|
96
|
+
if (v <= 3) "low" else if (v <= 7) "medium" else "high"
|
|
97
|
+
|
|
98
|
+
// Tab / Shift+Tab walk the thumbs in a fixed order, independent of values
|
|
99
|
+
def tabNext(f: str): str =
|
|
100
|
+
if (f == "none") "lo" else if (f == "lo") "hi" else "none"
|
|
101
|
+
def tabPrev(f: str): str =
|
|
102
|
+
if (f == "none") "hi" else if (f == "hi") "lo" else "none"
|
|
103
|
+
|
|
104
|
+
// --- request / commit (map ticket rt1p) ----------------------------------
|
|
105
|
+
// injective for the 0..10 grid: lo * 11 + hi
|
|
106
|
+
def snap(l: int, h: int): int = l * 11 + h
|
|
107
|
+
def loOf(n: int): int = n / 11
|
|
108
|
+
def hiOf(n: int): int = n % 11
|
|
109
|
+
def nowPacked: int = snap(lo, hi)
|
|
110
|
+
// controlled host that keeps the old value: the request is rejected
|
|
111
|
+
def holds: bool = and(controlled, not(accepting))
|
|
112
|
+
// controlled host that writes the intent back through the value prop
|
|
113
|
+
def follows: bool = and(controlled, accepting)
|
|
114
|
+
def verdict: str = if (holds) "rejected" else "accepted"
|
|
115
|
+
def landed(h: bool, current: int, want: int): int = if (h) current else want
|
|
116
|
+
def propAfter(f: bool, p: int, want: int): int = if (f) want else p
|
|
117
|
+
// a key moves only the focused thumb; the other stays put
|
|
118
|
+
def nextLoFor(k: str, o: str, d: str): int =
|
|
119
|
+
if (focused == "lo") nextOf("lo", k, o, d) else lo
|
|
120
|
+
def nextHiFor(k: str, o: str, d: str): int =
|
|
121
|
+
if (focused == "hi") nextOf("hi", k, o, d) else hi
|
|
122
|
+
def keyIntent(k: str, o: str, d: str): int =
|
|
123
|
+
snap(nextLoFor(k, o, d), nextHiFor(k, o, d))
|
|
124
|
+
def keyMoves(k: str, o: str, d: str): bool = keyIntent(k, o, d) != nowPacked
|
|
125
|
+
def pointerLoValue(raw: int): int = clampTo(raw, minOf("lo"), maxOf("lo"))
|
|
126
|
+
def pointerHiValue(raw: int): int = clampTo(raw, minOf("hi"), maxOf("hi"))
|
|
127
|
+
|
|
128
|
+
// --- ARIA contract, one slider per thumb (derived from the state) --------
|
|
129
|
+
def ariaRole: str = "slider"
|
|
130
|
+
def ariaValueNowLo: int = lo
|
|
131
|
+
def ariaValueMinLo: int = minOf("lo")
|
|
132
|
+
def ariaValueMaxLo: int = maxOf("lo")
|
|
133
|
+
def ariaValueTextLo: str = valueTextFor(lo)
|
|
134
|
+
def ariaValueNowHi: int = hi
|
|
135
|
+
def ariaValueMinHi: int = minOf("hi")
|
|
136
|
+
def ariaValueMaxHi: int = maxOf("hi")
|
|
137
|
+
def ariaValueTextHi: str = valueTextFor(hi)
|
|
138
|
+
def ariaOrientation: str = orientation
|
|
139
|
+
def tabIndexLo: int = 0
|
|
140
|
+
def tabIndexHi: int = 0
|
|
141
|
+
|
|
142
|
+
// --- machine -----------------------------------------------------------
|
|
143
|
+
action init = all {
|
|
144
|
+
lo' = 3,
|
|
145
|
+
hi' = 7,
|
|
146
|
+
focused' = "none",
|
|
147
|
+
orientation' = "horizontal",
|
|
148
|
+
dir' = "ltr",
|
|
149
|
+
handled' = false,
|
|
150
|
+
controlled' = false,
|
|
151
|
+
accepting' = false,
|
|
152
|
+
valueProp' = 40,
|
|
153
|
+
requested' = 40,
|
|
154
|
+
outcome' = "none",
|
|
155
|
+
base' = 40,
|
|
156
|
+
requests' = 0
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// click / programmatic focus on a thumb
|
|
160
|
+
action focusThumb = {
|
|
161
|
+
nondet t = Set("lo", "hi").oneOf()
|
|
162
|
+
all {
|
|
163
|
+
lo' = lo,
|
|
164
|
+
hi' = hi,
|
|
165
|
+
focused' = t,
|
|
166
|
+
orientation' = orientation,
|
|
167
|
+
dir' = dir,
|
|
168
|
+
handled' = false,
|
|
169
|
+
controlled' = controlled,
|
|
170
|
+
accepting' = accepting,
|
|
171
|
+
valueProp' = valueProp,
|
|
172
|
+
requested' = requested,
|
|
173
|
+
outcome' = outcome,
|
|
174
|
+
base' = base,
|
|
175
|
+
requests' = requests
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
action blur = all {
|
|
180
|
+
lo' = lo,
|
|
181
|
+
hi' = hi,
|
|
182
|
+
focused' = "none",
|
|
183
|
+
orientation' = orientation,
|
|
184
|
+
dir' = dir,
|
|
185
|
+
handled' = false,
|
|
186
|
+
controlled' = controlled,
|
|
187
|
+
accepting' = accepting,
|
|
188
|
+
valueProp' = valueProp,
|
|
189
|
+
requested' = requested,
|
|
190
|
+
outcome' = outcome,
|
|
191
|
+
base' = base,
|
|
192
|
+
requests' = requests
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
action tab = all {
|
|
196
|
+
lo' = lo,
|
|
197
|
+
hi' = hi,
|
|
198
|
+
focused' = tabNext(focused),
|
|
199
|
+
orientation' = orientation,
|
|
200
|
+
dir' = dir,
|
|
201
|
+
handled' = false,
|
|
202
|
+
controlled' = controlled,
|
|
203
|
+
accepting' = accepting,
|
|
204
|
+
valueProp' = valueProp,
|
|
205
|
+
requested' = requested,
|
|
206
|
+
outcome' = outcome,
|
|
207
|
+
base' = base,
|
|
208
|
+
requests' = requests
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
action shiftTab = all {
|
|
212
|
+
lo' = lo,
|
|
213
|
+
hi' = hi,
|
|
214
|
+
focused' = tabPrev(focused),
|
|
215
|
+
orientation' = orientation,
|
|
216
|
+
dir' = dir,
|
|
217
|
+
handled' = false,
|
|
218
|
+
controlled' = controlled,
|
|
219
|
+
accepting' = accepting,
|
|
220
|
+
valueProp' = valueProp,
|
|
221
|
+
requested' = requested,
|
|
222
|
+
outcome' = outcome,
|
|
223
|
+
base' = base,
|
|
224
|
+
requests' = requests
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// request: a focused key moves its thumb (APG keyboard section)
|
|
228
|
+
action keydown = {
|
|
229
|
+
nondet key = Set(
|
|
230
|
+
"ArrowRight", "ArrowLeft", "ArrowUp", "ArrowDown",
|
|
231
|
+
"Home", "End", "PageUp", "PageDown", "Enter"
|
|
232
|
+
).oneOf()
|
|
233
|
+
all {
|
|
234
|
+
lo' = landed(holds, lo, nextLoFor(key, orientation, dir)),
|
|
235
|
+
hi' = landed(holds, hi, nextHiFor(key, orientation, dir)),
|
|
236
|
+
focused' = focused,
|
|
237
|
+
orientation' = orientation,
|
|
238
|
+
dir' = dir,
|
|
239
|
+
handled' = and(focused != "none", isHandledKey(key)),
|
|
240
|
+
controlled' = controlled,
|
|
241
|
+
accepting' = accepting,
|
|
242
|
+
valueProp' = propAfter(follows, valueProp, keyIntent(key, orientation, dir)),
|
|
243
|
+
requested' = if (keyMoves(key, orientation, dir)) keyIntent(key, orientation, dir) else requested,
|
|
244
|
+
outcome' = if (keyMoves(key, orientation, dir)) verdict else outcome,
|
|
245
|
+
base' = if (keyMoves(key, orientation, dir)) nowPacked else base,
|
|
246
|
+
requests' = if (keyMoves(key, orientation, dir)) requests + 1 else requests
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// request: pointer press for the lower thumb at a raw value; focuses it
|
|
251
|
+
action pointerLo = {
|
|
252
|
+
nondet raw = Set(-3, 0, 4, 7, 10, 14).oneOf()
|
|
253
|
+
all {
|
|
254
|
+
lo' = landed(holds, lo, pointerLoValue(raw)),
|
|
255
|
+
hi' = hi,
|
|
256
|
+
focused' = "lo",
|
|
257
|
+
orientation' = orientation,
|
|
258
|
+
dir' = dir,
|
|
259
|
+
handled' = false,
|
|
260
|
+
controlled' = controlled,
|
|
261
|
+
accepting' = accepting,
|
|
262
|
+
valueProp' = propAfter(follows, valueProp, snap(pointerLoValue(raw), hi)),
|
|
263
|
+
requested' = if (snap(pointerLoValue(raw), hi) != nowPacked) snap(pointerLoValue(raw), hi) else requested,
|
|
264
|
+
outcome' = if (snap(pointerLoValue(raw), hi) != nowPacked) verdict else outcome,
|
|
265
|
+
base' = if (snap(pointerLoValue(raw), hi) != nowPacked) nowPacked else base,
|
|
266
|
+
requests' = if (snap(pointerLoValue(raw), hi) != nowPacked) requests + 1 else requests
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// request: pointer press for the upper thumb at a raw value; focuses it
|
|
271
|
+
action pointerHi = {
|
|
272
|
+
nondet raw = Set(-3, 0, 4, 7, 10, 14).oneOf()
|
|
273
|
+
all {
|
|
274
|
+
lo' = lo,
|
|
275
|
+
hi' = landed(holds, hi, pointerHiValue(raw)),
|
|
276
|
+
focused' = "hi",
|
|
277
|
+
orientation' = orientation,
|
|
278
|
+
dir' = dir,
|
|
279
|
+
handled' = false,
|
|
280
|
+
controlled' = controlled,
|
|
281
|
+
accepting' = accepting,
|
|
282
|
+
valueProp' = propAfter(follows, valueProp, snap(lo, pointerHiValue(raw))),
|
|
283
|
+
requested' = if (snap(lo, pointerHiValue(raw)) != nowPacked) snap(lo, pointerHiValue(raw)) else requested,
|
|
284
|
+
outcome' = if (snap(lo, pointerHiValue(raw)) != nowPacked) verdict else outcome,
|
|
285
|
+
base' = if (snap(lo, pointerHiValue(raw)) != nowPacked) nowPacked else base,
|
|
286
|
+
requests' = if (snap(lo, pointerHiValue(raw)) != nowPacked) requests + 1 else requests
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
action setOrientation = {
|
|
291
|
+
nondet o = Set("horizontal", "vertical").oneOf()
|
|
292
|
+
all {
|
|
293
|
+
lo' = lo,
|
|
294
|
+
hi' = hi,
|
|
295
|
+
focused' = focused,
|
|
296
|
+
orientation' = o,
|
|
297
|
+
dir' = dir,
|
|
298
|
+
handled' = false,
|
|
299
|
+
controlled' = controlled,
|
|
300
|
+
accepting' = accepting,
|
|
301
|
+
valueProp' = valueProp,
|
|
302
|
+
requested' = requested,
|
|
303
|
+
outcome' = outcome,
|
|
304
|
+
base' = base,
|
|
305
|
+
requests' = requests
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
action setDir = {
|
|
310
|
+
nondet d = Set("ltr", "rtl").oneOf()
|
|
311
|
+
all {
|
|
312
|
+
lo' = lo,
|
|
313
|
+
hi' = hi,
|
|
314
|
+
focused' = focused,
|
|
315
|
+
orientation' = orientation,
|
|
316
|
+
dir' = d,
|
|
317
|
+
handled' = false,
|
|
318
|
+
controlled' = controlled,
|
|
319
|
+
accepting' = accepting,
|
|
320
|
+
valueProp' = valueProp,
|
|
321
|
+
requested' = requested,
|
|
322
|
+
outcome' = outcome,
|
|
323
|
+
base' = base,
|
|
324
|
+
requests' = requests
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// commit: the host writes the controlled pair (a no-op while uncontrolled)
|
|
329
|
+
action setValue = {
|
|
330
|
+
nondet v = Set(0, 10, 27, 40, 60, 120).oneOf()
|
|
331
|
+
all {
|
|
332
|
+
lo' = if (controlled) loOf(v) else lo,
|
|
333
|
+
hi' = if (controlled) hiOf(v) else hi,
|
|
334
|
+
focused' = focused,
|
|
335
|
+
orientation' = orientation,
|
|
336
|
+
dir' = dir,
|
|
337
|
+
handled' = false,
|
|
338
|
+
controlled' = controlled,
|
|
339
|
+
accepting' = accepting,
|
|
340
|
+
valueProp' = if (controlled) v else valueProp,
|
|
341
|
+
requested' = requested,
|
|
342
|
+
outcome' = if (controlled) "none" else outcome,
|
|
343
|
+
base' = base,
|
|
344
|
+
requests' = requests
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// configuration: the host starts owning the pair, adopting the current one
|
|
349
|
+
action control = all {
|
|
350
|
+
lo' = lo,
|
|
351
|
+
hi' = hi,
|
|
352
|
+
focused' = focused,
|
|
353
|
+
orientation' = orientation,
|
|
354
|
+
dir' = dir,
|
|
355
|
+
handled' = false,
|
|
356
|
+
controlled' = true,
|
|
357
|
+
accepting' = accepting,
|
|
358
|
+
valueProp' = nowPacked,
|
|
359
|
+
requested' = requested,
|
|
360
|
+
outcome' = outcome,
|
|
361
|
+
base' = base,
|
|
362
|
+
requests' = requests
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// configuration: the controlled host answers every request with its intent
|
|
366
|
+
action acceptRequests = all {
|
|
367
|
+
lo' = lo,
|
|
368
|
+
hi' = hi,
|
|
369
|
+
focused' = focused,
|
|
370
|
+
orientation' = orientation,
|
|
371
|
+
dir' = dir,
|
|
372
|
+
handled' = false,
|
|
373
|
+
controlled' = controlled,
|
|
374
|
+
accepting' = true,
|
|
375
|
+
valueProp' = valueProp,
|
|
376
|
+
requested' = requested,
|
|
377
|
+
outcome' = outcome,
|
|
378
|
+
base' = base,
|
|
379
|
+
requests' = requests
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
action step = any {
|
|
383
|
+
init, focusThumb, blur, tab, shiftTab, keydown,
|
|
384
|
+
pointerLo, pointerHi, setOrientation, setDir,
|
|
385
|
+
setValue, control, acceptRequests
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// --- invariants --------------------------------------------------------
|
|
389
|
+
// min <= lo <= hi <= max: the thumbs are ordered and inside the range
|
|
390
|
+
val ThumbsOrdered: bool =
|
|
391
|
+
and(rangeMin <= lo, lo <= hi, hi <= rangeMax)
|
|
392
|
+
// aria-valuemax of the lower thumb is the upper thumb's value, and
|
|
393
|
+
// aria-valuemin of the upper thumb is the lower thumb's value
|
|
394
|
+
val BoundsFollowNeighbour: bool =
|
|
395
|
+
and(ariaValueMaxLo == hi, ariaValueMinHi == lo)
|
|
396
|
+
// the outer bounds are the full range
|
|
397
|
+
val OuterBoundsAreRange: bool =
|
|
398
|
+
and(ariaValueMinLo == rangeMin, ariaValueMaxHi == rangeMax)
|
|
399
|
+
// every thumb's aria-valuenow lies inside its own [valuemin, valuemax]
|
|
400
|
+
val NowWithinOwnBounds: bool = and(
|
|
401
|
+
ariaValueMinLo <= ariaValueNowLo,
|
|
402
|
+
ariaValueNowLo <= ariaValueMaxLo,
|
|
403
|
+
ariaValueMinHi <= ariaValueNowHi,
|
|
404
|
+
ariaValueNowHi <= ariaValueMaxHi
|
|
405
|
+
)
|
|
406
|
+
// every reachable value is on the step grid
|
|
407
|
+
val ValuesOnStepGrid: bool = and(
|
|
408
|
+
(lo - rangeMin) % smallStep == 0,
|
|
409
|
+
(hi - rangeMin) % smallStep == 0
|
|
410
|
+
)
|
|
411
|
+
// aria-valuetext is present for each thumb and orders with the values
|
|
412
|
+
val ValueTextDerived: bool = and(
|
|
413
|
+
ariaValueTextLo != "",
|
|
414
|
+
ariaValueTextHi != "",
|
|
415
|
+
or(lo != rangeMin, ariaValueTextLo == "low"),
|
|
416
|
+
or(hi != rangeMax, ariaValueTextHi == "high")
|
|
417
|
+
)
|
|
418
|
+
val OrientationAndDirValid: bool = and(
|
|
419
|
+
ariaOrientationKnown(ariaOrientation),
|
|
420
|
+
or(dir == "ltr", dir == "rtl")
|
|
421
|
+
)
|
|
422
|
+
val FocusValid: bool = or(focused == "none", focused == "lo", focused == "hi")
|
|
423
|
+
// a key is only consumed while a thumb has focus
|
|
424
|
+
val HandledImpliesFocused: bool = or(not(handled), focused != "none")
|
|
425
|
+
// both thumbs are in the tab sequence and the order never depends on values
|
|
426
|
+
val TabOrderConstant: bool = and(
|
|
427
|
+
ariaRole == "slider",
|
|
428
|
+
tabIndexLo == 0,
|
|
429
|
+
tabIndexHi == 0,
|
|
430
|
+
tabNext("none") == "lo",
|
|
431
|
+
tabNext("lo") == "hi"
|
|
432
|
+
)
|
|
433
|
+
// callbacks only ever count up from zero
|
|
434
|
+
val RequestsNonNegative: bool = requests >= 0
|
|
435
|
+
// controlled: only the value prop sets the pair, so no event commits
|
|
436
|
+
val ControlledFollowsProp: bool = or(not(controlled), nowPacked == valueProp)
|
|
437
|
+
// a rejected request leaves the pair as it found it
|
|
438
|
+
val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), nowPacked == base)
|
|
439
|
+
// an accepted request committed exactly its intent
|
|
440
|
+
val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), nowPacked == requested)
|
|
441
|
+
}
|