@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,152 @@
|
|
|
1
|
+
// shared/focusgroup.qnt — the platform contract of the `focusgroup` attribute:
|
|
2
|
+
// the roving-tabindex behaviour a widget gets for free from the browser (or
|
|
3
|
+
// from @microsoft/focusgroup-polyfill) so a pattern spec models only its delta.
|
|
4
|
+
//
|
|
5
|
+
// Pure defs only: import with `import focusgroup.* from "../../shared/focusgroup"`.
|
|
6
|
+
// Every def is prefixed `fg` so it never collides with a pattern's own defs.
|
|
7
|
+
// State (focus, tab stop) is passed IN and the next value comes back OUT; the
|
|
8
|
+
// pattern owns the vars. A `val` here would be inlined as an invariant into
|
|
9
|
+
// every importer, so invariants are predicate defs the importer wraps in a `val`.
|
|
10
|
+
//
|
|
11
|
+
// Provided (the contract):
|
|
12
|
+
// - ordered items 0..n-1, exactly ONE tab stop (`fgTabIndex`)
|
|
13
|
+
// - arrows along the axis move focus (`fgDelta`, `fgStep`), perpendicular
|
|
14
|
+
// arrows are left to the author; Home / End jump to first / last
|
|
15
|
+
// (`fgLanding`); `wrap` / `nowrap`; Shift/Ctrl/Meta+key are ignored
|
|
16
|
+
// - last-focused memory on sequential re-entry, `nomemory`, `focusgroupstart`
|
|
17
|
+
// (`fgEntry`, `fgStopOnLeave`)
|
|
18
|
+
// - Tab / Shift+Tab inside a native key-conflict control (<input>, per the
|
|
19
|
+
// explainer also <textarea>, <select>, contenteditable) steps to the
|
|
20
|
+
// NEIGHBOURING item instead of leaving the group (`fgEscape`); at either
|
|
21
|
+
// end it leaves
|
|
22
|
+
// - a keydown whose default was prevented by an item handler is not moved
|
|
23
|
+
// - behaviour tokens: default axis / wrap and the inferred owner and item
|
|
24
|
+
// roles (`fgDefaultAxis`, `fgDefaultWrap`, `fgInferredItemRole`)
|
|
25
|
+
//
|
|
26
|
+
// NOT provided, so still the pattern's own (the gap list):
|
|
27
|
+
// - selection (explicitly decoupled), activation (Enter/Space), typeahead
|
|
28
|
+
// - `aria-activedescendant` focus models (a combobox popup is not a focusgroup)
|
|
29
|
+
// - submenu open/close keys, tree expand/collapse, grids (V2, unreleased),
|
|
30
|
+
// `itemcontrols`, `menuitemcheckbox` / `menuitemradio` (never inferred)
|
|
31
|
+
// - `aria-orientation`: the axis does NOT set it (verified: `toolbar block`
|
|
32
|
+
// still exposes orientation=horizontal)
|
|
33
|
+
//
|
|
34
|
+
// NOT expressible as pure defs (no collections / vars here), so NOT modelled:
|
|
35
|
+
// - item eligibility: disabled / hidden / `tabindex=-1` / `focusgroup=none`
|
|
36
|
+
// items are skipped by the platform. These defs assume all n items are
|
|
37
|
+
// focusable; a pattern that wants a disabled item to stay reachable uses
|
|
38
|
+
// aria-disabled (a native `disabled` button is skipped, verified).
|
|
39
|
+
// - segments and nested groups (one segment is assumed), insert / remove of
|
|
40
|
+
// items, RTL and vertical writing modes (left-to-right, horizontal-tb)
|
|
41
|
+
// Both need `Set[int]` / `List[int]` collections: see
|
|
42
|
+
// docs/research/quint-generator-ui-extensions.md (item 6) and the follow-ups
|
|
43
|
+
// of effects-of-atmosphere-dx00.
|
|
44
|
+
//
|
|
45
|
+
// Pins (bump deliberately; the explainer is the only spec text):
|
|
46
|
+
// explainer open-ui scoped-focusgroup.explainer, lastUpdated 2026-07-28
|
|
47
|
+
// polyfill @microsoft/focusgroup-polyfill 1.6.0 (MIT)
|
|
48
|
+
// verified Chromium 153.0.8010.12 (Playwright 1.63.0, 2026-10-07): native
|
|
49
|
+
// `focusgroup` (`'focusGroup' in HTMLElement.prototype` is true) and
|
|
50
|
+
// the polyfill, forced with --disable-blink-features=Focusgroup,
|
|
51
|
+
// replayed the same 40 focus/key traces (toolbar, tablist, listbox,
|
|
52
|
+
// menu, menubar, radiogroup): 38 agree. The 2 that differ are
|
|
53
|
+
// Alt+Arrow / Alt+End, which move focus in the polyfill and are
|
|
54
|
+
// ignored natively. Tab-escape was checked for <input type=text>
|
|
55
|
+
// only (arrows consumed on both axes; <select> consumes the inline
|
|
56
|
+
// arrows; <input type=number> consumes them all). Role inference
|
|
57
|
+
// (CDP accessibility tree, 10 markup cases): 7 agree; the polyfill
|
|
58
|
+
// infers `option` on a <button> and `tab` inside a non-generic or
|
|
59
|
+
// role'd owner, native does not. Native `FocusGroup` / `polyfill()`
|
|
60
|
+
// are inert in Chromium >= 150, so a Solid layer sets the attribute
|
|
61
|
+
// there and drives the polyfill's low-level `FocusGroup` + `update()`
|
|
62
|
+
// only where `focusGroup` is absent (teardown: `update({ definition:
|
|
63
|
+
// {} })` BEFORE `disconnect()`, which alone leaves tabindex rewritten).
|
|
64
|
+
// The toolbar model replays against both providers with no
|
|
65
|
+
// divergence (see specs/apg/toolbar.qnt).
|
|
66
|
+
module focusgroup {
|
|
67
|
+
// --- behaviour tokens ----------------------------------------------------
|
|
68
|
+
def fgBehaviorKnown(b: str): bool =
|
|
69
|
+
or(
|
|
70
|
+
or(or(b == "toolbar", b == "tablist"), or(b == "radiogroup", b == "listbox")),
|
|
71
|
+
or(b == "menu", b == "menubar")
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
// axis: "inline" | "block" | "both" (no explicit axis token)
|
|
75
|
+
def fgDefaultAxis(b: str): str =
|
|
76
|
+
if (or(b == "listbox", b == "menu")) "block"
|
|
77
|
+
else if (b == "radiogroup") "both"
|
|
78
|
+
else "inline"
|
|
79
|
+
|
|
80
|
+
def fgDefaultWrap(b: str): bool = and(b != "toolbar", b != "listbox")
|
|
81
|
+
|
|
82
|
+
// --- role inference (generic owners only; explicit roles always win) -----
|
|
83
|
+
// ownerKind: "generic" (div/span) | "native"; itemKind: "generic" | "button" | "native"
|
|
84
|
+
// "" means no role is inferred.
|
|
85
|
+
def fgItemRole(b: str): str =
|
|
86
|
+
if (b == "tablist") "tab"
|
|
87
|
+
else if (b == "radiogroup") "radio"
|
|
88
|
+
else if (b == "listbox") "option"
|
|
89
|
+
else if (or(b == "menu", b == "menubar")) "menuitem"
|
|
90
|
+
else ""
|
|
91
|
+
// ARIA-in-HTML: a <button> may take tab / radio / menuitem but not option
|
|
92
|
+
def fgButtonMayTakeRole(role: str): bool =
|
|
93
|
+
or(or(role == "tab", role == "radio"), role == "menuitem")
|
|
94
|
+
def fgInferredOwnerRole(b: str, ownerKind: str, ownerHasRole: bool): str =
|
|
95
|
+
if (and(ownerKind == "generic", not(ownerHasRole))) b else ""
|
|
96
|
+
def fgInferredItemRole(
|
|
97
|
+
b: str,
|
|
98
|
+
ownerKind: str,
|
|
99
|
+
ownerHasRole: bool,
|
|
100
|
+
itemKind: str,
|
|
101
|
+
itemHasRole: bool
|
|
102
|
+
): str =
|
|
103
|
+
if (or(fgInferredOwnerRole(b, ownerKind, ownerHasRole) == "", itemHasRole)) ""
|
|
104
|
+
else if (itemKind == "generic") fgItemRole(b)
|
|
105
|
+
else if (and(itemKind == "button", fgButtonMayTakeRole(fgItemRole(b)))) fgItemRole(b)
|
|
106
|
+
else ""
|
|
107
|
+
|
|
108
|
+
// --- keys ------------------------------------------------------------------
|
|
109
|
+
def fgInlineDelta(axis: str, key: str): int =
|
|
110
|
+
if (axis == "block") 0 else if (key == "Right") 1 else if (key == "Left") -1 else 0
|
|
111
|
+
def fgBlockDelta(axis: str, key: str): int =
|
|
112
|
+
if (axis == "inline") 0 else if (key == "Down") 1 else if (key == "Up") -1 else 0
|
|
113
|
+
// +1 / -1 for a key along the axis, 0 for a perpendicular or unrelated key
|
|
114
|
+
def fgDelta(axis: str, key: str): int = fgInlineDelta(axis, key) + fgBlockDelta(axis, key)
|
|
115
|
+
def fgIsEdgeKey(key: str): bool = or(key == "Home", key == "End")
|
|
116
|
+
// a key the group would act on; a control that consumes it blocks the group
|
|
117
|
+
def fgIsNavKey(axis: str, key: str): bool = or(fgDelta(axis, key) != 0, fgIsEdgeKey(key))
|
|
118
|
+
|
|
119
|
+
def fgStep(n: int, wrap: bool, f: int, d: int): int =
|
|
120
|
+
if (d == 1) (if (f >= n - 1) (if (wrap) 0 else f) else f + 1)
|
|
121
|
+
else if (d == -1) (if (f <= 0) (if (wrap) n - 1 else f) else f - 1)
|
|
122
|
+
else f
|
|
123
|
+
|
|
124
|
+
// where focus lands after a key; outside the group (f < 0) nothing happens
|
|
125
|
+
def fgLanding(n: int, axis: str, wrap: bool, f: int, key: str): int =
|
|
126
|
+
if (f < 0) f
|
|
127
|
+
else if (key == "Home") 0
|
|
128
|
+
else if (key == "End") n - 1
|
|
129
|
+
else fgStep(n, wrap, f, fgDelta(axis, key))
|
|
130
|
+
|
|
131
|
+
// Tab (back = false) / Shift+Tab (back = true) from a native key-conflict
|
|
132
|
+
// control: the neighbouring item, or -1 when it is the last / first (leave)
|
|
133
|
+
def fgEscape(n: int, f: int, back: bool): int =
|
|
134
|
+
if (back) (if (f <= 0) -1 else f - 1)
|
|
135
|
+
else (if (f >= n - 1) -1 else f + 1)
|
|
136
|
+
|
|
137
|
+
// --- tab stop and memory ---------------------------------------------------
|
|
138
|
+
// last = -1 is an empty memory; start is the `focusgroupstart` item (or 0)
|
|
139
|
+
def fgEntry(memory: bool, last: int, start: int): int =
|
|
140
|
+
if (and(memory, last >= 0)) last else start
|
|
141
|
+
// the tab stop once focus has left the group
|
|
142
|
+
def fgStopOnLeave(memory: bool, current: int, start: int): int =
|
|
143
|
+
if (memory) current else start
|
|
144
|
+
// the rendered tabindex of item i given the tab stop
|
|
145
|
+
def fgTabIndex(stop: int, i: int): int = if (stop == i) 0 else -1
|
|
146
|
+
|
|
147
|
+
// --- invariant predicates (wrap in a `val` in the pattern) -----------------
|
|
148
|
+
def fgStopInRange(n: int, stop: int): bool = and(stop >= 0, stop <= n - 1)
|
|
149
|
+
def fgFocusInRange(n: int, focus: int): bool = and(focus >= -1, focus <= n - 1)
|
|
150
|
+
// roving tabindex: whatever holds focus inside the group is the tab stop
|
|
151
|
+
def fgFocusIsStop(focus: int, stop: int): bool = or(focus < 0, focus == stop)
|
|
152
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// shared/invokers.qnt — the platform contract of invoker commands
|
|
2
|
+
// (`<button commandfor="id" command="...">`) on the element they target.
|
|
3
|
+
//
|
|
4
|
+
// Pure defs only: import with `import invokers.* from "../../shared/invokers"`.
|
|
5
|
+
// Defs are prefixed `invoker`. The target's open flag is passed in and the next
|
|
6
|
+
// one comes back out. For a popover target feed `invokerPopoverEvent(cmd)` to
|
|
7
|
+
// popover.qnt's `popoverOpenAfter`, so a menu-button imports both modules and
|
|
8
|
+
// states only its own delta.
|
|
9
|
+
//
|
|
10
|
+
// Built-in commands (exactly six in the WHATWG source):
|
|
11
|
+
// popover target toggle-popover | show-popover | hide-popover
|
|
12
|
+
// dialog target show-modal | close | request-close
|
|
13
|
+
// custom any `--name`: no state change, only a `command` event
|
|
14
|
+
// on the target (the author's listener decides)
|
|
15
|
+
// A built-in command aimed at the wrong kind of target does nothing (a
|
|
16
|
+
// `show-modal` on a popover is a no-op). A disabled button or a `commandfor`
|
|
17
|
+
// that resolves to no element does nothing. A command that is neither built in
|
|
18
|
+
// nor `--`-prefixed is not delivered (verified).
|
|
19
|
+
//
|
|
20
|
+
// Provided: the activation (click, Enter, Space) -> command mapping and the
|
|
21
|
+
// implicit `aria-expanded` on an invoker of a popover (verified: false closed,
|
|
22
|
+
// true open), no script.
|
|
23
|
+
//
|
|
24
|
+
// NOT provided, still the pattern's: any state beyond open/closed, the
|
|
25
|
+
// `request-close` outcome (the target's `cancel` handler may veto it, so the
|
|
26
|
+
// platform contract leaves `open` unchanged here), `aria-haspopup`, menu keys,
|
|
27
|
+
// `details` commands (none exist yet), focus placement inside the target.
|
|
28
|
+
//
|
|
29
|
+
// Verified in Chromium 153.0.8010.12 (Playwright 1.63.0, native, 2026-10-07):
|
|
30
|
+
// toggle/show/hide-popover (incl. show twice stays open, hide when closed is a
|
|
31
|
+
// no-op), show-modal makes the dialog `:modal`, request-close closes it,
|
|
32
|
+
// `--foo` reaches the target as a `command` event, `nonsense` does not,
|
|
33
|
+
// show-modal on a popover is a no-op. Not verified: `aria-expanded` for dialog
|
|
34
|
+
// invokers (the modal makes the invoker inert).
|
|
35
|
+
// Pin: WHATWG HTML button `command` (Baseline newly 2025-12-12:
|
|
36
|
+
// C135 / F144 / S26.2); polyfill `invokers-polyfill` 1.0.4.
|
|
37
|
+
//
|
|
38
|
+
// Strings are opaque in the supported subset (no prefix test), so the pattern
|
|
39
|
+
// says whether a command is `--`-prefixed via the `custom` flag.
|
|
40
|
+
module invokers {
|
|
41
|
+
def invokerIsPopoverCmd(cmd: str): bool =
|
|
42
|
+
or(or(cmd == "toggle-popover", cmd == "show-popover"), cmd == "hide-popover")
|
|
43
|
+
def invokerIsDialogCmd(cmd: str): bool =
|
|
44
|
+
or(or(cmd == "show-modal", cmd == "close"), cmd == "request-close")
|
|
45
|
+
def invokerIsBuiltin(cmd: str): bool =
|
|
46
|
+
or(invokerIsPopoverCmd(cmd), invokerIsDialogCmd(cmd))
|
|
47
|
+
|
|
48
|
+
// targetKind: "popover" | "dialog" | "other"
|
|
49
|
+
def invokerCmdApplies(cmd: str, targetKind: str): bool =
|
|
50
|
+
or(
|
|
51
|
+
and(invokerIsPopoverCmd(cmd), targetKind == "popover"),
|
|
52
|
+
and(invokerIsDialogCmd(cmd), targetKind == "dialog")
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
// the button fires only when enabled and its commandfor resolves
|
|
56
|
+
def invokerFires(disabled: bool, targetExists: bool): bool =
|
|
57
|
+
and(not(disabled), targetExists)
|
|
58
|
+
|
|
59
|
+
// the `command` event is dispatched on the target for built-ins and `--` names
|
|
60
|
+
def invokerDispatches(cmd: str, custom: bool): bool = or(invokerIsBuiltin(cmd), custom)
|
|
61
|
+
|
|
62
|
+
// the popover.qnt event a popover command stands for
|
|
63
|
+
def invokerPopoverEvent(cmd: str): str =
|
|
64
|
+
if (cmd == "toggle-popover") "toggle"
|
|
65
|
+
else if (cmd == "show-popover") "show"
|
|
66
|
+
else if (cmd == "hide-popover") "hide"
|
|
67
|
+
else ""
|
|
68
|
+
|
|
69
|
+
// open flag of the target after the command (request-close: unchanged, the
|
|
70
|
+
// target's cancel handler decides)
|
|
71
|
+
def invokerOpenAfter(cmd: str, targetKind: str, open: bool): bool =
|
|
72
|
+
if (not(invokerCmdApplies(cmd, targetKind))) open
|
|
73
|
+
else if (cmd == "toggle-popover") not(open)
|
|
74
|
+
else if (or(cmd == "show-popover", cmd == "show-modal")) true
|
|
75
|
+
else if (or(cmd == "hide-popover", cmd == "close")) false
|
|
76
|
+
else open
|
|
77
|
+
|
|
78
|
+
// implicit aria-expanded of a popover invoker; no value for other targets
|
|
79
|
+
def invokerExpandedKnown(cmd: str, targetKind: str): bool =
|
|
80
|
+
and(invokerIsPopoverCmd(cmd), targetKind == "popover")
|
|
81
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// shared/popover.qnt — the platform contract of the `popover` attribute
|
|
2
|
+
// (`:popover-open`, light dismiss, Esc, focus return) for ONE popover.
|
|
3
|
+
//
|
|
4
|
+
// Pure defs only: import with `import popover.* from "../../shared/popover"`.
|
|
5
|
+
// Defs are prefixed `popover`. The open flag is passed in and the next flag
|
|
6
|
+
// comes back out; the pattern owns the var and feeds platform events to it, so
|
|
7
|
+
// a menu-button writes `open' = popoverOpenAfter(kind, open, "outsidePress")`
|
|
8
|
+
// and models only its own delta (menu keys, typeahead, aria-haspopup).
|
|
9
|
+
//
|
|
10
|
+
// Kinds: "auto" (light dismiss, closes unrelated autos and hints), "hint"
|
|
11
|
+
// (light dismiss, closes only other hints, built for tooltips), "manual"
|
|
12
|
+
// (no light dismiss, no Esc, closes nothing).
|
|
13
|
+
//
|
|
14
|
+
// Events a pattern may pass to `popoverOpenAfter`:
|
|
15
|
+
// show | hide | toggle invoker command or showPopover()/hidePopover()
|
|
16
|
+
// esc the close request reached THIS popover (it is
|
|
17
|
+
// the topmost one)
|
|
18
|
+
// outsidePress pointer press outside the popover AND its invoker
|
|
19
|
+
// (a press on the invoker is `toggle`, not a dismiss
|
|
20
|
+
// followed by a re-open)
|
|
21
|
+
// focusOut focus moved elsewhere: never closes a popover
|
|
22
|
+
// showUnrelatedAuto | showUnrelatedHint another popover of that kind opened
|
|
23
|
+
// and is neither an ancestor nor a descendant
|
|
24
|
+
//
|
|
25
|
+
// Provided: `:popover-open` is the open flag; show on an open popover and hide
|
|
26
|
+
// on a closed one are no-ops; focus that was inside returns to the element
|
|
27
|
+
// focused before it opened.
|
|
28
|
+
//
|
|
29
|
+
// NOT provided, still the pattern's: menu semantics, typeahead, hover intent,
|
|
30
|
+
// submenus, aria-haspopup, positioning (anchor positioning is CSS). The invoker
|
|
31
|
+
// button exposes `aria-expanded` implicitly (see invokers.qnt).
|
|
32
|
+
//
|
|
33
|
+
// NOT expressible as pure defs, so NOT modelled: the stack of open popovers.
|
|
34
|
+
// Which popover is topmost, ancestor / descendant relations (a nested auto
|
|
35
|
+
// popover keeps its ancestors open) and "Esc closes only the topmost" need a
|
|
36
|
+
// collection of open popovers, which a pure module cannot carry. A pattern with
|
|
37
|
+
// nested popovers states the relation itself as environment events
|
|
38
|
+
// (`esc` only reaches the topmost, `showUnrelatedAuto` only for non-ancestors).
|
|
39
|
+
// Follow-up of effects-of-atmosphere-dx00 (collections, item 6).
|
|
40
|
+
//
|
|
41
|
+
// Verified in Chromium 153.0.8010.12 (Playwright 1.63.0, native, 2026-10-07):
|
|
42
|
+
// toggle/show/hide, Esc with focus inside returns focus to the invoker, outside
|
|
43
|
+
// click closes an auto popover and focus goes to the click target, Tab or
|
|
44
|
+
// script focus moving outside does not close, `manual` ignores Esc and outside
|
|
45
|
+
// press, opening an unrelated auto closes the other, a nested child keeps its
|
|
46
|
+
// ancestor, Esc closes only the topmost, `hint` stays alongside an open auto,
|
|
47
|
+
// a second hint closes the first, showing an auto closes an open hint.
|
|
48
|
+
// Pin: WHATWG HTML popover API (Baseline newly: C114 / F125 / S17); `hint`
|
|
49
|
+
// C151 / F153 per docs/research/apg-platform-features.md.
|
|
50
|
+
module popover {
|
|
51
|
+
def popoverKindKnown(kind: str): bool =
|
|
52
|
+
or(or(kind == "auto", kind == "hint"), kind == "manual")
|
|
53
|
+
|
|
54
|
+
// Esc and an outside press dismiss every kind but manual
|
|
55
|
+
def popoverLightDismisses(kind: str): bool = kind != "manual"
|
|
56
|
+
|
|
57
|
+
def popoverOpenAfter(kind: str, open: bool, ev: str): bool =
|
|
58
|
+
if (ev == "show") true
|
|
59
|
+
else if (ev == "hide") false
|
|
60
|
+
else if (ev == "toggle") not(open)
|
|
61
|
+
else if (or(ev == "esc", ev == "outsidePress"))
|
|
62
|
+
(if (popoverLightDismisses(kind)) false else open)
|
|
63
|
+
else if (ev == "showUnrelatedAuto") (if (kind == "manual") open else false)
|
|
64
|
+
else if (ev == "showUnrelatedHint") (if (kind == "hint") false else open)
|
|
65
|
+
else open
|
|
66
|
+
|
|
67
|
+
// does closing by this event hand focus back to the pre-open element?
|
|
68
|
+
// (only when focus was inside; a pointer press moves focus to its target)
|
|
69
|
+
def popoverReturnsFocus(ev: str, focusInside: bool): bool =
|
|
70
|
+
and(focusInside, or(or(ev == "hide", ev == "toggle"), ev == "esc"))
|
|
71
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# APG static patterns — ARIA contracts
|
|
2
|
+
|
|
3
|
+
Patterns covered by `effects-of-atmosphere-3wsk`: alert, breadcrumb, landmarks, link, meter, table.
|
|
4
|
+
|
|
5
|
+
The rule for this workspace is _behaviour first_: a Quint state machine exists only where there is state that changes over time. This file holds everything else — the **static role/attribute contract** that a DOM test can assert, as a checklist, with the reason a pattern has (or has no) machine.
|
|
6
|
+
|
|
7
|
+
| Pattern | Machine | Why |
|
|
8
|
+
| ---------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
|
|
9
|
+
| alert | [`alert.qnt`](./alert.qnt) | announcement-after-load, no focus theft, no auto-dismiss are all about change over time |
|
|
10
|
+
| breadcrumb | [`breadcrumb.qnt`](./breadcrumb.qnt) | `aria-current` must follow the last item as the trail changes |
|
|
11
|
+
| link | [`link.qnt`](./link.qnt) | Enter activation + focus move; non-native links must stay keyboard reachable |
|
|
12
|
+
| meter | [`meter.qnt`](./meter.qnt) | clamping, range changes and `aria-valuetext` freshness are derived state |
|
|
13
|
+
| landmarks | none | purely structural; no states, no events, no keyboard interaction. Correctness is a property of the DOM tree |
|
|
14
|
+
| table | none | "a static tabular structure … not an interactive widget"; the only dynamic bit (`aria-sort`) is data |
|
|
15
|
+
|
|
16
|
+
Each machine's invariants (`val …` in the `.qnt`) cover its dynamic rules. The sections for those four below list only the **static** remainder the machine does not model.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## landmarks (no machine)
|
|
21
|
+
|
|
22
|
+
APG: <https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/> · practice: <https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/>
|
|
23
|
+
|
|
24
|
+
Keyboard interaction: not applicable (navigation among landmarks is provided by the browser / assistive technology).
|
|
25
|
+
|
|
26
|
+
Why no machine: a landmark is a role on a section of the page. There is no state to hold, no transition, and nothing to replay; a machine would only restate the DOM.
|
|
27
|
+
|
|
28
|
+
Checklist (assert on the rendered document):
|
|
29
|
+
|
|
30
|
+
- [ ] Every landmark is one of the eight roles: `banner`, `navigation`, `main`, `complementary`, `contentinfo`, `search`, `form`, `region` — either explicit `role=…` or the implied HTML element (`header`, `nav`, `main`, `aside`, `footer`, `form`, `section`).
|
|
31
|
+
- [ ] The page has exactly one `main` landmark.
|
|
32
|
+
- [ ] Top-level `banner` and `contentinfo` appear at most once each, and are not nested inside another landmark (`header`/`footer` inside `article`/`section` do not map to them).
|
|
33
|
+
- [ ] When the page has **more than one** landmark of the same role (e.g. two `navigation`s), each carries a distinct accessible name (`aria-labelledby` preferred when a visible label exists, otherwise `aria-label`). The name does not repeat the role ("Main navigation", not "Main navigation navigation").
|
|
34
|
+
- [ ] `form`, `region` and (when named) `search` landmarks have an accessible name — an unnamed `form` or `region` is not exposed as a landmark.
|
|
35
|
+
- [ ] A `search` landmark wraps the whole search facility (input and button), not just the input.
|
|
36
|
+
- [ ] All perceivable content sits inside some landmark (rule of thumb from the APG).
|
|
37
|
+
- [ ] Landmark count stays small — the APG rule of thumb is seven or fewer; flag the page above that.
|
|
38
|
+
- [ ] No landmark is hidden (`hidden`, `display: none`, `aria-hidden=true`) while its content stays reachable.
|
|
39
|
+
|
|
40
|
+
## table (no machine)
|
|
41
|
+
|
|
42
|
+
APG: <https://www.w3.org/WAI/ARIA/apg/patterns/table/> · practice: <https://www.w3.org/WAI/ARIA/apg/practices/grid-and-table-properties/>
|
|
43
|
+
|
|
44
|
+
Keyboard interaction: not applicable. Cells are not focusable or selectable; each widget inside a cell is its own tab stop. An interactive tabular widget is the **grid** pattern, a different spec.
|
|
45
|
+
|
|
46
|
+
Why no machine: a table is static structure. Sorting is the one dynamic aspect and it is plain data (`aria-sort` follows the sorted column) — if a sortable table gains real behaviour (sort cycle, focus on the header button) model that as its own sortable-table machine, not here.
|
|
47
|
+
|
|
48
|
+
Checklist:
|
|
49
|
+
|
|
50
|
+
- [ ] The container has `role=table` (or is a native `<table>`; a native element is strongly preferred and needs no role).
|
|
51
|
+
- [ ] Every row has `role=row` and is a DOM descendant of, or owned (`aria-owns`) by, the `table` or by an element with `role=rowgroup`.
|
|
52
|
+
- [ ] Every cell has exactly one of `columnheader`, `rowheader`, `cell`, and is a descendant of / owned by a `row`:
|
|
53
|
+
- [ ] `columnheader` for a cell that titles its column,
|
|
54
|
+
- [ ] `rowheader` for a cell that titles its row,
|
|
55
|
+
- [ ] `cell` otherwise.
|
|
56
|
+
- [ ] The table has an accessible name: `aria-labelledby` pointing at a visible label if one exists, otherwise `aria-label`.
|
|
57
|
+
- [ ] If the table has a caption or description, `aria-describedby` on the table refers to it.
|
|
58
|
+
- [ ] Sortable table: at most one header has `aria-sort`, and its value is `ascending`, `descending` (or `other`); unsorted headers carry no `aria-sort="none"` noise unless the design needs it.
|
|
59
|
+
- [ ] If any rows or columns are absent from the DOM (virtualised or hidden): `aria-rowcount` / `aria-colcount` hold the **total** count (`-1` when unknown), and every row/cell present has `aria-rowindex` / `aria-colindex` with its 1-based position.
|
|
60
|
+
- [ ] Cells spanning several rows or columns carry `aria-rowspan` / `aria-colspan`.
|
|
61
|
+
- [ ] Rows or cells added through `aria-owns` are not forgotten in the owner's `aria-owns` list — owned elements are announced after the DOM descendants.
|
|
62
|
+
- [ ] No cell has `tabindex` or a selection state (`aria-selected`): that would make it a grid.
|
|
63
|
+
- [ ] Interactive content inside cells (buttons, links) is individually focusable; the cell itself is not.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## alert — static remainder (machine: `alert.qnt`)
|
|
68
|
+
|
|
69
|
+
APG: <https://www.w3.org/WAI/ARIA/apg/patterns/alert/> · keyboard: not applicable.
|
|
70
|
+
|
|
71
|
+
- [ ] The element carrying the message has `role=alert` (implicit `aria-live=assertive`, `aria-atomic=true`).
|
|
72
|
+
- [ ] The alert has no `tabindex`, and showing it never calls `focus()` — needing focus means the alertdialog pattern instead.
|
|
73
|
+
- [ ] For a message that must be announced, the `role=alert` container is rendered **empty first** (or the node is inserted/changed after load); content present at load time is not spoken.
|
|
74
|
+
- [ ] No timer removes the alert; any dismiss control is an ordinary button in the tab order.
|
|
75
|
+
- [ ] Machine invariants: `FocusNeverOnAlert`, `PreloadedNotAnnounced`, `LiveMessageAnnounced`, `SilentBeforeLoad`, `RoleWhenMounted`.
|
|
76
|
+
|
|
77
|
+
## breadcrumb — static remainder (machine: `breadcrumb.qnt`)
|
|
78
|
+
|
|
79
|
+
APG: <https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/> · keyboard: not applicable (ordinary links).
|
|
80
|
+
|
|
81
|
+
- [ ] The trail is inside a `navigation` landmark (`<nav>`).
|
|
82
|
+
- [ ] The landmark has a label via `aria-label` or `aria-labelledby` (e.g. "Breadcrumb").
|
|
83
|
+
- [ ] The trail is an ordered list (`<ol>`) of items, ancestors first, current page last.
|
|
84
|
+
- [ ] Every ancestor item is a link; the current page item is last.
|
|
85
|
+
- [ ] Machine invariants: `CurrentIsLastOrAbsent`, `LinkCurrentIsMarked` (`aria-current="page"` on the current-page link; optional on a non-link current item).
|
|
86
|
+
|
|
87
|
+
## link — static remainder (machine: `link.qnt`)
|
|
88
|
+
|
|
89
|
+
APG: <https://www.w3.org/WAI/ARIA/apg/patterns/link/> · keyboard: Enter executes and moves focus to the target; Shift+F10 (optional) opens the context menu.
|
|
90
|
+
|
|
91
|
+
- [ ] Prefer a native `<a href>`; it needs no role, `tabindex`, or key handler.
|
|
92
|
+
- [ ] A non-native element has `role=link`, `tabindex=0`, an accessible name from its content (or `aria-label`), and a click **and** Enter key handler. Space does not activate it.
|
|
93
|
+
- [ ] Machine invariants: `KeyboardReachable`, `FocusOnLinkImpliesFocusable`, `TargetFocusImpliesArrived`, `ArrivedImpliesActivated`.
|
|
94
|
+
|
|
95
|
+
## meter — static remainder (machine: `meter.qnt`)
|
|
96
|
+
|
|
97
|
+
APG: <https://www.w3.org/WAI/ARIA/apg/patterns/meter/> · keyboard: not applicable.
|
|
98
|
+
|
|
99
|
+
- [ ] The element has `role=meter` (or a native `<meter>`), and is not used for progress (`progressbar`) or for unbounded values.
|
|
100
|
+
- [ ] `aria-valuenow`, `aria-valuemin`, `aria-valuemax` are decimals with `min < max` and `min <= now <= max`.
|
|
101
|
+
- [ ] `aria-valuetext` is set when a bare percentage is not understandable (e.g. `50% (6 hours) remaining`).
|
|
102
|
+
- [ ] Visible label → `aria-labelledby` on the meter; otherwise `aria-label`.
|
|
103
|
+
- [ ] Machine invariants: `RangeValid`, `NowInRange`, `NowIsClampedRaw`, `TextWhenNotFriendly`, `TextNeverStale`.
|