@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,732 @@
|
|
|
1
|
+
// WAI-ARIA APG — Carousel (Slide Show or Image Rotator) pattern.
|
|
2
|
+
//
|
|
3
|
+
// Modelled (https://www.w3.org/WAI/ARIA/apg/patterns/carousel/):
|
|
4
|
+
// - "About This Pattern": prev/next controls; optional slide picker; for an
|
|
5
|
+
// auto-rotating carousel a rotation control, stop on keyboard focus, stop
|
|
6
|
+
// while the mouse hovers.
|
|
7
|
+
// - "Keyboard Interaction": rotation stops when any element receives focus
|
|
8
|
+
// and does not resume unless the rotation control is activated; Tab /
|
|
9
|
+
// Shift+Tab walk the page tab sequence with the rotation control FIRST;
|
|
10
|
+
// buttons activate with Enter/Space without moving focus.
|
|
11
|
+
// - "WAI-ARIA Roles, States, and Properties": basic + tabbed variants; the
|
|
12
|
+
// slide wrapper's aria-live is "off" while rotating, "polite" otherwise;
|
|
13
|
+
// the rotation control label flips between Stop/Start and carries no
|
|
14
|
+
// aria-pressed; slides are group (basic) or tabpanel (tabbed); roving
|
|
15
|
+
// tabindex (only the selected tab is in the tab sequence).
|
|
16
|
+
// - Examples (…/examples/carousel-1-prev-next/ and …/carousel-2-tablist/):
|
|
17
|
+
// hover pause resumes on leave; an explicit Start ignores focus/hover until
|
|
18
|
+
// Stop; reduced motion / "pause on load" starts paused; "disable
|
|
19
|
+
// auto-rotation" removes the control; tabs use Left/Right (wrapping), Home,
|
|
20
|
+
// End with automatic activation.
|
|
21
|
+
//
|
|
22
|
+
// Configuration: props are events (set_slide_count, set_tabbed,
|
|
23
|
+
// set_auto_rotate, set_sticky_focus_pause, set_reduced_motion), so a Solid
|
|
24
|
+
// component can be driven through every variant. `stickyFocusPause` encodes the
|
|
25
|
+
// pattern text (true) vs the basic example (false: rotation resumes on blur).
|
|
26
|
+
//
|
|
27
|
+
// Position encoding for `focus`: 0 none, 1 rotation control, 2 previous,
|
|
28
|
+
// 3 next, 4..6 the tabs, 7 a link inside the slide.
|
|
29
|
+
//
|
|
30
|
+
// Request vs commit (map ticket rt1p). The shown slide is the value. Every
|
|
31
|
+
// event a user sees as a slide change — `tick`, `click_prev`, `click_next`,
|
|
32
|
+
// `click_tab`, the tab keys (`key_arrow_*`, `key_home`, `key_end`) and
|
|
33
|
+
// `key_activate` on a prev/next control — is a *request* carrying an intent
|
|
34
|
+
// (`requested`); it fires onIndexChange (`requests` counts the calls) when the
|
|
35
|
+
// intent differs from the current `current`, and then:
|
|
36
|
+
// uncontrolled the request is accepted: the slide commits the intent
|
|
37
|
+
// controlled, holds the host ignores it ("rejected"): the slide is untouched
|
|
38
|
+
// controlled, accepts the host answers with the same value (`accepting`): the
|
|
39
|
+
// slide commits through the controlled value, not the event
|
|
40
|
+
// `valueProp` is the only thing that changes the slide while `controlled`;
|
|
41
|
+
// `setValue` is the host writing it. `control` / `acceptRequests` are
|
|
42
|
+
// configuration events (init is literal-only). DOM focus still moves with the
|
|
43
|
+
// request (roving focus / tab walking), independent of the value. Invariants
|
|
44
|
+
// for the model: ControlledFollowsProp, RejectedRequestUntouched,
|
|
45
|
+
// AcceptedRequestCommits.
|
|
46
|
+
//
|
|
47
|
+
// ARIA contract (derived defs, evaluated by the invariants below):
|
|
48
|
+
// ariaLive(rotating) -> aria-live on the slide wrapper
|
|
49
|
+
// rotationLabel(rotating) -> accessible name of the rotation control
|
|
50
|
+
// slideRole(tabbed) -> role of each slide container
|
|
51
|
+
// slideHidden / tabSelected / tabStop -> per-slide hidden, aria-selected, and
|
|
52
|
+
// tabindex=0 (a non-stop tab has tabindex=-1; negatives avoided in the spec)
|
|
53
|
+
// The rotation control is rendered iff autoRotate.
|
|
54
|
+
//
|
|
55
|
+
// Deliberate omissions:
|
|
56
|
+
// - Static attributes (role region|group + aria-roledescription="carousel" +
|
|
57
|
+
// label on the container, aria-roledescription="slide" and "N of M" labels,
|
|
58
|
+
// aria-atomic="false", tablist label, aria-controls) are constants, not state.
|
|
59
|
+
// - Discrepancy: the pattern text says tabpanel slides have NO
|
|
60
|
+
// aria-roledescription; the tabbed example sets it to "slide". Static, so
|
|
61
|
+
// left to the implementation (follow the example).
|
|
62
|
+
// - Grouped variant (picker buttons with aria-disabled) is not modelled.
|
|
63
|
+
// - Pointer clicks on controls do not move focus (Safari); focus_control covers
|
|
64
|
+
// click-focus. Timer duration and slide transition animation are not modelled.
|
|
65
|
+
// - Slide content is opaque; focus inside it is the single position 7.
|
|
66
|
+
// - Re-enabling autoRotate leaves rotation paused (user must Start).
|
|
67
|
+
//
|
|
68
|
+
// Generator notes: the evaluator resolves def-calls but guards take their vars
|
|
69
|
+
// as parameters; no action preconditions, so guards live in the assignment RHS.
|
|
70
|
+
module apg_carousel {
|
|
71
|
+
var slides: int // prop: slide count (1..3)
|
|
72
|
+
var current: int // index of the displayed slide (0-based)
|
|
73
|
+
var tabbed: bool // prop: tabbed slide picker rendered (tablist/tab/tabpanel)
|
|
74
|
+
var autoRotate: bool // prop: auto-rotation feature enabled (false = no rotation control rendered)
|
|
75
|
+
var stickyFocusPause: bool // prop: focus entry stops rotation until the control restarts it (pattern text); false = resume on blur (basic example)
|
|
76
|
+
var rotate: bool // user intent: rotation wanted (page-load default true; cleared by Stop, reduced motion, sticky focus entry)
|
|
77
|
+
var forced: bool // user explicitly started rotation: focus/hover are ignored until Stop
|
|
78
|
+
var focus: int // focus position: 0 none, 1 rotation, 2 prev, 3 next, 4..6 tab 0..2, 7 link inside slide
|
|
79
|
+
var hover: bool // pointer is over the carousel
|
|
80
|
+
var controlled: bool // the host owns the shown slide (`value` prop present)
|
|
81
|
+
var accepting: bool // controlled host that answers every request with its intent
|
|
82
|
+
var valueProp: int // the controlled value prop (meaningful while controlled)
|
|
83
|
+
var requested: int // intent of the last request
|
|
84
|
+
var requests: int // number of requests that fired onIndexChange
|
|
85
|
+
var outcome: str // "none" | "accepted" | "rejected"
|
|
86
|
+
var base: int // slide when the last request was received
|
|
87
|
+
|
|
88
|
+
def isTab(f: int): bool = and(f >= 4, f <= 6)
|
|
89
|
+
def prevIdx(i: int, n: int): int = if (i <= 0) n - 1 else i - 1
|
|
90
|
+
def nextIdx(i: int, n: int): int = if (i + 1 >= n) 0 else i + 1
|
|
91
|
+
def clampIdx(i: int, n: int): int = if (i >= n) n - 1 else i
|
|
92
|
+
|
|
93
|
+
// rotation actually runs: feature on, user wants it, and either the user
|
|
94
|
+
// explicitly started it or neither focus nor hover is inside the carousel
|
|
95
|
+
def rotating(auto: bool, rot: bool, frc: bool, f: int, hov: bool): bool =
|
|
96
|
+
and(and(auto, rot), or(frc, and(f == 0, not(hov))))
|
|
97
|
+
|
|
98
|
+
// focus moving from outside to inside stops rotation under the sticky policy
|
|
99
|
+
def stopsOnEnter(sticky: bool, f0: int, f1: int): bool =
|
|
100
|
+
and(sticky, and(f0 == 0, f1 != 0))
|
|
101
|
+
|
|
102
|
+
// the rotation control: Stop when rotating, else Start; both flags follow
|
|
103
|
+
def toggled(auto: bool, going: bool, cur: bool): bool = if (auto) not(going) else cur
|
|
104
|
+
|
|
105
|
+
// Tab / Shift+Tab; from the last / first stop focus leaves the carousel (0)
|
|
106
|
+
def tabFwd(f: int, auto: bool, tb: bool, cur: int): int =
|
|
107
|
+
if (f == 0) (if (auto) 1 else 2)
|
|
108
|
+
else if (f == 1) 2
|
|
109
|
+
else if (f == 2) 3
|
|
110
|
+
else if (f == 3) (if (tb) 4 + cur else 7)
|
|
111
|
+
else if (f == 7) 0
|
|
112
|
+
else 7
|
|
113
|
+
def tabBack(f: int, auto: bool, tb: bool, cur: int): int =
|
|
114
|
+
if (f == 0) 7
|
|
115
|
+
else if (f == 7) (if (tb) 4 + cur else 3)
|
|
116
|
+
else if (isTab(f)) 3
|
|
117
|
+
else if (f == 3) 2
|
|
118
|
+
else if (f == 2) (if (auto) 1 else 0)
|
|
119
|
+
else 0
|
|
120
|
+
|
|
121
|
+
// direct focus: unavailable targets leave focus where it is
|
|
122
|
+
def focusTarget(t: int, f: int, auto: bool, tb: bool, cur: int): int =
|
|
123
|
+
if (t == 1) (if (auto) 1 else f)
|
|
124
|
+
else if (t == 4) (if (tb) 4 + cur else f)
|
|
125
|
+
else t
|
|
126
|
+
|
|
127
|
+
// ---- request / commit (map ticket rt1p) ----------------------------------
|
|
128
|
+
def moved(before: int, after: int): int = if (before == after) 0 else 1
|
|
129
|
+
// the slide each request would show
|
|
130
|
+
def tickWant(auto: bool, rot: bool, frc: bool, f: int, hov: bool, cur: int, n: int): int =
|
|
131
|
+
if (rotating(auto, rot, frc, f, hov)) nextIdx(cur, n) else cur
|
|
132
|
+
def activateWant(f: int, cur: int, n: int): int =
|
|
133
|
+
if (f == 2) prevIdx(cur, n) else if (f == 3) nextIdx(cur, n) else cur
|
|
134
|
+
def arrowRightWant(f: int, cur: int, n: int): int =
|
|
135
|
+
if (isTab(f)) nextIdx(f - 4, n) else cur
|
|
136
|
+
def arrowLeftWant(f: int, cur: int, n: int): int =
|
|
137
|
+
if (isTab(f)) prevIdx(f - 4, n) else cur
|
|
138
|
+
def homeWant(f: int, cur: int): int = if (isTab(f)) 0 else cur
|
|
139
|
+
def endWant(f: int, cur: int, n: int): int = if (isTab(f)) n - 1 else cur
|
|
140
|
+
def tabClickWant(tb: bool, i: int, cur: int, n: int): int =
|
|
141
|
+
if (and(tb, i < n)) i else cur
|
|
142
|
+
// controlled host that keeps the old value: the request is rejected
|
|
143
|
+
def holds: bool = and(controlled, not(accepting))
|
|
144
|
+
// controlled host that writes the intent back through the value prop
|
|
145
|
+
def follows: bool = and(controlled, accepting)
|
|
146
|
+
def verdict: str = if (holds) "rejected" else "accepted"
|
|
147
|
+
// the slide after a request for `want`
|
|
148
|
+
def landed(h: bool, cur: int, want: int): int = if (h) cur else want
|
|
149
|
+
// the value prop after a request for `want`
|
|
150
|
+
def propAfter(f: bool, p: int, want: int): int = if (f) want else p
|
|
151
|
+
|
|
152
|
+
// ARIA contract
|
|
153
|
+
def ariaLive(going: bool): str = if (going) "off" else "polite"
|
|
154
|
+
def rotationLabel(going: bool): str =
|
|
155
|
+
if (going) "Stop automatic slide show" else "Start automatic slide show"
|
|
156
|
+
def slideRole(tb: bool): str = if (tb) "tabpanel" else "group"
|
|
157
|
+
def slideShown(i: int, n: int, cur: int): bool = and(i < n, i == cur)
|
|
158
|
+
def slideHidden(i: int, n: int, cur: int): bool = not(slideShown(i, n, cur))
|
|
159
|
+
def tabSelected(i: int, n: int, cur: int): bool = slideShown(i, n, cur)
|
|
160
|
+
def tabStop(i: int, n: int, cur: int): bool = tabSelected(i, n, cur)
|
|
161
|
+
def b2i(b: bool): int = if (b) 1 else 0
|
|
162
|
+
def shownCount(n: int, cur: int): int =
|
|
163
|
+
b2i(slideShown(0, n, cur)) + b2i(slideShown(1, n, cur)) + b2i(slideShown(2, n, cur))
|
|
164
|
+
def stopCount(n: int, cur: int): int =
|
|
165
|
+
b2i(tabStop(0, n, cur)) + b2i(tabStop(1, n, cur)) + b2i(tabStop(2, n, cur))
|
|
166
|
+
|
|
167
|
+
// page load: rotating, nothing focused or hovered, basic (untabbed) carousel
|
|
168
|
+
action init = all {
|
|
169
|
+
slides' = 3,
|
|
170
|
+
current' = 0,
|
|
171
|
+
tabbed' = false,
|
|
172
|
+
autoRotate' = true,
|
|
173
|
+
stickyFocusPause' = true,
|
|
174
|
+
rotate' = true,
|
|
175
|
+
forced' = false,
|
|
176
|
+
focus' = 0,
|
|
177
|
+
hover' = false,
|
|
178
|
+
controlled' = false,
|
|
179
|
+
accepting' = false,
|
|
180
|
+
valueProp' = 0,
|
|
181
|
+
requested' = 0,
|
|
182
|
+
requests' = 0,
|
|
183
|
+
outcome' = "none",
|
|
184
|
+
base' = 0
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// request: the rotation timer advances the slide only while rotating
|
|
188
|
+
action tick = all {
|
|
189
|
+
slides' = slides,
|
|
190
|
+
current' = landed(holds, current, tickWant(autoRotate, rotate, forced, focus, hover, current, slides)),
|
|
191
|
+
tabbed' = tabbed,
|
|
192
|
+
autoRotate' = autoRotate,
|
|
193
|
+
stickyFocusPause' = stickyFocusPause,
|
|
194
|
+
rotate' = rotate,
|
|
195
|
+
forced' = forced,
|
|
196
|
+
focus' = focus,
|
|
197
|
+
hover' = hover,
|
|
198
|
+
controlled' = controlled,
|
|
199
|
+
accepting' = accepting,
|
|
200
|
+
valueProp' = propAfter(follows, valueProp, tickWant(autoRotate, rotate, forced, focus, hover, current, slides)),
|
|
201
|
+
requested' = if (moved(current, tickWant(autoRotate, rotate, forced, focus, hover, current, slides)) == 1) tickWant(autoRotate, rotate, forced, focus, hover, current, slides) else requested,
|
|
202
|
+
requests' = requests + moved(current, tickWant(autoRotate, rotate, forced, focus, hover, current, slides)),
|
|
203
|
+
outcome' = if (moved(current, tickWant(autoRotate, rotate, forced, focus, hover, current, slides)) == 1) verdict else outcome,
|
|
204
|
+
base' = if (moved(current, tickWant(autoRotate, rotate, forced, focus, hover, current, slides)) == 1) current else base
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// mouse enters the carousel: rotation pauses (derived)
|
|
208
|
+
action pointer_enter = all {
|
|
209
|
+
slides' = slides,
|
|
210
|
+
current' = current,
|
|
211
|
+
tabbed' = tabbed,
|
|
212
|
+
autoRotate' = autoRotate,
|
|
213
|
+
stickyFocusPause' = stickyFocusPause,
|
|
214
|
+
rotate' = rotate,
|
|
215
|
+
forced' = forced,
|
|
216
|
+
focus' = focus,
|
|
217
|
+
hover' = true,
|
|
218
|
+
controlled' = controlled,
|
|
219
|
+
accepting' = accepting,
|
|
220
|
+
valueProp' = valueProp,
|
|
221
|
+
requested' = requested,
|
|
222
|
+
requests' = requests,
|
|
223
|
+
outcome' = outcome,
|
|
224
|
+
base' = base
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// mouse leaves the carousel
|
|
228
|
+
action pointer_leave = all {
|
|
229
|
+
slides' = slides,
|
|
230
|
+
current' = current,
|
|
231
|
+
tabbed' = tabbed,
|
|
232
|
+
autoRotate' = autoRotate,
|
|
233
|
+
stickyFocusPause' = stickyFocusPause,
|
|
234
|
+
rotate' = rotate,
|
|
235
|
+
forced' = forced,
|
|
236
|
+
focus' = focus,
|
|
237
|
+
hover' = false,
|
|
238
|
+
controlled' = controlled,
|
|
239
|
+
accepting' = accepting,
|
|
240
|
+
valueProp' = valueProp,
|
|
241
|
+
requested' = requested,
|
|
242
|
+
requests' = requests,
|
|
243
|
+
outcome' = outcome,
|
|
244
|
+
base' = base
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Tab: next stop in the tab sequence (rotation, prev, next, selected tab, slide link)
|
|
248
|
+
action key_tab = all {
|
|
249
|
+
slides' = slides,
|
|
250
|
+
current' = current,
|
|
251
|
+
tabbed' = tabbed,
|
|
252
|
+
autoRotate' = autoRotate,
|
|
253
|
+
stickyFocusPause' = stickyFocusPause,
|
|
254
|
+
rotate' = if (stopsOnEnter(stickyFocusPause, focus, tabFwd(focus, autoRotate, tabbed, current))) false else rotate,
|
|
255
|
+
forced' = if (stopsOnEnter(stickyFocusPause, focus, tabFwd(focus, autoRotate, tabbed, current))) false else forced,
|
|
256
|
+
focus' = tabFwd(focus, autoRotate, tabbed, current),
|
|
257
|
+
hover' = hover,
|
|
258
|
+
controlled' = controlled,
|
|
259
|
+
accepting' = accepting,
|
|
260
|
+
valueProp' = valueProp,
|
|
261
|
+
requested' = requested,
|
|
262
|
+
requests' = requests,
|
|
263
|
+
outcome' = outcome,
|
|
264
|
+
base' = base
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Shift+Tab: previous stop in the tab sequence
|
|
268
|
+
action key_shift_tab = all {
|
|
269
|
+
slides' = slides,
|
|
270
|
+
current' = current,
|
|
271
|
+
tabbed' = tabbed,
|
|
272
|
+
autoRotate' = autoRotate,
|
|
273
|
+
stickyFocusPause' = stickyFocusPause,
|
|
274
|
+
rotate' = if (stopsOnEnter(stickyFocusPause, focus, tabBack(focus, autoRotate, tabbed, current))) false else rotate,
|
|
275
|
+
forced' = if (stopsOnEnter(stickyFocusPause, focus, tabBack(focus, autoRotate, tabbed, current))) false else forced,
|
|
276
|
+
focus' = tabBack(focus, autoRotate, tabbed, current),
|
|
277
|
+
hover' = hover,
|
|
278
|
+
controlled' = controlled,
|
|
279
|
+
accepting' = accepting,
|
|
280
|
+
valueProp' = valueProp,
|
|
281
|
+
requested' = requested,
|
|
282
|
+
requests' = requests,
|
|
283
|
+
outcome' = outcome,
|
|
284
|
+
base' = base
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// focus lands directly on a control (click focus, AT virtual cursor, script); t: 1 rotation, 2 prev, 3 next, 4 selected tab, 7 slide link
|
|
288
|
+
action focus_control = {
|
|
289
|
+
nondet t = Set(1, 2, 3, 4, 7).oneOf()
|
|
290
|
+
all {
|
|
291
|
+
slides' = slides,
|
|
292
|
+
current' = current,
|
|
293
|
+
tabbed' = tabbed,
|
|
294
|
+
autoRotate' = autoRotate,
|
|
295
|
+
stickyFocusPause' = stickyFocusPause,
|
|
296
|
+
rotate' = if (stopsOnEnter(stickyFocusPause, focus, focusTarget(t, focus, autoRotate, tabbed, current))) false else rotate,
|
|
297
|
+
forced' = if (stopsOnEnter(stickyFocusPause, focus, focusTarget(t, focus, autoRotate, tabbed, current))) false else forced,
|
|
298
|
+
focus' = focusTarget(t, focus, autoRotate, tabbed, current),
|
|
299
|
+
hover' = hover,
|
|
300
|
+
controlled' = controlled,
|
|
301
|
+
accepting' = accepting,
|
|
302
|
+
valueProp' = valueProp,
|
|
303
|
+
requested' = requested,
|
|
304
|
+
requests' = requests,
|
|
305
|
+
outcome' = outcome,
|
|
306
|
+
base' = base
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// focus leaves the carousel
|
|
311
|
+
action blur_out = all {
|
|
312
|
+
slides' = slides,
|
|
313
|
+
current' = current,
|
|
314
|
+
tabbed' = tabbed,
|
|
315
|
+
autoRotate' = autoRotate,
|
|
316
|
+
stickyFocusPause' = stickyFocusPause,
|
|
317
|
+
rotate' = rotate,
|
|
318
|
+
forced' = forced,
|
|
319
|
+
focus' = 0,
|
|
320
|
+
hover' = hover,
|
|
321
|
+
controlled' = controlled,
|
|
322
|
+
accepting' = accepting,
|
|
323
|
+
valueProp' = valueProp,
|
|
324
|
+
requested' = requested,
|
|
325
|
+
requests' = requests,
|
|
326
|
+
outcome' = outcome,
|
|
327
|
+
base' = base
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// request: Enter/Space on the focused element toggles rotation, or goes
|
|
331
|
+
// prev/next (focus does not move); on a tab it is a no-op (already selected)
|
|
332
|
+
action key_activate = all {
|
|
333
|
+
slides' = slides,
|
|
334
|
+
current' = landed(holds, current, activateWant(focus, current, slides)),
|
|
335
|
+
tabbed' = tabbed,
|
|
336
|
+
autoRotate' = autoRotate,
|
|
337
|
+
stickyFocusPause' = stickyFocusPause,
|
|
338
|
+
rotate' = if (focus == 1) toggled(autoRotate, rotating(autoRotate, rotate, forced, focus, hover), rotate) else rotate,
|
|
339
|
+
forced' = if (focus == 1) toggled(autoRotate, rotating(autoRotate, rotate, forced, focus, hover), forced) else forced,
|
|
340
|
+
focus' = focus,
|
|
341
|
+
hover' = hover,
|
|
342
|
+
controlled' = controlled,
|
|
343
|
+
accepting' = accepting,
|
|
344
|
+
valueProp' = propAfter(follows, valueProp, activateWant(focus, current, slides)),
|
|
345
|
+
requested' = if (moved(current, activateWant(focus, current, slides)) == 1) activateWant(focus, current, slides) else requested,
|
|
346
|
+
requests' = requests + moved(current, activateWant(focus, current, slides)),
|
|
347
|
+
outcome' = if (moved(current, activateWant(focus, current, slides)) == 1) verdict else outcome,
|
|
348
|
+
base' = if (moved(current, activateWant(focus, current, slides)) == 1) current else base
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// request: Right Arrow on a tab focuses + shows the next tab (wraps)
|
|
352
|
+
action key_arrow_right = all {
|
|
353
|
+
slides' = slides,
|
|
354
|
+
current' = landed(holds, current, arrowRightWant(focus, current, slides)),
|
|
355
|
+
tabbed' = tabbed,
|
|
356
|
+
autoRotate' = autoRotate,
|
|
357
|
+
stickyFocusPause' = stickyFocusPause,
|
|
358
|
+
rotate' = rotate,
|
|
359
|
+
forced' = forced,
|
|
360
|
+
focus' = if (isTab(focus)) 4 + nextIdx(focus - 4, slides) else focus,
|
|
361
|
+
hover' = hover,
|
|
362
|
+
controlled' = controlled,
|
|
363
|
+
accepting' = accepting,
|
|
364
|
+
valueProp' = propAfter(follows, valueProp, arrowRightWant(focus, current, slides)),
|
|
365
|
+
requested' = if (moved(current, arrowRightWant(focus, current, slides)) == 1) arrowRightWant(focus, current, slides) else requested,
|
|
366
|
+
requests' = requests + moved(current, arrowRightWant(focus, current, slides)),
|
|
367
|
+
outcome' = if (moved(current, arrowRightWant(focus, current, slides)) == 1) verdict else outcome,
|
|
368
|
+
base' = if (moved(current, arrowRightWant(focus, current, slides)) == 1) current else base
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// request: Left Arrow on a tab focuses + shows the previous tab (wraps)
|
|
372
|
+
action key_arrow_left = all {
|
|
373
|
+
slides' = slides,
|
|
374
|
+
current' = landed(holds, current, arrowLeftWant(focus, current, slides)),
|
|
375
|
+
tabbed' = tabbed,
|
|
376
|
+
autoRotate' = autoRotate,
|
|
377
|
+
stickyFocusPause' = stickyFocusPause,
|
|
378
|
+
rotate' = rotate,
|
|
379
|
+
forced' = forced,
|
|
380
|
+
focus' = if (isTab(focus)) 4 + prevIdx(focus - 4, slides) else focus,
|
|
381
|
+
hover' = hover,
|
|
382
|
+
controlled' = controlled,
|
|
383
|
+
accepting' = accepting,
|
|
384
|
+
valueProp' = propAfter(follows, valueProp, arrowLeftWant(focus, current, slides)),
|
|
385
|
+
requested' = if (moved(current, arrowLeftWant(focus, current, slides)) == 1) arrowLeftWant(focus, current, slides) else requested,
|
|
386
|
+
requests' = requests + moved(current, arrowLeftWant(focus, current, slides)),
|
|
387
|
+
outcome' = if (moved(current, arrowLeftWant(focus, current, slides)) == 1) verdict else outcome,
|
|
388
|
+
base' = if (moved(current, arrowLeftWant(focus, current, slides)) == 1) current else base
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// request: Home on a tab shows the first slide
|
|
392
|
+
action key_home = all {
|
|
393
|
+
slides' = slides,
|
|
394
|
+
current' = landed(holds, current, homeWant(focus, current)),
|
|
395
|
+
tabbed' = tabbed,
|
|
396
|
+
autoRotate' = autoRotate,
|
|
397
|
+
stickyFocusPause' = stickyFocusPause,
|
|
398
|
+
rotate' = rotate,
|
|
399
|
+
forced' = forced,
|
|
400
|
+
focus' = if (isTab(focus)) 4 + 0 else focus,
|
|
401
|
+
hover' = hover,
|
|
402
|
+
controlled' = controlled,
|
|
403
|
+
accepting' = accepting,
|
|
404
|
+
valueProp' = propAfter(follows, valueProp, homeWant(focus, current)),
|
|
405
|
+
requested' = if (moved(current, homeWant(focus, current)) == 1) homeWant(focus, current) else requested,
|
|
406
|
+
requests' = requests + moved(current, homeWant(focus, current)),
|
|
407
|
+
outcome' = if (moved(current, homeWant(focus, current)) == 1) verdict else outcome,
|
|
408
|
+
base' = if (moved(current, homeWant(focus, current)) == 1) current else base
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// request: End on a tab shows the last slide
|
|
412
|
+
action key_end = all {
|
|
413
|
+
slides' = slides,
|
|
414
|
+
current' = landed(holds, current, endWant(focus, current, slides)),
|
|
415
|
+
tabbed' = tabbed,
|
|
416
|
+
autoRotate' = autoRotate,
|
|
417
|
+
stickyFocusPause' = stickyFocusPause,
|
|
418
|
+
rotate' = rotate,
|
|
419
|
+
forced' = forced,
|
|
420
|
+
focus' = if (isTab(focus)) 4 + slides - 1 else focus,
|
|
421
|
+
hover' = hover,
|
|
422
|
+
controlled' = controlled,
|
|
423
|
+
accepting' = accepting,
|
|
424
|
+
valueProp' = propAfter(follows, valueProp, endWant(focus, current, slides)),
|
|
425
|
+
requested' = if (moved(current, endWant(focus, current, slides)) == 1) endWant(focus, current, slides) else requested,
|
|
426
|
+
requests' = requests + moved(current, endWant(focus, current, slides)),
|
|
427
|
+
outcome' = if (moved(current, endWant(focus, current, slides)) == 1) verdict else outcome,
|
|
428
|
+
base' = if (moved(current, endWant(focus, current, slides)) == 1) current else base
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// pointer click on the rotation control (focus unchanged): Stop when rotating, else explicit Start (focus/hover ignored)
|
|
432
|
+
action click_rotation = all {
|
|
433
|
+
slides' = slides,
|
|
434
|
+
current' = current,
|
|
435
|
+
tabbed' = tabbed,
|
|
436
|
+
autoRotate' = autoRotate,
|
|
437
|
+
stickyFocusPause' = stickyFocusPause,
|
|
438
|
+
rotate' = toggled(autoRotate, rotating(autoRotate, rotate, forced, focus, hover), rotate),
|
|
439
|
+
forced' = toggled(autoRotate, rotating(autoRotate, rotate, forced, focus, hover), forced),
|
|
440
|
+
focus' = focus,
|
|
441
|
+
hover' = hover,
|
|
442
|
+
controlled' = controlled,
|
|
443
|
+
accepting' = accepting,
|
|
444
|
+
valueProp' = valueProp,
|
|
445
|
+
requested' = requested,
|
|
446
|
+
requests' = requests,
|
|
447
|
+
outcome' = outcome,
|
|
448
|
+
base' = base
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// request: pointer click on Previous slide
|
|
452
|
+
action click_prev = all {
|
|
453
|
+
slides' = slides,
|
|
454
|
+
current' = landed(holds, current, prevIdx(current, slides)),
|
|
455
|
+
tabbed' = tabbed,
|
|
456
|
+
autoRotate' = autoRotate,
|
|
457
|
+
stickyFocusPause' = stickyFocusPause,
|
|
458
|
+
rotate' = rotate,
|
|
459
|
+
forced' = forced,
|
|
460
|
+
focus' = focus,
|
|
461
|
+
hover' = hover,
|
|
462
|
+
controlled' = controlled,
|
|
463
|
+
accepting' = accepting,
|
|
464
|
+
valueProp' = propAfter(follows, valueProp, prevIdx(current, slides)),
|
|
465
|
+
requested' = if (moved(current, prevIdx(current, slides)) == 1) prevIdx(current, slides) else requested,
|
|
466
|
+
requests' = requests + moved(current, prevIdx(current, slides)),
|
|
467
|
+
outcome' = if (moved(current, prevIdx(current, slides)) == 1) verdict else outcome,
|
|
468
|
+
base' = if (moved(current, prevIdx(current, slides)) == 1) current else base
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// request: pointer click on Next slide
|
|
472
|
+
action click_next = all {
|
|
473
|
+
slides' = slides,
|
|
474
|
+
current' = landed(holds, current, nextIdx(current, slides)),
|
|
475
|
+
tabbed' = tabbed,
|
|
476
|
+
autoRotate' = autoRotate,
|
|
477
|
+
stickyFocusPause' = stickyFocusPause,
|
|
478
|
+
rotate' = rotate,
|
|
479
|
+
forced' = forced,
|
|
480
|
+
focus' = focus,
|
|
481
|
+
hover' = hover,
|
|
482
|
+
controlled' = controlled,
|
|
483
|
+
accepting' = accepting,
|
|
484
|
+
valueProp' = propAfter(follows, valueProp, nextIdx(current, slides)),
|
|
485
|
+
requested' = if (moved(current, nextIdx(current, slides)) == 1) nextIdx(current, slides) else requested,
|
|
486
|
+
requests' = requests + moved(current, nextIdx(current, slides)),
|
|
487
|
+
outcome' = if (moved(current, nextIdx(current, slides)) == 1) verdict else outcome,
|
|
488
|
+
base' = if (moved(current, nextIdx(current, slides)) == 1) current else base
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// request: pointer click on tab i (tabbed only) shows that slide
|
|
492
|
+
action click_tab = {
|
|
493
|
+
nondet i = Set(0, 1, 2).oneOf()
|
|
494
|
+
all {
|
|
495
|
+
slides' = slides,
|
|
496
|
+
current' = landed(holds, current, tabClickWant(tabbed, i, current, slides)),
|
|
497
|
+
tabbed' = tabbed,
|
|
498
|
+
autoRotate' = autoRotate,
|
|
499
|
+
stickyFocusPause' = stickyFocusPause,
|
|
500
|
+
rotate' = rotate,
|
|
501
|
+
forced' = forced,
|
|
502
|
+
focus' = focus,
|
|
503
|
+
hover' = hover,
|
|
504
|
+
controlled' = controlled,
|
|
505
|
+
accepting' = accepting,
|
|
506
|
+
valueProp' = propAfter(follows, valueProp, tabClickWant(tabbed, i, current, slides)),
|
|
507
|
+
requested' = if (moved(current, tabClickWant(tabbed, i, current, slides)) == 1) tabClickWant(tabbed, i, current, slides) else requested,
|
|
508
|
+
requests' = requests + moved(current, tabClickWant(tabbed, i, current, slides)),
|
|
509
|
+
outcome' = if (moved(current, tabClickWant(tabbed, i, current, slides)) == 1) verdict else outcome,
|
|
510
|
+
base' = if (moved(current, tabClickWant(tabbed, i, current, slides)) == 1) current else base
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// prop change: number of slides; current slide and tab focus clamp into range.
|
|
515
|
+
// While controlled the value prop clamps with it, and a pending request settles.
|
|
516
|
+
action set_slide_count = {
|
|
517
|
+
nondet m = Set(1, 2, 3).oneOf()
|
|
518
|
+
all {
|
|
519
|
+
slides' = m,
|
|
520
|
+
current' = clampIdx(current, m),
|
|
521
|
+
tabbed' = tabbed,
|
|
522
|
+
autoRotate' = autoRotate,
|
|
523
|
+
stickyFocusPause' = stickyFocusPause,
|
|
524
|
+
rotate' = rotate,
|
|
525
|
+
forced' = forced,
|
|
526
|
+
focus' = if (and(isTab(focus), focus - 4 >= m)) 4 + m - 1 else focus,
|
|
527
|
+
hover' = hover,
|
|
528
|
+
controlled' = controlled,
|
|
529
|
+
accepting' = accepting,
|
|
530
|
+
valueProp' = if (controlled) clampIdx(valueProp, m) else valueProp,
|
|
531
|
+
requested' = requested,
|
|
532
|
+
requests' = requests,
|
|
533
|
+
outcome' = "none",
|
|
534
|
+
base' = base
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// prop change: tabbed variant; the tablist unmounting drops tab focus
|
|
539
|
+
action set_tabbed = {
|
|
540
|
+
nondet on = Set(true, false).oneOf()
|
|
541
|
+
all {
|
|
542
|
+
slides' = slides,
|
|
543
|
+
current' = current,
|
|
544
|
+
tabbed' = on,
|
|
545
|
+
autoRotate' = autoRotate,
|
|
546
|
+
stickyFocusPause' = stickyFocusPause,
|
|
547
|
+
rotate' = rotate,
|
|
548
|
+
forced' = forced,
|
|
549
|
+
focus' = if (and(not(on), isTab(focus))) 0 else focus,
|
|
550
|
+
hover' = hover,
|
|
551
|
+
controlled' = controlled,
|
|
552
|
+
accepting' = accepting,
|
|
553
|
+
valueProp' = valueProp,
|
|
554
|
+
requested' = requested,
|
|
555
|
+
requests' = requests,
|
|
556
|
+
outcome' = outcome,
|
|
557
|
+
base' = base
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// prop change: auto-rotation feature; disabling stops rotation and unmounts the rotation control
|
|
562
|
+
action set_auto_rotate = {
|
|
563
|
+
nondet on = Set(true, false).oneOf()
|
|
564
|
+
all {
|
|
565
|
+
slides' = slides,
|
|
566
|
+
current' = current,
|
|
567
|
+
tabbed' = tabbed,
|
|
568
|
+
autoRotate' = on,
|
|
569
|
+
stickyFocusPause' = stickyFocusPause,
|
|
570
|
+
rotate' = if (on) rotate else false,
|
|
571
|
+
forced' = if (on) forced else false,
|
|
572
|
+
focus' = if (and(not(on), focus == 1)) 0 else focus,
|
|
573
|
+
hover' = hover,
|
|
574
|
+
controlled' = controlled,
|
|
575
|
+
accepting' = accepting,
|
|
576
|
+
valueProp' = valueProp,
|
|
577
|
+
requested' = requested,
|
|
578
|
+
requests' = requests,
|
|
579
|
+
outcome' = outcome,
|
|
580
|
+
base' = base
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// prop change: focus-pause policy; turning sticky on while focus is inside stops rotation unless user-started
|
|
585
|
+
action set_sticky_focus_pause = {
|
|
586
|
+
nondet on = Set(true, false).oneOf()
|
|
587
|
+
all {
|
|
588
|
+
slides' = slides,
|
|
589
|
+
current' = current,
|
|
590
|
+
tabbed' = tabbed,
|
|
591
|
+
autoRotate' = autoRotate,
|
|
592
|
+
stickyFocusPause' = on,
|
|
593
|
+
rotate' = if (and(on, and(focus != 0, not(forced)))) false else rotate,
|
|
594
|
+
forced' = forced,
|
|
595
|
+
focus' = focus,
|
|
596
|
+
hover' = hover,
|
|
597
|
+
controlled' = controlled,
|
|
598
|
+
accepting' = accepting,
|
|
599
|
+
valueProp' = valueProp,
|
|
600
|
+
requested' = requested,
|
|
601
|
+
requests' = requests,
|
|
602
|
+
outcome' = outcome,
|
|
603
|
+
base' = base
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// prefers-reduced-motion (or "pause on load"): rotation paused until the user starts it
|
|
608
|
+
action set_reduced_motion = all {
|
|
609
|
+
slides' = slides,
|
|
610
|
+
current' = current,
|
|
611
|
+
tabbed' = tabbed,
|
|
612
|
+
autoRotate' = autoRotate,
|
|
613
|
+
stickyFocusPause' = stickyFocusPause,
|
|
614
|
+
rotate' = false,
|
|
615
|
+
forced' = false,
|
|
616
|
+
focus' = focus,
|
|
617
|
+
hover' = hover,
|
|
618
|
+
controlled' = controlled,
|
|
619
|
+
accepting' = accepting,
|
|
620
|
+
valueProp' = valueProp,
|
|
621
|
+
requested' = requested,
|
|
622
|
+
requests' = requests,
|
|
623
|
+
outcome' = outcome,
|
|
624
|
+
base' = base
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// commit: the host writes the controlled slide (a no-op while uncontrolled)
|
|
628
|
+
action setValue = {
|
|
629
|
+
nondet v = Set(0, 1, 2).oneOf()
|
|
630
|
+
all {
|
|
631
|
+
slides' = slides,
|
|
632
|
+
current' = if (controlled) clampIdx(v, slides) else current,
|
|
633
|
+
tabbed' = tabbed,
|
|
634
|
+
autoRotate' = autoRotate,
|
|
635
|
+
stickyFocusPause' = stickyFocusPause,
|
|
636
|
+
rotate' = rotate,
|
|
637
|
+
forced' = forced,
|
|
638
|
+
focus' = focus,
|
|
639
|
+
hover' = hover,
|
|
640
|
+
controlled' = controlled,
|
|
641
|
+
accepting' = accepting,
|
|
642
|
+
valueProp' = if (controlled) clampIdx(v, slides) else valueProp,
|
|
643
|
+
requested' = requested,
|
|
644
|
+
requests' = requests,
|
|
645
|
+
outcome' = if (controlled) "none" else outcome,
|
|
646
|
+
base' = base
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// configuration: the host starts owning the slide, adopting the current one
|
|
651
|
+
action control = all {
|
|
652
|
+
slides' = slides,
|
|
653
|
+
current' = current,
|
|
654
|
+
tabbed' = tabbed,
|
|
655
|
+
autoRotate' = autoRotate,
|
|
656
|
+
stickyFocusPause' = stickyFocusPause,
|
|
657
|
+
rotate' = rotate,
|
|
658
|
+
forced' = forced,
|
|
659
|
+
focus' = focus,
|
|
660
|
+
hover' = hover,
|
|
661
|
+
controlled' = true,
|
|
662
|
+
accepting' = accepting,
|
|
663
|
+
valueProp' = current,
|
|
664
|
+
requested' = requested,
|
|
665
|
+
requests' = requests,
|
|
666
|
+
outcome' = outcome,
|
|
667
|
+
base' = base
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// configuration: the controlled host answers every request with its intent
|
|
671
|
+
action acceptRequests = all {
|
|
672
|
+
slides' = slides,
|
|
673
|
+
current' = current,
|
|
674
|
+
tabbed' = tabbed,
|
|
675
|
+
autoRotate' = autoRotate,
|
|
676
|
+
stickyFocusPause' = stickyFocusPause,
|
|
677
|
+
rotate' = rotate,
|
|
678
|
+
forced' = forced,
|
|
679
|
+
focus' = focus,
|
|
680
|
+
hover' = hover,
|
|
681
|
+
controlled' = controlled,
|
|
682
|
+
accepting' = true,
|
|
683
|
+
valueProp' = valueProp,
|
|
684
|
+
requested' = requested,
|
|
685
|
+
requests' = requests,
|
|
686
|
+
outcome' = outcome,
|
|
687
|
+
base' = base
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
action step = any { init, tick, pointer_enter, pointer_leave, key_tab, key_shift_tab, focus_control, blur_out, key_activate, key_arrow_right, key_arrow_left, key_home, key_end, click_rotation, click_prev, click_next, click_tab, set_slide_count, set_tabbed, set_auto_rotate, set_sticky_focus_pause, set_reduced_motion, setValue, control, acceptRequests }
|
|
691
|
+
|
|
692
|
+
// APG: never rotating while focus or hover is inside, unless the user
|
|
693
|
+
// explicitly started rotation
|
|
694
|
+
val NeverRotatingWhileEngaged: bool =
|
|
695
|
+
or(not(rotating(autoRotate, rotate, forced, focus, hover)), or(forced, and(focus == 0, not(hover))))
|
|
696
|
+
// an explicit Start implies rotation is wanted
|
|
697
|
+
val ForcedImpliesRotate: bool = or(not(forced), rotate)
|
|
698
|
+
// with the feature off nothing is wanted and nothing is forced
|
|
699
|
+
val DisabledIsIdle: bool = or(autoRotate, and(not(rotate), not(forced)))
|
|
700
|
+
// pattern text: focus inside stops rotation and it stays stopped (unless the user restarted it)
|
|
701
|
+
val StickyFocusStaysStopped: bool =
|
|
702
|
+
or(not(stickyFocusPause), or(focus == 0, or(forced, not(rotate))))
|
|
703
|
+
// aria-live is off exactly while rotating
|
|
704
|
+
val LiveOffIffRotating: bool = (ariaLive(rotating(autoRotate, rotate, forced, focus, hover)) == "off") == rotating(autoRotate, rotate, forced, focus, hover)
|
|
705
|
+
// the rotation label announces the action the button will perform
|
|
706
|
+
val RotationLabelMatchesAction: bool =
|
|
707
|
+
(rotationLabel(rotating(autoRotate, rotate, forced, focus, hover)) == "Stop automatic slide show") == rotating(autoRotate, rotate, forced, focus, hover)
|
|
708
|
+
// the rotation control is the first stop in the tab sequence
|
|
709
|
+
val RotationControlFirst: bool =
|
|
710
|
+
or(not(autoRotate), tabFwd(0, autoRotate, tabbed, current) == 1)
|
|
711
|
+
// slide/tab bookkeeping
|
|
712
|
+
val CurrentInRange: bool = and(current >= 0, current < slides)
|
|
713
|
+
val SlideCountValid: bool = and(slides >= 1, slides <= 3)
|
|
714
|
+
val ExactlyOneSlideShown: bool = shownCount(slides, current) == 1
|
|
715
|
+
val TabbedHasOneTabStop: bool = or(not(tabbed), stopCount(slides, current) == 1)
|
|
716
|
+
val SlideRoleMatchesVariant: bool = (slideRole(tabbed) == "tabpanel") == tabbed
|
|
717
|
+
// focus only ever sits on something that is rendered
|
|
718
|
+
val FocusValid: bool = and(
|
|
719
|
+
focus >= 0,
|
|
720
|
+
focus <= 7,
|
|
721
|
+
or(focus != 1, autoRotate),
|
|
722
|
+
or(not(isTab(focus)), and(tabbed, focus - 4 < slides))
|
|
723
|
+
)
|
|
724
|
+
// callbacks only ever count up from zero
|
|
725
|
+
val RequestsNonNegative: bool = requests >= 0
|
|
726
|
+
// controlled: only the value prop sets the slide, so no event commits
|
|
727
|
+
val ControlledFollowsProp: bool = or(not(controlled), current == valueProp)
|
|
728
|
+
// a rejected request leaves the slide as it found it
|
|
729
|
+
val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), current == base)
|
|
730
|
+
// an accepted request committed exactly its intent
|
|
731
|
+
val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), current == requested)
|
|
732
|
+
}
|