@cssints/apg-specs 0.0.0-stage → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +65 -2
- package/dist/apg/accordion.bridge.d.mts +35 -0
- package/dist/apg/accordion.bridge.mjs +326 -0
- package/dist/apg/accordion.model.json +8397 -0
- package/dist/apg/alert.bridge.d.mts +22 -0
- package/dist/apg/alert.bridge.mjs +86 -0
- package/dist/apg/alert.model.json +871 -0
- package/dist/apg/alertdialog.bridge.d.mts +21 -0
- package/dist/apg/alertdialog.bridge.mjs +221 -0
- package/dist/apg/alertdialog.model.json +5232 -0
- package/dist/apg/breadcrumb.bridge.d.mts +26 -0
- package/dist/apg/breadcrumb.bridge.mjs +120 -0
- package/dist/apg/breadcrumb.model.json +574 -0
- package/dist/apg/button.bridge.d.mts +35 -0
- package/dist/apg/button.bridge.mjs +208 -0
- package/dist/apg/button.model.json +5814 -0
- package/dist/apg/carousel.bridge.d.mts +44 -0
- package/dist/apg/carousel.bridge.mjs +262 -0
- package/dist/apg/carousel.model.json +8963 -0
- package/dist/apg/checkbox-group.bridge.d.mts +25 -0
- package/dist/apg/checkbox-group.bridge.mjs +276 -0
- package/dist/apg/checkbox-group.model.json +3863 -0
- package/dist/apg/checkbox.bridge.d.mts +34 -0
- package/dist/apg/checkbox.bridge.mjs +253 -0
- package/dist/apg/checkbox.model.json +3158 -0
- package/dist/apg/combobox-autocomplete.bridge.d.mts +14 -0
- package/dist/apg/combobox-autocomplete.bridge.mjs +326 -0
- package/dist/apg/combobox-autocomplete.model.json +11586 -0
- package/dist/apg/combobox-dialog.bridge.d.mts +17 -0
- package/dist/apg/combobox-dialog.bridge.mjs +250 -0
- package/dist/apg/combobox-dialog.model.json +8981 -0
- package/dist/apg/combobox-grid.bridge.d.mts +17 -0
- package/dist/apg/combobox-grid.bridge.mjs +227 -0
- package/dist/apg/combobox-grid.model.json +8554 -0
- package/dist/apg/combobox-select-only.bridge.d.mts +24 -0
- package/dist/apg/combobox-select-only.bridge.mjs +304 -0
- package/dist/apg/combobox-select-only.model.json +7846 -0
- package/dist/apg/combobox-tree.bridge.d.mts +17 -0
- package/dist/apg/combobox-tree.bridge.mjs +211 -0
- package/dist/apg/combobox-tree.model.json +9345 -0
- package/dist/apg/dialog-modal.bridge.d.mts +35 -0
- package/dist/apg/dialog-modal.bridge.mjs +186 -0
- package/dist/apg/dialog-modal.model.json +7580 -0
- package/dist/apg/disclosure.bridge.d.mts +25 -0
- package/dist/apg/disclosure.bridge.mjs +202 -0
- package/dist/apg/disclosure.model.json +4703 -0
- package/dist/apg/feed.bridge.d.mts +31 -0
- package/dist/apg/feed.bridge.mjs +246 -0
- package/dist/apg/feed.model.json +4523 -0
- package/dist/apg/grid-selection.bridge.d.mts +13 -0
- package/dist/apg/grid-selection.bridge.mjs +160 -0
- package/dist/apg/grid-selection.model.json +6974 -0
- package/dist/apg/grid-sort.bridge.d.mts +12 -0
- package/dist/apg/grid-sort.bridge.mjs +138 -0
- package/dist/apg/grid-sort.model.json +3799 -0
- package/dist/apg/grid.bridge.d.mts +54 -0
- package/dist/apg/grid.bridge.mjs +441 -0
- package/dist/apg/grid.model.json +3802 -0
- package/dist/apg/link.bridge.d.mts +24 -0
- package/dist/apg/link.bridge.mjs +126 -0
- package/dist/apg/link.model.json +694 -0
- package/dist/apg/listbox-multi.bridge.d.mts +10 -0
- package/dist/apg/listbox-multi.bridge.mjs +261 -0
- package/dist/apg/listbox-multi.model.json +10893 -0
- package/dist/apg/listbox-rearrangeable.bridge.d.mts +20 -0
- package/dist/apg/listbox-rearrangeable.bridge.mjs +237 -0
- package/dist/apg/listbox-rearrangeable.model.json +17592 -0
- package/dist/apg/listbox-shared.d.mts +58 -0
- package/dist/apg/listbox-shared.mjs +171 -0
- package/dist/apg/listbox-single.bridge.d.mts +10 -0
- package/dist/apg/listbox-single.bridge.mjs +196 -0
- package/dist/apg/listbox-single.model.json +8024 -0
- package/dist/apg/listbox-typeahead.bridge.d.mts +10 -0
- package/dist/apg/listbox-typeahead.bridge.mjs +153 -0
- package/dist/apg/listbox-typeahead.model.json +5615 -0
- package/dist/apg/menu-button.bridge.d.mts +29 -0
- package/dist/apg/menu-button.bridge.mjs +259 -0
- package/dist/apg/menu-button.model.json +5706 -0
- package/dist/apg/menu-checkable.bridge.d.mts +25 -0
- package/dist/apg/menu-checkable.bridge.mjs +226 -0
- package/dist/apg/menu-checkable.model.json +4277 -0
- package/dist/apg/menu-menubar.bridge.d.mts +27 -0
- package/dist/apg/menu-menubar.bridge.mjs +273 -0
- package/dist/apg/menu-menubar.model.json +9540 -0
- package/dist/apg/meter.bridge.d.mts +28 -0
- package/dist/apg/meter.bridge.mjs +159 -0
- package/dist/apg/meter.model.json +1185 -0
- package/dist/apg/player.bridge.d.mts +24 -0
- package/dist/apg/player.bridge.mjs +342 -0
- package/dist/apg/player.model.json +11013 -0
- package/dist/apg/radio.bridge.d.mts +35 -0
- package/dist/apg/radio.bridge.mjs +371 -0
- package/dist/apg/radio.model.json +5011 -0
- package/dist/apg/slider-multithumb.bridge.d.mts +34 -0
- package/dist/apg/slider-multithumb.bridge.mjs +365 -0
- package/dist/apg/slider-multithumb.model.json +5774 -0
- package/dist/apg/slider.bridge.d.mts +54 -0
- package/dist/apg/slider.bridge.mjs +359 -0
- package/dist/apg/slider.model.json +4881 -0
- package/dist/apg/spinbutton.bridge.d.mts +41 -0
- package/dist/apg/spinbutton.bridge.mjs +327 -0
- package/dist/apg/spinbutton.model.json +7571 -0
- package/dist/apg/switch.bridge.d.mts +30 -0
- package/dist/apg/switch.bridge.mjs +243 -0
- package/dist/apg/switch.model.json +3159 -0
- package/dist/apg/tabs.bridge.d.mts +41 -0
- package/dist/apg/tabs.bridge.mjs +294 -0
- package/dist/apg/tabs.model.json +7659 -0
- package/dist/apg/timeline.bridge.d.mts +36 -0
- package/dist/apg/timeline.bridge.mjs +265 -0
- package/dist/apg/timeline.model.json +6189 -0
- package/dist/apg/toolbar.bridge.d.mts +25 -0
- package/dist/apg/toolbar.bridge.mjs +307 -0
- package/dist/apg/toolbar.model.json +6616 -0
- package/dist/apg/tooltip.bridge.d.mts +36 -0
- package/dist/apg/tooltip.bridge.mjs +208 -0
- package/dist/apg/tooltip.model.json +8295 -0
- package/dist/apg/treegrid.bridge.d.mts +28 -0
- package/dist/apg/treegrid.bridge.mjs +311 -0
- package/dist/apg/treegrid.model.json +17358 -0
- package/dist/apg/treeview.bridge.d.mts +26 -0
- package/dist/apg/treeview.bridge.mjs +396 -0
- package/dist/apg/treeview.model.json +13745 -0
- package/dist/apg/windowsplitter.bridge.d.mts +36 -0
- package/dist/apg/windowsplitter.bridge.mjs +242 -0
- package/dist/apg/windowsplitter.model.json +6253 -0
- package/dist/canvas/camera.model.json +6568 -0
- package/dist/canvas/canvas.bridge.d.mts +190 -0
- package/dist/canvas/canvas.bridge.mjs +729 -0
- package/dist/canvas/pointer.model.json +17095 -0
- package/dist/scenario.d.mts +233 -0
- package/dist/scenario.mjs +313 -0
- package/package.json +57 -4
- package/scenarios/apg/README.md +221 -0
- package/scenarios/apg/accordion.base-ui.json +1005 -0
- package/scenarios/apg/accordion.react-aria.json +929 -0
- package/scenarios/apg/alert.base-ui.json +81 -0
- package/scenarios/apg/alert.react-aria.json +98 -0
- package/scenarios/apg/alertdialog.base-ui.json +331 -0
- package/scenarios/apg/alertdialog.react-aria.json +225 -0
- package/scenarios/apg/breadcrumb.react-aria.json +312 -0
- package/scenarios/apg/button.ariakit.json +136 -0
- package/scenarios/apg/button.base-ui.json +747 -0
- package/scenarios/apg/button.react-aria.json +443 -0
- package/scenarios/apg/carousel.apg.json +909 -0
- package/scenarios/apg/checkbox.ariakit.json +299 -0
- package/scenarios/apg/checkbox.base-ui.json +1126 -0
- package/scenarios/apg/checkbox.react-aria.json +518 -0
- package/scenarios/apg/combobox.ariakit.json +945 -0
- package/scenarios/apg/combobox.base-ui.json +2444 -0
- package/scenarios/apg/combobox.react-aria.json +845 -0
- package/scenarios/apg/dialog-modal.ariakit.json +1102 -0
- package/scenarios/apg/dialog-modal.base-ui.json +775 -0
- package/scenarios/apg/dialog-modal.react-aria.json +638 -0
- package/scenarios/apg/disclosure.apg.json +349 -0
- package/scenarios/apg/disclosure.ariakit.json +163 -0
- package/scenarios/apg/disclosure.base-ui.json +451 -0
- package/scenarios/apg/disclosure.react-aria.json +643 -0
- package/scenarios/apg/feed.apg.json +442 -0
- package/scenarios/apg/grid.ariakit.json +197 -0
- package/scenarios/apg/grid.base-ui.json +720 -0
- package/scenarios/apg/grid.react-aria.json +969 -0
- package/scenarios/apg/landmarks.ariakit.json +93 -0
- package/scenarios/apg/landmarks.react-aria.json +330 -0
- package/scenarios/apg/link.ariakit.json +83 -0
- package/scenarios/apg/link.base-ui.json +88 -0
- package/scenarios/apg/link.react-aria.json +186 -0
- package/scenarios/apg/listbox.ariakit.json +78 -0
- package/scenarios/apg/listbox.base-ui.json +163 -0
- package/scenarios/apg/listbox.react-aria.json +1118 -0
- package/scenarios/apg/menu-button.ariakit.json +924 -0
- package/scenarios/apg/menu-button.base-ui.json +1155 -0
- package/scenarios/apg/menu-button.react-aria.json +1080 -0
- package/scenarios/apg/menubar.ariakit.json +758 -0
- package/scenarios/apg/menubar.base-ui.json +673 -0
- package/scenarios/apg/meter.base-ui.json +243 -0
- package/scenarios/apg/meter.react-aria.json +201 -0
- package/scenarios/apg/radio.ariakit.json +459 -0
- package/scenarios/apg/radio.base-ui.json +581 -0
- package/scenarios/apg/radio.react-aria.json +576 -0
- package/scenarios/apg/slider-multithumb.base-ui.json +316 -0
- package/scenarios/apg/slider-multithumb.react-aria.json +363 -0
- package/scenarios/apg/slider.base-ui.json +544 -0
- package/scenarios/apg/slider.react-aria.json +456 -0
- package/scenarios/apg/spinbutton.base-ui.json +1688 -0
- package/scenarios/apg/spinbutton.react-aria.json +426 -0
- package/scenarios/apg/switch.base-ui.json +456 -0
- package/scenarios/apg/switch.react-aria.json +303 -0
- package/scenarios/apg/table.ariakit.json +65 -0
- package/scenarios/apg/table.react-aria.json +221 -0
- package/scenarios/apg/tabs.ariakit.json +329 -0
- package/scenarios/apg/tabs.base-ui.json +875 -0
- package/scenarios/apg/tabs.react-aria.json +644 -0
- package/scenarios/apg/toolbar.ariakit.json +595 -0
- package/scenarios/apg/toolbar.base-ui.json +1189 -0
- package/scenarios/apg/toolbar.react-aria.json +383 -0
- package/scenarios/apg/tooltip.ariakit.json +441 -0
- package/scenarios/apg/tooltip.base-ui.json +411 -0
- package/scenarios/apg/tooltip.react-aria.json +454 -0
- package/scenarios/apg/treegrid.apg.json +2558 -0
- package/scenarios/apg/treegrid.react-aria.json +5094 -0
- package/scenarios/apg/treeview.apg.json +928 -0
- package/scenarios/apg/windowsplitter.apg.json +557 -0
- package/scenarios/canvas/README.md +69 -0
- package/scenarios/canvas/camera.contract.json +237 -0
- package/scenarios/canvas/camera.excalidraw.json +57 -0
- package/scenarios/canvas/marquee.excalidraw.json +122 -0
- package/scenarios/canvas/pan.excalidraw.json +46 -0
- package/scenarios/canvas/pointer.contract.json +528 -0
- package/scenarios/parts.json +291 -0
- package/shared/aria.qnt +186 -0
- package/shared/details-group.qnt +50 -0
- package/shared/focusgroup.qnt +152 -0
- package/shared/invokers.qnt +81 -0
- package/shared/popover.qnt +71 -0
- package/specs/apg/STATIC.md +103 -0
- package/specs/apg/accordion.qnt +578 -0
- package/specs/apg/alert.qnt +131 -0
- package/specs/apg/alertdialog.qnt +455 -0
- package/specs/apg/breadcrumb.qnt +88 -0
- package/specs/apg/button.qnt +445 -0
- package/specs/apg/carousel.qnt +732 -0
- package/specs/apg/checkbox-group.qnt +388 -0
- package/specs/apg/checkbox.qnt +394 -0
- package/specs/apg/combobox-autocomplete.qnt +764 -0
- package/specs/apg/combobox-dialog.qnt +627 -0
- package/specs/apg/combobox-grid.qnt +632 -0
- package/specs/apg/combobox-select-only.qnt +626 -0
- package/specs/apg/combobox-tree.qnt +633 -0
- package/specs/apg/dialog-modal.qnt +499 -0
- package/specs/apg/disclosure.qnt +309 -0
- package/specs/apg/feed.qnt +485 -0
- package/specs/apg/grid-selection.qnt +512 -0
- package/specs/apg/grid-sort.qnt +330 -0
- package/specs/apg/grid.qnt +360 -0
- package/specs/apg/link.qnt +119 -0
- package/specs/apg/listbox-multi.qnt +799 -0
- package/specs/apg/listbox-rearrangeable.qnt +801 -0
- package/specs/apg/listbox-single.qnt +511 -0
- package/specs/apg/listbox-typeahead.qnt +396 -0
- package/specs/apg/menu-button.qnt +363 -0
- package/specs/apg/menu-checkable.qnt +304 -0
- package/specs/apg/menu-menubar.qnt +531 -0
- package/specs/apg/meter.qnt +129 -0
- package/specs/apg/player.qnt +1081 -0
- package/specs/apg/radio.qnt +394 -0
- package/specs/apg/slider-multithumb.qnt +441 -0
- package/specs/apg/slider.qnt +396 -0
- package/specs/apg/spinbutton.qnt +494 -0
- package/specs/apg/switch.qnt +389 -0
- package/specs/apg/tabs.qnt +412 -0
- package/specs/apg/timeline.qnt +612 -0
- package/specs/apg/toolbar.qnt +364 -0
- package/specs/apg/tooltip.qnt +553 -0
- package/specs/apg/treegrid.qnt +983 -0
- package/specs/apg/treeview.qnt +1004 -0
- package/specs/apg/windowsplitter.qnt +487 -0
- package/specs/canvas/camera.qnt +407 -0
- package/specs/canvas/pointer.qnt +841 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
// WAI-ARIA APG — Feed pattern: the feed behaviour machine.
|
|
2
|
+
//
|
|
3
|
+
// Sources (https://www.w3.org/WAI/ARIA/apg/patterns/feed/):
|
|
4
|
+
// - "About This Pattern": the page loads/removes articles based on which
|
|
5
|
+
// article holds DOM focus; the reading cursor is the focused article.
|
|
6
|
+
// - "Keyboard Interaction": Page Down / Page Up move focus to the next /
|
|
7
|
+
// previous article; Control+End / Control+Home move focus to the first
|
|
8
|
+
// focusable element after / before the feed.
|
|
9
|
+
// - "WAI-ARIA Roles, States, and Properties": aria-posinset (position of the
|
|
10
|
+
// article, numbering from 1), aria-setsize (articles loaded, or the total;
|
|
11
|
+
// -1 when the total is undetermined), aria-busy=true on the feed while
|
|
12
|
+
// articles are being added/removed and false again when the operation is
|
|
13
|
+
// complete (also on a failed load).
|
|
14
|
+
// - Example https://www.w3.org/WAI/ARIA/apg/patterns/feed/examples/feed/:
|
|
15
|
+
// articles are tabindex=0 (focus can enter the feed on any loaded article
|
|
16
|
+
// with Tab), aria-setsize = articles currently contained by the feed,
|
|
17
|
+
// loading is triggered by focus reaching the last loaded article.
|
|
18
|
+
//
|
|
19
|
+
// Request vs commit (map ticket rt1p). The number of loaded articles is the
|
|
20
|
+
// value. A batch arriving (`load_finish`) is a *request* carrying the intent
|
|
21
|
+
// (`requested`); it fires onLoadedChange (`requests` counts the calls) when the
|
|
22
|
+
// intent differs from the current `loaded`, and then:
|
|
23
|
+
// uncontrolled the request is accepted: the count commits the intent
|
|
24
|
+
// controlled, holds the host ignores it ("rejected"): the count is untouched
|
|
25
|
+
// controlled, accepts the host answers with the same value (`accepting`): the
|
|
26
|
+
// count commits through the controlled value, not the event
|
|
27
|
+
// `valueProp` is the only thing that changes the count while `controlled`;
|
|
28
|
+
// `setValue` is the host writing it. `control` / `acceptRequests` are
|
|
29
|
+
// configuration events (init is literal-only). `load_start` / `load_fail` are
|
|
30
|
+
// adapter lifecycle events (busy), not value changes. Invariants for the model:
|
|
31
|
+
// ControlledFollowsProp, RejectedRequestUntouched, AcceptedRequestCommits.
|
|
32
|
+
//
|
|
33
|
+
// Machine: one feed with `loaded` articles (posinset 1..loaded) out of a fixed
|
|
34
|
+
// server-side `totalArticles`. Focus is in one of three zones: before the
|
|
35
|
+
// feed, on an article of the feed, or after the feed. Loading is a two-phase
|
|
36
|
+
// operation (load_start -> load_finish | load_fail) bracketed by aria-busy.
|
|
37
|
+
//
|
|
38
|
+
// Configuration variants:
|
|
39
|
+
// - totalArticles (constant def): size of the whole feed.
|
|
40
|
+
// - sizeMode ("loaded" | "total"): which value aria-setsize reports; a prop
|
|
41
|
+
// change at any time (set_size_mode).
|
|
42
|
+
// - totalKnown (bool): whether the page knows the total; with sizeMode
|
|
43
|
+
// "total" and an unknown total aria-setsize is -1 (set_total_known).
|
|
44
|
+
// - batch sizes 1..3 per load_finish (pool).
|
|
45
|
+
//
|
|
46
|
+
// Deliberate omissions:
|
|
47
|
+
// - Removing articles / prepending articles (renumbering of posinset); the
|
|
48
|
+
// feed only grows at its end.
|
|
49
|
+
// - Nested feeds and their navigation keys (Alt+Page Down, Ctrl+End to the
|
|
50
|
+
// next outer article); articles that capture the feed keys themselves.
|
|
51
|
+
// - Roles, labels and descriptions (role=feed/article, aria-labelledby,
|
|
52
|
+
// aria-describedby): static DOM contract with no behaviour to model.
|
|
53
|
+
// - Visual scrolling, scroll-position-triggered loading (load_start stands in
|
|
54
|
+
// for it), loading delay, and which focusable element exactly is "after"
|
|
55
|
+
// or "before" the feed (zones only).
|
|
56
|
+
// - Control+Home from outside the feed and Tab order inside articles.
|
|
57
|
+
module apg_feed {
|
|
58
|
+
var phase: str // "idle" | "loading"
|
|
59
|
+
var busy: bool // the feed's aria-busy attribute
|
|
60
|
+
var loaded: int // articles currently in the DOM (posinset 1..loaded)
|
|
61
|
+
var zone: str // focus zone: "before" | "feed" | "after"
|
|
62
|
+
var focus: int // posinset of the focused article (0 = focus outside the feed)
|
|
63
|
+
var sizeMode: str // "loaded" | "total": what aria-setsize reports
|
|
64
|
+
var totalKnown: bool // the page knows the total number of articles
|
|
65
|
+
var controlled: bool // the host owns the loaded count (`value` prop present)
|
|
66
|
+
var accepting: bool // controlled host that answers every request with its intent
|
|
67
|
+
var valueProp: int // the controlled value prop (meaningful while controlled)
|
|
68
|
+
var requested: int // intent of the last request
|
|
69
|
+
var requests: int // number of requests that fired onLoadedChange
|
|
70
|
+
var outcome: str // "none" | "accepted" | "rejected"
|
|
71
|
+
var base: int // count when the last request was received
|
|
72
|
+
|
|
73
|
+
// the number of articles the whole feed holds on the server
|
|
74
|
+
def totalArticles: int = 6
|
|
75
|
+
|
|
76
|
+
// --- derived ARIA contract ------------------------------------------------
|
|
77
|
+
// aria-posinset of the article at (1-based) DOM index i
|
|
78
|
+
def ariaPosinset(i: int): int = i
|
|
79
|
+
// aria-setsize of every article: loaded count, total, or -1 when undetermined
|
|
80
|
+
def ariaSetsize(mode: str, known: bool, count: int): int =
|
|
81
|
+
if (mode == "loaded") count else if (known) totalArticles else -1
|
|
82
|
+
// aria-busy on the feed element
|
|
83
|
+
def ariaBusy(p: str): bool = p == "loading"
|
|
84
|
+
// tabindex of an article (every article is in the Tab sequence)
|
|
85
|
+
def articleTabindex(i: int): int = 0
|
|
86
|
+
|
|
87
|
+
// --- guards ---------------------------------------------------------------
|
|
88
|
+
def hasMore(count: int): bool = count < totalArticles
|
|
89
|
+
def canStartLoad(p: str, count: int): bool = and(p == "idle", hasMore(count))
|
|
90
|
+
def inFeed(z: str): bool = z == "feed"
|
|
91
|
+
// Page Down: next article, stay on the last loaded one
|
|
92
|
+
def pageDownTarget(z: str, f: int, count: int): int =
|
|
93
|
+
if (and(inFeed(z), f < count)) f + 1 else f
|
|
94
|
+
// Page Up: previous article, stay on the first
|
|
95
|
+
def pageUpTarget(z: str, f: int): int =
|
|
96
|
+
if (and(inFeed(z), f > 1)) f - 1 else f
|
|
97
|
+
// focus on the last loaded article while more exist triggers a load
|
|
98
|
+
def focusTriggersLoad(z: str, f: int, p: str, count: int): bool =
|
|
99
|
+
and(and(inFeed(z), f == count), canStartLoad(p, count))
|
|
100
|
+
def addedArticles(count: int, n: int): int =
|
|
101
|
+
if (count + n <= totalArticles) n else totalArticles - count
|
|
102
|
+
|
|
103
|
+
// --- request / commit (map ticket rt1p) ----------------------------------
|
|
104
|
+
def moved(before: int, after: int): int = if (before == after) 0 else 1
|
|
105
|
+
// the count after a batch when a load is in flight (else unchanged)
|
|
106
|
+
def finishWant(loading: bool, count: int, n: int): int =
|
|
107
|
+
if (loading) count + addedArticles(count, n) else count
|
|
108
|
+
// controlled host that keeps the old value: the request is rejected
|
|
109
|
+
def holds: bool = and(controlled, not(accepting))
|
|
110
|
+
// controlled host that writes the intent back through the value prop
|
|
111
|
+
def follows: bool = and(controlled, accepting)
|
|
112
|
+
def verdict: str = if (holds) "rejected" else "accepted"
|
|
113
|
+
// the count after a request for `want`
|
|
114
|
+
def landed(h: bool, current: int, want: int): int = if (h) current else want
|
|
115
|
+
// the value prop after a request for `want`
|
|
116
|
+
def propAfter(f: bool, p: int, want: int): int = if (f) want else p
|
|
117
|
+
|
|
118
|
+
action init = all {
|
|
119
|
+
phase' = "idle",
|
|
120
|
+
busy' = false,
|
|
121
|
+
loaded' = 1,
|
|
122
|
+
zone' = "before",
|
|
123
|
+
focus' = 0,
|
|
124
|
+
sizeMode' = "loaded",
|
|
125
|
+
totalKnown' = false,
|
|
126
|
+
controlled' = false,
|
|
127
|
+
accepting' = false,
|
|
128
|
+
valueProp' = 1,
|
|
129
|
+
requested' = 1,
|
|
130
|
+
requests' = 0,
|
|
131
|
+
outcome' = "none",
|
|
132
|
+
base' = 1
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Tab (or a click) puts focus on loaded article n; an unloaded n is a no-op
|
|
136
|
+
action focus_article = {
|
|
137
|
+
nondet n = Set(1, 2, 3, 4, 5, 6).oneOf()
|
|
138
|
+
all {
|
|
139
|
+
phase' = if (focusTriggersLoad(if (and(n >= 1, n <= loaded)) "feed" else zone, if (and(n >= 1, n <= loaded)) n else focus, phase, loaded)) "loading" else phase,
|
|
140
|
+
busy' = if (focusTriggersLoad(if (and(n >= 1, n <= loaded)) "feed" else zone, if (and(n >= 1, n <= loaded)) n else focus, phase, loaded)) true else busy,
|
|
141
|
+
loaded' = loaded,
|
|
142
|
+
zone' = if (and(n >= 1, n <= loaded)) "feed" else zone,
|
|
143
|
+
focus' = if (and(n >= 1, n <= loaded)) n else focus,
|
|
144
|
+
sizeMode' = sizeMode,
|
|
145
|
+
totalKnown' = totalKnown,
|
|
146
|
+
controlled' = controlled,
|
|
147
|
+
accepting' = accepting,
|
|
148
|
+
valueProp' = valueProp,
|
|
149
|
+
requested' = requested,
|
|
150
|
+
requests' = requests,
|
|
151
|
+
outcome' = outcome,
|
|
152
|
+
base' = base
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Page Down inside the feed
|
|
157
|
+
action page_down = all {
|
|
158
|
+
phase' = if (focusTriggersLoad(zone, pageDownTarget(zone, focus, loaded), phase, loaded)) "loading" else phase,
|
|
159
|
+
busy' = if (focusTriggersLoad(zone, pageDownTarget(zone, focus, loaded), phase, loaded)) true else busy,
|
|
160
|
+
loaded' = loaded,
|
|
161
|
+
zone' = zone,
|
|
162
|
+
focus' = pageDownTarget(zone, focus, loaded),
|
|
163
|
+
sizeMode' = sizeMode,
|
|
164
|
+
totalKnown' = totalKnown,
|
|
165
|
+
controlled' = controlled,
|
|
166
|
+
accepting' = accepting,
|
|
167
|
+
valueProp' = valueProp,
|
|
168
|
+
requested' = requested,
|
|
169
|
+
requests' = requests,
|
|
170
|
+
outcome' = outcome,
|
|
171
|
+
base' = base
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Page Up inside the feed
|
|
175
|
+
action page_up = all {
|
|
176
|
+
phase' = phase,
|
|
177
|
+
busy' = busy,
|
|
178
|
+
loaded' = loaded,
|
|
179
|
+
zone' = zone,
|
|
180
|
+
focus' = pageUpTarget(zone, focus),
|
|
181
|
+
sizeMode' = sizeMode,
|
|
182
|
+
totalKnown' = totalKnown,
|
|
183
|
+
controlled' = controlled,
|
|
184
|
+
accepting' = accepting,
|
|
185
|
+
valueProp' = valueProp,
|
|
186
|
+
requested' = requested,
|
|
187
|
+
requests' = requests,
|
|
188
|
+
outcome' = outcome,
|
|
189
|
+
base' = base
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Control+End: leave the feed forwards
|
|
193
|
+
action ctrl_end = all {
|
|
194
|
+
phase' = phase,
|
|
195
|
+
busy' = busy,
|
|
196
|
+
loaded' = loaded,
|
|
197
|
+
zone' = if (inFeed(zone)) "after" else zone,
|
|
198
|
+
focus' = if (inFeed(zone)) 0 else focus,
|
|
199
|
+
sizeMode' = sizeMode,
|
|
200
|
+
totalKnown' = totalKnown,
|
|
201
|
+
controlled' = controlled,
|
|
202
|
+
accepting' = accepting,
|
|
203
|
+
valueProp' = valueProp,
|
|
204
|
+
requested' = requested,
|
|
205
|
+
requests' = requests,
|
|
206
|
+
outcome' = outcome,
|
|
207
|
+
base' = base
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Control+Home: leave the feed backwards
|
|
211
|
+
action ctrl_home = all {
|
|
212
|
+
phase' = phase,
|
|
213
|
+
busy' = busy,
|
|
214
|
+
loaded' = loaded,
|
|
215
|
+
zone' = if (inFeed(zone)) "before" else zone,
|
|
216
|
+
focus' = if (inFeed(zone)) 0 else focus,
|
|
217
|
+
sizeMode' = sizeMode,
|
|
218
|
+
totalKnown' = totalKnown,
|
|
219
|
+
controlled' = controlled,
|
|
220
|
+
accepting' = accepting,
|
|
221
|
+
valueProp' = valueProp,
|
|
222
|
+
requested' = requested,
|
|
223
|
+
requests' = requests,
|
|
224
|
+
outcome' = outcome,
|
|
225
|
+
base' = base
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// focus moves somewhere outside the feed (Tab out, click elsewhere)
|
|
229
|
+
action focus_outside = {
|
|
230
|
+
nondet side = Set("before", "after").oneOf()
|
|
231
|
+
all {
|
|
232
|
+
phase' = phase,
|
|
233
|
+
busy' = busy,
|
|
234
|
+
loaded' = loaded,
|
|
235
|
+
zone' = side,
|
|
236
|
+
focus' = 0,
|
|
237
|
+
sizeMode' = sizeMode,
|
|
238
|
+
totalKnown' = totalKnown,
|
|
239
|
+
controlled' = controlled,
|
|
240
|
+
accepting' = accepting,
|
|
241
|
+
valueProp' = valueProp,
|
|
242
|
+
requested' = requested,
|
|
243
|
+
requests' = requests,
|
|
244
|
+
outcome' = outcome,
|
|
245
|
+
base' = base
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// scroll position asks for more articles: aria-busy goes true
|
|
250
|
+
action load_start = all {
|
|
251
|
+
phase' = if (canStartLoad(phase, loaded)) "loading" else phase,
|
|
252
|
+
busy' = if (canStartLoad(phase, loaded)) true else busy,
|
|
253
|
+
loaded' = loaded,
|
|
254
|
+
zone' = zone,
|
|
255
|
+
focus' = focus,
|
|
256
|
+
sizeMode' = sizeMode,
|
|
257
|
+
totalKnown' = totalKnown,
|
|
258
|
+
controlled' = controlled,
|
|
259
|
+
accepting' = accepting,
|
|
260
|
+
valueProp' = valueProp,
|
|
261
|
+
requested' = requested,
|
|
262
|
+
requests' = requests,
|
|
263
|
+
outcome' = outcome,
|
|
264
|
+
base' = base
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// request: the batch arrives; articles appended (through the controlled
|
|
268
|
+
// value), aria-busy back to false
|
|
269
|
+
action load_finish = {
|
|
270
|
+
nondet n = Set(1, 2, 3).oneOf()
|
|
271
|
+
all {
|
|
272
|
+
phase' = if (phase == "loading") "idle" else phase,
|
|
273
|
+
busy' = if (phase == "loading") false else busy,
|
|
274
|
+
loaded' = if (phase == "loading") landed(holds, loaded, finishWant(true, loaded, n)) else loaded,
|
|
275
|
+
zone' = zone,
|
|
276
|
+
focus' = focus,
|
|
277
|
+
sizeMode' = sizeMode,
|
|
278
|
+
totalKnown' = totalKnown,
|
|
279
|
+
controlled' = controlled,
|
|
280
|
+
accepting' = accepting,
|
|
281
|
+
valueProp' = if (moved(loaded, finishWant(phase == "loading", loaded, n)) == 1) propAfter(follows, valueProp, finishWant(true, loaded, n)) else valueProp,
|
|
282
|
+
requested' = if (moved(loaded, finishWant(phase == "loading", loaded, n)) == 1) finishWant(true, loaded, n) else requested,
|
|
283
|
+
requests' = requests + moved(loaded, finishWant(phase == "loading", loaded, n)),
|
|
284
|
+
outcome' = if (moved(loaded, finishWant(phase == "loading", loaded, n)) == 1) verdict else outcome,
|
|
285
|
+
base' = if (moved(loaded, finishWant(phase == "loading", loaded, n)) == 1) loaded else base
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// the load fails: nothing added, aria-busy must still be cleared
|
|
290
|
+
action load_fail = all {
|
|
291
|
+
phase' = if (phase == "loading") "idle" else phase,
|
|
292
|
+
busy' = if (phase == "loading") false else busy,
|
|
293
|
+
loaded' = loaded,
|
|
294
|
+
zone' = zone,
|
|
295
|
+
focus' = focus,
|
|
296
|
+
sizeMode' = sizeMode,
|
|
297
|
+
totalKnown' = totalKnown,
|
|
298
|
+
controlled' = controlled,
|
|
299
|
+
accepting' = accepting,
|
|
300
|
+
valueProp' = valueProp,
|
|
301
|
+
requested' = requested,
|
|
302
|
+
requests' = requests,
|
|
303
|
+
outcome' = outcome,
|
|
304
|
+
base' = base
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// prop change: which value aria-setsize reports
|
|
308
|
+
action set_size_mode = {
|
|
309
|
+
nondet mode = Set("loaded", "total").oneOf()
|
|
310
|
+
all {
|
|
311
|
+
phase' = phase,
|
|
312
|
+
busy' = busy,
|
|
313
|
+
loaded' = loaded,
|
|
314
|
+
zone' = zone,
|
|
315
|
+
focus' = focus,
|
|
316
|
+
sizeMode' = mode,
|
|
317
|
+
totalKnown' = totalKnown,
|
|
318
|
+
controlled' = controlled,
|
|
319
|
+
accepting' = accepting,
|
|
320
|
+
valueProp' = valueProp,
|
|
321
|
+
requested' = requested,
|
|
322
|
+
requests' = requests,
|
|
323
|
+
outcome' = outcome,
|
|
324
|
+
base' = base
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// prop change: the page learns (or loses) the total number of articles
|
|
329
|
+
action set_total_known = {
|
|
330
|
+
nondet known = Set(true, false).oneOf()
|
|
331
|
+
all {
|
|
332
|
+
phase' = phase,
|
|
333
|
+
busy' = busy,
|
|
334
|
+
loaded' = loaded,
|
|
335
|
+
zone' = zone,
|
|
336
|
+
focus' = focus,
|
|
337
|
+
sizeMode' = sizeMode,
|
|
338
|
+
totalKnown' = known,
|
|
339
|
+
controlled' = controlled,
|
|
340
|
+
accepting' = accepting,
|
|
341
|
+
valueProp' = valueProp,
|
|
342
|
+
requested' = requested,
|
|
343
|
+
requests' = requests,
|
|
344
|
+
outcome' = outcome,
|
|
345
|
+
base' = base
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// commit: the host writes the controlled count (a no-op while uncontrolled).
|
|
350
|
+
// An in-flight load settles; focus clamps into the new range, and refocusing
|
|
351
|
+
// the clamped, now-last article asks for more (the load trigger again).
|
|
352
|
+
action setValue = {
|
|
353
|
+
nondet v = Set(1, 2, 3, 4, 5, 6).oneOf()
|
|
354
|
+
all {
|
|
355
|
+
phase' = if (controlled) (if (and(inFeed(zone), focus > v, hasMore(v))) "loading" else "idle") else phase,
|
|
356
|
+
busy' = if (controlled) (and(inFeed(zone), focus > v, hasMore(v))) else busy,
|
|
357
|
+
loaded' = if (controlled) v else loaded,
|
|
358
|
+
zone' = zone,
|
|
359
|
+
focus' = if (and(controlled, inFeed(zone), focus > v)) v else focus,
|
|
360
|
+
sizeMode' = sizeMode,
|
|
361
|
+
totalKnown' = totalKnown,
|
|
362
|
+
controlled' = controlled,
|
|
363
|
+
accepting' = accepting,
|
|
364
|
+
valueProp' = if (controlled) v else valueProp,
|
|
365
|
+
requested' = requested,
|
|
366
|
+
requests' = requests,
|
|
367
|
+
outcome' = if (controlled) "none" else outcome,
|
|
368
|
+
base' = base
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// configuration: the host starts owning the count, adopting the current one
|
|
373
|
+
action control = all {
|
|
374
|
+
phase' = phase,
|
|
375
|
+
busy' = busy,
|
|
376
|
+
loaded' = loaded,
|
|
377
|
+
zone' = zone,
|
|
378
|
+
focus' = focus,
|
|
379
|
+
sizeMode' = sizeMode,
|
|
380
|
+
totalKnown' = totalKnown,
|
|
381
|
+
controlled' = true,
|
|
382
|
+
accepting' = accepting,
|
|
383
|
+
valueProp' = loaded,
|
|
384
|
+
requested' = requested,
|
|
385
|
+
requests' = requests,
|
|
386
|
+
outcome' = outcome,
|
|
387
|
+
base' = base
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// configuration: the controlled host answers every request with its intent
|
|
391
|
+
action acceptRequests = all {
|
|
392
|
+
phase' = phase,
|
|
393
|
+
busy' = busy,
|
|
394
|
+
loaded' = loaded,
|
|
395
|
+
zone' = zone,
|
|
396
|
+
focus' = focus,
|
|
397
|
+
sizeMode' = sizeMode,
|
|
398
|
+
totalKnown' = totalKnown,
|
|
399
|
+
controlled' = controlled,
|
|
400
|
+
accepting' = true,
|
|
401
|
+
valueProp' = valueProp,
|
|
402
|
+
requested' = requested,
|
|
403
|
+
requests' = requests,
|
|
404
|
+
outcome' = outcome,
|
|
405
|
+
base' = base
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
action step = any {
|
|
409
|
+
init,
|
|
410
|
+
focus_article,
|
|
411
|
+
page_down,
|
|
412
|
+
page_up,
|
|
413
|
+
ctrl_end,
|
|
414
|
+
ctrl_home,
|
|
415
|
+
focus_outside,
|
|
416
|
+
load_start,
|
|
417
|
+
load_finish,
|
|
418
|
+
load_fail,
|
|
419
|
+
set_size_mode,
|
|
420
|
+
set_total_known,
|
|
421
|
+
setValue,
|
|
422
|
+
control,
|
|
423
|
+
acceptRequests
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// aria-busy is true exactly while a load is in flight
|
|
427
|
+
val BusyIffLoading: bool = or(
|
|
428
|
+
and(busy, ariaBusy(phase)),
|
|
429
|
+
and(not(busy), not(ariaBusy(phase)))
|
|
430
|
+
)
|
|
431
|
+
val PhaseValid: bool = or(phase == "idle", phase == "loading")
|
|
432
|
+
// a load only runs while articles remain, and the feed never overshoots
|
|
433
|
+
val LoadingImpliesMore: bool = or(not(ariaBusy(phase)), hasMore(loaded))
|
|
434
|
+
val LoadedWithinTotal: bool = and(loaded >= 1, loaded <= totalArticles)
|
|
435
|
+
// focus zone and focused article agree; the focused article exists
|
|
436
|
+
val ZoneValid: bool = or(zone == "before", zone == "feed", zone == "after")
|
|
437
|
+
val FocusMatchesZone: bool = or(
|
|
438
|
+
and(inFeed(zone), focus >= 1),
|
|
439
|
+
and(not(inFeed(zone)), focus == 0)
|
|
440
|
+
)
|
|
441
|
+
val FocusWithinLoaded: bool = focus <= loaded
|
|
442
|
+
val SizeModeValid: bool = or(sizeMode == "loaded", sizeMode == "total")
|
|
443
|
+
// posinset numbers articles 1..loaded without gaps or repeats
|
|
444
|
+
val PosinsetsContiguous: bool = and(
|
|
445
|
+
ariaPosinset(1) == 1,
|
|
446
|
+
ariaPosinset(loaded) == loaded
|
|
447
|
+
)
|
|
448
|
+
// every posinset fits in aria-setsize (unless it is the unknown value -1)
|
|
449
|
+
val PosinsetWithinSetsize: bool = or(
|
|
450
|
+
ariaSetsize(sizeMode, totalKnown, loaded) == -1,
|
|
451
|
+
ariaPosinset(loaded) <= ariaSetsize(sizeMode, totalKnown, loaded)
|
|
452
|
+
)
|
|
453
|
+
val FocusedPosinsetWithinSetsize: bool = or(
|
|
454
|
+
not(inFeed(zone)),
|
|
455
|
+
or(
|
|
456
|
+
ariaSetsize(sizeMode, totalKnown, loaded) == -1,
|
|
457
|
+
ariaPosinset(focus) <= ariaSetsize(sizeMode, totalKnown, loaded)
|
|
458
|
+
)
|
|
459
|
+
)
|
|
460
|
+
// aria-setsize is -1 only when the total was asked for but is unknown
|
|
461
|
+
val SetsizeUnknownOnlyWhenTotalUnknown: bool = or(
|
|
462
|
+
and(
|
|
463
|
+
ariaSetsize(sizeMode, totalKnown, loaded) == -1,
|
|
464
|
+
and(sizeMode == "total", not(totalKnown))
|
|
465
|
+
),
|
|
466
|
+
and(
|
|
467
|
+
not(ariaSetsize(sizeMode, totalKnown, loaded) == -1),
|
|
468
|
+
not(and(sizeMode == "total", not(totalKnown)))
|
|
469
|
+
)
|
|
470
|
+
)
|
|
471
|
+
val SetsizeValid: bool = or(
|
|
472
|
+
ariaSetsize(sizeMode, totalKnown, loaded) == -1,
|
|
473
|
+
ariaSetsize(sizeMode, totalKnown, loaded) >= 1
|
|
474
|
+
)
|
|
475
|
+
// every article is reachable with Tab
|
|
476
|
+
val ArticlesTabbable: bool = articleTabindex(loaded) == 0
|
|
477
|
+
// callbacks only ever count up from zero
|
|
478
|
+
val RequestsNonNegative: bool = requests >= 0
|
|
479
|
+
// controlled: only the value prop sets the count, so no event commits
|
|
480
|
+
val ControlledFollowsProp: bool = or(not(controlled), loaded == valueProp)
|
|
481
|
+
// a rejected request leaves the count as it found it
|
|
482
|
+
val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), loaded == base)
|
|
483
|
+
// an accepted request committed exactly its intent
|
|
484
|
+
val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), loaded == requested)
|
|
485
|
+
}
|