@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,531 @@
|
|
|
1
|
+
// WAI-ARIA APG — Menu and Menubar: roving focus, nested submenus, typeahead.
|
|
2
|
+
//
|
|
3
|
+
// Pattern: https://www.w3.org/WAI/ARIA/apg/patterns/menubar/
|
|
4
|
+
// - "Keyboard Interaction": Tab / Shift+Tab, Enter, Space, Down/Up/Right/Left
|
|
5
|
+
// Arrow, Home/End, printable-character typeahead, Escape, and the rule that
|
|
6
|
+
// Right/Left Arrow on a menubar item with a submenu open it "without moving
|
|
7
|
+
// focus into the submenu" (the Recommended variant, modelled here).
|
|
8
|
+
// - "WAI-ARIA Roles, States, and Properties": aria-haspopup / aria-expanded on
|
|
9
|
+
// parent menuitems, roving tabindex (one menubar item has tabindex=0).
|
|
10
|
+
// Examples: https://www.w3.org/WAI/ARIA/apg/patterns/menubar/examples/menubar-editor/
|
|
11
|
+
// https://www.w3.org/WAI/ARIA/apg/patterns/menubar/examples/menubar-navigation/
|
|
12
|
+
//
|
|
13
|
+
// Request vs commit (map ticket rt1p). The controlled value is the open *top*
|
|
14
|
+
// menu (`openTop`, 0 = none): a menubar whose open menu a host owns. Every event
|
|
15
|
+
// that would open or close/replace that menu — tab_out, arrow_right/left/down/up,
|
|
16
|
+
// key_home/end, activate, escape, typeahead when it moves along the bar — is a
|
|
17
|
+
// *request* carrying the intended `openTop` (`requested`); it fires the change
|
|
18
|
+
// callback (`requests` counts the calls) when the intent differs, and then:
|
|
19
|
+
// uncontrolled the request is accepted: the whole transition commits
|
|
20
|
+
// controlled, holds the host ignores it ("rejected"): nothing moves
|
|
21
|
+
// controlled, accepts the host answers with the same value (`accepting`): the
|
|
22
|
+
// transition commits through the controlled value
|
|
23
|
+
// `valueProp` is the only thing that changes `openTop` while `controlled`;
|
|
24
|
+
// `setValue` / `hostToggle` are the host writing it. `control` / `acceptRequests`
|
|
25
|
+
// are configuration events (init is literal-only). A rejected request reverts the
|
|
26
|
+
// *whole* transition (focus and roving tab stop included), so the ARIA
|
|
27
|
+
// invariants still hold. `openSub` (which item's submenu is open) stays inside
|
|
28
|
+
// the open menu and is not part of the controlled value.
|
|
29
|
+
//
|
|
30
|
+
// Fixed fixture (the menu tree is data of the model, indices are 1-based, 0 = none):
|
|
31
|
+
//
|
|
32
|
+
// menubar: 1 "File" ▸ 2 "Edit" ▸ 3 "About" (plain link, no menu)
|
|
33
|
+
// menu(1): 1 plain 2 plain ▸ (submenu, 2 items) 3 plain
|
|
34
|
+
// menu(2): 1 plain 2 plain
|
|
35
|
+
// submenu(1,2): 1 plain 2 plain (depth bound: menubar + 2 levels)
|
|
36
|
+
//
|
|
37
|
+
// Focus is `(focusLevel, focusIdx)`: level 0 = outside the widget, 1 = menubar,
|
|
38
|
+
// 2 = a top-level menu, 3 = the submenu of the open item. The open path is
|
|
39
|
+
// `(openTop, openSub)`: at most one open menu per level is structural (each is one
|
|
40
|
+
// slot), `openSub` is the index inside menu(openTop) whose submenu is open.
|
|
41
|
+
//
|
|
42
|
+
// ARIA contract (derived defs, consumed by the invariants and by an implementation):
|
|
43
|
+
// tabIndexBar(i, barStop) roving tabindex: 0 on barStop, -1 elsewhere
|
|
44
|
+
// ariaHasPopupBar / ariaHasPopupItem parent menuitems (aria-haspopup)
|
|
45
|
+
// ariaExpandedBar(i, openTop) aria-expanded on a menubar item
|
|
46
|
+
// ariaExpandedItem(t, i, openTop, openSub) aria-expanded on a menu item
|
|
47
|
+
//
|
|
48
|
+
// Deliberate omissions (generator subset / scope): Shift+Tab is the same event as
|
|
49
|
+
// Tab (`tab_out`); no pointer/hover events; no disabled items and no
|
|
50
|
+
// menuitemcheckbox/menuitemradio (see menu-checkable.qnt); no aria-activedescendant
|
|
51
|
+
// variant (focus is modelled as `element.focus()`); no vertical orientation; no
|
|
52
|
+
// separators; Home/End are always available alongside wrapping; Enter vs Space is
|
|
53
|
+
// one `activate` event with a `key` pool because they only differ on checkable
|
|
54
|
+
// items; typeahead matches a one-letter label code (1..3) rather than a string,
|
|
55
|
+
// starting after the focused item and wrapping; activating a leaf closes every menu
|
|
56
|
+
// and returns focus to the menubar item that owns the menu (APG note 3 allows the
|
|
57
|
+
// invoking context instead); activations are recorded as an opaque `lastActivated`
|
|
58
|
+
// code (top*100 + item*10 + subitem).
|
|
59
|
+
module apg_menu_menubar {
|
|
60
|
+
import aria.* from "../../shared/aria"
|
|
61
|
+
|
|
62
|
+
var focusLevel: int // 0 outside | 1 menubar | 2 top-level menu | 3 submenu
|
|
63
|
+
var focusIdx: int // focused item inside its container (0 when outside)
|
|
64
|
+
var barStop: int // roving-tabindex holder of the menubar (1..3)
|
|
65
|
+
var openTop: int // menubar item whose menu is open (0 none)
|
|
66
|
+
var openSub: int // item of menu(openTop) whose submenu is open (0 none)
|
|
67
|
+
var lastActivated: int // code of the last activated leaf (0 none)
|
|
68
|
+
var controlled: bool // the host owns `openTop` (`value` prop present)
|
|
69
|
+
var accepting: bool // controlled host that answers every request with its intent
|
|
70
|
+
var valueProp: int // the controlled open top menu (meaningful while controlled)
|
|
71
|
+
var requested: int // intent of the last request
|
|
72
|
+
var outcome: str // "none" | "accepted" | "rejected"
|
|
73
|
+
var base: int // openTop when the last request was received
|
|
74
|
+
var requests: int // number of requests that fired the change callback
|
|
75
|
+
|
|
76
|
+
// ---- fixture ----------------------------------------------------------
|
|
77
|
+
def menuSize(t: int): int = if (t == 1) 3 else if (t == 2) 2 else 0
|
|
78
|
+
def subSize(t: int, i: int): int = if (and(t == 1, i == 2)) 2 else 0
|
|
79
|
+
def hasMenu(t: int): bool = menuSize(t) > 0
|
|
80
|
+
def hasSub(t: int, i: int): bool = subSize(t, i) > 0
|
|
81
|
+
def containerSize(l: int, t: int, s: int): int =
|
|
82
|
+
if (l == 1) 3 else if (l == 2) menuSize(t) else if (l == 3) subSize(t, s) else 0
|
|
83
|
+
// one-letter label codes (typeahead): bar 1,2,3 | menu(1) 1,2,1 | menu(2) 2,3 | sub 3,1
|
|
84
|
+
def letterOf(l: int, t: int, i: int): int =
|
|
85
|
+
if (l == 1) i
|
|
86
|
+
else if (l == 2) (if (t == 1) (if (i == 2) 2 else 1) else (if (i == 1) 2 else 3))
|
|
87
|
+
else (if (i == 1) 3 else 1)
|
|
88
|
+
|
|
89
|
+
// ---- movement helpers ---------------------------------------------------
|
|
90
|
+
def barNext(i: int): int = ariaWrapNext1(i, 3)
|
|
91
|
+
def barPrev(i: int): int = ariaWrapPrev1(i, 3)
|
|
92
|
+
// after moving along the menubar, the new item's menu is opened (focus stays on
|
|
93
|
+
// the bar item) only when a menu was already open and the new item has one
|
|
94
|
+
def follow(wasOpen: int, nb: int): int = if (and(wasOpen > 0, hasMenu(nb))) nb else 0
|
|
95
|
+
def cn(l: int, t: int, s: int, i: int): int = ariaWrapNext1(i, containerSize(l, t, s))
|
|
96
|
+
def typeTarget(l: int, t: int, s: int, i: int, ch: int): int =
|
|
97
|
+
if (letterOf(l, t, cn(l, t, s, i)) == ch) cn(l, t, s, i)
|
|
98
|
+
else if (letterOf(l, t, cn(l, t, s, cn(l, t, s, i))) == ch) cn(l, t, s, cn(l, t, s, i))
|
|
99
|
+
else if (letterOf(l, t, cn(l, t, s, cn(l, t, s, cn(l, t, s, i)))) == ch) cn(l, t, s, cn(l, t, s, cn(l, t, s, i)))
|
|
100
|
+
else i
|
|
101
|
+
|
|
102
|
+
// ---- activation decisions -----------------------------------------------
|
|
103
|
+
def entersSub(l: int, t: int, i: int): bool = and(l == 2, hasSub(t, i))
|
|
104
|
+
def opensMenu(l: int, b: int): bool = and(l == 1, hasMenu(b))
|
|
105
|
+
def fires(l: int, b: int, t: int, i: int): bool =
|
|
106
|
+
and(l > 0, not(opensMenu(l, b)), not(entersSub(l, t, i)))
|
|
107
|
+
def activationCode(l: int, b: int, t: int, s: int, i: int): int =
|
|
108
|
+
if (l == 1) b * 100 else if (l == 2) t * 100 + i * 10 else t * 100 + s * 10 + i
|
|
109
|
+
|
|
110
|
+
// ---- ARIA contract (derived) -------------------------------------------
|
|
111
|
+
def b2i(b: bool): int = if (b) 1 else 0
|
|
112
|
+
def tabIndexBar(i: int, b: int): int = if (i == b) 0 else -1
|
|
113
|
+
def ariaHasPopupBar(i: int): bool = hasMenu(i)
|
|
114
|
+
def ariaHasPopupItem(t: int, i: int): bool = hasSub(t, i)
|
|
115
|
+
def ariaExpandedBar(i: int, t: int): bool = and(hasMenu(i), t == i)
|
|
116
|
+
def ariaExpandedItem(t: int, i: int, ot: int, s: int): bool =
|
|
117
|
+
and(t > 0, t == ot, hasSub(t, i), s == i)
|
|
118
|
+
|
|
119
|
+
// ---- request / commit (map ticket rt1p) ---------------------------------
|
|
120
|
+
def holds: bool = and(controlled, not(accepting))
|
|
121
|
+
def follows: bool = and(controlled, accepting)
|
|
122
|
+
def verdict: str = if (holds) "rejected" else "accepted"
|
|
123
|
+
def landed(h: bool, cur: int, want: int): int = if (h) cur else want
|
|
124
|
+
def propAfter(f: bool, p: int, want: int): int = if (f) want else p
|
|
125
|
+
// the intended open top menu of each request
|
|
126
|
+
def rightWant: int =
|
|
127
|
+
if (entersSub(focusLevel, openTop, focusIdx)) openTop
|
|
128
|
+
else follow(openTop, barNext(barStop))
|
|
129
|
+
def leftWant: int =
|
|
130
|
+
if (or(focusLevel == 0, focusLevel == 3)) openTop
|
|
131
|
+
else follow(openTop, barPrev(barStop))
|
|
132
|
+
def downWant: int = if (opensMenu(focusLevel, barStop)) barStop else openTop
|
|
133
|
+
def homeWant: int = if (focusLevel == 1) follow(openTop, 1) else openTop
|
|
134
|
+
def endWant: int = if (focusLevel == 1) follow(openTop, 3) else openTop
|
|
135
|
+
def activateWant: int =
|
|
136
|
+
if (opensMenu(focusLevel, barStop)) barStop
|
|
137
|
+
else if (and(focusLevel >= 2, not(entersSub(focusLevel, openTop, focusIdx)))) 0
|
|
138
|
+
else openTop
|
|
139
|
+
def escapeWant: int = if (focusLevel == 3) openTop else 0
|
|
140
|
+
def typeWant(ch: int): int =
|
|
141
|
+
if (focusLevel == 1) follow(openTop, typeTarget(focusLevel, openTop, openSub, focusIdx, ch))
|
|
142
|
+
else openTop
|
|
143
|
+
def moves(want: int): bool = want != openTop
|
|
144
|
+
def rejects(want: int): bool = and(holds, moves(want))
|
|
145
|
+
|
|
146
|
+
action init = all {
|
|
147
|
+
focusLevel' = 0,
|
|
148
|
+
focusIdx' = 0,
|
|
149
|
+
barStop' = 1,
|
|
150
|
+
openTop' = 0,
|
|
151
|
+
openSub' = 0,
|
|
152
|
+
lastActivated' = 0,
|
|
153
|
+
controlled' = false,
|
|
154
|
+
accepting' = false,
|
|
155
|
+
valueProp' = 0,
|
|
156
|
+
requested' = 0,
|
|
157
|
+
outcome' = "none",
|
|
158
|
+
base' = 0,
|
|
159
|
+
requests' = 0
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Tab into the menubar: focus lands on the remembered (roving) item
|
|
163
|
+
action focus_in = all {
|
|
164
|
+
focusLevel' = if (focusLevel == 0) 1 else focusLevel,
|
|
165
|
+
focusIdx' = if (focusLevel == 0) barStop else focusIdx,
|
|
166
|
+
barStop' = barStop,
|
|
167
|
+
openTop' = openTop,
|
|
168
|
+
openSub' = openSub,
|
|
169
|
+
lastActivated' = lastActivated,
|
|
170
|
+
controlled' = controlled,
|
|
171
|
+
accepting' = accepting,
|
|
172
|
+
valueProp' = valueProp,
|
|
173
|
+
requested' = requested,
|
|
174
|
+
outcome' = outcome,
|
|
175
|
+
base' = base,
|
|
176
|
+
requests' = requests
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// request: Tab / Shift+Tab from any item closes every menu and leaves
|
|
180
|
+
action tab_out = all {
|
|
181
|
+
focusLevel' = if (rejects(0)) focusLevel else 0,
|
|
182
|
+
focusIdx' = if (rejects(0)) focusIdx else 0,
|
|
183
|
+
barStop' = barStop,
|
|
184
|
+
openTop' = landed(holds, openTop, 0),
|
|
185
|
+
openSub' = if (rejects(0)) openSub else 0,
|
|
186
|
+
lastActivated' = lastActivated,
|
|
187
|
+
controlled' = controlled,
|
|
188
|
+
accepting' = accepting,
|
|
189
|
+
valueProp' = propAfter(follows, valueProp, 0),
|
|
190
|
+
requested' = if (moves(0)) 0 else requested,
|
|
191
|
+
outcome' = if (moves(0)) verdict else outcome,
|
|
192
|
+
base' = if (moves(0)) openTop else base,
|
|
193
|
+
requests' = if (moves(0)) requests + 1 else requests
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// request: menubar -> next item (its menu follows); menu -> submenu; else no move
|
|
197
|
+
action arrow_right = all {
|
|
198
|
+
focusLevel' = if (rejects(rightWant)) focusLevel
|
|
199
|
+
else if (focusLevel == 0) 0
|
|
200
|
+
else if (entersSub(focusLevel, openTop, focusIdx)) 3
|
|
201
|
+
else 1,
|
|
202
|
+
focusIdx' = if (rejects(rightWant)) focusIdx
|
|
203
|
+
else if (focusLevel == 0) focusIdx
|
|
204
|
+
else if (entersSub(focusLevel, openTop, focusIdx)) 1
|
|
205
|
+
else barNext(barStop),
|
|
206
|
+
barStop' = if (rejects(rightWant)) barStop
|
|
207
|
+
else if (or(focusLevel == 0, entersSub(focusLevel, openTop, focusIdx))) barStop
|
|
208
|
+
else barNext(barStop),
|
|
209
|
+
openTop' = landed(holds, openTop, rightWant),
|
|
210
|
+
openSub' = if (rejects(rightWant)) openSub
|
|
211
|
+
else if (entersSub(focusLevel, openTop, focusIdx)) focusIdx
|
|
212
|
+
else 0,
|
|
213
|
+
lastActivated' = lastActivated,
|
|
214
|
+
controlled' = controlled,
|
|
215
|
+
accepting' = accepting,
|
|
216
|
+
valueProp' = propAfter(follows, valueProp, rightWant),
|
|
217
|
+
requested' = if (moves(rightWant)) rightWant else requested,
|
|
218
|
+
outcome' = if (moves(rightWant)) verdict else outcome,
|
|
219
|
+
base' = if (moves(rightWant)) openTop else base,
|
|
220
|
+
requests' = if (moves(rightWant)) requests + 1 else requests
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// request: submenu -> parent item; top-level menu -> previous menubar item
|
|
224
|
+
action arrow_left = all {
|
|
225
|
+
focusLevel' = if (rejects(leftWant)) focusLevel
|
|
226
|
+
else if (focusLevel == 0) 0
|
|
227
|
+
else if (focusLevel == 3) 2
|
|
228
|
+
else 1,
|
|
229
|
+
focusIdx' = if (rejects(leftWant)) focusIdx
|
|
230
|
+
else if (focusLevel == 0) focusIdx
|
|
231
|
+
else if (focusLevel == 3) openSub
|
|
232
|
+
else barPrev(barStop),
|
|
233
|
+
barStop' = if (rejects(leftWant)) barStop
|
|
234
|
+
else if (or(focusLevel == 0, focusLevel == 3)) barStop
|
|
235
|
+
else barPrev(barStop),
|
|
236
|
+
openTop' = landed(holds, openTop, leftWant),
|
|
237
|
+
openSub' = if (rejects(leftWant)) openSub else 0,
|
|
238
|
+
lastActivated' = lastActivated,
|
|
239
|
+
controlled' = controlled,
|
|
240
|
+
accepting' = accepting,
|
|
241
|
+
valueProp' = propAfter(follows, valueProp, leftWant),
|
|
242
|
+
requested' = if (moves(leftWant)) leftWant else requested,
|
|
243
|
+
outcome' = if (moves(leftWant)) verdict else outcome,
|
|
244
|
+
base' = if (moves(leftWant)) openTop else base,
|
|
245
|
+
requests' = if (moves(leftWant)) requests + 1 else requests
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// request: menubar -> open the menu on its first item; menu -> next item, wrapping
|
|
249
|
+
action arrow_down = all {
|
|
250
|
+
focusLevel' = if (rejects(downWant)) focusLevel
|
|
251
|
+
else if (opensMenu(focusLevel, barStop)) 2
|
|
252
|
+
else focusLevel,
|
|
253
|
+
focusIdx' = if (rejects(downWant)) focusIdx
|
|
254
|
+
else if (opensMenu(focusLevel, barStop)) 1
|
|
255
|
+
else if (focusLevel >= 2) ariaWrapNext1(focusIdx, containerSize(focusLevel, openTop, openSub))
|
|
256
|
+
else focusIdx,
|
|
257
|
+
barStop' = barStop,
|
|
258
|
+
openTop' = landed(holds, openTop, downWant),
|
|
259
|
+
openSub' = if (rejects(downWant)) openSub else openSub,
|
|
260
|
+
lastActivated' = lastActivated,
|
|
261
|
+
controlled' = controlled,
|
|
262
|
+
accepting' = accepting,
|
|
263
|
+
valueProp' = propAfter(follows, valueProp, downWant),
|
|
264
|
+
requested' = if (moves(downWant)) downWant else requested,
|
|
265
|
+
outcome' = if (moves(downWant)) verdict else outcome,
|
|
266
|
+
base' = if (moves(downWant)) openTop else base,
|
|
267
|
+
requests' = if (moves(downWant)) requests + 1 else requests
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// request: menubar (optional APG behaviour) -> open the menu on its last item
|
|
271
|
+
action arrow_up = all {
|
|
272
|
+
focusLevel' = if (rejects(downWant)) focusLevel
|
|
273
|
+
else if (opensMenu(focusLevel, barStop)) 2
|
|
274
|
+
else focusLevel,
|
|
275
|
+
focusIdx' = if (rejects(downWant)) focusIdx
|
|
276
|
+
else if (opensMenu(focusLevel, barStop)) menuSize(barStop)
|
|
277
|
+
else if (focusLevel >= 2) ariaWrapPrev1(focusIdx, containerSize(focusLevel, openTop, openSub))
|
|
278
|
+
else focusIdx,
|
|
279
|
+
barStop' = barStop,
|
|
280
|
+
openTop' = landed(holds, openTop, downWant),
|
|
281
|
+
openSub' = if (rejects(downWant)) openSub else openSub,
|
|
282
|
+
lastActivated' = lastActivated,
|
|
283
|
+
controlled' = controlled,
|
|
284
|
+
accepting' = accepting,
|
|
285
|
+
valueProp' = propAfter(follows, valueProp, downWant),
|
|
286
|
+
requested' = if (moves(downWant)) downWant else requested,
|
|
287
|
+
outcome' = if (moves(downWant)) verdict else outcome,
|
|
288
|
+
base' = if (moves(downWant)) openTop else base,
|
|
289
|
+
requests' = if (moves(downWant)) requests + 1 else requests
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// request: Home on the menubar; a menu re-opens when one was open
|
|
293
|
+
action key_home = all {
|
|
294
|
+
focusLevel' = focusLevel,
|
|
295
|
+
focusIdx' = if (rejects(homeWant)) focusIdx else if (focusLevel == 0) focusIdx else 1,
|
|
296
|
+
barStop' = if (rejects(homeWant)) barStop else if (focusLevel == 1) 1 else barStop,
|
|
297
|
+
openTop' = landed(holds, openTop, homeWant),
|
|
298
|
+
openSub' = openSub,
|
|
299
|
+
lastActivated' = lastActivated,
|
|
300
|
+
controlled' = controlled,
|
|
301
|
+
accepting' = accepting,
|
|
302
|
+
valueProp' = propAfter(follows, valueProp, homeWant),
|
|
303
|
+
requested' = if (moves(homeWant)) homeWant else requested,
|
|
304
|
+
outcome' = if (moves(homeWant)) verdict else outcome,
|
|
305
|
+
base' = if (moves(homeWant)) openTop else base,
|
|
306
|
+
requests' = if (moves(homeWant)) requests + 1 else requests
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// request: End on the menubar; a menu re-opens when one was open
|
|
310
|
+
action key_end = all {
|
|
311
|
+
focusLevel' = focusLevel,
|
|
312
|
+
focusIdx' = if (rejects(endWant)) focusIdx else if (focusLevel == 0) focusIdx else containerSize(focusLevel, openTop, openSub),
|
|
313
|
+
barStop' = if (rejects(endWant)) barStop else if (focusLevel == 1) 3 else barStop,
|
|
314
|
+
openTop' = landed(holds, openTop, endWant),
|
|
315
|
+
openSub' = openSub,
|
|
316
|
+
lastActivated' = lastActivated,
|
|
317
|
+
controlled' = controlled,
|
|
318
|
+
accepting' = accepting,
|
|
319
|
+
valueProp' = propAfter(follows, valueProp, endWant),
|
|
320
|
+
requested' = if (moves(endWant)) endWant else requested,
|
|
321
|
+
outcome' = if (moves(endWant)) verdict else outcome,
|
|
322
|
+
base' = if (moves(endWant)) openTop else base,
|
|
323
|
+
requests' = if (moves(endWant)) requests + 1 else requests
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// request: Enter / Space: a parent item opens its submenu on the first item; any
|
|
327
|
+
// other item is activated, every menu closes and focus returns to the menubar item
|
|
328
|
+
action activate = {
|
|
329
|
+
nondet key = Set("Enter", "Space").oneOf()
|
|
330
|
+
all {
|
|
331
|
+
focusLevel' = if (rejects(activateWant)) focusLevel
|
|
332
|
+
else if (opensMenu(focusLevel, barStop)) 2
|
|
333
|
+
else if (entersSub(focusLevel, openTop, focusIdx)) 3
|
|
334
|
+
else if (focusLevel == 0) 0
|
|
335
|
+
else 1,
|
|
336
|
+
focusIdx' = if (rejects(activateWant)) focusIdx
|
|
337
|
+
else if (or(opensMenu(focusLevel, barStop), entersSub(focusLevel, openTop, focusIdx))) 1
|
|
338
|
+
else if (focusLevel >= 2) openTop
|
|
339
|
+
else focusIdx,
|
|
340
|
+
barStop' = barStop,
|
|
341
|
+
openTop' = landed(holds, openTop, activateWant),
|
|
342
|
+
openSub' = if (rejects(activateWant)) openSub
|
|
343
|
+
else if (entersSub(focusLevel, openTop, focusIdx)) focusIdx
|
|
344
|
+
else 0,
|
|
345
|
+
lastActivated' = if (rejects(activateWant)) lastActivated
|
|
346
|
+
else if (fires(focusLevel, barStop, openTop, focusIdx))
|
|
347
|
+
activationCode(focusLevel, barStop, openTop, openSub, focusIdx)
|
|
348
|
+
else lastActivated,
|
|
349
|
+
controlled' = controlled,
|
|
350
|
+
accepting' = accepting,
|
|
351
|
+
valueProp' = propAfter(follows, valueProp, activateWant),
|
|
352
|
+
requested' = if (moves(activateWant)) activateWant else requested,
|
|
353
|
+
outcome' = if (moves(activateWant)) verdict else outcome,
|
|
354
|
+
base' = if (moves(activateWant)) openTop else base,
|
|
355
|
+
requests' = if (moves(activateWant)) requests + 1 else requests
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// request: Escape: close the menu holding focus and return focus to its invoker
|
|
360
|
+
// (parent item for a submenu, menubar item for a top-level menu); on the
|
|
361
|
+
// menubar it dismisses a menu that is open without focus
|
|
362
|
+
action escape = all {
|
|
363
|
+
focusLevel' = if (rejects(escapeWant)) focusLevel
|
|
364
|
+
else if (focusLevel == 3) 2
|
|
365
|
+
else if (focusLevel == 2) 1
|
|
366
|
+
else focusLevel,
|
|
367
|
+
focusIdx' = if (rejects(escapeWant)) focusIdx
|
|
368
|
+
else if (focusLevel == 3) openSub
|
|
369
|
+
else if (focusLevel == 2) openTop
|
|
370
|
+
else focusIdx,
|
|
371
|
+
barStop' = barStop,
|
|
372
|
+
openTop' = landed(holds, openTop, escapeWant),
|
|
373
|
+
openSub' = if (rejects(escapeWant)) openSub else 0,
|
|
374
|
+
lastActivated' = lastActivated,
|
|
375
|
+
controlled' = controlled,
|
|
376
|
+
accepting' = accepting,
|
|
377
|
+
valueProp' = propAfter(follows, valueProp, escapeWant),
|
|
378
|
+
requested' = if (moves(escapeWant)) escapeWant else requested,
|
|
379
|
+
outcome' = if (moves(escapeWant)) verdict else outcome,
|
|
380
|
+
base' = if (moves(escapeWant)) openTop else base,
|
|
381
|
+
requests' = if (moves(escapeWant)) requests + 1 else requests
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// request (on the menubar): printable character moves the roving focus (and a
|
|
385
|
+
// followed menu); inside a menu it only moves focus within the container
|
|
386
|
+
action typeahead = {
|
|
387
|
+
nondet ch = Set(1, 2, 3).oneOf()
|
|
388
|
+
all {
|
|
389
|
+
focusLevel' = focusLevel,
|
|
390
|
+
focusIdx' = if (focusLevel == 0)
|
|
391
|
+
focusIdx
|
|
392
|
+
else if (rejects(typeWant(ch))) focusIdx
|
|
393
|
+
else typeTarget(focusLevel, openTop, openSub, focusIdx, ch),
|
|
394
|
+
barStop' = if (rejects(typeWant(ch))) barStop
|
|
395
|
+
else if (focusLevel == 1) typeTarget(focusLevel, openTop, openSub, focusIdx, ch)
|
|
396
|
+
else barStop,
|
|
397
|
+
openTop' = landed(holds, openTop, typeWant(ch)),
|
|
398
|
+
openSub' = openSub,
|
|
399
|
+
lastActivated' = lastActivated,
|
|
400
|
+
controlled' = controlled,
|
|
401
|
+
accepting' = accepting,
|
|
402
|
+
valueProp' = propAfter(follows, valueProp, typeWant(ch)),
|
|
403
|
+
requested' = if (moves(typeWant(ch))) typeWant(ch) else requested,
|
|
404
|
+
outcome' = if (moves(typeWant(ch))) verdict else outcome,
|
|
405
|
+
base' = if (moves(typeWant(ch))) openTop else base,
|
|
406
|
+
requests' = if (moves(typeWant(ch))) requests + 1 else requests
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// commit: the host writes the controlled open top menu (a no-op while uncontrolled)
|
|
411
|
+
action setValue = {
|
|
412
|
+
nondet t = Set(0, 1, 2).oneOf()
|
|
413
|
+
all {
|
|
414
|
+
focusLevel' = if (controlled) 1 else focusLevel,
|
|
415
|
+
focusIdx' = if (controlled) (if (t == 0) barStop else t) else focusIdx,
|
|
416
|
+
barStop' = if (controlled) (if (t == 0) barStop else t) else barStop,
|
|
417
|
+
openTop' = if (controlled) t else openTop,
|
|
418
|
+
openSub' = if (controlled) 0 else openSub,
|
|
419
|
+
lastActivated' = lastActivated,
|
|
420
|
+
controlled' = controlled,
|
|
421
|
+
accepting' = accepting,
|
|
422
|
+
valueProp' = if (controlled) t else valueProp,
|
|
423
|
+
requested' = requested,
|
|
424
|
+
outcome' = if (controlled) "none" else outcome,
|
|
425
|
+
base' = base,
|
|
426
|
+
requests' = requests
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// commit: the host flips its own copy of the open top menu (open the first menu
|
|
431
|
+
// with one, close with none)
|
|
432
|
+
action hostToggle = all {
|
|
433
|
+
focusLevel' = if (controlled) 1 else focusLevel,
|
|
434
|
+
focusIdx' = if (controlled) (if (valueProp == 0) 1 else barStop) else focusIdx,
|
|
435
|
+
barStop' = if (controlled) (if (valueProp == 0) 1 else barStop) else barStop,
|
|
436
|
+
openTop' = if (controlled) (if (valueProp == 0) 1 else 0) else openTop,
|
|
437
|
+
openSub' = if (controlled) 0 else openSub,
|
|
438
|
+
lastActivated' = lastActivated,
|
|
439
|
+
controlled' = controlled,
|
|
440
|
+
accepting' = accepting,
|
|
441
|
+
valueProp' = if (controlled) (if (valueProp == 0) 1 else 0) else valueProp,
|
|
442
|
+
requested' = requested,
|
|
443
|
+
outcome' = if (controlled) "none" else outcome,
|
|
444
|
+
base' = base,
|
|
445
|
+
requests' = requests
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// configuration: the host starts owning the open top menu, adopting the current one
|
|
449
|
+
action control = all {
|
|
450
|
+
focusLevel' = focusLevel,
|
|
451
|
+
focusIdx' = focusIdx,
|
|
452
|
+
barStop' = barStop,
|
|
453
|
+
openTop' = openTop,
|
|
454
|
+
openSub' = openSub,
|
|
455
|
+
lastActivated' = lastActivated,
|
|
456
|
+
controlled' = true,
|
|
457
|
+
accepting' = accepting,
|
|
458
|
+
valueProp' = openTop,
|
|
459
|
+
requested' = requested,
|
|
460
|
+
outcome' = outcome,
|
|
461
|
+
base' = base,
|
|
462
|
+
requests' = requests
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// configuration: the controlled host answers every request with its intent
|
|
466
|
+
action acceptRequests = all {
|
|
467
|
+
focusLevel' = focusLevel,
|
|
468
|
+
focusIdx' = focusIdx,
|
|
469
|
+
barStop' = barStop,
|
|
470
|
+
openTop' = openTop,
|
|
471
|
+
openSub' = openSub,
|
|
472
|
+
lastActivated' = lastActivated,
|
|
473
|
+
controlled' = controlled,
|
|
474
|
+
accepting' = true,
|
|
475
|
+
valueProp' = valueProp,
|
|
476
|
+
requested' = requested,
|
|
477
|
+
outcome' = outcome,
|
|
478
|
+
base' = base,
|
|
479
|
+
requests' = requests
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
action step = any {
|
|
483
|
+
init, focus_in, tab_out, arrow_right, arrow_left, arrow_down, arrow_up,
|
|
484
|
+
key_home, key_end, activate, escape, typeahead,
|
|
485
|
+
setValue, hostToggle, control, acceptRequests
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
val LevelValid: bool = and(focusLevel >= 0, focusLevel <= 3)
|
|
489
|
+
// outside the widget nothing is open and nothing is focused
|
|
490
|
+
val FocusOutsideClosesAll: bool =
|
|
491
|
+
or(focusLevel != 0, and(focusIdx == 0, openTop == 0, openSub == 0))
|
|
492
|
+
// focus on the menubar sits on the roving tab stop
|
|
493
|
+
val BarFocusIsTabStop: bool =
|
|
494
|
+
or(focusLevel != 1, and(focusIdx == barStop, barStop >= 1, barStop <= 3))
|
|
495
|
+
// focus inside a menu is on an item of the open path
|
|
496
|
+
val FocusInsideOpenPath: bool = and(
|
|
497
|
+
or(focusLevel != 2, and(openTop > 0, openTop == barStop, openSub == 0,
|
|
498
|
+
focusIdx >= 1, focusIdx <= menuSize(openTop))),
|
|
499
|
+
or(focusLevel != 3, and(openTop > 0, hasSub(openTop, openSub),
|
|
500
|
+
focusIdx >= 1, focusIdx <= subSize(openTop, openSub)))
|
|
501
|
+
)
|
|
502
|
+
// the open path is well formed: a menu belongs to the roving item, a submenu
|
|
503
|
+
// to a parent item of that menu and only while focus is inside it
|
|
504
|
+
val OpenPathWellFormed: bool = and(
|
|
505
|
+
or(openTop == 0, and(hasMenu(openTop), openTop == barStop)),
|
|
506
|
+
or(openSub == 0, and(openTop > 0, hasSub(openTop, openSub), focusLevel == 3))
|
|
507
|
+
)
|
|
508
|
+
// exactly one menubar item has tabindex=0
|
|
509
|
+
val ExactlyOneBarTabStop: bool =
|
|
510
|
+
b2i(tabIndexBar(1, barStop) == 0) + b2i(tabIndexBar(2, barStop) == 0) + b2i(tabIndexBar(3, barStop) == 0) == 1
|
|
511
|
+
// at most one menubar item is aria-expanded, and it is the one with the open menu
|
|
512
|
+
val AtMostOneBarExpanded: bool =
|
|
513
|
+
and(
|
|
514
|
+
b2i(ariaExpandedBar(1, openTop)) + b2i(ariaExpandedBar(2, openTop)) + b2i(ariaExpandedBar(3, openTop)) <= 1,
|
|
515
|
+
or(openTop == 0, ariaExpandedBar(openTop, openTop))
|
|
516
|
+
)
|
|
517
|
+
// at most one menu item is aria-expanded, and it is the parent of the open submenu
|
|
518
|
+
val AtMostOneItemExpanded: bool =
|
|
519
|
+
and(
|
|
520
|
+
b2i(ariaExpandedItem(openTop, 1, openTop, openSub)) + b2i(ariaExpandedItem(openTop, 2, openTop, openSub)) + b2i(ariaExpandedItem(openTop, 3, openTop, openSub)) <= 1,
|
|
521
|
+
or(openSub == 0, ariaExpandedItem(openTop, openSub, openTop, openSub))
|
|
522
|
+
)
|
|
523
|
+
// callbacks only ever count up from zero
|
|
524
|
+
val RequestsNonNegative: bool = requests >= 0
|
|
525
|
+
// controlled: only the value prop sets the open menu, so no event commits
|
|
526
|
+
val ControlledFollowsProp: bool = or(not(controlled), openTop == valueProp)
|
|
527
|
+
// a rejected request leaves the state as it found it
|
|
528
|
+
val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), openTop == base)
|
|
529
|
+
// an accepted request committed exactly its intent
|
|
530
|
+
val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), openTop == requested)
|
|
531
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// APG Meter — https://www.w3.org/WAI/ARIA/apg/patterns/meter/
|
|
2
|
+
//
|
|
3
|
+
// A graphical display of a numeric value within a defined range (battery level,
|
|
4
|
+
// fuel). NOT a progressbar (no completion semantics) and not for unbounded
|
|
5
|
+
// values. The spec models the rendered attributes next to the input so the APG
|
|
6
|
+
// contract is checkable:
|
|
7
|
+
// - aria-valuemin < aria-valuemax;
|
|
8
|
+
// - aria-valuenow lies in [min, max] (the raw value is clamped, and is
|
|
9
|
+
// re-clamped when the range changes);
|
|
10
|
+
// - aria-valuetext is present when a bare percentage would not be friendly,
|
|
11
|
+
// and a present valuetext describes the CURRENT value (never stale).
|
|
12
|
+
//
|
|
13
|
+
// Events:
|
|
14
|
+
// setValue the consumer feeds a raw value (may be out of range)
|
|
15
|
+
// setRange the consumer changes [min, max] (one selector `r`
|
|
16
|
+
// encodes the pair); an invalid pair (min >= max) is ignored
|
|
17
|
+
// addText/removeText render / drop aria-valuetext
|
|
18
|
+
// percentFriendly / percentUnfriendly
|
|
19
|
+
// whether a percentage alone is understandable
|
|
20
|
+
//
|
|
21
|
+
// Keyboard: not applicable. Label (aria-labelledby / aria-label) is a static DOM
|
|
22
|
+
// contract, see specs/apg/STATIC.md.
|
|
23
|
+
module apg_meter {
|
|
24
|
+
var min: int // aria-valuemin
|
|
25
|
+
var max: int // aria-valuemax
|
|
26
|
+
var raw: int // the consumer's unclamped value
|
|
27
|
+
var now: int // aria-valuenow (clamped raw)
|
|
28
|
+
var friendly: bool // a percentage alone is understandable
|
|
29
|
+
var hasText: bool // aria-valuetext is rendered
|
|
30
|
+
var textNow: int // the value aria-valuetext describes (0 when absent)
|
|
31
|
+
|
|
32
|
+
def clamp(v: int, lo: int, hi: int): int =
|
|
33
|
+
if (v < lo) lo else if (v > hi) hi else v
|
|
34
|
+
// range selector: 0..2 are valid ranges, 3 (min > max) and 4 (min == max) are not
|
|
35
|
+
def loOf(r: int): int = if (r == 0) 0 else if (r == 1) 10 else if (r == 2) 20 else if (r == 3) 20 else 10
|
|
36
|
+
def hiOf(r: int): int = if (r == 0) 100 else if (r == 1) 50 else if (r == 2) 200 else if (r == 3) 10 else 10
|
|
37
|
+
def pickMin(lo: int, hi: int): int = if (lo < hi) lo else min
|
|
38
|
+
def pickMax(lo: int, hi: int): int = if (lo < hi) hi else max
|
|
39
|
+
|
|
40
|
+
action init = all {
|
|
41
|
+
min' = 0,
|
|
42
|
+
max' = 100,
|
|
43
|
+
raw' = 50,
|
|
44
|
+
now' = 50,
|
|
45
|
+
friendly' = true,
|
|
46
|
+
hasText' = false,
|
|
47
|
+
textNow' = 0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
action setValue = {
|
|
51
|
+
nondet v = Set(-5, 0, 5, 15, 25, 60, 100, 150).oneOf()
|
|
52
|
+
all {
|
|
53
|
+
min' = min,
|
|
54
|
+
max' = max,
|
|
55
|
+
raw' = v,
|
|
56
|
+
now' = clamp(v, min, max),
|
|
57
|
+
friendly' = friendly,
|
|
58
|
+
hasText' = hasText,
|
|
59
|
+
textNow' = if (hasText) clamp(v, min, max) else 0
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
action setRange = {
|
|
64
|
+
nondet r = Set(0, 1, 2, 3, 4).oneOf()
|
|
65
|
+
all {
|
|
66
|
+
min' = pickMin(loOf(r), hiOf(r)),
|
|
67
|
+
max' = pickMax(loOf(r), hiOf(r)),
|
|
68
|
+
raw' = raw,
|
|
69
|
+
now' = clamp(raw, pickMin(loOf(r), hiOf(r)), pickMax(loOf(r), hiOf(r))),
|
|
70
|
+
friendly' = friendly,
|
|
71
|
+
hasText' = hasText,
|
|
72
|
+
textNow' = if (hasText) clamp(raw, pickMin(loOf(r), hiOf(r)), pickMax(loOf(r), hiOf(r))) else 0
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
action addText = all {
|
|
77
|
+
min' = min,
|
|
78
|
+
max' = max,
|
|
79
|
+
raw' = raw,
|
|
80
|
+
now' = now,
|
|
81
|
+
friendly' = friendly,
|
|
82
|
+
hasText' = true,
|
|
83
|
+
textNow' = now
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// a meter whose percentage is not friendly keeps its valuetext
|
|
87
|
+
action removeText = all {
|
|
88
|
+
min' = min,
|
|
89
|
+
max' = max,
|
|
90
|
+
raw' = raw,
|
|
91
|
+
now' = now,
|
|
92
|
+
friendly' = friendly,
|
|
93
|
+
hasText' = if (friendly) false else hasText,
|
|
94
|
+
textNow' = if (friendly) 0 else textNow
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
action percentFriendly = all {
|
|
98
|
+
min' = min,
|
|
99
|
+
max' = max,
|
|
100
|
+
raw' = raw,
|
|
101
|
+
now' = now,
|
|
102
|
+
friendly' = true,
|
|
103
|
+
hasText' = hasText,
|
|
104
|
+
textNow' = textNow
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
action percentUnfriendly = all {
|
|
108
|
+
min' = min,
|
|
109
|
+
max' = max,
|
|
110
|
+
raw' = raw,
|
|
111
|
+
now' = now,
|
|
112
|
+
friendly' = false,
|
|
113
|
+
hasText' = true,
|
|
114
|
+
textNow' = now
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
action step = any { init, setValue, setRange, addText, removeText, percentFriendly, percentUnfriendly }
|
|
118
|
+
|
|
119
|
+
// APG: aria-valuemin is less than aria-valuemax
|
|
120
|
+
val RangeValid: bool = min < max
|
|
121
|
+
// APG: aria-valuenow is between aria-valuemin and aria-valuemax
|
|
122
|
+
val NowInRange: bool = and(min <= now, now <= max)
|
|
123
|
+
// the rendered value is exactly the clamped raw value
|
|
124
|
+
val NowIsClampedRaw: bool = now == clamp(raw, min, max)
|
|
125
|
+
// APG: valuetext is set when the percentage alone is not user friendly
|
|
126
|
+
val TextWhenNotFriendly: bool = or(friendly, hasText)
|
|
127
|
+
// aria-valuetext never describes a stale value
|
|
128
|
+
val TextNeverStale: bool = if (hasText) textNow == now else textNow == 0
|
|
129
|
+
}
|