@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,364 @@
|
|
|
1
|
+
// APG Toolbar — https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/
|
|
2
|
+
//
|
|
3
|
+
// A toolbar of five items behind ONE tab stop. Items are addressed by index
|
|
4
|
+
// 0..4; their kinds are fixed by the spec:
|
|
5
|
+
// 0 menu-button (a button that opens a 3-item menu) 1 button
|
|
6
|
+
// 2 text (native <input type=text>) 3 spin (custom role=spinbutton with its
|
|
7
|
+
// own key handler) 4 button
|
|
8
|
+
//
|
|
9
|
+
// The roving-tabindex part is the platform's `focusgroup="toolbar"` contract,
|
|
10
|
+
// imported from shared/focusgroup.qnt and NOT restated here: single tab stop,
|
|
11
|
+
// arrows along the axis, wrap, Home / End, last-focused memory, and Tab /
|
|
12
|
+
// Shift+Tab stepping to the neighbouring item out of a native key-conflict
|
|
13
|
+
// control. This spec models only the delta:
|
|
14
|
+
// - the axis comes from aria-orientation (the focusgroup axis does not set it)
|
|
15
|
+
// and the wrap flag is the author's choice
|
|
16
|
+
// - disabled-but-focusable: a disabled item stays reachable by arrows, click
|
|
17
|
+
// and Tab (aria-disabled; a native `disabled` button would be SKIPPED by the
|
|
18
|
+
// platform, which is the gap the shared module records); it just cannot be
|
|
19
|
+
// activated
|
|
20
|
+
// - embedded controls consume keys: `text` swallows every key the group would
|
|
21
|
+
// act on (Left/Right/Home/End horizontally, Up/Down/Home/End vertically),
|
|
22
|
+
// `spin` swallows Up/Down/Home/End (its handler prevents default). A
|
|
23
|
+
// swallowed key never moves focus; Tab / Shift+Tab is the way on: `text`
|
|
24
|
+
// is a native key-conflict control, so Tab steps to the neighbouring item
|
|
25
|
+
// (focus leaves the toolbar only at either end), `spin` is not, so Tab
|
|
26
|
+
// leaves
|
|
27
|
+
// - activation of an enabled button
|
|
28
|
+
// - the menu-button kind below
|
|
29
|
+
//
|
|
30
|
+
// Menu button (item 0). The APG Toolbar pattern does NOT define a menu button's
|
|
31
|
+
// open key. It only says the arrows perpendicular to the toolbar axis "can be
|
|
32
|
+
// reserved for operating controls" ("Keyboard Interaction",
|
|
33
|
+
// https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/), and that a vertical
|
|
34
|
+
// toolbar navigates with Up/Down. So the open key is settled as:
|
|
35
|
+
// - Enter / Space open the menu on the FIRST item;
|
|
36
|
+
// - the key PERPENDICULAR to the toolbar axis also opens, and the end it opens
|
|
37
|
+
// on is the sign of that key: the forward perpendicular key opens on the
|
|
38
|
+
// first item, the backward one on the last. Horizontal toolbar: Down opens
|
|
39
|
+
// on the first, Up on the last. Vertical toolbar: Right opens on the first,
|
|
40
|
+
// Left on the last -- Down/Up stay the toolbar's own navigation keys, which
|
|
41
|
+
// is exactly why the vertical case needs its own rule.
|
|
42
|
+
// Once open, the menu owns the keyboard (APG menubar, the behaviour
|
|
43
|
+
// menu-button.qnt cites): Up/Down wrap within the menu, Home/End jump,
|
|
44
|
+
// Enter/Space activate and close returning focus to the trigger, Escape closes
|
|
45
|
+
// and returns focus to the trigger, Tab closes and leaves the toolbar. The
|
|
46
|
+
// toolbar's roving focus is suspended: the trigger keeps the tab stop, and the
|
|
47
|
+
// menu CAPTURES every navigation key through the same `captures` / `swallowed`
|
|
48
|
+
// mechanism the embedded controls use, so the keys never move the toolbar.
|
|
49
|
+
//
|
|
50
|
+
// Why item 0 is the menu-button: the ported base-ui scenario
|
|
51
|
+
// `toolbar.base-ui.menu-trigger-consumes-arrows` renders its single menu trigger
|
|
52
|
+
// as the toolbar's item 0, so making the spec's item 0 the menu-button lets the
|
|
53
|
+
// scenario's `item:0` / `menuitem:N` mapping replay honestly. The menu is the
|
|
54
|
+
// scenario's three items (menuitem 0..2).
|
|
55
|
+
//
|
|
56
|
+
// Events (user level): tabIn, tab(back), leave, click(item), key(Left|Right|Up|
|
|
57
|
+
// Down|Home|End), activate (Enter/Space), escape, toggleDisabled(item),
|
|
58
|
+
// flipOrientation, flipWrap. `tabIn` / `leave` are the plain enter / focus-lost
|
|
59
|
+
// events; `tab` is the real Tab / Shift+Tab key from wherever focus is.
|
|
60
|
+
// `activations` counts activations of enabled buttons; `consumed` counts keys
|
|
61
|
+
// swallowed by an embedded control or by the open menu. The rendered tabindex of
|
|
62
|
+
// item i is `fgTabIndex(tabStop, i)`.
|
|
63
|
+
//
|
|
64
|
+
// Conformance: seeded random walks of this model (40 walks x 40 events, every
|
|
65
|
+
// event kind) were replayed against a real DOM toolbar in Chromium
|
|
66
|
+
// 153.0.8010.12, once with native `focusgroup` and once with
|
|
67
|
+
// @microsoft/focusgroup-polyfill 1.6.0 forced: 1600 events each, focus matched
|
|
68
|
+
// the spec after every one (script kept outside the repo; see
|
|
69
|
+
// shared/focusgroup.qnt for the provider divergences).
|
|
70
|
+
//
|
|
71
|
+
// ponytail: item count and kinds are fixed, not configurable; grouping
|
|
72
|
+
// separators, RTL (Left/Right swap), the menu-button's own menu activation and
|
|
73
|
+
// the live value of the embedded controls are not modelled. Widen the pools if a
|
|
74
|
+
// consumer needs them.
|
|
75
|
+
module apg_toolbar {
|
|
76
|
+
import aria.* from "../../shared/aria"
|
|
77
|
+
import focusgroup.* from "../../shared/focusgroup"
|
|
78
|
+
|
|
79
|
+
var orientation: str // "horizontal" | "vertical" (aria-orientation)
|
|
80
|
+
var wrap: bool // arrows wrap around the ends
|
|
81
|
+
var focus: int // focused item, -1 = focus is outside the toolbar
|
|
82
|
+
var tabStop: int // the item that owns tabindex=0 (focus memory)
|
|
83
|
+
var disabledMask: int // bit i set = item i is aria-disabled
|
|
84
|
+
var activations: int // activations of enabled buttons
|
|
85
|
+
var lastActivated: int // item of the last activation, -1 = none
|
|
86
|
+
var consumed: int // keys swallowed by an embedded control or the menu
|
|
87
|
+
var lastKeyConsumed: bool // the previous event was a key an embedded control swallowed
|
|
88
|
+
var menuOpen: bool // the menu-button's menu is displayed (aria-expanded)
|
|
89
|
+
var menuIndex: int // focused menu item while open, 0 otherwise
|
|
90
|
+
|
|
91
|
+
def itemCount: int = 5
|
|
92
|
+
def menuButton: int = 0
|
|
93
|
+
def menuCount: int = 3
|
|
94
|
+
def bit(i: int): int =
|
|
95
|
+
if (i == 0) 1 else if (i == 1) 2 else if (i == 2) 4 else if (i == 3) 8 else if (i == 4) 16 else 0
|
|
96
|
+
def isDisabledIn(mask: int, i: int): bool = (mask / bit(i)) % 2 == 1
|
|
97
|
+
def kindOf(i: int): str =
|
|
98
|
+
if (i == menuButton) "menu-button" else if (i == 2) "text" else if (i == 3) "spin" else "button"
|
|
99
|
+
def isMenuButton(i: int): bool = i == menuButton
|
|
100
|
+
def axisOf(o: str): str = if (o == "horizontal") "inline" else "block"
|
|
101
|
+
|
|
102
|
+
// the axis perpendicular to the toolbar: the axis the open keys live on
|
|
103
|
+
def perpAxis(o: str): str = if (o == "horizontal") "block" else "inline"
|
|
104
|
+
def perpDelta(o: str, key: str): int = fgDelta(perpAxis(o), key)
|
|
105
|
+
def perpKey(o: str, key: str): bool = perpDelta(o, key) != 0
|
|
106
|
+
def opensFirst(o: str, key: str): bool = perpDelta(o, key) == 1
|
|
107
|
+
def opensLast(o: str, key: str): bool = perpDelta(o, key) == -1
|
|
108
|
+
|
|
109
|
+
// the menu is a flat block list: Up/Down wrap, Home/End jump
|
|
110
|
+
def menuNavKey(key: str): bool = fgIsNavKey("block", key)
|
|
111
|
+
def menuLanding(key: str, idx: int): int = fgLanding(menuCount, "block", true, idx, key)
|
|
112
|
+
|
|
113
|
+
// does the embedded control -- or the open menu -- capture the key? A native
|
|
114
|
+
// text field consumes every key the group would act on; the spinbutton's own
|
|
115
|
+
// handler prevents default for Up/Down/Home/End; while the menu is open the
|
|
116
|
+
// menu owns every navigation key.
|
|
117
|
+
def captures(kind: str, o: str, key: str, open: bool): bool =
|
|
118
|
+
or(
|
|
119
|
+
and(kind == "text", fgIsNavKey(axisOf(o), key)),
|
|
120
|
+
and(kind == "spin", or(or(key == "Up", key == "Down"), fgIsEdgeKey(key))),
|
|
121
|
+
and(kind == "menu-button", open)
|
|
122
|
+
)
|
|
123
|
+
def swallowed(o: str, f: int, key: str, open: bool): bool =
|
|
124
|
+
and(f >= 0, captures(kindOf(f), o, key, open))
|
|
125
|
+
|
|
126
|
+
// where focus lands after a key: a captured key pins it, otherwise the
|
|
127
|
+
// platform contract decides
|
|
128
|
+
def landing(o: str, w: bool, f: int, key: str, open: bool): int =
|
|
129
|
+
if (swallowed(o, f, key, open)) f else fgLanding(itemCount, axisOf(o), w, f, key)
|
|
130
|
+
|
|
131
|
+
// where the menu focus goes after a key while the menu is open; also the item
|
|
132
|
+
// a perpendicular key opens on when the menu-button is focused and closed
|
|
133
|
+
def menuKeyTarget(o: str, k: str): int =
|
|
134
|
+
if (menuOpen) (if (menuNavKey(k)) menuLanding(k, menuIndex) else menuIndex)
|
|
135
|
+
else if (and(isMenuButton(focus), perpKey(o, k)))
|
|
136
|
+
(if (opensLast(o, k)) menuCount - 1 else 0)
|
|
137
|
+
else 0
|
|
138
|
+
|
|
139
|
+
// where Tab (back = false) / Shift+Tab (back = true) puts focus: an open menu
|
|
140
|
+
// closes and leaves; outside it enters on the remembered item; a native text
|
|
141
|
+
// field steps to its neighbour; any other item leaves the toolbar
|
|
142
|
+
def tabLanding(f: int, stop: int, back: bool, open: bool): int =
|
|
143
|
+
if (open) -1
|
|
144
|
+
else if (f < 0) fgEntry(true, stop, 0)
|
|
145
|
+
else if (kindOf(f) == "text") fgEscape(itemCount, f, back)
|
|
146
|
+
else -1
|
|
147
|
+
|
|
148
|
+
def activates(mask: int, i: int): bool =
|
|
149
|
+
and(and(i >= 0, kindOf(i) == "button"), not(isDisabledIn(mask, i)))
|
|
150
|
+
def flip(mask: int, i: int): int =
|
|
151
|
+
if (isDisabledIn(mask, i)) mask - bit(i) else mask + bit(i)
|
|
152
|
+
|
|
153
|
+
// ARIA contract: aria-expanded on the menu-button item
|
|
154
|
+
def ariaExpandedMenuButton(open: bool): bool = open
|
|
155
|
+
|
|
156
|
+
action init = all {
|
|
157
|
+
orientation' = "horizontal",
|
|
158
|
+
wrap' = false,
|
|
159
|
+
focus' = -1,
|
|
160
|
+
tabStop' = 0,
|
|
161
|
+
disabledMask' = 0,
|
|
162
|
+
activations' = 0,
|
|
163
|
+
lastActivated' = -1,
|
|
164
|
+
consumed' = 0,
|
|
165
|
+
lastKeyConsumed' = false,
|
|
166
|
+
menuOpen' = false,
|
|
167
|
+
menuIndex' = 0
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Tab / Shift+Tab into the toolbar lands on the remembered item
|
|
171
|
+
action tabIn = all {
|
|
172
|
+
orientation' = orientation,
|
|
173
|
+
wrap' = wrap,
|
|
174
|
+
focus' = if (focus < 0) fgEntry(true, tabStop, 0) else focus,
|
|
175
|
+
tabStop' = tabStop,
|
|
176
|
+
disabledMask' = disabledMask,
|
|
177
|
+
activations' = activations,
|
|
178
|
+
lastActivated' = lastActivated,
|
|
179
|
+
consumed' = consumed,
|
|
180
|
+
lastKeyConsumed' = false,
|
|
181
|
+
menuOpen' = menuOpen,
|
|
182
|
+
menuIndex' = menuIndex
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// focus lost elsewhere (pointer, script): the memory stays, an open menu closes
|
|
186
|
+
action leave = all {
|
|
187
|
+
orientation' = orientation,
|
|
188
|
+
wrap' = wrap,
|
|
189
|
+
focus' = -1,
|
|
190
|
+
tabStop' = fgStopOnLeave(true, tabStop, 0),
|
|
191
|
+
disabledMask' = disabledMask,
|
|
192
|
+
activations' = activations,
|
|
193
|
+
lastActivated' = lastActivated,
|
|
194
|
+
consumed' = consumed,
|
|
195
|
+
lastKeyConsumed' = false,
|
|
196
|
+
menuOpen' = false,
|
|
197
|
+
menuIndex' = 0
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// the Tab key itself: an open menu closes and leaves; otherwise enter, step out
|
|
201
|
+
// of a native text field to the neighbouring item, or leave
|
|
202
|
+
action tab = {
|
|
203
|
+
nondet back = Set(false, true).oneOf()
|
|
204
|
+
all {
|
|
205
|
+
orientation' = orientation,
|
|
206
|
+
wrap' = wrap,
|
|
207
|
+
focus' = tabLanding(focus, tabStop, back, menuOpen),
|
|
208
|
+
tabStop' = if (tabLanding(focus, tabStop, back, menuOpen) >= 0) tabLanding(focus, tabStop, back, menuOpen) else tabStop,
|
|
209
|
+
disabledMask' = disabledMask,
|
|
210
|
+
activations' = activations,
|
|
211
|
+
lastActivated' = lastActivated,
|
|
212
|
+
consumed' = consumed,
|
|
213
|
+
lastKeyConsumed' = false,
|
|
214
|
+
menuOpen' = false,
|
|
215
|
+
menuIndex' = 0
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// pointer: while the menu is open a toolbar click closes it (light dismiss);
|
|
220
|
+
// otherwise it focuses the item (even a disabled one) and makes it the tab
|
|
221
|
+
// stop, opens the menu-button's menu, and activates an enabled button
|
|
222
|
+
action click = {
|
|
223
|
+
nondet idx = Set(0, 1, 2, 3, 4).oneOf()
|
|
224
|
+
all {
|
|
225
|
+
orientation' = orientation,
|
|
226
|
+
wrap' = wrap,
|
|
227
|
+
focus' = if (menuOpen) focus else idx,
|
|
228
|
+
tabStop' = if (menuOpen) tabStop else idx,
|
|
229
|
+
disabledMask' = disabledMask,
|
|
230
|
+
activations' = if (and(not(menuOpen), activates(disabledMask, idx))) activations + 1 else activations,
|
|
231
|
+
lastActivated' = if (and(not(menuOpen), activates(disabledMask, idx))) idx else lastActivated,
|
|
232
|
+
consumed' = consumed,
|
|
233
|
+
lastKeyConsumed' = false,
|
|
234
|
+
menuOpen' = if (menuOpen) false else isMenuButton(idx),
|
|
235
|
+
menuIndex' = 0
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
action key = {
|
|
240
|
+
nondet k = Set("Left", "Right", "Up", "Down", "Home", "End").oneOf()
|
|
241
|
+
all {
|
|
242
|
+
orientation' = orientation,
|
|
243
|
+
wrap' = wrap,
|
|
244
|
+
focus' = landing(orientation, wrap, focus, k, menuOpen),
|
|
245
|
+
tabStop' = if (focus < 0) tabStop else landing(orientation, wrap, focus, k, menuOpen),
|
|
246
|
+
disabledMask' = disabledMask,
|
|
247
|
+
activations' = activations,
|
|
248
|
+
lastActivated' = lastActivated,
|
|
249
|
+
consumed' = if (swallowed(orientation, focus, k, menuOpen)) consumed + 1 else consumed,
|
|
250
|
+
lastKeyConsumed' = swallowed(orientation, focus, k, menuOpen),
|
|
251
|
+
menuOpen' = if (menuOpen) true else and(isMenuButton(focus), perpKey(orientation, k)),
|
|
252
|
+
menuIndex' = menuKeyTarget(orientation, k)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Enter / Space: on the closed menu-button it opens the menu on the first item;
|
|
257
|
+
// while the menu is open it closes the menu and returns focus to the trigger;
|
|
258
|
+
// otherwise it activates the focused enabled button
|
|
259
|
+
action activate = all {
|
|
260
|
+
orientation' = orientation,
|
|
261
|
+
wrap' = wrap,
|
|
262
|
+
focus' = if (menuOpen) menuButton else focus,
|
|
263
|
+
tabStop' = tabStop,
|
|
264
|
+
disabledMask' = disabledMask,
|
|
265
|
+
activations' = if (and(not(menuOpen), activates(disabledMask, focus))) activations + 1 else activations,
|
|
266
|
+
lastActivated' = if (and(not(menuOpen), activates(disabledMask, focus))) focus else lastActivated,
|
|
267
|
+
consumed' = consumed,
|
|
268
|
+
lastKeyConsumed' = false,
|
|
269
|
+
menuOpen' = if (menuOpen) false else isMenuButton(focus),
|
|
270
|
+
menuIndex' = 0
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Escape: while the menu is open it closes and returns focus to the trigger
|
|
274
|
+
action escape = all {
|
|
275
|
+
orientation' = orientation,
|
|
276
|
+
wrap' = wrap,
|
|
277
|
+
focus' = if (menuOpen) menuButton else focus,
|
|
278
|
+
tabStop' = tabStop,
|
|
279
|
+
disabledMask' = disabledMask,
|
|
280
|
+
activations' = activations,
|
|
281
|
+
lastActivated' = lastActivated,
|
|
282
|
+
consumed' = consumed,
|
|
283
|
+
lastKeyConsumed' = false,
|
|
284
|
+
menuOpen' = false,
|
|
285
|
+
menuIndex' = 0
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// disabling never moves focus or the tab stop: the idx stays focusable
|
|
289
|
+
action toggleDisabled = {
|
|
290
|
+
nondet idx = Set(0, 1, 2, 3, 4).oneOf()
|
|
291
|
+
all {
|
|
292
|
+
orientation' = orientation,
|
|
293
|
+
wrap' = wrap,
|
|
294
|
+
focus' = focus,
|
|
295
|
+
tabStop' = tabStop,
|
|
296
|
+
disabledMask' = flip(disabledMask, idx),
|
|
297
|
+
activations' = activations,
|
|
298
|
+
lastActivated' = lastActivated,
|
|
299
|
+
consumed' = consumed,
|
|
300
|
+
lastKeyConsumed' = false,
|
|
301
|
+
menuOpen' = menuOpen,
|
|
302
|
+
menuIndex' = menuIndex
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
action flipOrientation = all {
|
|
307
|
+
orientation' = if (orientation == "horizontal") "vertical" else "horizontal",
|
|
308
|
+
wrap' = wrap,
|
|
309
|
+
focus' = focus,
|
|
310
|
+
tabStop' = tabStop,
|
|
311
|
+
disabledMask' = disabledMask,
|
|
312
|
+
activations' = activations,
|
|
313
|
+
lastActivated' = lastActivated,
|
|
314
|
+
consumed' = consumed,
|
|
315
|
+
lastKeyConsumed' = false,
|
|
316
|
+
menuOpen' = menuOpen,
|
|
317
|
+
menuIndex' = menuIndex
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
action flipWrap = all {
|
|
321
|
+
orientation' = orientation,
|
|
322
|
+
wrap' = not(wrap),
|
|
323
|
+
focus' = focus,
|
|
324
|
+
tabStop' = tabStop,
|
|
325
|
+
disabledMask' = disabledMask,
|
|
326
|
+
activations' = activations,
|
|
327
|
+
lastActivated' = lastActivated,
|
|
328
|
+
consumed' = consumed,
|
|
329
|
+
lastKeyConsumed' = false,
|
|
330
|
+
menuOpen' = menuOpen,
|
|
331
|
+
menuIndex' = menuIndex
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
action step = any {
|
|
335
|
+
init, tabIn, tab, leave, click, key, activate, escape, toggleDisabled, flipOrientation, flipWrap
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
def tabbable(i: int): int = if (fgTabIndex(tabStop, i) == 0) 1 else 0
|
|
339
|
+
|
|
340
|
+
// APG: exactly one item is in the tab sequence, and it is the remembered one
|
|
341
|
+
val ExactlyOneTabbable: bool =
|
|
342
|
+
tabbable(0) + tabbable(1) + tabbable(2) + tabbable(3) + tabbable(4) == 1
|
|
343
|
+
// focus memory always points at an existing item
|
|
344
|
+
val TabStopInRange: bool = fgStopInRange(itemCount, tabStop)
|
|
345
|
+
// focus is outside the toolbar or on an item that is in range
|
|
346
|
+
val FocusInRange: bool = fgFocusInRange(itemCount, focus)
|
|
347
|
+
// roving tabindex: whatever holds focus inside the toolbar is the tab stop
|
|
348
|
+
val FocusIsTabStop: bool = fgFocusIsStop(focus, tabStop)
|
|
349
|
+
val DisabledMaskValid: bool = and(disabledMask >= 0, disabledMask <= 31)
|
|
350
|
+
val OrientationKnown: bool = ariaOrientationKnown(orientation)
|
|
351
|
+
// only an enabled-at-the-time button is ever activated
|
|
352
|
+
val OnlyButtonsActivate: bool = or(lastActivated < 0, kindOf(lastActivated) == "button")
|
|
353
|
+
val ActivationLogValid: bool = and(lastActivated >= -1, lastActivated <= 4)
|
|
354
|
+
// a swallowed key leaves focus on the embedded control (or the menu-button)
|
|
355
|
+
val ConsumedStaysEmbedded: bool = or(not(lastKeyConsumed), and(focus >= 0, kindOf(focus) != "button"))
|
|
356
|
+
val CountersNonNegative: bool = and(activations >= 0, consumed >= 0)
|
|
357
|
+
// while the menu is open the roving focus is suspended on the menu-button item
|
|
358
|
+
val MenuOpenImpliesMenuButtonFocused: bool = or(not(menuOpen), focus == menuButton)
|
|
359
|
+
// the menu index is a real menu item while open, and 0 while closed
|
|
360
|
+
val MenuIndexInRange: bool =
|
|
361
|
+
if (menuOpen) and(menuIndex >= 0, menuIndex <= menuCount - 1) else menuIndex == 0
|
|
362
|
+
// aria-expanded on the menu-button item is true exactly while the menu is open
|
|
363
|
+
val MenuButtonExpandedIffOpen: bool = ariaExpandedMenuButton(menuOpen) == menuOpen
|
|
364
|
+
}
|