@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,499 @@
|
|
|
1
|
+
// APG Dialog (Modal) pattern — the layered modal-dialog stack.
|
|
2
|
+
//
|
|
3
|
+
// Source: https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
|
|
4
|
+
// sections: About This Pattern, Keyboard Interaction (+ Notes 1-3),
|
|
5
|
+
// WAI-ARIA Roles, States, and Properties
|
|
6
|
+
// example: https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/dialog/
|
|
7
|
+
// (four dialogs: Add Delivery Address -> Verification Result / Address Added
|
|
8
|
+
// -> End of the Road; the dialog table below is that example's content).
|
|
9
|
+
//
|
|
10
|
+
// One page behind up to MAX_DEPTH stacked modal dialogs. Focus is a single
|
|
11
|
+
// encoded target `layer * 100 + position`: layer 0 is the page, layer k the
|
|
12
|
+
// k-th open dialog; position 0 is a static `tabindex=-1` element (only dialogs
|
|
13
|
+
// with a static start have one), positions 1..tabCount are the tab sequence.
|
|
14
|
+
//
|
|
15
|
+
// Modelled
|
|
16
|
+
// - open: activating an invoker moves focus INTO the new dialog, at the
|
|
17
|
+
// dialog's configured initial focus (Keyboard Interaction, Note 1).
|
|
18
|
+
// - Tab / Shift+Tab: move within the top dialog's tab sequence and wrap
|
|
19
|
+
// first<->last; they never leave the dialog (focus trap). A static
|
|
20
|
+
// `tabindex=-1` start element is focusable but not in the tab sequence.
|
|
21
|
+
// - Escape: closes the top dialog only; focus returns to its invoker
|
|
22
|
+
// (Note 2), popping one layer of the nested stack.
|
|
23
|
+
// - activate (Enter/Space on the focused element): open a nested dialog,
|
|
24
|
+
// close the top dialog, or replace it (Add: the Address Added dialog
|
|
25
|
+
// replaces Add Delivery Address and inherits its return-focus target).
|
|
26
|
+
// - focus_attempt: programmatic focus / pointer click on any element; the
|
|
27
|
+
// trap refuses targets outside the top dialog (the dialog container itself
|
|
28
|
+
// is never focusable) and focus stays put.
|
|
29
|
+
// - pointer_outside: a click on the inert content; a no-op unless the
|
|
30
|
+
// CLOSE_ON_OUTSIDE variant is enabled ("in some implementations, attempts
|
|
31
|
+
// to interact with the inert content cause the dialog to close").
|
|
32
|
+
// - remove_invoker: the invoking element no longer exists; the return
|
|
33
|
+
// target becomes a logical-workflow fallback element (Note 2).
|
|
34
|
+
// - ARIA contract (derived defs): role=dialog, aria-modal=true, accessible
|
|
35
|
+
// name via aria-labelledby or aria-label, optional aria-describedby, and
|
|
36
|
+
// the inert state of every layer beneath the top.
|
|
37
|
+
//
|
|
38
|
+
// Request vs commit (map ticket rt1p). Whether any dialog is open is the
|
|
39
|
+
// controlled value: `escape`, `activate` and `pointer_outside` are requests
|
|
40
|
+
// carrying the open intent (`requested`). A request fires onValueChange
|
|
41
|
+
// (`requests` counts the calls) when the intent differs from the current open
|
|
42
|
+
// state, and then:
|
|
43
|
+
// uncontrolled the request is accepted: the stack commits the intent
|
|
44
|
+
// controlled, holds the host ignores it ("rejected"): the stack is untouched
|
|
45
|
+
// controlled, accepts the host writes the intent back (`accepting`)
|
|
46
|
+
// `valueProp` is the host's copy (meaningful while `controlled`); `setValue` /
|
|
47
|
+
// `hostToggle` are the host writing it, the only commit while controlled.
|
|
48
|
+
// `control` / `acceptRequests` configure the host (init is literal-only). A
|
|
49
|
+
// rejected open/close request leaves the whole stack (depth, layers, return
|
|
50
|
+
// targets, focus) untouched.
|
|
51
|
+
//
|
|
52
|
+
// Invariants for the model: ControlledFollowsProp (in controlled mode no event
|
|
53
|
+
// commits), RejectedRequestUntouched, AcceptedRequestCommits.
|
|
54
|
+
//
|
|
55
|
+
// Deliberately omitted
|
|
56
|
+
// - the "work-flow design" exception that returns focus to a different
|
|
57
|
+
// element than the invoker (only the invoker-removed fallback is modelled);
|
|
58
|
+
// - the visible close-button recommendation (Note 3) is modelled as content
|
|
59
|
+
// of the example dialogs, not enforced as an invariant;
|
|
60
|
+
// - legacy `aria-hidden` handling of the inert layer, visual dimming, and
|
|
61
|
+
// page scrolling;
|
|
62
|
+
// - window blur/focus and focus escaping into browser chrome;
|
|
63
|
+
// - dialog 2's aria-describedby/static-start advice interplay (the config
|
|
64
|
+
// records what the example does, no invariant forbids the combination);
|
|
65
|
+
// - the Alert Dialog variant: see alertdialog.qnt.
|
|
66
|
+
module apg_dialog_modal {
|
|
67
|
+
var depth: int // number of open dialogs: 0 = page only .. MAX_DEPTH
|
|
68
|
+
var l1: int // dialog id open at layer 1 (0 = none)
|
|
69
|
+
var l2: int
|
|
70
|
+
var l3: int
|
|
71
|
+
var r1: int // focus target to restore when layer 1 closes (0 = unused)
|
|
72
|
+
var r2: int
|
|
73
|
+
var r3: int
|
|
74
|
+
var focus: int // layer * 100 + position
|
|
75
|
+
var requests: int // number of requests that fired onValueChange
|
|
76
|
+
var controlled: bool // the host owns the open state (`value` prop present)
|
|
77
|
+
var accepting: bool // controlled host that answers every request with its intent
|
|
78
|
+
var valueProp: bool // the controlled value prop (meaningful while controlled)
|
|
79
|
+
var requested: bool // intent of the last request
|
|
80
|
+
var outcome: str // "none" | "accepted" | "rejected"
|
|
81
|
+
var base: bool // open state when the last request was received
|
|
82
|
+
|
|
83
|
+
// --- configuration: variants as constants and tables -----------------------
|
|
84
|
+
def MAX_DEPTH: int = 3
|
|
85
|
+
def CLOSE_ON_OUTSIDE: bool = false
|
|
86
|
+
|
|
87
|
+
// tab-sequence length per dialog id (0 = the page: invoker, link, fallback)
|
|
88
|
+
def tabCount(d: int): int =
|
|
89
|
+
if (d == 0) 3
|
|
90
|
+
else if (d == 1) 8 // street, city, state, zip, instructions, Verify, Add, Cancel
|
|
91
|
+
else if (d == 2) 2 // help link, Close (the first paragraph is the static start)
|
|
92
|
+
else if (d == 3) 2 // profile link, OK
|
|
93
|
+
else 1 // Close
|
|
94
|
+
// a `tabindex=-1` static element at the start of the dialog (position 0)
|
|
95
|
+
def hasStatic(d: int): bool = d == 2
|
|
96
|
+
// initial focus per dialog (Keyboard Interaction, Note 1)
|
|
97
|
+
def initialFocus(d: int): int =
|
|
98
|
+
if (d == 1) 1 // first input
|
|
99
|
+
else if (d == 2) 0 // first paragraph: first interactive element is out of view
|
|
100
|
+
else if (d == 3) 2 // OK, the most used control
|
|
101
|
+
else 1 // sole focusable element
|
|
102
|
+
// dialog opened by activating position `pos` of dialog `d` (0 = none)
|
|
103
|
+
def opens(d: int, pos: int): int =
|
|
104
|
+
if (and(d == 0, pos == 1)) 1 // Add Delivery Address button
|
|
105
|
+
else if (and(d == 1, pos == 6)) 2 // Verify Address
|
|
106
|
+
else if (and(d == 1, pos == 7)) 3 // Add (replaces dialog 1)
|
|
107
|
+
else if (and(d == 2, pos == 1)) 4 // link to help
|
|
108
|
+
else if (and(d == 3, pos == 1)) 4 // profile link
|
|
109
|
+
else 0
|
|
110
|
+
// activating position `pos` of dialog `d` closes it
|
|
111
|
+
def closes(d: int, pos: int): bool =
|
|
112
|
+
or(
|
|
113
|
+
and(d == 1, or(pos == 7, pos == 8)),
|
|
114
|
+
and(d == 2, pos == 2),
|
|
115
|
+
and(d == 3, pos == 2),
|
|
116
|
+
and(d == 4, pos == 1)
|
|
117
|
+
)
|
|
118
|
+
// fallback focus when the invoker is gone: page 3, otherwise first tabbable
|
|
119
|
+
def fallbackPos(d: int): int = if (d == 0) 3 else 1
|
|
120
|
+
|
|
121
|
+
// --- ARIA contract of a dialog id ------------------------------------------
|
|
122
|
+
def dialogRole(d: int): str = "dialog"
|
|
123
|
+
def ariaModal(d: int): bool = true
|
|
124
|
+
// aria-labelledby vs aria-label: the example labels every dialog by title
|
|
125
|
+
def labelling(d: int): str = "labelledby"
|
|
126
|
+
def hasAccessibleName(d: int): bool =
|
|
127
|
+
or(labelling(d) == "labelledby", labelling(d) == "label")
|
|
128
|
+
// optional; the example omits it from dialog 1 (no static descriptive text)
|
|
129
|
+
def ariaDescribedBy(d: int): bool = d != 1
|
|
130
|
+
def contractOk(d: int): bool =
|
|
131
|
+
and(dialogRole(d) == "dialog", ariaModal(d), hasAccessibleName(d))
|
|
132
|
+
// every layer beneath the top is inert; so is the page once a dialog is open
|
|
133
|
+
def isInert(layer: int): bool = layer < depth
|
|
134
|
+
|
|
135
|
+
// --- derived state ---------------------------------------------------------
|
|
136
|
+
def dlgAt(layer: int): int =
|
|
137
|
+
if (layer == 1) l1 else if (layer == 2) l2 else if (layer == 3) l3 else 0
|
|
138
|
+
def retAt(layer: int): int =
|
|
139
|
+
if (layer == 1) r1 else if (layer == 2) r2 else if (layer == 3) r3 else 0
|
|
140
|
+
def topDialog: int = dlgAt(depth)
|
|
141
|
+
def topRet: int = retAt(depth)
|
|
142
|
+
def focusLayer: int = focus / 100
|
|
143
|
+
def focusPos: int = focus % 100
|
|
144
|
+
|
|
145
|
+
def validPos(d: int, pos: int): bool =
|
|
146
|
+
if (d == 0) and(pos >= 1, pos <= tabCount(0))
|
|
147
|
+
else and(pos <= tabCount(d), or(pos >= 1, and(pos == 0, hasStatic(d))))
|
|
148
|
+
def validTarget(target: int): bool =
|
|
149
|
+
and(target / 100 == depth, validPos(topDialog, target % 100))
|
|
150
|
+
def nextPos(pos: int, n: int): int = if (pos >= n) 1 else pos + 1
|
|
151
|
+
def prevPos(pos: int, n: int): int = if (pos <= 1) n else pos - 1
|
|
152
|
+
|
|
153
|
+
// --- transition kernel -----------------------------------------------------
|
|
154
|
+
// ev: 1 escape, 2 activate, 3 pointer_outside, 4 tab, 5 shift_tab,
|
|
155
|
+
// 6 focus_attempt, 7 remove_invoker
|
|
156
|
+
// mode: 0 none, 1 close top, 2 open nested, 3 replace top
|
|
157
|
+
def activateOpens: int = opens(topDialog, focusPos)
|
|
158
|
+
def activateCloses: bool = closes(topDialog, focusPos)
|
|
159
|
+
def activateMode: int =
|
|
160
|
+
if (and(activateOpens > 0, activateCloses)) 3
|
|
161
|
+
else if (and(activateOpens > 0, depth < MAX_DEPTH)) 2
|
|
162
|
+
else if (activateCloses) 1
|
|
163
|
+
else 0
|
|
164
|
+
def modeOf(ev: int): int =
|
|
165
|
+
if (ev == 1) (if (depth > 0) 1 else 0)
|
|
166
|
+
else if (ev == 2) activateMode
|
|
167
|
+
else if (ev == 3) (if (and(CLOSE_ON_OUTSIDE, depth > 0)) 1 else 0)
|
|
168
|
+
else 0
|
|
169
|
+
def newDlg(ev: int): int = if (ev == 2) activateOpens else 0
|
|
170
|
+
|
|
171
|
+
def nextDepth(ev: int): int =
|
|
172
|
+
if (modeOf(ev) == 1) depth - 1
|
|
173
|
+
else if (modeOf(ev) == 2) depth + 1
|
|
174
|
+
else depth
|
|
175
|
+
def nextL(k: int, cur: int, ev: int): int =
|
|
176
|
+
if (and(modeOf(ev) == 1, depth == k)) 0
|
|
177
|
+
else if (and(modeOf(ev) == 2, depth + 1 == k)) newDlg(ev)
|
|
178
|
+
else if (and(modeOf(ev) == 3, depth == k)) newDlg(ev)
|
|
179
|
+
else cur
|
|
180
|
+
def nextR(k: int, cur: int, ev: int): int =
|
|
181
|
+
if (and(modeOf(ev) == 1, depth == k)) 0
|
|
182
|
+
else if (and(modeOf(ev) == 2, depth + 1 == k)) focus
|
|
183
|
+
else if (and(ev == 7, and(depth > 0, depth == k))) (k - 1) * 100 + fallbackPos(dlgAt(k - 1))
|
|
184
|
+
else cur
|
|
185
|
+
def nextFocus(ev: int, arg: int): int =
|
|
186
|
+
if (modeOf(ev) == 1) topRet
|
|
187
|
+
else if (modeOf(ev) == 2) (depth + 1) * 100 + initialFocus(newDlg(ev))
|
|
188
|
+
else if (modeOf(ev) == 3) depth * 100 + initialFocus(newDlg(ev))
|
|
189
|
+
else if (ev == 4) depth * 100 + nextPos(focusPos, tabCount(topDialog))
|
|
190
|
+
else if (ev == 5) depth * 100 + prevPos(focusPos, tabCount(topDialog))
|
|
191
|
+
else if (and(ev == 6, validTarget(arg))) arg
|
|
192
|
+
else focus
|
|
193
|
+
|
|
194
|
+
// --- request / commit (map ticket rt1p) ----------------------------------
|
|
195
|
+
def moved(a: bool, b: bool): int = if (a == b) 0 else 1
|
|
196
|
+
def holds: bool = and(controlled, not(accepting))
|
|
197
|
+
def follows: bool = and(controlled, accepting)
|
|
198
|
+
def verdict: str = if (holds) "rejected" else "accepted"
|
|
199
|
+
def landed(h: bool, v: bool, want: bool): bool = if (h) v else want
|
|
200
|
+
def propAfter(f: bool, p: bool, want: bool): bool = if (f) want else p
|
|
201
|
+
// the open state is "any dialog is open"; only a change of it is a request
|
|
202
|
+
def openNow: bool = depth > 0
|
|
203
|
+
def wantOpenEscape: bool = nextDepth(1) > 0
|
|
204
|
+
def wantOpenActivate: bool = nextDepth(2) > 0
|
|
205
|
+
def wantOpenOutside: bool = nextDepth(3) > 0
|
|
206
|
+
def rejectEscape: bool = and(holds, wantOpenEscape != openNow)
|
|
207
|
+
def rejectActivate: bool = and(holds, wantOpenActivate != openNow)
|
|
208
|
+
def rejectOutside: bool = and(holds, wantOpenOutside != openNow)
|
|
209
|
+
|
|
210
|
+
// --- host writes (the only commit while controlled) ----------------------
|
|
211
|
+
def setDepth(c: bool, v: bool, d: int): int =
|
|
212
|
+
if (c) (if (v) (if (d > 0) d else 1) else 0) else d
|
|
213
|
+
def openingSet(c: bool, v: bool, d: int): bool = and(c, and(v, d == 0))
|
|
214
|
+
def closingSet(c: bool, v: bool, d: int): bool = and(c, and(not(v), d > 0))
|
|
215
|
+
def toggleDepth(c: bool, d: int): int = if (c) (if (d > 0) 0 else 1) else d
|
|
216
|
+
|
|
217
|
+
action init = all {
|
|
218
|
+
depth' = 0,
|
|
219
|
+
l1' = 0,
|
|
220
|
+
l2' = 0,
|
|
221
|
+
l3' = 0,
|
|
222
|
+
r1' = 0,
|
|
223
|
+
r2' = 0,
|
|
224
|
+
r3' = 0,
|
|
225
|
+
focus' = 1,
|
|
226
|
+
requests' = 0,
|
|
227
|
+
controlled' = false,
|
|
228
|
+
accepting' = false,
|
|
229
|
+
valueProp' = false,
|
|
230
|
+
requested' = false,
|
|
231
|
+
outcome' = "none",
|
|
232
|
+
base' = false
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// request: Escape closes the top dialog only
|
|
236
|
+
action escape = all {
|
|
237
|
+
depth' = if (rejectEscape) depth else nextDepth(1),
|
|
238
|
+
l1' = if (rejectEscape) l1 else nextL(1, l1, 1),
|
|
239
|
+
l2' = if (rejectEscape) l2 else nextL(2, l2, 1),
|
|
240
|
+
l3' = if (rejectEscape) l3 else nextL(3, l3, 1),
|
|
241
|
+
r1' = if (rejectEscape) r1 else nextR(1, r1, 1),
|
|
242
|
+
r2' = if (rejectEscape) r2 else nextR(2, r2, 1),
|
|
243
|
+
r3' = if (rejectEscape) r3 else nextR(3, r3, 1),
|
|
244
|
+
focus' = if (rejectEscape) focus else nextFocus(1, 0),
|
|
245
|
+
requests' = requests + moved(openNow, wantOpenEscape),
|
|
246
|
+
controlled' = controlled,
|
|
247
|
+
accepting' = accepting,
|
|
248
|
+
valueProp' = propAfter(follows, valueProp, wantOpenEscape),
|
|
249
|
+
requested' = if (openNow == wantOpenEscape) requested else wantOpenEscape,
|
|
250
|
+
outcome' = if (openNow == wantOpenEscape) outcome else verdict,
|
|
251
|
+
base' = if (openNow == wantOpenEscape) base else openNow
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// request: activate opens a nested dialog, closes the top, or replaces it
|
|
255
|
+
action activate = all {
|
|
256
|
+
depth' = if (rejectActivate) depth else nextDepth(2),
|
|
257
|
+
l1' = if (rejectActivate) l1 else nextL(1, l1, 2),
|
|
258
|
+
l2' = if (rejectActivate) l2 else nextL(2, l2, 2),
|
|
259
|
+
l3' = if (rejectActivate) l3 else nextL(3, l3, 2),
|
|
260
|
+
r1' = if (rejectActivate) r1 else nextR(1, r1, 2),
|
|
261
|
+
r2' = if (rejectActivate) r2 else nextR(2, r2, 2),
|
|
262
|
+
r3' = if (rejectActivate) r3 else nextR(3, r3, 2),
|
|
263
|
+
focus' = if (rejectActivate) focus else nextFocus(2, 0),
|
|
264
|
+
requests' = requests + moved(openNow, wantOpenActivate),
|
|
265
|
+
controlled' = controlled,
|
|
266
|
+
accepting' = accepting,
|
|
267
|
+
valueProp' = propAfter(follows, valueProp, wantOpenActivate),
|
|
268
|
+
requested' = if (openNow == wantOpenActivate) requested else wantOpenActivate,
|
|
269
|
+
outcome' = if (openNow == wantOpenActivate) outcome else verdict,
|
|
270
|
+
base' = if (openNow == wantOpenActivate) base else openNow
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// request: a click on the inert content (only acts with CLOSE_ON_OUTSIDE)
|
|
274
|
+
action pointer_outside = all {
|
|
275
|
+
depth' = if (rejectOutside) depth else nextDepth(3),
|
|
276
|
+
l1' = if (rejectOutside) l1 else nextL(1, l1, 3),
|
|
277
|
+
l2' = if (rejectOutside) l2 else nextL(2, l2, 3),
|
|
278
|
+
l3' = if (rejectOutside) l3 else nextL(3, l3, 3),
|
|
279
|
+
r1' = if (rejectOutside) r1 else nextR(1, r1, 3),
|
|
280
|
+
r2' = if (rejectOutside) r2 else nextR(2, r2, 3),
|
|
281
|
+
r3' = if (rejectOutside) r3 else nextR(3, r3, 3),
|
|
282
|
+
focus' = if (rejectOutside) focus else nextFocus(3, 0),
|
|
283
|
+
requests' = requests + moved(openNow, wantOpenOutside),
|
|
284
|
+
controlled' = controlled,
|
|
285
|
+
accepting' = accepting,
|
|
286
|
+
valueProp' = propAfter(follows, valueProp, wantOpenOutside),
|
|
287
|
+
requested' = if (openNow == wantOpenOutside) requested else wantOpenOutside,
|
|
288
|
+
outcome' = if (openNow == wantOpenOutside) outcome else verdict,
|
|
289
|
+
base' = if (openNow == wantOpenOutside) base else openNow
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
action tab = all {
|
|
293
|
+
depth' = nextDepth(4),
|
|
294
|
+
l1' = nextL(1, l1, 4),
|
|
295
|
+
l2' = nextL(2, l2, 4),
|
|
296
|
+
l3' = nextL(3, l3, 4),
|
|
297
|
+
r1' = nextR(1, r1, 4),
|
|
298
|
+
r2' = nextR(2, r2, 4),
|
|
299
|
+
r3' = nextR(3, r3, 4),
|
|
300
|
+
focus' = nextFocus(4, 0),
|
|
301
|
+
requests' = requests,
|
|
302
|
+
controlled' = controlled,
|
|
303
|
+
accepting' = accepting,
|
|
304
|
+
valueProp' = valueProp,
|
|
305
|
+
requested' = requested,
|
|
306
|
+
outcome' = outcome,
|
|
307
|
+
base' = base
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
action shift_tab = all {
|
|
311
|
+
depth' = nextDepth(5),
|
|
312
|
+
l1' = nextL(1, l1, 5),
|
|
313
|
+
l2' = nextL(2, l2, 5),
|
|
314
|
+
l3' = nextL(3, l3, 5),
|
|
315
|
+
r1' = nextR(1, r1, 5),
|
|
316
|
+
r2' = nextR(2, r2, 5),
|
|
317
|
+
r3' = nextR(3, r3, 5),
|
|
318
|
+
focus' = nextFocus(5, 0),
|
|
319
|
+
requests' = requests,
|
|
320
|
+
controlled' = controlled,
|
|
321
|
+
accepting' = accepting,
|
|
322
|
+
valueProp' = valueProp,
|
|
323
|
+
requested' = requested,
|
|
324
|
+
outcome' = outcome,
|
|
325
|
+
base' = base
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// pointer click / programmatic focus on any encoded target (valid or not)
|
|
329
|
+
action focus_attempt = {
|
|
330
|
+
nondet target = Set(0, 1, 3, 4, 100, 101, 102, 106, 108, 109, 200, 201, 202, 203, 300, 301, 302).oneOf()
|
|
331
|
+
all {
|
|
332
|
+
depth' = nextDepth(6),
|
|
333
|
+
l1' = nextL(1, l1, 6),
|
|
334
|
+
l2' = nextL(2, l2, 6),
|
|
335
|
+
l3' = nextL(3, l3, 6),
|
|
336
|
+
r1' = nextR(1, r1, 6),
|
|
337
|
+
r2' = nextR(2, r2, 6),
|
|
338
|
+
r3' = nextR(3, r3, 6),
|
|
339
|
+
focus' = nextFocus(6, target),
|
|
340
|
+
requests' = requests,
|
|
341
|
+
controlled' = controlled,
|
|
342
|
+
accepting' = accepting,
|
|
343
|
+
valueProp' = valueProp,
|
|
344
|
+
requested' = requested,
|
|
345
|
+
outcome' = outcome,
|
|
346
|
+
base' = base
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
action remove_invoker = all {
|
|
351
|
+
depth' = nextDepth(7),
|
|
352
|
+
l1' = nextL(1, l1, 7),
|
|
353
|
+
l2' = nextL(2, l2, 7),
|
|
354
|
+
l3' = nextL(3, l3, 7),
|
|
355
|
+
r1' = nextR(1, r1, 7),
|
|
356
|
+
r2' = nextR(2, r2, 7),
|
|
357
|
+
r3' = nextR(3, r3, 7),
|
|
358
|
+
focus' = nextFocus(7, 0),
|
|
359
|
+
requests' = requests,
|
|
360
|
+
controlled' = controlled,
|
|
361
|
+
accepting' = accepting,
|
|
362
|
+
valueProp' = valueProp,
|
|
363
|
+
requested' = requested,
|
|
364
|
+
outcome' = outcome,
|
|
365
|
+
base' = base
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// commit: the host writes the controlled open state (a no-op while uncontrolled)
|
|
369
|
+
action setValue = {
|
|
370
|
+
nondet v = Set(true, false).oneOf()
|
|
371
|
+
all {
|
|
372
|
+
depth' = if (controlled) setDepth(true, v, depth) else depth,
|
|
373
|
+
l1' = if (openingSet(controlled, v, depth)) 1
|
|
374
|
+
else if (closingSet(controlled, v, depth)) 0 else l1,
|
|
375
|
+
l2' = if (closingSet(controlled, v, depth)) 0 else l2,
|
|
376
|
+
l3' = if (closingSet(controlled, v, depth)) 0 else l3,
|
|
377
|
+
r1' = if (openingSet(controlled, v, depth)) 1
|
|
378
|
+
else if (closingSet(controlled, v, depth)) 0 else r1,
|
|
379
|
+
r2' = if (closingSet(controlled, v, depth)) 0 else r2,
|
|
380
|
+
r3' = if (closingSet(controlled, v, depth)) 0 else r3,
|
|
381
|
+
focus' = if (openingSet(controlled, v, depth)) (100 + initialFocus(1))
|
|
382
|
+
else if (closingSet(controlled, v, depth)) 1 else focus,
|
|
383
|
+
requests' = requests,
|
|
384
|
+
controlled' = controlled,
|
|
385
|
+
accepting' = accepting,
|
|
386
|
+
valueProp' = if (controlled) (setDepth(true, v, depth) > 0) else valueProp,
|
|
387
|
+
requested' = requested,
|
|
388
|
+
outcome' = if (controlled) "none" else outcome,
|
|
389
|
+
base' = base
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// commit: the host flips its own copy of the open state
|
|
394
|
+
action hostToggle = all {
|
|
395
|
+
depth' = if (controlled) toggleDepth(true, depth) else depth,
|
|
396
|
+
l1' = if (openingSet(controlled, not(openNow), depth)) 1
|
|
397
|
+
else if (closingSet(controlled, not(openNow), depth)) 0 else l1,
|
|
398
|
+
l2' = if (closingSet(controlled, not(openNow), depth)) 0 else l2,
|
|
399
|
+
l3' = if (closingSet(controlled, not(openNow), depth)) 0 else l3,
|
|
400
|
+
r1' = if (openingSet(controlled, not(openNow), depth)) 1
|
|
401
|
+
else if (closingSet(controlled, not(openNow), depth)) 0 else r1,
|
|
402
|
+
r2' = if (closingSet(controlled, not(openNow), depth)) 0 else r2,
|
|
403
|
+
r3' = if (closingSet(controlled, not(openNow), depth)) 0 else r3,
|
|
404
|
+
focus' = if (openingSet(controlled, not(openNow), depth)) (100 + initialFocus(1))
|
|
405
|
+
else if (closingSet(controlled, not(openNow), depth)) 1 else focus,
|
|
406
|
+
requests' = requests,
|
|
407
|
+
controlled' = controlled,
|
|
408
|
+
accepting' = accepting,
|
|
409
|
+
valueProp' = if (controlled) (toggleDepth(true, depth) > 0) else valueProp,
|
|
410
|
+
requested' = requested,
|
|
411
|
+
outcome' = if (controlled) "none" else outcome,
|
|
412
|
+
base' = base
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// configuration: the host starts owning the open state, adopting the current one
|
|
416
|
+
action control = all {
|
|
417
|
+
depth' = depth,
|
|
418
|
+
l1' = l1,
|
|
419
|
+
l2' = l2,
|
|
420
|
+
l3' = l3,
|
|
421
|
+
r1' = r1,
|
|
422
|
+
r2' = r2,
|
|
423
|
+
r3' = r3,
|
|
424
|
+
focus' = focus,
|
|
425
|
+
requests' = requests,
|
|
426
|
+
controlled' = true,
|
|
427
|
+
accepting' = accepting,
|
|
428
|
+
valueProp' = openNow,
|
|
429
|
+
requested' = requested,
|
|
430
|
+
outcome' = outcome,
|
|
431
|
+
base' = base
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// configuration: the controlled host answers every request with its intent
|
|
435
|
+
action acceptRequests = all {
|
|
436
|
+
depth' = depth,
|
|
437
|
+
l1' = l1,
|
|
438
|
+
l2' = l2,
|
|
439
|
+
l3' = l3,
|
|
440
|
+
r1' = r1,
|
|
441
|
+
r2' = r2,
|
|
442
|
+
r3' = r3,
|
|
443
|
+
focus' = focus,
|
|
444
|
+
requests' = requests,
|
|
445
|
+
controlled' = controlled,
|
|
446
|
+
accepting' = true,
|
|
447
|
+
valueProp' = valueProp,
|
|
448
|
+
requested' = requested,
|
|
449
|
+
outcome' = outcome,
|
|
450
|
+
base' = base
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
action step = any {
|
|
454
|
+
init, escape, activate, pointer_outside, tab, shift_tab, focus_attempt,
|
|
455
|
+
remove_invoker, setValue, hostToggle, control, acceptRequests
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// --- invariants ------------------------------------------------------------
|
|
459
|
+
// focus never leaves the top modal while it is open (page when none is)
|
|
460
|
+
val FocusInTopLayer: bool = focusLayer == depth
|
|
461
|
+
// nothing beneath the top layer holds focus (aria-modal: those layers are inert)
|
|
462
|
+
val NoFocusInInertLayer: bool = not(isInert(focusLayer))
|
|
463
|
+
// focus sits on a real element; the dialog container itself is never focused
|
|
464
|
+
val FocusPositionValid: bool = validPos(topDialog, focusPos)
|
|
465
|
+
val DepthBounded: bool = and(depth >= 0, depth <= MAX_DEPTH)
|
|
466
|
+
// exactly layers 1..depth hold a dialog and a return target; the rest are empty
|
|
467
|
+
val StackContiguous: bool = and(
|
|
468
|
+
or(and(depth >= 1, l1 > 0), and(depth < 1, l1 == 0, r1 == 0)),
|
|
469
|
+
or(and(depth >= 2, l2 > 0), and(depth < 2, l2 == 0, r2 == 0)),
|
|
470
|
+
or(and(depth >= 3, l3 > 0), and(depth < 3, l3 == 0, r3 == 0))
|
|
471
|
+
)
|
|
472
|
+
// each open layer's return target is a real element in the layer beneath it
|
|
473
|
+
val ReturnTargetsValid: bool = and(
|
|
474
|
+
or(depth < 1, and(r1 / 100 == 0, validPos(dlgAt(0), r1 % 100))),
|
|
475
|
+
or(depth < 2, and(r2 / 100 == 1, validPos(dlgAt(1), r2 % 100))),
|
|
476
|
+
or(depth < 3, and(r3 / 100 == 2, validPos(dlgAt(2), r3 % 100)))
|
|
477
|
+
)
|
|
478
|
+
// role=dialog, aria-modal=true and an accessible name on every open dialog
|
|
479
|
+
val OpenDialogsHonourContract: bool = and(
|
|
480
|
+
or(depth < 1, contractOk(l1)),
|
|
481
|
+
or(depth < 2, contractOk(l2)),
|
|
482
|
+
or(depth < 3, contractOk(l3))
|
|
483
|
+
)
|
|
484
|
+
// configuration sanity: every dialog's initial focus is a focusable position
|
|
485
|
+
val InitialFocusConfigValid: bool = and(
|
|
486
|
+
validPos(1, initialFocus(1)),
|
|
487
|
+
validPos(2, initialFocus(2)),
|
|
488
|
+
validPos(3, initialFocus(3)),
|
|
489
|
+
validPos(4, initialFocus(4))
|
|
490
|
+
)
|
|
491
|
+
// callbacks only ever count up from zero
|
|
492
|
+
val RequestsNonNegative: bool = requests >= 0
|
|
493
|
+
// controlled: only the value prop sets the state, so no event commits
|
|
494
|
+
val ControlledFollowsProp: bool = or(not(controlled), openNow == valueProp)
|
|
495
|
+
// a rejected request leaves the state as it found it
|
|
496
|
+
val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), openNow == base)
|
|
497
|
+
// an accepted request committed exactly its intent
|
|
498
|
+
val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), openNow == requested)
|
|
499
|
+
}
|