@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,396 @@
|
|
|
1
|
+
// WAI-ARIA APG — Slider pattern (single thumb).
|
|
2
|
+
//
|
|
3
|
+
// Modelled (https://www.w3.org/WAI/ARIA/apg/patterns/slider/):
|
|
4
|
+
// - "Keyboard Interaction": Right/Up Arrow +1 step, Left/Down Arrow -1 step,
|
|
5
|
+
// Home -> first allowed value, End -> last allowed value, PageUp/PageDown
|
|
6
|
+
// (optional) +/- a large step. Keys act only while the thumb has focus
|
|
7
|
+
// ("Focus is placed on the slider ... also known as the thumb").
|
|
8
|
+
// - "Note 2": reversing the arrow direction. Modelled as the RTL rule: in a
|
|
9
|
+
// horizontal slider under dir=rtl, Left Arrow increases and Right Arrow
|
|
10
|
+
// decreases; Up/Down never flip. In a vertical slider Left/Right keep their
|
|
11
|
+
// LTR meaning (APG lists all four arrows for the vertical temperature
|
|
12
|
+
// example), so orientation only changes the RTL rule and aria-orientation.
|
|
13
|
+
// - "WAI-ARIA Roles, States, and Properties": role=slider, aria-valuenow,
|
|
14
|
+
// aria-valuemin, aria-valuemax, aria-valuetext, aria-orientation
|
|
15
|
+
// (default horizontal), as derived defs of the state.
|
|
16
|
+
// - Examples (slider-color-viewer, slider-temperature, slider-rating,
|
|
17
|
+
// slider-seek): the range 0..10 stands in for all four; `valueText` is the
|
|
18
|
+
// aria-valuetext hook (a low/medium/high band, like the rating example's
|
|
19
|
+
// "Very Dissatisfied ... Very Satisfied").
|
|
20
|
+
// - Configuration variants (bean qtmy): `configure` remounts the widget with
|
|
21
|
+
// a packed variant int, because init is literal-only and the generator
|
|
22
|
+
// allows one nondet per action. Variant 0 is step 1 / large step 3 / value
|
|
23
|
+
// 5; variant 1 is step 2 / large step 4 / value 6 (large step on the grid,
|
|
24
|
+
// so a page jump never leaves it). The range stays 0..10 so one walk
|
|
25
|
+
// alphabet is valid for every variant and a step that does not divide the
|
|
26
|
+
// range is still a deliberate omission.
|
|
27
|
+
//
|
|
28
|
+
// Request vs commit (map ticket rt1p). A key or a rail press is a *request*
|
|
29
|
+
// carrying an intent (`requested`); it fires onValueChange (`requests` counts
|
|
30
|
+
// the calls) when the intent differs from the current value, and then:
|
|
31
|
+
// uncontrolled the request is accepted: the value commits the intent
|
|
32
|
+
// controlled, holds the host ignores it ("rejected"): the value is untouched
|
|
33
|
+
// controlled, accepts the host answers with the same value (`accepting`): the
|
|
34
|
+
// value commits through the controlled prop, not the event
|
|
35
|
+
// `valueProp` is the only thing that changes the value while `controlled`;
|
|
36
|
+
// `setValue` / `hostToggle` are the host writing it. `control` / `acceptRequests`
|
|
37
|
+
// are configuration events (init is literal-only). Invariants for the model:
|
|
38
|
+
// ControlledFollowsProp (no event commits while controlled),
|
|
39
|
+
// RejectedRequestUntouched, AcceptedRequestCommits.
|
|
40
|
+
//
|
|
41
|
+
// Deliberate omissions (ponytail: ceilings, not bugs):
|
|
42
|
+
// - aria-label / aria-labelledby (labelling is a rendering concern).
|
|
43
|
+
// - Non-integer values and a step that does not divide the range: the range
|
|
44
|
+
// is 0..10 and every variant's step divides it, so `End` and a pointer
|
|
45
|
+
// press always land on the grid.
|
|
46
|
+
// - Dragging (pointer capture, move events): only a discrete pointer
|
|
47
|
+
// "press at value" is modelled, snapped to the step grid and clamped.
|
|
48
|
+
// - Disabled / read-only, touch assistive-technology key synthesis, and
|
|
49
|
+
// aria-valuetext formatting (units, locale): a real widget formats the text
|
|
50
|
+
// from its own value; the spec only fixes WHEN the text is derived (always,
|
|
51
|
+
// from valuenow).
|
|
52
|
+
// - The range (0..10) is fixed; `configure` varies step, large step and the
|
|
53
|
+
// starting value. The bridge rejects a config outside the variant table.
|
|
54
|
+
module apg_slider {
|
|
55
|
+
import aria.* from "../../shared/aria"
|
|
56
|
+
|
|
57
|
+
var flavor: int // configuration variant (0 | 1), see `configure`
|
|
58
|
+
var value: int // the current value (aria-valuenow)
|
|
59
|
+
var focused: bool // the thumb has keyboard focus
|
|
60
|
+
var orientation: str // "horizontal" | "vertical"
|
|
61
|
+
var dir: str // "ltr" | "rtl"
|
|
62
|
+
var handled: bool // the last key was consumed (preventDefault)
|
|
63
|
+
var controlled: bool // the host owns the value (`value` prop present)
|
|
64
|
+
var accepting: bool // controlled host that answers every request with its intent
|
|
65
|
+
var valueProp: int // the controlled value prop (meaningful while controlled)
|
|
66
|
+
var requested: int // intent of the last request
|
|
67
|
+
var outcome: str // "none" | "accepted" | "rejected"
|
|
68
|
+
var base: int // value when the last request was received
|
|
69
|
+
var requests: int // number of requests that fired onValueChange
|
|
70
|
+
|
|
71
|
+
// --- configuration -----------------------------------------------------
|
|
72
|
+
def rangeMin: int = 0
|
|
73
|
+
def rangeMax: int = 10
|
|
74
|
+
// packed variant (0 | 1); see the header and `configure`
|
|
75
|
+
def variantSmallStep(v: int): int = if (v == 1) 2 else 1
|
|
76
|
+
def variantLargeStep(v: int): int = if (v == 1) 4 else 3
|
|
77
|
+
def variantStart(v: int): int = if (v == 1) 6 else 5
|
|
78
|
+
def smallStep: int = variantSmallStep(flavor)
|
|
79
|
+
def largeStep: int = variantLargeStep(flavor)
|
|
80
|
+
|
|
81
|
+
// --- pure behaviour ----------------------------------------------------
|
|
82
|
+
def clamp(v: int): int =
|
|
83
|
+
if (v < rangeMin) rangeMin else if (v > rangeMax) rangeMax else v
|
|
84
|
+
|
|
85
|
+
// a raw pointer position lands on the nearest step (the widget corrects an
|
|
86
|
+
// off-grid press; APG's Home/End name "first/last allowed value")
|
|
87
|
+
def snap(v: int): int =
|
|
88
|
+
rangeMin + ((v - rangeMin + smallStep / 2) / smallStep) * smallStep
|
|
89
|
+
|
|
90
|
+
def pointerValue(raw: int): int = clamp(snap(raw))
|
|
91
|
+
|
|
92
|
+
// signed change for a key; 0 for keys that do not change by a delta
|
|
93
|
+
// (arrows and RTL mirroring: shared/aria.qnt ariaValueArrow)
|
|
94
|
+
def keyDelta(k: str, o: str, d: str): int =
|
|
95
|
+
if (k == "PageUp") largeStep
|
|
96
|
+
else if (k == "PageDown") -largeStep
|
|
97
|
+
else smallStep * ariaValueArrow(k, o, d)
|
|
98
|
+
|
|
99
|
+
def isHandledKey(k: str): bool =
|
|
100
|
+
or(k == "Home", k == "End", keyDelta(k, "horizontal", "ltr") != 0)
|
|
101
|
+
|
|
102
|
+
def nextValue(v: int, k: str, o: str, d: str): int =
|
|
103
|
+
if (k == "Home") rangeMin
|
|
104
|
+
else if (k == "End") rangeMax
|
|
105
|
+
else clamp(v + keyDelta(k, o, d))
|
|
106
|
+
|
|
107
|
+
def valueTextFor(v: int): str =
|
|
108
|
+
if (v <= 3) "low" else if (v <= 7) "medium" else "high"
|
|
109
|
+
|
|
110
|
+
// --- request / commit (map ticket rt1p) ----------------------------------
|
|
111
|
+
def moved(before: int, after: int): int = if (before == after) 0 else 1
|
|
112
|
+
// a focused key that would land somewhere else is a request; an inert key
|
|
113
|
+
// (delta 0, or a Home/End already at the bound) is not
|
|
114
|
+
def wants(f: bool, current: int, want: int): bool = and(f, want != current)
|
|
115
|
+
def willRequest(f: bool, k: str, v: int, o: str, d: str): bool =
|
|
116
|
+
wants(f, v, nextValue(v, k, o, d))
|
|
117
|
+
// controlled host that keeps the old value: the request is rejected
|
|
118
|
+
def holds: bool = and(controlled, not(accepting))
|
|
119
|
+
// controlled host that writes the intent back through the value prop
|
|
120
|
+
def follows: bool = and(controlled, accepting)
|
|
121
|
+
def verdict: str = if (holds) "rejected" else "accepted"
|
|
122
|
+
// the value after a request for `want`
|
|
123
|
+
def landed(h: bool, current: int, want: int): int = if (h) current else want
|
|
124
|
+
// the value prop after a request for `want`
|
|
125
|
+
def propAfter(f: bool, p: int, want: int): int = if (f) want else p
|
|
126
|
+
|
|
127
|
+
// --- ARIA contract (derived from the state) -----------------------------
|
|
128
|
+
def ariaRole: str = "slider"
|
|
129
|
+
def ariaValueNow: int = value
|
|
130
|
+
def ariaValueMin: int = rangeMin
|
|
131
|
+
def ariaValueMax: int = rangeMax
|
|
132
|
+
def ariaValueText: str = valueTextFor(value)
|
|
133
|
+
def ariaOrientation: str = orientation
|
|
134
|
+
def tabIndex: int = 0
|
|
135
|
+
|
|
136
|
+
// --- machine -----------------------------------------------------------
|
|
137
|
+
action init = all {
|
|
138
|
+
flavor' = 0,
|
|
139
|
+
value' = 5,
|
|
140
|
+
focused' = false,
|
|
141
|
+
orientation' = "horizontal",
|
|
142
|
+
dir' = "ltr",
|
|
143
|
+
handled' = false,
|
|
144
|
+
controlled' = false,
|
|
145
|
+
accepting' = false,
|
|
146
|
+
valueProp' = 5,
|
|
147
|
+
requested' = 5,
|
|
148
|
+
outcome' = "none",
|
|
149
|
+
base' = 5,
|
|
150
|
+
requests' = 0
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
action focus = all {
|
|
154
|
+
flavor' = flavor,
|
|
155
|
+
value' = value,
|
|
156
|
+
focused' = true,
|
|
157
|
+
orientation' = orientation,
|
|
158
|
+
dir' = dir,
|
|
159
|
+
handled' = false,
|
|
160
|
+
controlled' = controlled,
|
|
161
|
+
accepting' = accepting,
|
|
162
|
+
valueProp' = valueProp,
|
|
163
|
+
requested' = requested,
|
|
164
|
+
outcome' = outcome,
|
|
165
|
+
base' = base,
|
|
166
|
+
requests' = requests
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
action blur = all {
|
|
170
|
+
flavor' = flavor,
|
|
171
|
+
value' = value,
|
|
172
|
+
focused' = false,
|
|
173
|
+
orientation' = orientation,
|
|
174
|
+
dir' = dir,
|
|
175
|
+
handled' = false,
|
|
176
|
+
controlled' = controlled,
|
|
177
|
+
accepting' = accepting,
|
|
178
|
+
valueProp' = valueProp,
|
|
179
|
+
requested' = requested,
|
|
180
|
+
outcome' = outcome,
|
|
181
|
+
base' = base,
|
|
182
|
+
requests' = requests
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// request: a focused key moves the value (APG keyboard section)
|
|
186
|
+
action keydown = {
|
|
187
|
+
nondet key = Set(
|
|
188
|
+
"ArrowRight", "ArrowLeft", "ArrowUp", "ArrowDown",
|
|
189
|
+
"Home", "End", "PageUp", "PageDown", "Enter"
|
|
190
|
+
).oneOf()
|
|
191
|
+
all {
|
|
192
|
+
flavor' = flavor,
|
|
193
|
+
value' = if (and(focused, isHandledKey(key))) landed(holds, value, nextValue(value, key, orientation, dir)) else value,
|
|
194
|
+
focused' = focused,
|
|
195
|
+
orientation' = orientation,
|
|
196
|
+
dir' = dir,
|
|
197
|
+
handled' = and(focused, isHandledKey(key)),
|
|
198
|
+
controlled' = controlled,
|
|
199
|
+
accepting' = accepting,
|
|
200
|
+
valueProp' = if (willRequest(focused, key, value, orientation, dir)) propAfter(follows, valueProp, nextValue(value, key, orientation, dir)) else valueProp,
|
|
201
|
+
requested' = if (willRequest(focused, key, value, orientation, dir)) nextValue(value, key, orientation, dir) else requested,
|
|
202
|
+
outcome' = if (willRequest(focused, key, value, orientation, dir)) verdict else outcome,
|
|
203
|
+
base' = if (willRequest(focused, key, value, orientation, dir)) value else base,
|
|
204
|
+
requests' = if (willRequest(focused, key, value, orientation, dir)) requests + 1 else requests
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// request: pointer press on the track at a raw value; focuses the thumb, clamps
|
|
209
|
+
action pointer = {
|
|
210
|
+
nondet raw = Set(-3, 0, 4, 7, 10, 14).oneOf()
|
|
211
|
+
all {
|
|
212
|
+
flavor' = flavor,
|
|
213
|
+
value' = landed(holds, value, pointerValue(raw)),
|
|
214
|
+
focused' = true,
|
|
215
|
+
orientation' = orientation,
|
|
216
|
+
dir' = dir,
|
|
217
|
+
handled' = false,
|
|
218
|
+
controlled' = controlled,
|
|
219
|
+
accepting' = accepting,
|
|
220
|
+
valueProp' = propAfter(follows, valueProp, pointerValue(raw)),
|
|
221
|
+
requested' = if (wants(true, value, pointerValue(raw))) pointerValue(raw) else requested,
|
|
222
|
+
outcome' = if (wants(true, value, pointerValue(raw))) verdict else outcome,
|
|
223
|
+
base' = if (wants(true, value, pointerValue(raw))) value else base,
|
|
224
|
+
requests' = if (wants(true, value, pointerValue(raw))) requests + 1 else requests
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
action setOrientation = {
|
|
229
|
+
nondet o = Set("horizontal", "vertical").oneOf()
|
|
230
|
+
all {
|
|
231
|
+
flavor' = flavor,
|
|
232
|
+
value' = value,
|
|
233
|
+
focused' = focused,
|
|
234
|
+
orientation' = o,
|
|
235
|
+
dir' = dir,
|
|
236
|
+
handled' = false,
|
|
237
|
+
controlled' = controlled,
|
|
238
|
+
accepting' = accepting,
|
|
239
|
+
valueProp' = valueProp,
|
|
240
|
+
requested' = requested,
|
|
241
|
+
outcome' = outcome,
|
|
242
|
+
base' = base,
|
|
243
|
+
requests' = requests
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
action setDir = {
|
|
248
|
+
nondet d = Set("ltr", "rtl").oneOf()
|
|
249
|
+
all {
|
|
250
|
+
flavor' = flavor,
|
|
251
|
+
value' = value,
|
|
252
|
+
focused' = focused,
|
|
253
|
+
orientation' = orientation,
|
|
254
|
+
dir' = d,
|
|
255
|
+
handled' = false,
|
|
256
|
+
controlled' = controlled,
|
|
257
|
+
accepting' = accepting,
|
|
258
|
+
valueProp' = valueProp,
|
|
259
|
+
requested' = requested,
|
|
260
|
+
outcome' = outcome,
|
|
261
|
+
base' = base,
|
|
262
|
+
requests' = requests
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// commit: the host writes the controlled value (a no-op while uncontrolled)
|
|
267
|
+
// a host write is a value on the step grid (0..10 is even for both variants)
|
|
268
|
+
action setValue = {
|
|
269
|
+
nondet v = Set(0, 2, 4, 6, 8, 10).oneOf()
|
|
270
|
+
all {
|
|
271
|
+
flavor' = flavor,
|
|
272
|
+
value' = if (controlled) v else value,
|
|
273
|
+
focused' = focused,
|
|
274
|
+
orientation' = orientation,
|
|
275
|
+
dir' = dir,
|
|
276
|
+
handled' = false,
|
|
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 its own copy of the value between the range ends
|
|
288
|
+
// (an outside two-state control)
|
|
289
|
+
action hostToggle = all {
|
|
290
|
+
flavor' = flavor,
|
|
291
|
+
value' = if (controlled) (if (valueProp == rangeMax) rangeMin else rangeMax) else value,
|
|
292
|
+
focused' = focused,
|
|
293
|
+
orientation' = orientation,
|
|
294
|
+
dir' = dir,
|
|
295
|
+
handled' = false,
|
|
296
|
+
controlled' = controlled,
|
|
297
|
+
accepting' = accepting,
|
|
298
|
+
valueProp' = if (controlled) (if (valueProp == rangeMax) rangeMin else rangeMax) else valueProp,
|
|
299
|
+
requested' = requested,
|
|
300
|
+
outcome' = if (controlled) "none" else outcome,
|
|
301
|
+
base' = base,
|
|
302
|
+
requests' = requests
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// configuration: the host starts owning the value, adopting the current one
|
|
306
|
+
action control = all {
|
|
307
|
+
flavor' = flavor,
|
|
308
|
+
value' = value,
|
|
309
|
+
focused' = focused,
|
|
310
|
+
orientation' = orientation,
|
|
311
|
+
dir' = dir,
|
|
312
|
+
handled' = false,
|
|
313
|
+
controlled' = true,
|
|
314
|
+
accepting' = accepting,
|
|
315
|
+
valueProp' = value,
|
|
316
|
+
requested' = requested,
|
|
317
|
+
outcome' = outcome,
|
|
318
|
+
base' = base,
|
|
319
|
+
requests' = requests
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// configuration: the controlled host answers every request with its intent
|
|
323
|
+
action acceptRequests = all {
|
|
324
|
+
flavor' = flavor,
|
|
325
|
+
value' = value,
|
|
326
|
+
focused' = focused,
|
|
327
|
+
orientation' = orientation,
|
|
328
|
+
dir' = dir,
|
|
329
|
+
handled' = false,
|
|
330
|
+
controlled' = controlled,
|
|
331
|
+
accepting' = true,
|
|
332
|
+
valueProp' = valueProp,
|
|
333
|
+
requested' = requested,
|
|
334
|
+
outcome' = outcome,
|
|
335
|
+
base' = base,
|
|
336
|
+
requests' = requests
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// configuration: remount with a packed variant (step, large step, start).
|
|
340
|
+
// Resets the widget to uncontrolled and horizontal/ltr, like a fresh mount.
|
|
341
|
+
action configure = {
|
|
342
|
+
nondet pick = Set(0, 1).oneOf()
|
|
343
|
+
all {
|
|
344
|
+
flavor' = pick,
|
|
345
|
+
value' = variantStart(pick),
|
|
346
|
+
focused' = false,
|
|
347
|
+
orientation' = "horizontal",
|
|
348
|
+
dir' = "ltr",
|
|
349
|
+
handled' = false,
|
|
350
|
+
controlled' = false,
|
|
351
|
+
accepting' = false,
|
|
352
|
+
valueProp' = variantStart(pick),
|
|
353
|
+
requested' = variantStart(pick),
|
|
354
|
+
outcome' = "none",
|
|
355
|
+
base' = variantStart(pick),
|
|
356
|
+
requests' = 0
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
action step = any {
|
|
361
|
+
init, configure, focus, blur, keydown, pointer, setOrientation, setDir,
|
|
362
|
+
setValue, hostToggle, control, acceptRequests
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// --- invariants --------------------------------------------------------
|
|
366
|
+
// min <= value <= max
|
|
367
|
+
val ValueInRange: bool = and(value >= rangeMin, value <= rangeMax)
|
|
368
|
+
// the exposed aria-valuenow sits inside the exposed [valuemin, valuemax]
|
|
369
|
+
val AriaNowWithinBounds: bool =
|
|
370
|
+
and(ariaValueMin <= ariaValueNow, ariaValueNow <= ariaValueMax)
|
|
371
|
+
// every reachable value is on the step grid
|
|
372
|
+
val ValueOnStepGrid: bool = (value - rangeMin) % smallStep == 0
|
|
373
|
+
// aria-valuetext is always present and the endpoints read as the extremes
|
|
374
|
+
val ValueTextDerived: bool = and(
|
|
375
|
+
ariaValueText != "",
|
|
376
|
+
or(value != rangeMin, ariaValueText == "low"),
|
|
377
|
+
or(value != rangeMax, ariaValueText == "high")
|
|
378
|
+
)
|
|
379
|
+
// aria-orientation is one of the two ARIA tokens; direction is ltr/rtl
|
|
380
|
+
val OrientationAndDirValid: bool = and(
|
|
381
|
+
ariaOrientationKnown(ariaOrientation),
|
|
382
|
+
or(dir == "ltr", dir == "rtl")
|
|
383
|
+
)
|
|
384
|
+
// a key is only consumed while the thumb has focus
|
|
385
|
+
val HandledImpliesFocused: bool = or(not(handled), focused)
|
|
386
|
+
// the slider thumb is always in the tab sequence
|
|
387
|
+
val InTabSequence: bool = and(ariaRole == "slider", tabIndex == 0)
|
|
388
|
+
// callbacks only ever count up from zero
|
|
389
|
+
val RequestsNonNegative: bool = requests >= 0
|
|
390
|
+
// controlled: only the value prop sets the state, so no event commits
|
|
391
|
+
val ControlledFollowsProp: bool = or(not(controlled), value == valueProp)
|
|
392
|
+
// a rejected request leaves the state as it found it
|
|
393
|
+
val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), value == base)
|
|
394
|
+
// an accepted request committed exactly its intent
|
|
395
|
+
val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), value == requested)
|
|
396
|
+
}
|