@cssints/apg-specs 0.0.0-stage → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +65 -2
- package/dist/apg/accordion.bridge.d.mts +35 -0
- package/dist/apg/accordion.bridge.mjs +326 -0
- package/dist/apg/accordion.model.json +8397 -0
- package/dist/apg/alert.bridge.d.mts +22 -0
- package/dist/apg/alert.bridge.mjs +86 -0
- package/dist/apg/alert.model.json +871 -0
- package/dist/apg/alertdialog.bridge.d.mts +21 -0
- package/dist/apg/alertdialog.bridge.mjs +221 -0
- package/dist/apg/alertdialog.model.json +5232 -0
- package/dist/apg/breadcrumb.bridge.d.mts +26 -0
- package/dist/apg/breadcrumb.bridge.mjs +120 -0
- package/dist/apg/breadcrumb.model.json +574 -0
- package/dist/apg/button.bridge.d.mts +35 -0
- package/dist/apg/button.bridge.mjs +208 -0
- package/dist/apg/button.model.json +5814 -0
- package/dist/apg/carousel.bridge.d.mts +44 -0
- package/dist/apg/carousel.bridge.mjs +262 -0
- package/dist/apg/carousel.model.json +8963 -0
- package/dist/apg/checkbox-group.bridge.d.mts +25 -0
- package/dist/apg/checkbox-group.bridge.mjs +276 -0
- package/dist/apg/checkbox-group.model.json +3863 -0
- package/dist/apg/checkbox.bridge.d.mts +34 -0
- package/dist/apg/checkbox.bridge.mjs +253 -0
- package/dist/apg/checkbox.model.json +3158 -0
- package/dist/apg/combobox-autocomplete.bridge.d.mts +14 -0
- package/dist/apg/combobox-autocomplete.bridge.mjs +326 -0
- package/dist/apg/combobox-autocomplete.model.json +11586 -0
- package/dist/apg/combobox-dialog.bridge.d.mts +17 -0
- package/dist/apg/combobox-dialog.bridge.mjs +250 -0
- package/dist/apg/combobox-dialog.model.json +8981 -0
- package/dist/apg/combobox-grid.bridge.d.mts +17 -0
- package/dist/apg/combobox-grid.bridge.mjs +227 -0
- package/dist/apg/combobox-grid.model.json +8554 -0
- package/dist/apg/combobox-select-only.bridge.d.mts +24 -0
- package/dist/apg/combobox-select-only.bridge.mjs +304 -0
- package/dist/apg/combobox-select-only.model.json +7846 -0
- package/dist/apg/combobox-tree.bridge.d.mts +17 -0
- package/dist/apg/combobox-tree.bridge.mjs +211 -0
- package/dist/apg/combobox-tree.model.json +9345 -0
- package/dist/apg/dialog-modal.bridge.d.mts +35 -0
- package/dist/apg/dialog-modal.bridge.mjs +186 -0
- package/dist/apg/dialog-modal.model.json +7580 -0
- package/dist/apg/disclosure.bridge.d.mts +25 -0
- package/dist/apg/disclosure.bridge.mjs +202 -0
- package/dist/apg/disclosure.model.json +4703 -0
- package/dist/apg/feed.bridge.d.mts +31 -0
- package/dist/apg/feed.bridge.mjs +246 -0
- package/dist/apg/feed.model.json +4523 -0
- package/dist/apg/grid-selection.bridge.d.mts +13 -0
- package/dist/apg/grid-selection.bridge.mjs +160 -0
- package/dist/apg/grid-selection.model.json +6974 -0
- package/dist/apg/grid-sort.bridge.d.mts +12 -0
- package/dist/apg/grid-sort.bridge.mjs +138 -0
- package/dist/apg/grid-sort.model.json +3799 -0
- package/dist/apg/grid.bridge.d.mts +54 -0
- package/dist/apg/grid.bridge.mjs +441 -0
- package/dist/apg/grid.model.json +3802 -0
- package/dist/apg/link.bridge.d.mts +24 -0
- package/dist/apg/link.bridge.mjs +126 -0
- package/dist/apg/link.model.json +694 -0
- package/dist/apg/listbox-multi.bridge.d.mts +10 -0
- package/dist/apg/listbox-multi.bridge.mjs +261 -0
- package/dist/apg/listbox-multi.model.json +10893 -0
- package/dist/apg/listbox-rearrangeable.bridge.d.mts +20 -0
- package/dist/apg/listbox-rearrangeable.bridge.mjs +237 -0
- package/dist/apg/listbox-rearrangeable.model.json +17592 -0
- package/dist/apg/listbox-shared.d.mts +58 -0
- package/dist/apg/listbox-shared.mjs +171 -0
- package/dist/apg/listbox-single.bridge.d.mts +10 -0
- package/dist/apg/listbox-single.bridge.mjs +196 -0
- package/dist/apg/listbox-single.model.json +8024 -0
- package/dist/apg/listbox-typeahead.bridge.d.mts +10 -0
- package/dist/apg/listbox-typeahead.bridge.mjs +153 -0
- package/dist/apg/listbox-typeahead.model.json +5615 -0
- package/dist/apg/menu-button.bridge.d.mts +29 -0
- package/dist/apg/menu-button.bridge.mjs +259 -0
- package/dist/apg/menu-button.model.json +5706 -0
- package/dist/apg/menu-checkable.bridge.d.mts +25 -0
- package/dist/apg/menu-checkable.bridge.mjs +226 -0
- package/dist/apg/menu-checkable.model.json +4277 -0
- package/dist/apg/menu-menubar.bridge.d.mts +27 -0
- package/dist/apg/menu-menubar.bridge.mjs +273 -0
- package/dist/apg/menu-menubar.model.json +9540 -0
- package/dist/apg/meter.bridge.d.mts +28 -0
- package/dist/apg/meter.bridge.mjs +159 -0
- package/dist/apg/meter.model.json +1185 -0
- package/dist/apg/player.bridge.d.mts +24 -0
- package/dist/apg/player.bridge.mjs +342 -0
- package/dist/apg/player.model.json +11013 -0
- package/dist/apg/radio.bridge.d.mts +35 -0
- package/dist/apg/radio.bridge.mjs +371 -0
- package/dist/apg/radio.model.json +5011 -0
- package/dist/apg/slider-multithumb.bridge.d.mts +34 -0
- package/dist/apg/slider-multithumb.bridge.mjs +365 -0
- package/dist/apg/slider-multithumb.model.json +5774 -0
- package/dist/apg/slider.bridge.d.mts +54 -0
- package/dist/apg/slider.bridge.mjs +359 -0
- package/dist/apg/slider.model.json +4881 -0
- package/dist/apg/spinbutton.bridge.d.mts +41 -0
- package/dist/apg/spinbutton.bridge.mjs +327 -0
- package/dist/apg/spinbutton.model.json +7571 -0
- package/dist/apg/switch.bridge.d.mts +30 -0
- package/dist/apg/switch.bridge.mjs +243 -0
- package/dist/apg/switch.model.json +3159 -0
- package/dist/apg/tabs.bridge.d.mts +41 -0
- package/dist/apg/tabs.bridge.mjs +294 -0
- package/dist/apg/tabs.model.json +7659 -0
- package/dist/apg/timeline.bridge.d.mts +36 -0
- package/dist/apg/timeline.bridge.mjs +265 -0
- package/dist/apg/timeline.model.json +6189 -0
- package/dist/apg/toolbar.bridge.d.mts +25 -0
- package/dist/apg/toolbar.bridge.mjs +307 -0
- package/dist/apg/toolbar.model.json +6616 -0
- package/dist/apg/tooltip.bridge.d.mts +36 -0
- package/dist/apg/tooltip.bridge.mjs +208 -0
- package/dist/apg/tooltip.model.json +8295 -0
- package/dist/apg/treegrid.bridge.d.mts +28 -0
- package/dist/apg/treegrid.bridge.mjs +311 -0
- package/dist/apg/treegrid.model.json +17358 -0
- package/dist/apg/treeview.bridge.d.mts +26 -0
- package/dist/apg/treeview.bridge.mjs +396 -0
- package/dist/apg/treeview.model.json +13745 -0
- package/dist/apg/windowsplitter.bridge.d.mts +36 -0
- package/dist/apg/windowsplitter.bridge.mjs +242 -0
- package/dist/apg/windowsplitter.model.json +6253 -0
- package/dist/canvas/camera.model.json +6568 -0
- package/dist/canvas/canvas.bridge.d.mts +190 -0
- package/dist/canvas/canvas.bridge.mjs +729 -0
- package/dist/canvas/pointer.model.json +17095 -0
- package/dist/scenario.d.mts +233 -0
- package/dist/scenario.mjs +313 -0
- package/package.json +57 -4
- package/scenarios/apg/README.md +221 -0
- package/scenarios/apg/accordion.base-ui.json +1005 -0
- package/scenarios/apg/accordion.react-aria.json +929 -0
- package/scenarios/apg/alert.base-ui.json +81 -0
- package/scenarios/apg/alert.react-aria.json +98 -0
- package/scenarios/apg/alertdialog.base-ui.json +331 -0
- package/scenarios/apg/alertdialog.react-aria.json +225 -0
- package/scenarios/apg/breadcrumb.react-aria.json +312 -0
- package/scenarios/apg/button.ariakit.json +136 -0
- package/scenarios/apg/button.base-ui.json +747 -0
- package/scenarios/apg/button.react-aria.json +443 -0
- package/scenarios/apg/carousel.apg.json +909 -0
- package/scenarios/apg/checkbox.ariakit.json +299 -0
- package/scenarios/apg/checkbox.base-ui.json +1126 -0
- package/scenarios/apg/checkbox.react-aria.json +518 -0
- package/scenarios/apg/combobox.ariakit.json +945 -0
- package/scenarios/apg/combobox.base-ui.json +2444 -0
- package/scenarios/apg/combobox.react-aria.json +845 -0
- package/scenarios/apg/dialog-modal.ariakit.json +1102 -0
- package/scenarios/apg/dialog-modal.base-ui.json +775 -0
- package/scenarios/apg/dialog-modal.react-aria.json +638 -0
- package/scenarios/apg/disclosure.apg.json +349 -0
- package/scenarios/apg/disclosure.ariakit.json +163 -0
- package/scenarios/apg/disclosure.base-ui.json +451 -0
- package/scenarios/apg/disclosure.react-aria.json +643 -0
- package/scenarios/apg/feed.apg.json +442 -0
- package/scenarios/apg/grid.ariakit.json +197 -0
- package/scenarios/apg/grid.base-ui.json +720 -0
- package/scenarios/apg/grid.react-aria.json +969 -0
- package/scenarios/apg/landmarks.ariakit.json +93 -0
- package/scenarios/apg/landmarks.react-aria.json +330 -0
- package/scenarios/apg/link.ariakit.json +83 -0
- package/scenarios/apg/link.base-ui.json +88 -0
- package/scenarios/apg/link.react-aria.json +186 -0
- package/scenarios/apg/listbox.ariakit.json +78 -0
- package/scenarios/apg/listbox.base-ui.json +163 -0
- package/scenarios/apg/listbox.react-aria.json +1118 -0
- package/scenarios/apg/menu-button.ariakit.json +924 -0
- package/scenarios/apg/menu-button.base-ui.json +1155 -0
- package/scenarios/apg/menu-button.react-aria.json +1080 -0
- package/scenarios/apg/menubar.ariakit.json +758 -0
- package/scenarios/apg/menubar.base-ui.json +673 -0
- package/scenarios/apg/meter.base-ui.json +243 -0
- package/scenarios/apg/meter.react-aria.json +201 -0
- package/scenarios/apg/radio.ariakit.json +459 -0
- package/scenarios/apg/radio.base-ui.json +581 -0
- package/scenarios/apg/radio.react-aria.json +576 -0
- package/scenarios/apg/slider-multithumb.base-ui.json +316 -0
- package/scenarios/apg/slider-multithumb.react-aria.json +363 -0
- package/scenarios/apg/slider.base-ui.json +544 -0
- package/scenarios/apg/slider.react-aria.json +456 -0
- package/scenarios/apg/spinbutton.base-ui.json +1688 -0
- package/scenarios/apg/spinbutton.react-aria.json +426 -0
- package/scenarios/apg/switch.base-ui.json +456 -0
- package/scenarios/apg/switch.react-aria.json +303 -0
- package/scenarios/apg/table.ariakit.json +65 -0
- package/scenarios/apg/table.react-aria.json +221 -0
- package/scenarios/apg/tabs.ariakit.json +329 -0
- package/scenarios/apg/tabs.base-ui.json +875 -0
- package/scenarios/apg/tabs.react-aria.json +644 -0
- package/scenarios/apg/toolbar.ariakit.json +595 -0
- package/scenarios/apg/toolbar.base-ui.json +1189 -0
- package/scenarios/apg/toolbar.react-aria.json +383 -0
- package/scenarios/apg/tooltip.ariakit.json +441 -0
- package/scenarios/apg/tooltip.base-ui.json +411 -0
- package/scenarios/apg/tooltip.react-aria.json +454 -0
- package/scenarios/apg/treegrid.apg.json +2558 -0
- package/scenarios/apg/treegrid.react-aria.json +5094 -0
- package/scenarios/apg/treeview.apg.json +928 -0
- package/scenarios/apg/windowsplitter.apg.json +557 -0
- package/scenarios/canvas/README.md +69 -0
- package/scenarios/canvas/camera.contract.json +237 -0
- package/scenarios/canvas/camera.excalidraw.json +57 -0
- package/scenarios/canvas/marquee.excalidraw.json +122 -0
- package/scenarios/canvas/pan.excalidraw.json +46 -0
- package/scenarios/canvas/pointer.contract.json +528 -0
- package/scenarios/parts.json +291 -0
- package/shared/aria.qnt +186 -0
- package/shared/details-group.qnt +50 -0
- package/shared/focusgroup.qnt +152 -0
- package/shared/invokers.qnt +81 -0
- package/shared/popover.qnt +71 -0
- package/specs/apg/STATIC.md +103 -0
- package/specs/apg/accordion.qnt +578 -0
- package/specs/apg/alert.qnt +131 -0
- package/specs/apg/alertdialog.qnt +455 -0
- package/specs/apg/breadcrumb.qnt +88 -0
- package/specs/apg/button.qnt +445 -0
- package/specs/apg/carousel.qnt +732 -0
- package/specs/apg/checkbox-group.qnt +388 -0
- package/specs/apg/checkbox.qnt +394 -0
- package/specs/apg/combobox-autocomplete.qnt +764 -0
- package/specs/apg/combobox-dialog.qnt +627 -0
- package/specs/apg/combobox-grid.qnt +632 -0
- package/specs/apg/combobox-select-only.qnt +626 -0
- package/specs/apg/combobox-tree.qnt +633 -0
- package/specs/apg/dialog-modal.qnt +499 -0
- package/specs/apg/disclosure.qnt +309 -0
- package/specs/apg/feed.qnt +485 -0
- package/specs/apg/grid-selection.qnt +512 -0
- package/specs/apg/grid-sort.qnt +330 -0
- package/specs/apg/grid.qnt +360 -0
- package/specs/apg/link.qnt +119 -0
- package/specs/apg/listbox-multi.qnt +799 -0
- package/specs/apg/listbox-rearrangeable.qnt +801 -0
- package/specs/apg/listbox-single.qnt +511 -0
- package/specs/apg/listbox-typeahead.qnt +396 -0
- package/specs/apg/menu-button.qnt +363 -0
- package/specs/apg/menu-checkable.qnt +304 -0
- package/specs/apg/menu-menubar.qnt +531 -0
- package/specs/apg/meter.qnt +129 -0
- package/specs/apg/player.qnt +1081 -0
- package/specs/apg/radio.qnt +394 -0
- package/specs/apg/slider-multithumb.qnt +441 -0
- package/specs/apg/slider.qnt +396 -0
- package/specs/apg/spinbutton.qnt +494 -0
- package/specs/apg/switch.qnt +389 -0
- package/specs/apg/tabs.qnt +412 -0
- package/specs/apg/timeline.qnt +612 -0
- package/specs/apg/toolbar.qnt +364 -0
- package/specs/apg/tooltip.qnt +553 -0
- package/specs/apg/treegrid.qnt +983 -0
- package/specs/apg/treeview.qnt +1004 -0
- package/specs/apg/windowsplitter.qnt +487 -0
- package/specs/canvas/camera.qnt +407 -0
- package/specs/canvas/pointer.qnt +841 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
// WAI-ARIA APG — Spinbutton pattern.
|
|
2
|
+
// https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/
|
|
3
|
+
// example: Quantity Spin Button (the APG ships no date-picker spinbutton
|
|
4
|
+
// example; the closest, "Quantity", is the one the pattern page links).
|
|
5
|
+
//
|
|
6
|
+
// Modelled (APG sections):
|
|
7
|
+
// - About This Pattern: text field + increase/decrease buttons; the text
|
|
8
|
+
// field is the only focusable part, focus stays on it during operation.
|
|
9
|
+
// - Keyboard Interaction: Up/Down (step), Page Up/Down (optional larger
|
|
10
|
+
// step), Home -> min, End -> max, printable characters edit the text.
|
|
11
|
+
// - WAI-ARIA Roles, States, Properties: role=spinbutton, aria-valuenow /
|
|
12
|
+
// aria-valuemin / aria-valuemax, aria-invalid, and the tab-order rule that
|
|
13
|
+
// the buttons are NOT focusable by Tab (tabindex -1).
|
|
14
|
+
//
|
|
15
|
+
// The committed `value` is separate from the typed `draft` text. The draft is a
|
|
16
|
+
// validity pool: the supported subset has no string parsing, so the spec
|
|
17
|
+
// carries a fixed set of sample texts ("" / "5" / "12" / "13" / "0" / "abc")
|
|
18
|
+
// and explicit defs say which parse to an in-range integer. Enter commits a
|
|
19
|
+
// valid draft, an invalid one reverts; blur and Escape follow the same rule
|
|
20
|
+
// (blur commits-or-reverts, Escape always reverts). Spin keys and the buttons
|
|
21
|
+
// resolve a pending draft first, then step from the result.
|
|
22
|
+
//
|
|
23
|
+
// Variant: `wrap` selects clamp-at-bounds (false) or wrap-around (true). It is
|
|
24
|
+
// a switchable var so one spec covers both; `setWrap` resets `result`.
|
|
25
|
+
//
|
|
26
|
+
// Request vs commit (map ticket rt1p). Every value-changing event (spin keys,
|
|
27
|
+
// Home/End, the stepper buttons, Enter commit and blur) is a *request* carrying
|
|
28
|
+
// an intent (`requested`); it fires onValueChange (`requests` counts the calls)
|
|
29
|
+
// when the intent differs from the current value, and then:
|
|
30
|
+
// uncontrolled the request is accepted: the value commits the intent
|
|
31
|
+
// controlled, holds the host ignores it ("rejected"): the value is untouched
|
|
32
|
+
// controlled, accepts the host answers with the same value (`accepting`): the
|
|
33
|
+
// value commits through the controlled prop, not the event
|
|
34
|
+
// `valueProp` is the only thing that changes the value while `controlled`;
|
|
35
|
+
// `setValue` / `hostToggle` are the host writing it. `control` / `acceptRequests`
|
|
36
|
+
// are configuration events (init is literal-only). Typing, Escape and `setWrap`
|
|
37
|
+
// change no committed value, so they are not requests. Invariants for the
|
|
38
|
+
// model: ControlledFollowsProp (no event commits while controlled),
|
|
39
|
+
// RejectedRequestUntouched, AcceptedRequestCommits.
|
|
40
|
+
//
|
|
41
|
+
// Range constants are small on purpose: min 1, max 12 (a month), step 1, page 3.
|
|
42
|
+
//
|
|
43
|
+
// Deliberately omitted:
|
|
44
|
+
// - aria-valuetext / labels (aria-label, aria-labelledby): strings with no
|
|
45
|
+
// state; the Solid layer supplies them.
|
|
46
|
+
// - spinbuttons without a min/max (aria-valuemin/max are always present),
|
|
47
|
+
// and non-integer ("decimal") values.
|
|
48
|
+
// - per-character text editing, caret/selection, and key filtering: `draft`
|
|
49
|
+
// is replaced wholesale by a sample text.
|
|
50
|
+
// - the commit/revert policy (Enter/Escape/blur) is a widely used
|
|
51
|
+
// convention, not APG text; so is the buttons' aria-disabled-at-bound.
|
|
52
|
+
// - press-and-hold auto-repeat on the buttons, mouse wheel.
|
|
53
|
+
//
|
|
54
|
+
// Generator notes: no action preconditions (guards live in the assignment
|
|
55
|
+
// RHS); every action assigns every var.
|
|
56
|
+
module apg_spinbutton {
|
|
57
|
+
var value: int // the committed value (aria-valuenow), always in [min,max]
|
|
58
|
+
var focused: bool // the text field has focus
|
|
59
|
+
var editing: bool // the text field holds an uncommitted draft
|
|
60
|
+
var draft: str // the typed text while editing, "" otherwise
|
|
61
|
+
var wrap: bool // variant: wrap-around instead of clamping at the bounds
|
|
62
|
+
var result: str // what the last action did: "none" | "focused" | "blurred" | "stepped" | "clamped" | "wrapped" | "jumped" | "typed" | "committed" | "reverted" | "mode"
|
|
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": fate of the last request
|
|
68
|
+
var base: int // value when the last request was received
|
|
69
|
+
var requests: int // number of requests that fired onValueChange
|
|
70
|
+
|
|
71
|
+
// --- range constants ---------------------------------------------------
|
|
72
|
+
def minValue: int = 1
|
|
73
|
+
def maxValue: int = 12
|
|
74
|
+
def smallStep: int = 1
|
|
75
|
+
def largeStep: int = 3
|
|
76
|
+
def span: int = maxValue - minValue + 1
|
|
77
|
+
|
|
78
|
+
// --- draft validity pool -----------------------------------------------
|
|
79
|
+
def draftIsNumber(t: str): bool =
|
|
80
|
+
or(or(t == "5", t == "12"), or(t == "13", t == "0"))
|
|
81
|
+
def draftNumber(t: str): int =
|
|
82
|
+
if (t == "5") 5 else if (t == "12") 12 else if (t == "13") 13 else 0
|
|
83
|
+
def draftInRange(t: str): bool =
|
|
84
|
+
and(draftIsNumber(t), and(draftNumber(t) >= minValue, draftNumber(t) <= maxValue))
|
|
85
|
+
|
|
86
|
+
// --- value arithmetic: clamp vs wrap -----------------------------------
|
|
87
|
+
def up(v: int, d: int, w: bool): int =
|
|
88
|
+
if (v + d <= maxValue) v + d else if (w) v + d - span else maxValue
|
|
89
|
+
def down(v: int, d: int, w: bool): int =
|
|
90
|
+
if (v - d >= minValue) v - d else if (w) v - d + span else minValue
|
|
91
|
+
def upResult(v: int, d: int, w: bool): str =
|
|
92
|
+
if (v + d <= maxValue) "stepped" else if (w) "wrapped" else "clamped"
|
|
93
|
+
def downResult(v: int, d: int, w: bool): str =
|
|
94
|
+
if (v - d >= minValue) "stepped" else if (w) "wrapped" else "clamped"
|
|
95
|
+
|
|
96
|
+
// the value a spin key / button steps from: a pending valid draft wins,
|
|
97
|
+
// an invalid one is discarded
|
|
98
|
+
def resolveBase(ed: bool, dr: str, v: int): int =
|
|
99
|
+
if (and(ed, draftInRange(dr))) draftNumber(dr) else v
|
|
100
|
+
def resolveResult(ed: bool, dr: str): str =
|
|
101
|
+
if (ed) (if (draftInRange(dr)) "committed" else "reverted") else "none"
|
|
102
|
+
|
|
103
|
+
// --- request / commit (map ticket rt1p) ----------------------------------
|
|
104
|
+
def moved(before: int, after: int): int = if (before == after) 0 else 1
|
|
105
|
+
def wants(f: bool, current: int, want: int): bool = and(f, want != current)
|
|
106
|
+
// controlled host that keeps the old value: the request is rejected
|
|
107
|
+
def holds: bool = and(controlled, not(accepting))
|
|
108
|
+
// controlled host that writes the intent back through the value prop
|
|
109
|
+
def follows: bool = and(controlled, accepting)
|
|
110
|
+
def verdict: str = if (holds) "rejected" else "accepted"
|
|
111
|
+
def landed(h: bool, current: int, want: int): int = if (h) current else want
|
|
112
|
+
def propAfter(f: bool, p: int, want: int): int = if (f) want else p
|
|
113
|
+
|
|
114
|
+
// --- ARIA contract, derived from the state -----------------------------
|
|
115
|
+
def role: str = "spinbutton"
|
|
116
|
+
def ariaValueNow(v: int): int = v
|
|
117
|
+
def ariaValueMin: int = minValue
|
|
118
|
+
def ariaValueMax: int = maxValue
|
|
119
|
+
// aria-invalid: the pending draft is not an in-range number
|
|
120
|
+
def ariaInvalid(ed: bool, dr: str): bool = and(ed, not(draftInRange(dr)))
|
|
121
|
+
def inputTabindex: int = 0
|
|
122
|
+
def incrementTabindex: int = -1
|
|
123
|
+
def decrementTabindex: int = -1
|
|
124
|
+
// buttons are inert at a bound only when clamping
|
|
125
|
+
def incrementDisabled(v: int, w: bool): bool = and(not(w), v == maxValue)
|
|
126
|
+
def decrementDisabled(v: int, w: bool): bool = and(not(w), v == minValue)
|
|
127
|
+
|
|
128
|
+
action init = all {
|
|
129
|
+
value' = 5,
|
|
130
|
+
focused' = false,
|
|
131
|
+
editing' = false,
|
|
132
|
+
draft' = "",
|
|
133
|
+
wrap' = false,
|
|
134
|
+
result' = "none",
|
|
135
|
+
controlled' = false,
|
|
136
|
+
accepting' = false,
|
|
137
|
+
valueProp' = 5,
|
|
138
|
+
requested' = 5,
|
|
139
|
+
outcome' = "none",
|
|
140
|
+
base' = 5,
|
|
141
|
+
requests' = 0
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
action focus = all {
|
|
145
|
+
value' = value,
|
|
146
|
+
focused' = true,
|
|
147
|
+
editing' = editing,
|
|
148
|
+
draft' = draft,
|
|
149
|
+
wrap' = wrap,
|
|
150
|
+
result' = "focused",
|
|
151
|
+
controlled' = controlled,
|
|
152
|
+
accepting' = accepting,
|
|
153
|
+
valueProp' = valueProp,
|
|
154
|
+
requested' = requested,
|
|
155
|
+
outcome' = outcome,
|
|
156
|
+
base' = base,
|
|
157
|
+
requests' = requests
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// request: blur ends editing and commits a valid draft / reverts an invalid one
|
|
161
|
+
action blur = all {
|
|
162
|
+
value' = landed(holds, value, resolveBase(editing, draft, value)),
|
|
163
|
+
focused' = false,
|
|
164
|
+
editing' = false,
|
|
165
|
+
draft' = "",
|
|
166
|
+
wrap' = wrap,
|
|
167
|
+
result' = if (editing) resolveResult(editing, draft) else "blurred",
|
|
168
|
+
controlled' = controlled,
|
|
169
|
+
accepting' = accepting,
|
|
170
|
+
valueProp' = if (wants(true, value, resolveBase(editing, draft, value))) propAfter(follows, valueProp, resolveBase(editing, draft, value)) else valueProp,
|
|
171
|
+
requested' = if (wants(true, value, resolveBase(editing, draft, value))) resolveBase(editing, draft, value) else requested,
|
|
172
|
+
outcome' = if (wants(true, value, resolveBase(editing, draft, value))) verdict else outcome,
|
|
173
|
+
base' = if (wants(true, value, resolveBase(editing, draft, value))) value else base,
|
|
174
|
+
requests' = if (wants(true, value, resolveBase(editing, draft, value))) requests + 1 else requests
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// request: keyboard, only acts while the text field has focus
|
|
178
|
+
action arrowUp = all {
|
|
179
|
+
value' = if (focused) landed(holds, value, up(resolveBase(editing, draft, value), smallStep, wrap)) else value,
|
|
180
|
+
focused' = focused,
|
|
181
|
+
editing' = false,
|
|
182
|
+
draft' = "",
|
|
183
|
+
wrap' = wrap,
|
|
184
|
+
result' = if (focused) upResult(resolveBase(editing, draft, value), smallStep, wrap) else result,
|
|
185
|
+
controlled' = controlled,
|
|
186
|
+
accepting' = accepting,
|
|
187
|
+
valueProp' = if (wants(focused, value, up(resolveBase(editing, draft, value), smallStep, wrap))) propAfter(follows, valueProp, up(resolveBase(editing, draft, value), smallStep, wrap)) else valueProp,
|
|
188
|
+
requested' = if (wants(focused, value, up(resolveBase(editing, draft, value), smallStep, wrap))) up(resolveBase(editing, draft, value), smallStep, wrap) else requested,
|
|
189
|
+
outcome' = if (wants(focused, value, up(resolveBase(editing, draft, value), smallStep, wrap))) verdict else outcome,
|
|
190
|
+
base' = if (wants(focused, value, up(resolveBase(editing, draft, value), smallStep, wrap))) value else base,
|
|
191
|
+
requests' = if (wants(focused, value, up(resolveBase(editing, draft, value), smallStep, wrap))) requests + 1 else requests
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
action arrowDown = all {
|
|
195
|
+
value' = if (focused) landed(holds, value, down(resolveBase(editing, draft, value), smallStep, wrap)) else value,
|
|
196
|
+
focused' = focused,
|
|
197
|
+
editing' = false,
|
|
198
|
+
draft' = "",
|
|
199
|
+
wrap' = wrap,
|
|
200
|
+
result' = if (focused) downResult(resolveBase(editing, draft, value), smallStep, wrap) else result,
|
|
201
|
+
controlled' = controlled,
|
|
202
|
+
accepting' = accepting,
|
|
203
|
+
valueProp' = if (wants(focused, value, down(resolveBase(editing, draft, value), smallStep, wrap))) propAfter(follows, valueProp, down(resolveBase(editing, draft, value), smallStep, wrap)) else valueProp,
|
|
204
|
+
requested' = if (wants(focused, value, down(resolveBase(editing, draft, value), smallStep, wrap))) down(resolveBase(editing, draft, value), smallStep, wrap) else requested,
|
|
205
|
+
outcome' = if (wants(focused, value, down(resolveBase(editing, draft, value), smallStep, wrap))) verdict else outcome,
|
|
206
|
+
base' = if (wants(focused, value, down(resolveBase(editing, draft, value), smallStep, wrap))) value else base,
|
|
207
|
+
requests' = if (wants(focused, value, down(resolveBase(editing, draft, value), smallStep, wrap))) requests + 1 else requests
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
action pageUp = all {
|
|
211
|
+
value' = if (focused) landed(holds, value, up(resolveBase(editing, draft, value), largeStep, wrap)) else value,
|
|
212
|
+
focused' = focused,
|
|
213
|
+
editing' = false,
|
|
214
|
+
draft' = "",
|
|
215
|
+
wrap' = wrap,
|
|
216
|
+
result' = if (focused) upResult(resolveBase(editing, draft, value), largeStep, wrap) else result,
|
|
217
|
+
controlled' = controlled,
|
|
218
|
+
accepting' = accepting,
|
|
219
|
+
valueProp' = if (wants(focused, value, up(resolveBase(editing, draft, value), largeStep, wrap))) propAfter(follows, valueProp, up(resolveBase(editing, draft, value), largeStep, wrap)) else valueProp,
|
|
220
|
+
requested' = if (wants(focused, value, up(resolveBase(editing, draft, value), largeStep, wrap))) up(resolveBase(editing, draft, value), largeStep, wrap) else requested,
|
|
221
|
+
outcome' = if (wants(focused, value, up(resolveBase(editing, draft, value), largeStep, wrap))) verdict else outcome,
|
|
222
|
+
base' = if (wants(focused, value, up(resolveBase(editing, draft, value), largeStep, wrap))) value else base,
|
|
223
|
+
requests' = if (wants(focused, value, up(resolveBase(editing, draft, value), largeStep, wrap))) requests + 1 else requests
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
action pageDown = all {
|
|
227
|
+
value' = if (focused) landed(holds, value, down(resolveBase(editing, draft, value), largeStep, wrap)) else value,
|
|
228
|
+
focused' = focused,
|
|
229
|
+
editing' = false,
|
|
230
|
+
draft' = "",
|
|
231
|
+
wrap' = wrap,
|
|
232
|
+
result' = if (focused) downResult(resolveBase(editing, draft, value), largeStep, wrap) else result,
|
|
233
|
+
controlled' = controlled,
|
|
234
|
+
accepting' = accepting,
|
|
235
|
+
valueProp' = if (wants(focused, value, down(resolveBase(editing, draft, value), largeStep, wrap))) propAfter(follows, valueProp, down(resolveBase(editing, draft, value), largeStep, wrap)) else valueProp,
|
|
236
|
+
requested' = if (wants(focused, value, down(resolveBase(editing, draft, value), largeStep, wrap))) down(resolveBase(editing, draft, value), largeStep, wrap) else requested,
|
|
237
|
+
outcome' = if (wants(focused, value, down(resolveBase(editing, draft, value), largeStep, wrap))) verdict else outcome,
|
|
238
|
+
base' = if (wants(focused, value, down(resolveBase(editing, draft, value), largeStep, wrap))) value else base,
|
|
239
|
+
requests' = if (wants(focused, value, down(resolveBase(editing, draft, value), largeStep, wrap))) requests + 1 else requests
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
action home = all {
|
|
243
|
+
value' = if (focused) landed(holds, value, minValue) else value,
|
|
244
|
+
focused' = focused,
|
|
245
|
+
editing' = false,
|
|
246
|
+
draft' = "",
|
|
247
|
+
wrap' = wrap,
|
|
248
|
+
result' = if (focused) "jumped" else result,
|
|
249
|
+
controlled' = controlled,
|
|
250
|
+
accepting' = accepting,
|
|
251
|
+
valueProp' = if (wants(focused, value, minValue)) propAfter(follows, valueProp, minValue) else valueProp,
|
|
252
|
+
requested' = if (wants(focused, value, minValue)) minValue else requested,
|
|
253
|
+
outcome' = if (wants(focused, value, minValue)) verdict else outcome,
|
|
254
|
+
base' = if (wants(focused, value, minValue)) value else base,
|
|
255
|
+
requests' = if (wants(focused, value, minValue)) requests + 1 else requests
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
action end = all {
|
|
259
|
+
value' = if (focused) landed(holds, value, maxValue) else value,
|
|
260
|
+
focused' = focused,
|
|
261
|
+
editing' = false,
|
|
262
|
+
draft' = "",
|
|
263
|
+
wrap' = wrap,
|
|
264
|
+
result' = if (focused) "jumped" else result,
|
|
265
|
+
controlled' = controlled,
|
|
266
|
+
accepting' = accepting,
|
|
267
|
+
valueProp' = if (wants(focused, value, maxValue)) propAfter(follows, valueProp, maxValue) else valueProp,
|
|
268
|
+
requested' = if (wants(focused, value, maxValue)) maxValue else requested,
|
|
269
|
+
outcome' = if (wants(focused, value, maxValue)) verdict else outcome,
|
|
270
|
+
base' = if (wants(focused, value, maxValue)) value else base,
|
|
271
|
+
requests' = if (wants(focused, value, maxValue)) requests + 1 else requests
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// request: pointer, the buttons are out of the tab order, work with or
|
|
275
|
+
// without focus, and leave focus on the text field
|
|
276
|
+
action clickIncrement = all {
|
|
277
|
+
value' = landed(holds, value, up(resolveBase(editing, draft, value), smallStep, wrap)),
|
|
278
|
+
focused' = true,
|
|
279
|
+
editing' = false,
|
|
280
|
+
draft' = "",
|
|
281
|
+
wrap' = wrap,
|
|
282
|
+
result' = upResult(resolveBase(editing, draft, value), smallStep, wrap),
|
|
283
|
+
controlled' = controlled,
|
|
284
|
+
accepting' = accepting,
|
|
285
|
+
valueProp' = if (wants(true, value, up(resolveBase(editing, draft, value), smallStep, wrap))) propAfter(follows, valueProp, up(resolveBase(editing, draft, value), smallStep, wrap)) else valueProp,
|
|
286
|
+
requested' = if (wants(true, value, up(resolveBase(editing, draft, value), smallStep, wrap))) up(resolveBase(editing, draft, value), smallStep, wrap) else requested,
|
|
287
|
+
outcome' = if (wants(true, value, up(resolveBase(editing, draft, value), smallStep, wrap))) verdict else outcome,
|
|
288
|
+
base' = if (wants(true, value, up(resolveBase(editing, draft, value), smallStep, wrap))) value else base,
|
|
289
|
+
requests' = if (wants(true, value, up(resolveBase(editing, draft, value), smallStep, wrap))) requests + 1 else requests
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
action clickDecrement = all {
|
|
293
|
+
value' = landed(holds, value, down(resolveBase(editing, draft, value), smallStep, wrap)),
|
|
294
|
+
focused' = true,
|
|
295
|
+
editing' = false,
|
|
296
|
+
draft' = "",
|
|
297
|
+
wrap' = wrap,
|
|
298
|
+
result' = downResult(resolveBase(editing, draft, value), smallStep, wrap),
|
|
299
|
+
controlled' = controlled,
|
|
300
|
+
accepting' = accepting,
|
|
301
|
+
valueProp' = if (wants(true, value, down(resolveBase(editing, draft, value), smallStep, wrap))) propAfter(follows, valueProp, down(resolveBase(editing, draft, value), smallStep, wrap)) else valueProp,
|
|
302
|
+
requested' = if (wants(true, value, down(resolveBase(editing, draft, value), smallStep, wrap))) down(resolveBase(editing, draft, value), smallStep, wrap) else requested,
|
|
303
|
+
outcome' = if (wants(true, value, down(resolveBase(editing, draft, value), smallStep, wrap))) verdict else outcome,
|
|
304
|
+
base' = if (wants(true, value, down(resolveBase(editing, draft, value), smallStep, wrap))) value else base,
|
|
305
|
+
requests' = if (wants(true, value, down(resolveBase(editing, draft, value), smallStep, wrap))) requests + 1 else requests
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// text editing: the draft is replaced by a sample text; value is untouched
|
|
309
|
+
action typeText = {
|
|
310
|
+
nondet text = Set("", "5", "12", "13", "0", "abc").oneOf()
|
|
311
|
+
all {
|
|
312
|
+
value' = value,
|
|
313
|
+
focused' = focused,
|
|
314
|
+
editing' = if (focused) true else editing,
|
|
315
|
+
draft' = if (focused) text else draft,
|
|
316
|
+
wrap' = wrap,
|
|
317
|
+
result' = if (focused) "typed" else result,
|
|
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
|
+
// request: Enter commits a valid draft, reverts an invalid one; stay focused
|
|
329
|
+
action commit = all {
|
|
330
|
+
value' = if (focused) landed(holds, value, resolveBase(editing, draft, value)) else value,
|
|
331
|
+
focused' = focused,
|
|
332
|
+
editing' = if (focused) false else editing,
|
|
333
|
+
draft' = if (focused) "" else draft,
|
|
334
|
+
wrap' = wrap,
|
|
335
|
+
result' = if (focused) resolveResult(editing, draft) else result,
|
|
336
|
+
controlled' = controlled,
|
|
337
|
+
accepting' = accepting,
|
|
338
|
+
valueProp' = if (wants(focused, value, resolveBase(editing, draft, value))) propAfter(follows, valueProp, resolveBase(editing, draft, value)) else valueProp,
|
|
339
|
+
requested' = if (wants(focused, value, resolveBase(editing, draft, value))) resolveBase(editing, draft, value) else requested,
|
|
340
|
+
outcome' = if (wants(focused, value, resolveBase(editing, draft, value))) verdict else outcome,
|
|
341
|
+
base' = if (wants(focused, value, resolveBase(editing, draft, value))) value else base,
|
|
342
|
+
requests' = if (wants(focused, value, resolveBase(editing, draft, value))) requests + 1 else requests
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Escape: drop the draft, keep the committed value (not a request)
|
|
346
|
+
action revert = all {
|
|
347
|
+
value' = value,
|
|
348
|
+
focused' = focused,
|
|
349
|
+
editing' = false,
|
|
350
|
+
draft' = "",
|
|
351
|
+
wrap' = wrap,
|
|
352
|
+
result' = if (editing) "reverted" else "none",
|
|
353
|
+
controlled' = controlled,
|
|
354
|
+
accepting' = accepting,
|
|
355
|
+
valueProp' = valueProp,
|
|
356
|
+
requested' = requested,
|
|
357
|
+
outcome' = outcome,
|
|
358
|
+
base' = base,
|
|
359
|
+
requests' = requests
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
action setWrap = {
|
|
363
|
+
nondet mode = Set(true, false).oneOf()
|
|
364
|
+
all {
|
|
365
|
+
value' = value,
|
|
366
|
+
focused' = focused,
|
|
367
|
+
editing' = editing,
|
|
368
|
+
draft' = draft,
|
|
369
|
+
wrap' = mode,
|
|
370
|
+
result' = "mode",
|
|
371
|
+
controlled' = controlled,
|
|
372
|
+
accepting' = accepting,
|
|
373
|
+
valueProp' = valueProp,
|
|
374
|
+
requested' = requested,
|
|
375
|
+
outcome' = outcome,
|
|
376
|
+
base' = base,
|
|
377
|
+
requests' = requests
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// commit: the host writes the controlled value (a no-op while uncontrolled)
|
|
382
|
+
action setValue = {
|
|
383
|
+
nondet v = Set(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12).oneOf()
|
|
384
|
+
all {
|
|
385
|
+
value' = if (controlled) v else value,
|
|
386
|
+
focused' = focused,
|
|
387
|
+
editing' = editing,
|
|
388
|
+
draft' = draft,
|
|
389
|
+
wrap' = wrap,
|
|
390
|
+
result' = result,
|
|
391
|
+
controlled' = controlled,
|
|
392
|
+
accepting' = accepting,
|
|
393
|
+
valueProp' = if (controlled) v else valueProp,
|
|
394
|
+
requested' = requested,
|
|
395
|
+
outcome' = if (controlled) "none" else outcome,
|
|
396
|
+
base' = base,
|
|
397
|
+
requests' = requests
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// commit: the host flips its own copy of the value between the range ends
|
|
402
|
+
// (an outside two-state control)
|
|
403
|
+
action hostToggle = all {
|
|
404
|
+
value' = if (controlled) (if (valueProp == maxValue) minValue else maxValue) else value,
|
|
405
|
+
focused' = focused,
|
|
406
|
+
editing' = editing,
|
|
407
|
+
draft' = draft,
|
|
408
|
+
wrap' = wrap,
|
|
409
|
+
result' = result,
|
|
410
|
+
controlled' = controlled,
|
|
411
|
+
accepting' = accepting,
|
|
412
|
+
valueProp' = if (controlled) (if (valueProp == maxValue) minValue else maxValue) else valueProp,
|
|
413
|
+
requested' = requested,
|
|
414
|
+
outcome' = if (controlled) "none" else outcome,
|
|
415
|
+
base' = base,
|
|
416
|
+
requests' = requests
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// configuration: the host starts owning the value, adopting the current one
|
|
420
|
+
action control = all {
|
|
421
|
+
value' = value,
|
|
422
|
+
focused' = focused,
|
|
423
|
+
editing' = editing,
|
|
424
|
+
draft' = draft,
|
|
425
|
+
wrap' = wrap,
|
|
426
|
+
result' = result,
|
|
427
|
+
controlled' = true,
|
|
428
|
+
accepting' = accepting,
|
|
429
|
+
valueProp' = value,
|
|
430
|
+
requested' = requested,
|
|
431
|
+
outcome' = outcome,
|
|
432
|
+
base' = base,
|
|
433
|
+
requests' = requests
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// configuration: the controlled host answers every request with its intent
|
|
437
|
+
action acceptRequests = all {
|
|
438
|
+
value' = value,
|
|
439
|
+
focused' = focused,
|
|
440
|
+
editing' = editing,
|
|
441
|
+
draft' = draft,
|
|
442
|
+
wrap' = wrap,
|
|
443
|
+
result' = result,
|
|
444
|
+
controlled' = controlled,
|
|
445
|
+
accepting' = true,
|
|
446
|
+
valueProp' = valueProp,
|
|
447
|
+
requested' = requested,
|
|
448
|
+
outcome' = outcome,
|
|
449
|
+
base' = base,
|
|
450
|
+
requests' = requests
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
action step = any {
|
|
454
|
+
init, focus, blur, arrowUp, arrowDown, pageUp, pageDown, home, end,
|
|
455
|
+
clickIncrement, clickDecrement, typeText, commit, revert, setWrap,
|
|
456
|
+
setValue, hostToggle, control, acceptRequests
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// --- invariants --------------------------------------------------------
|
|
460
|
+
// the committed value is always inside [min, max]; invalid text never lands in it
|
|
461
|
+
val ValueInRange: bool = and(value >= minValue, value <= maxValue)
|
|
462
|
+
// aria-valuenow lies within aria-valuemin..aria-valuemax
|
|
463
|
+
val AriaNowWithinBounds: bool =
|
|
464
|
+
and(ariaValueMin <= ariaValueNow(value), ariaValueNow(value) <= ariaValueMax)
|
|
465
|
+
// the derived ARIA bounds are the range constants
|
|
466
|
+
val AriaBoundsMatchRange: bool =
|
|
467
|
+
and(ariaValueMin == minValue, ariaValueMax == maxValue)
|
|
468
|
+
// the text field is the one Tab stop; the buttons never are
|
|
469
|
+
val ButtonsOutOfTabOrder: bool =
|
|
470
|
+
and(inputTabindex == 0, and(incrementTabindex == -1, decrementTabindex == -1))
|
|
471
|
+
// a draft exists exactly while editing
|
|
472
|
+
val DraftOnlyWhileEditing: bool = or(editing, draft == "")
|
|
473
|
+
// typing needs focus, and blur/Escape/Enter end it
|
|
474
|
+
val EditingImpliesFocus: bool = or(not(editing), focused)
|
|
475
|
+
// aria-invalid is only ever a property of the pending draft
|
|
476
|
+
val InvalidImpliesEditing: bool = or(not(ariaInvalid(editing, draft)), editing)
|
|
477
|
+
// a bound hit in clamp mode never reports a wrap, and vice versa
|
|
478
|
+
val WrapResultMatchesMode: bool =
|
|
479
|
+
and(or(not(result == "wrapped"), wrap), or(not(result == "clamped"), not(wrap)))
|
|
480
|
+
// disabled buttons are only offered when clamping at that exact bound
|
|
481
|
+
val DisabledOnlyAtBound: bool =
|
|
482
|
+
and(
|
|
483
|
+
or(not(incrementDisabled(value, wrap)), and(not(wrap), value == maxValue)),
|
|
484
|
+
or(not(decrementDisabled(value, wrap)), and(not(wrap), value == minValue))
|
|
485
|
+
)
|
|
486
|
+
// callbacks only ever count up from zero
|
|
487
|
+
val RequestsNonNegative: bool = requests >= 0
|
|
488
|
+
// controlled: only the value prop sets the state, so no event commits
|
|
489
|
+
val ControlledFollowsProp: bool = or(not(controlled), value == valueProp)
|
|
490
|
+
// a rejected request leaves the state as it found it
|
|
491
|
+
val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), value == base)
|
|
492
|
+
// an accepted request committed exactly its intent
|
|
493
|
+
val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), value == requested)
|
|
494
|
+
}
|