@hypen-space/web 0.4.980 → 0.5.3
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/README.md +20 -13
- package/dist/canvas/accessibility.d.ts +118 -11
- package/dist/canvas/accessibility.js +470 -108
- package/dist/canvas/accessibility.js.map +1 -1
- package/dist/canvas/dispatch.d.ts +24 -0
- package/dist/canvas/dispatch.js +62 -0
- package/dist/canvas/dispatch.js.map +1 -0
- package/dist/canvas/editing.d.ts +178 -0
- package/dist/canvas/editing.js +590 -0
- package/dist/canvas/editing.js.map +1 -0
- package/dist/canvas/events.d.ts +28 -23
- package/dist/canvas/events.js +90 -103
- package/dist/canvas/events.js.map +1 -1
- package/dist/canvas/focus.d.ts +78 -0
- package/dist/canvas/focus.js +182 -0
- package/dist/canvas/focus.js.map +1 -0
- package/dist/canvas/index.d.ts +3 -2
- package/dist/canvas/index.js +3 -2
- package/dist/canvas/index.js.map +1 -1
- package/dist/canvas/layout.js +68 -98
- package/dist/canvas/layout.js.map +1 -1
- package/dist/canvas/paint.d.ts +16 -3
- package/dist/canvas/paint.js +449 -110
- package/dist/canvas/paint.js.map +1 -1
- package/dist/canvas/renderer.d.ts +28 -4
- package/dist/canvas/renderer.js +201 -81
- package/dist/canvas/renderer.js.map +1 -1
- package/dist/canvas/selection.js +31 -177
- package/dist/canvas/selection.js.map +1 -1
- package/dist/canvas/text-geometry.d.ts +72 -0
- package/dist/canvas/text-geometry.js +244 -0
- package/dist/canvas/text-geometry.js.map +1 -0
- package/dist/canvas/text.js +44 -18
- package/dist/canvas/text.js.map +1 -1
- package/dist/canvas/types.d.ts +10 -1
- package/dist/canvas/utils.d.ts +0 -23
- package/dist/canvas/utils.js +3 -67
- package/dist/canvas/utils.js.map +1 -1
- package/dist/canvas/variants.d.ts +52 -0
- package/dist/canvas/variants.js +156 -0
- package/dist/canvas/variants.js.map +1 -0
- package/dist/dom/a11y-styles.d.ts +20 -0
- package/dist/dom/a11y-styles.js +61 -0
- package/dist/dom/a11y-styles.js.map +1 -0
- package/dist/dom/applicators/aria.d.ts +19 -0
- package/dist/dom/applicators/aria.js +36 -0
- package/dist/dom/applicators/aria.js.map +1 -0
- package/dist/dom/applicators/events.d.ts +7 -0
- package/dist/dom/applicators/events.js +36 -8
- package/dist/dom/applicators/events.js.map +1 -1
- package/dist/dom/applicators/font.d.ts +11 -6
- package/dist/dom/applicators/font.js +16 -27
- package/dist/dom/applicators/font.js.map +1 -1
- package/dist/dom/applicators/index.js +51 -45
- package/dist/dom/applicators/index.js.map +1 -1
- package/dist/dom/canvas/index.js +2 -2
- package/dist/dom/canvas/index.js.map +1 -1
- package/dist/dom/components/app.js +2 -2
- package/dist/dom/components/app.js.map +1 -1
- package/dist/dom/components/audio.js +2 -2
- package/dist/dom/components/audio.js.map +1 -1
- package/dist/dom/components/avatar.js +3 -4
- package/dist/dom/components/avatar.js.map +1 -1
- package/dist/dom/components/badge.js +3 -4
- package/dist/dom/components/badge.js.map +1 -1
- package/dist/dom/components/button.js +2 -2
- package/dist/dom/components/button.js.map +1 -1
- package/dist/dom/components/card.js +2 -2
- package/dist/dom/components/card.js.map +1 -1
- package/dist/dom/components/center.js +2 -2
- package/dist/dom/components/center.js.map +1 -1
- package/dist/dom/components/checkbox.js +4 -5
- package/dist/dom/components/checkbox.js.map +1 -1
- package/dist/dom/components/column.js +2 -2
- package/dist/dom/components/column.js.map +1 -1
- package/dist/dom/components/container.js +2 -2
- package/dist/dom/components/container.js.map +1 -1
- package/dist/dom/components/divider.js +2 -2
- package/dist/dom/components/divider.js.map +1 -1
- package/dist/dom/components/grid.js +10 -10
- package/dist/dom/components/grid.js.map +1 -1
- package/dist/dom/components/heading.js +4 -6
- package/dist/dom/components/heading.js.map +1 -1
- package/dist/dom/components/hypenapp.d.ts +25 -1
- package/dist/dom/components/hypenapp.js +215 -247
- package/dist/dom/components/hypenapp.js.map +1 -1
- package/dist/dom/components/icon.js +4 -5
- package/dist/dom/components/icon.js.map +1 -1
- package/dist/dom/components/image.js +2 -2
- package/dist/dom/components/image.js.map +1 -1
- package/dist/dom/components/index.d.ts +2 -2
- package/dist/dom/components/index.js +9 -2
- package/dist/dom/components/index.js.map +1 -1
- package/dist/dom/components/input.js +2 -2
- package/dist/dom/components/input.js.map +1 -1
- package/dist/dom/components/link.js +2 -2
- package/dist/dom/components/link.js.map +1 -1
- package/dist/dom/components/list.js +2 -2
- package/dist/dom/components/list.js.map +1 -1
- package/dist/dom/components/paragraph.js +3 -4
- package/dist/dom/components/paragraph.js.map +1 -1
- package/dist/dom/components/progressbar.js +3 -3
- package/dist/dom/components/progressbar.js.map +1 -1
- package/dist/dom/components/route.js +2 -2
- package/dist/dom/components/route.js.map +1 -1
- package/dist/dom/components/router.js +2 -2
- package/dist/dom/components/router.js.map +1 -1
- package/dist/dom/components/row.js +10 -10
- package/dist/dom/components/row.js.map +1 -1
- package/dist/dom/components/select.js +3 -4
- package/dist/dom/components/select.js.map +1 -1
- package/dist/dom/components/slider.js +2 -2
- package/dist/dom/components/slider.js.map +1 -1
- package/dist/dom/components/spacer.js +2 -2
- package/dist/dom/components/spacer.js.map +1 -1
- package/dist/dom/components/spinner.js +27 -13
- package/dist/dom/components/spinner.js.map +1 -1
- package/dist/dom/components/stack.js +10 -10
- package/dist/dom/components/stack.js.map +1 -1
- package/dist/dom/components/switch.js +5 -6
- package/dist/dom/components/switch.js.map +1 -1
- package/dist/dom/components/tabs.d.ts +27 -0
- package/dist/dom/components/tabs.js +69 -0
- package/dist/dom/components/tabs.js.map +1 -0
- package/dist/dom/components/text.js +4 -7
- package/dist/dom/components/text.js.map +1 -1
- package/dist/dom/components/textarea.js +2 -2
- package/dist/dom/components/textarea.js.map +1 -1
- package/dist/dom/components/video.js +2 -2
- package/dist/dom/components/video.js.map +1 -1
- package/dist/dom/components/visuallyhidden.d.ts +9 -0
- package/dist/dom/components/visuallyhidden.js +26 -0
- package/dist/dom/components/visuallyhidden.js.map +1 -0
- package/dist/dom/debug.js +6 -8
- package/dist/dom/debug.js.map +1 -1
- package/dist/dom/index.d.ts +0 -1
- package/dist/dom/index.js +0 -1
- package/dist/dom/index.js.map +1 -1
- package/dist/dom/operability.d.ts +100 -0
- package/dist/dom/operability.js +298 -0
- package/dist/dom/operability.js.map +1 -0
- package/dist/dom/renderer.d.ts +98 -5
- package/dist/dom/renderer.js +400 -66
- package/dist/dom/renderer.js.map +1 -1
- package/dist/dom/route-focus.d.ts +42 -0
- package/dist/dom/route-focus.js +88 -0
- package/dist/dom/route-focus.js.map +1 -0
- package/dist/dom/semantics.d.ts +35 -0
- package/dist/dom/semantics.js +184 -0
- package/dist/dom/semantics.js.map +1 -0
- package/dist/variants.d.ts +90 -0
- package/dist/variants.js +206 -0
- package/dist/variants.js.map +1 -0
- package/package.json +2 -2
- package/src/canvas/QUICKSTART.md +16 -11
- package/src/canvas/README.md +63 -34
- package/src/canvas/accessibility.ts +507 -115
- package/src/canvas/dispatch.ts +78 -0
- package/src/canvas/editing.ts +697 -0
- package/src/canvas/events.ts +104 -115
- package/src/canvas/focus.ts +216 -0
- package/src/canvas/index.ts +8 -9
- package/src/canvas/layout.ts +68 -103
- package/src/canvas/paint.ts +509 -109
- package/src/canvas/renderer.ts +239 -99
- package/src/canvas/selection.ts +52 -210
- package/src/canvas/text-geometry.ts +311 -0
- package/src/canvas/text.ts +45 -18
- package/src/canvas/types.ts +28 -1
- package/src/canvas/utils.ts +3 -69
- package/src/canvas/variants.ts +172 -0
- package/src/dom/README.md +8 -7
- package/src/dom/a11y-styles.ts +65 -0
- package/src/dom/applicators/aria.ts +41 -0
- package/src/dom/applicators/events.ts +39 -7
- package/src/dom/applicators/font.ts +16 -29
- package/src/dom/applicators/index.ts +61 -46
- package/src/dom/canvas/index.ts +2 -2
- package/src/dom/components/app.ts +2 -2
- package/src/dom/components/audio.ts +2 -2
- package/src/dom/components/avatar.ts +3 -4
- package/src/dom/components/badge.ts +3 -4
- package/src/dom/components/button.ts +2 -2
- package/src/dom/components/card.ts +2 -2
- package/src/dom/components/center.ts +2 -2
- package/src/dom/components/checkbox.ts +4 -5
- package/src/dom/components/column.ts +2 -2
- package/src/dom/components/container.ts +2 -2
- package/src/dom/components/divider.ts +2 -2
- package/src/dom/components/grid.ts +10 -10
- package/src/dom/components/heading.ts +4 -6
- package/src/dom/components/hypenapp.ts +246 -274
- package/src/dom/components/icon.ts +4 -5
- package/src/dom/components/image.ts +2 -2
- package/src/dom/components/index.ts +10 -3
- package/src/dom/components/input.ts +2 -2
- package/src/dom/components/link.ts +2 -2
- package/src/dom/components/list.ts +2 -2
- package/src/dom/components/paragraph.ts +3 -4
- package/src/dom/components/progressbar.ts +3 -3
- package/src/dom/components/route.ts +2 -2
- package/src/dom/components/router.ts +2 -2
- package/src/dom/components/row.ts +10 -10
- package/src/dom/components/select.ts +3 -4
- package/src/dom/components/slider.ts +2 -2
- package/src/dom/components/spacer.ts +2 -2
- package/src/dom/components/spinner.ts +34 -16
- package/src/dom/components/stack.ts +10 -10
- package/src/dom/components/switch.ts +5 -6
- package/src/dom/components/tabs.ts +76 -0
- package/src/dom/components/text.ts +4 -7
- package/src/dom/components/textarea.ts +2 -2
- package/src/dom/components/video.ts +2 -2
- package/src/dom/components/visuallyhidden.ts +30 -0
- package/src/dom/debug.ts +6 -8
- package/src/dom/index.ts +0 -8
- package/src/dom/operability.ts +312 -0
- package/src/dom/renderer.ts +457 -66
- package/src/dom/route-focus.ts +98 -0
- package/src/dom/semantics.ts +199 -0
- package/src/variants.ts +249 -0
- package/dist/canvas/input.d.ts +0 -76
- package/dist/canvas/input.js +0 -207
- package/dist/canvas/input.js.map +0 -1
- package/dist/client.d.ts +0 -36
- package/dist/client.js +0 -47
- package/dist/client.js.map +0 -1
- package/src/canvas/input.ts +0 -251
- package/src/client.ts +0 -88
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard operability for actionable elements.
|
|
3
|
+
*
|
|
4
|
+
* Native interactive elements (`<button>`, `<a href>`) are keyboard-operable
|
|
5
|
+
* for free — the browser activates them on Enter/Space. But Hypen's actionable
|
|
6
|
+
* `Card` renders a plain `<div>`, which is neither focusable nor
|
|
7
|
+
* keyboard-activatable. This module closes that gap: a non-native actionable
|
|
8
|
+
* host is given a `button` role, made focusable (`tabindex=0`), and wired so
|
|
9
|
+
* Enter/Space dispatch the same action as a click.
|
|
10
|
+
*
|
|
11
|
+
* This is intentionally client-side: focus and key handling are synchronous
|
|
12
|
+
* interactions the streaming engine cannot mediate.
|
|
13
|
+
*/
|
|
14
|
+
import { triggerElementAction } from "./applicators/events.js";
|
|
15
|
+
import { getMeta, setMeta } from "./element-data.js";
|
|
16
|
+
import { focusRouteTarget } from "./route-focus.js";
|
|
17
|
+
/** Host tags that the browser already makes keyboard-operable. */
|
|
18
|
+
const NATIVE_INTERACTIVE = new Set(["BUTTON", "A"]);
|
|
19
|
+
/**
|
|
20
|
+
* Make a non-native actionable element keyboard-operable. Idempotent across
|
|
21
|
+
* re-renders (guarded by a dataset flag). No-op for native interactive tags.
|
|
22
|
+
*/
|
|
23
|
+
export function makeKeyboardActivatable(element, actionValue) {
|
|
24
|
+
const tag = element.tagName?.toUpperCase();
|
|
25
|
+
if (tag && NATIVE_INTERACTIVE.has(tag))
|
|
26
|
+
return;
|
|
27
|
+
// The keydown listener reads the action from element meta, so a later
|
|
28
|
+
// re-apply with a different action retargets keyboard activation exactly
|
|
29
|
+
// as it does the click listener — no stale action, no second listener.
|
|
30
|
+
setMeta(element, "kbdAction", actionValue);
|
|
31
|
+
// Wire the keydown listener only once.
|
|
32
|
+
if (element.dataset?.hypenKbd)
|
|
33
|
+
return;
|
|
34
|
+
if (element.dataset)
|
|
35
|
+
element.dataset.hypenKbd = "1";
|
|
36
|
+
// Announce as a button, make it reachable, and activate on Enter/Space.
|
|
37
|
+
element.setAttribute("role", "button");
|
|
38
|
+
element.tabIndex = 0;
|
|
39
|
+
element.addEventListener("keydown", (event) => {
|
|
40
|
+
const key = event.key;
|
|
41
|
+
if (key === "Enter" || key === " " || key === "Spacebar") {
|
|
42
|
+
// Space would otherwise scroll the page.
|
|
43
|
+
event.preventDefault?.();
|
|
44
|
+
triggerElementAction(element, getMeta(element, "kbdAction"));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** CSS selector for the elements that can hold keyboard focus. */
|
|
49
|
+
const FOCUSABLE_SELECTOR = "a,button,input,textarea,select,[tabindex]";
|
|
50
|
+
/**
|
|
51
|
+
* Pick the focus target for a Tab keypress within a trapped container, wrapping
|
|
52
|
+
* at the ends.
|
|
53
|
+
*
|
|
54
|
+
* Pure over the ordered list of focusable descendants so it is testable without
|
|
55
|
+
* a real DOM: given the currently-focused element (`active`) and the Shift
|
|
56
|
+
* state, returns the element that should receive focus next, or `null` when
|
|
57
|
+
* there is nothing to focus. Tab past the last element wraps to the first;
|
|
58
|
+
* Shift+Tab past the first wraps to the last. When focus is outside the set,
|
|
59
|
+
* Tab lands on the first element and Shift+Tab on the last.
|
|
60
|
+
*/
|
|
61
|
+
export function nextTrapFocus(focusable, active, shift) {
|
|
62
|
+
if (focusable.length === 0)
|
|
63
|
+
return null;
|
|
64
|
+
const first = focusable[0];
|
|
65
|
+
const last = focusable[focusable.length - 1];
|
|
66
|
+
const index = active == null ? -1 : focusable.indexOf(active);
|
|
67
|
+
if (index === -1)
|
|
68
|
+
return shift ? last : first;
|
|
69
|
+
if (shift)
|
|
70
|
+
return index === 0 ? last : focusable[index - 1];
|
|
71
|
+
return index === focusable.length - 1 ? first : focusable[index + 1];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Pick the tab that should receive focus for a keypress inside a roving
|
|
75
|
+
* tablist (WAI-ARIA APG "Tabs" keyboard contract). Pure over the ordered tab
|
|
76
|
+
* list so it is testable without a real DOM: Arrow Right/Down move to the
|
|
77
|
+
* next tab (wrapping), Arrow Left/Up to the previous (wrapping), Home/End to
|
|
78
|
+
* the first/last. Any other key — or an empty list — returns `null` (leave
|
|
79
|
+
* focus alone).
|
|
80
|
+
*/
|
|
81
|
+
export function nextRovingFocus(tabs, active, key) {
|
|
82
|
+
if (tabs.length === 0)
|
|
83
|
+
return null;
|
|
84
|
+
const first = tabs[0];
|
|
85
|
+
const last = tabs[tabs.length - 1];
|
|
86
|
+
if (key === "Home")
|
|
87
|
+
return first;
|
|
88
|
+
if (key === "End")
|
|
89
|
+
return last;
|
|
90
|
+
const forward = key === "ArrowRight" || key === "ArrowDown";
|
|
91
|
+
const backward = key === "ArrowLeft" || key === "ArrowUp";
|
|
92
|
+
if (!forward && !backward)
|
|
93
|
+
return null;
|
|
94
|
+
const index = active == null ? -1 : tabs.indexOf(active);
|
|
95
|
+
if (index === -1)
|
|
96
|
+
return first;
|
|
97
|
+
if (forward)
|
|
98
|
+
return index === tabs.length - 1 ? first : tabs[index + 1];
|
|
99
|
+
return index === 0 ? last : tabs[index - 1];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Pick the item typeahead should move focus to (WAI-ARIA APG first-character
|
|
103
|
+
* navigation). Pure over the ordered item list so it is testable without a
|
|
104
|
+
* real DOM: the search is a case-insensitive `startsWith` over `textOf(item)`,
|
|
105
|
+
* starting from the item AFTER `active` and wrapping; `active` itself is the
|
|
106
|
+
* last candidate, so a query the current item already matches keeps focus in
|
|
107
|
+
* place (multi-char accumulation refining onto the same item). Returns `null`
|
|
108
|
+
* when nothing matches or the query is empty. Query accumulation and its
|
|
109
|
+
* timeout live at the caller.
|
|
110
|
+
*/
|
|
111
|
+
export function nextTypeaheadFocus(items, active, query, textOf) {
|
|
112
|
+
if (items.length === 0 || query.length === 0)
|
|
113
|
+
return null;
|
|
114
|
+
const q = query.toLowerCase();
|
|
115
|
+
const start = active == null ? -1 : items.indexOf(active);
|
|
116
|
+
for (let step = 1; step <= items.length; step++) {
|
|
117
|
+
const item = items[(start + step + items.length) % items.length];
|
|
118
|
+
if (textOf(item).trim().toLowerCase().startsWith(q))
|
|
119
|
+
return item;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
/** Pause after which an accumulated typeahead query starts over. */
|
|
124
|
+
const TYPEAHEAD_RESET_MS = 500;
|
|
125
|
+
/**
|
|
126
|
+
* Shared roving-tabindex installer for composite widgets (tablist, listbox):
|
|
127
|
+
* arrow keys move focus among the `itemSelector` descendants (wrapping),
|
|
128
|
+
* Home/End jump to the ends, and only the focused item stays in the page Tab
|
|
129
|
+
* order (`tabindex=0`; the rest drop to `-1`) so a single Tab keypress leaves
|
|
130
|
+
* the widget. Printable characters accumulate into a short-lived typeahead
|
|
131
|
+
* query (reset after a 500ms pause, tracked by timestamp — no timers) that
|
|
132
|
+
* roves to the next item whose text starts with the query. Space is excluded:
|
|
133
|
+
* it activates the focused item, and stealing it would break native Button
|
|
134
|
+
* hosts. Activation stays with the item's own click/Enter handling —
|
|
135
|
+
* selection is app state, not focus state. Idempotent across re-renders,
|
|
136
|
+
* guarded by a dataset flag.
|
|
137
|
+
*/
|
|
138
|
+
function installRovingWidget(container, itemSelector) {
|
|
139
|
+
if (container.dataset?.hypenRoving)
|
|
140
|
+
return;
|
|
141
|
+
if (container.dataset)
|
|
142
|
+
container.dataset.hypenRoving = "1";
|
|
143
|
+
const itemsOf = () => Array.from(container.querySelectorAll(itemSelector));
|
|
144
|
+
const rove = (target, items) => {
|
|
145
|
+
for (const item of items) {
|
|
146
|
+
item.tabIndex = item === target ? 0 : -1;
|
|
147
|
+
}
|
|
148
|
+
target.focus?.();
|
|
149
|
+
};
|
|
150
|
+
let typeahead = "";
|
|
151
|
+
let typeaheadAt = 0;
|
|
152
|
+
container.addEventListener("keydown", (event) => {
|
|
153
|
+
const keyEvent = event;
|
|
154
|
+
const key = keyEvent.key;
|
|
155
|
+
const items = itemsOf();
|
|
156
|
+
const focused = (container.ownerDocument?.activeElement ?? null);
|
|
157
|
+
const active = focused && items.includes(focused) ? focused : null;
|
|
158
|
+
let target = nextRovingFocus(items, active, key);
|
|
159
|
+
if (!target &&
|
|
160
|
+
key.length === 1 &&
|
|
161
|
+
key !== " " &&
|
|
162
|
+
!keyEvent.ctrlKey &&
|
|
163
|
+
!keyEvent.metaKey &&
|
|
164
|
+
!keyEvent.altKey) {
|
|
165
|
+
const now = Date.now();
|
|
166
|
+
if (now - typeaheadAt > TYPEAHEAD_RESET_MS)
|
|
167
|
+
typeahead = "";
|
|
168
|
+
typeaheadAt = now;
|
|
169
|
+
typeahead += key;
|
|
170
|
+
target = nextTypeaheadFocus(items, active, typeahead, (item) => item.textContent ?? "");
|
|
171
|
+
}
|
|
172
|
+
if (target) {
|
|
173
|
+
event.preventDefault?.();
|
|
174
|
+
rove(target, items);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
// First entry into the widget establishes the roving state (focused item
|
|
178
|
+
// reachable, siblings parked at -1) without needing any keydown first.
|
|
179
|
+
container.addEventListener("focusin", (event) => {
|
|
180
|
+
const items = itemsOf();
|
|
181
|
+
const focused = event.target;
|
|
182
|
+
if (focused && items.includes(focused)) {
|
|
183
|
+
for (const item of items) {
|
|
184
|
+
item.tabIndex = item === focused ? 0 : -1;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Install the WAI-ARIA APG "Tabs" keyboard contract on a tablist container:
|
|
191
|
+
* roving tabindex over the `role="tab"` descendants plus first-character
|
|
192
|
+
* typeahead (see `installRovingWidget`).
|
|
193
|
+
*/
|
|
194
|
+
export function makeRovingTablist(container) {
|
|
195
|
+
installRovingWidget(container, '[role="tab"]');
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Install the WAI-ARIA APG "Listbox" keyboard contract on a listbox
|
|
199
|
+
* container: roving tabindex over the `role="option"` descendants plus
|
|
200
|
+
* first-character typeahead (see `installRovingWidget`). No-op for a native
|
|
201
|
+
* `<select>` host (Hypen's `Select`), where the browser already owns arrow
|
|
202
|
+
* and typeahead behaviour.
|
|
203
|
+
*/
|
|
204
|
+
export function makeRovingListbox(container) {
|
|
205
|
+
if (container.tagName?.toUpperCase() === "SELECT")
|
|
206
|
+
return;
|
|
207
|
+
installRovingWidget(container, '[role="option"]');
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Install a focus trap on a dialog-like container: Tab / Shift+Tab cycle focus
|
|
211
|
+
* among the container's focusable descendants and wrap at the ends. Closing is
|
|
212
|
+
* app state: Escape is handled by `installDialogEscape` only when the dialog
|
|
213
|
+
* declares an `onClose` action. Idempotent across re-renders, guarded by a
|
|
214
|
+
* dataset flag.
|
|
215
|
+
*/
|
|
216
|
+
export function makeFocusTrap(container) {
|
|
217
|
+
if (container.dataset?.hypenTrap)
|
|
218
|
+
return;
|
|
219
|
+
if (container.dataset)
|
|
220
|
+
container.dataset.hypenTrap = "1";
|
|
221
|
+
container.addEventListener("keydown", (event) => {
|
|
222
|
+
if (event.key !== "Tab")
|
|
223
|
+
return;
|
|
224
|
+
const focusable = Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
|
|
225
|
+
const active = (container.ownerDocument?.activeElement ?? null);
|
|
226
|
+
const target = nextTrapFocus(focusable, active, event.shiftKey === true);
|
|
227
|
+
if (target) {
|
|
228
|
+
event.preventDefault?.();
|
|
229
|
+
target.focus?.();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/** Tags that can hold focus without a tabindex — the DFS twin of `FOCUSABLE_SELECTOR`. */
|
|
234
|
+
const FOCUSABLE_TAGS = new Set(["A", "BUTTON", "INPUT", "TEXTAREA", "SELECT"]);
|
|
235
|
+
/**
|
|
236
|
+
* First focusable descendant of `root` in document order, or `null`. Matches
|
|
237
|
+
* the focus trap's element set (`FOCUSABLE_SELECTOR`) but walks `children`
|
|
238
|
+
* recursively instead of `querySelectorAll`, so it works on hosts without
|
|
239
|
+
* selector support (test doubles) — document order is exactly child order.
|
|
240
|
+
*/
|
|
241
|
+
export function findFirstFocusable(root) {
|
|
242
|
+
const children = Array.from((root.children ?? []));
|
|
243
|
+
for (const child of children) {
|
|
244
|
+
const tag = child.tagName?.toUpperCase() ?? "";
|
|
245
|
+
if (FOCUSABLE_TAGS.has(tag) || child.getAttribute?.("tabindex") != null)
|
|
246
|
+
return child;
|
|
247
|
+
const nested = findFirstFocusable(child);
|
|
248
|
+
if (nested)
|
|
249
|
+
return nested;
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Dialog auto-focus on mount (WAI-ARIA APG "Dialog (Modal)"): focus the
|
|
255
|
+
* dialog's first focusable descendant, else the dialog itself (granted
|
|
256
|
+
* programmatic focusability by `focusRouteTarget`). Returns the element that
|
|
257
|
+
* held focus before the dialog opened, so the caller can restore it on close.
|
|
258
|
+
*/
|
|
259
|
+
export function focusDialogOnOpen(dialog) {
|
|
260
|
+
const doc = dialog.ownerDocument ?? (typeof document !== "undefined" ? document : null);
|
|
261
|
+
const previous = (doc?.activeElement ?? null);
|
|
262
|
+
focusRouteTarget(findFirstFocusable(dialog) ?? dialog);
|
|
263
|
+
return previous;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Restore focus to the element that had it before a dialog opened. Skipped
|
|
267
|
+
* when the opener is gone: no longer connected to the document, or living
|
|
268
|
+
* inside the (now closed) dialog itself.
|
|
269
|
+
*/
|
|
270
|
+
export function restoreDialogFocus(dialog, opener) {
|
|
271
|
+
if (!opener)
|
|
272
|
+
return;
|
|
273
|
+
if (dialog === opener || dialog.contains?.(opener))
|
|
274
|
+
return;
|
|
275
|
+
if (opener.isConnected === false)
|
|
276
|
+
return;
|
|
277
|
+
opener.focus?.();
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Escape-to-close for a dialog that declares an `onClose` action: Escape
|
|
281
|
+
* dispatches the action; the actual close (state flip, unmount) stays with
|
|
282
|
+
* the app. A dialog without `onClose` gets no Escape behaviour — the renderer
|
|
283
|
+
* cannot know how to close it. Idempotent across re-renders, guarded by a
|
|
284
|
+
* dataset flag.
|
|
285
|
+
*/
|
|
286
|
+
export function installDialogEscape(dialog, onCloseValue) {
|
|
287
|
+
if (dialog.dataset?.hypenDialogEsc)
|
|
288
|
+
return;
|
|
289
|
+
if (dialog.dataset)
|
|
290
|
+
dialog.dataset.hypenDialogEsc = "1";
|
|
291
|
+
dialog.addEventListener("keydown", (event) => {
|
|
292
|
+
if (event.key !== "Escape")
|
|
293
|
+
return;
|
|
294
|
+
event.preventDefault?.();
|
|
295
|
+
triggerElementAction(dialog, onCloseValue);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
//# sourceMappingURL=operability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operability.js","sourceRoot":"","sources":["../../src/dom/operability.ts"],"names":[],"mappings":"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"}
|
package/dist/dom/renderer.d.ts
CHANGED
|
@@ -12,6 +12,17 @@ import { type DebugConfig } from "./debug.js";
|
|
|
12
12
|
interface IEngine {
|
|
13
13
|
dispatchAction(name: string, payload?: any): void;
|
|
14
14
|
}
|
|
15
|
+
export interface DOMRendererOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Route-change focus contract (route-focus.ts). `"auto"` (default) moves
|
|
18
|
+
* focus to the incoming route's first heading / `main` landmark / subtree
|
|
19
|
+
* root on every navigation. `"off"` skips `focusIncomingRoute` entirely —
|
|
20
|
+
* the escape hatch for authors who own focus management themselves (custom
|
|
21
|
+
* transition choreography, embedded shells) and would otherwise fight the
|
|
22
|
+
* renderer for focus on every navigation.
|
|
23
|
+
*/
|
|
24
|
+
routeFocus?: "auto" | "off";
|
|
25
|
+
}
|
|
15
26
|
export declare class DOMRenderer {
|
|
16
27
|
private container;
|
|
17
28
|
private nodes;
|
|
@@ -24,12 +35,45 @@ export declare class DOMRenderer {
|
|
|
24
35
|
private globalContext;
|
|
25
36
|
private componentInstances;
|
|
26
37
|
private debugTracker;
|
|
38
|
+
/**
|
|
39
|
+
* Text elements whose template contains an `@{...}` binding, with the
|
|
40
|
+
* template pre-compiled into segments. Only these nodes are visited on
|
|
41
|
+
* state changes — static text never re-interpolates.
|
|
42
|
+
*/
|
|
43
|
+
private textBindings;
|
|
27
44
|
private canvasRenderers;
|
|
28
45
|
private canvasSubtreeMap;
|
|
46
|
+
private canvasNodeChildren;
|
|
47
|
+
private canvasNodeParent;
|
|
29
48
|
private canvasElements;
|
|
30
49
|
/** Props stashed at create-time so we can forward them to CanvasRenderer */
|
|
31
50
|
private pendingCreateProps;
|
|
32
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Focus-restore memory for the Router subtree cache, keyed by detached
|
|
53
|
+
* subtree root id: the element that held focus when the route was
|
|
54
|
+
* detached, restored on cached re-`attach`. Entries are dropped on
|
|
55
|
+
* `remove` (Router LRU eviction) so focus restore can never target an
|
|
56
|
+
* evicted NodeId — the route-focus contract (see route-focus.ts).
|
|
57
|
+
*/
|
|
58
|
+
private routeFocusMemory;
|
|
59
|
+
/** See `DOMRendererOptions.routeFocus` — `"off"` disables `focusIncomingRoute`. */
|
|
60
|
+
private routeFocus;
|
|
61
|
+
/** Ids created with `semantics.role === "dialog"`, so insert/remove hooks can act. */
|
|
62
|
+
private dialogIds;
|
|
63
|
+
/**
|
|
64
|
+
* Open dialog id → the element that held focus before the dialog took it
|
|
65
|
+
* (the trigger), restored on dialog remove/detach when still connected.
|
|
66
|
+
* Presence of an entry means "already focused" — re-inserts (moves) of an
|
|
67
|
+
* open dialog never re-run the mount focus.
|
|
68
|
+
*/
|
|
69
|
+
private dialogOpeners;
|
|
70
|
+
/**
|
|
71
|
+
* Dialog ids inserted during the current batch. Mount focus runs after the
|
|
72
|
+
* whole batch, because a dialog's focusable children may be inserted after
|
|
73
|
+
* the dialog's own insert patch.
|
|
74
|
+
*/
|
|
75
|
+
private pendingDialogMounts;
|
|
76
|
+
constructor(container: HTMLElement, engine: IEngine, debugConfig?: Partial<DebugConfig>, options?: DOMRendererOptions);
|
|
33
77
|
/**
|
|
34
78
|
* Set router and global context for component composition
|
|
35
79
|
*/
|
|
@@ -42,11 +86,37 @@ export declare class DOMRenderer {
|
|
|
42
86
|
* the DOMRenderer or the owning CanvasRenderer.
|
|
43
87
|
*/
|
|
44
88
|
applyPatches(patches: Patch[]): void;
|
|
89
|
+
/**
|
|
90
|
+
* Apply the dialog focus contract to dialogs inserted by the finished
|
|
91
|
+
* batch: focus the first focusable descendant, else the dialog itself, and
|
|
92
|
+
* remember the previously-focused element (the trigger) for restore on
|
|
93
|
+
* close. Deferred to batch end so descendants inserted after the dialog's
|
|
94
|
+
* own insert patch are visible to the focus search.
|
|
95
|
+
*/
|
|
96
|
+
private flushDialogMounts;
|
|
97
|
+
/**
|
|
98
|
+
* Apply the route-focus contract to a freshly-shown route subtree: restore
|
|
99
|
+
* the remembered focus of a cached route when it is still inside the
|
|
100
|
+
* subtree, otherwise land on the first heading / main landmark / the
|
|
101
|
+
* subtree root (see route-focus.ts for the full contract).
|
|
102
|
+
*/
|
|
103
|
+
private focusIncomingRoute;
|
|
45
104
|
/**
|
|
46
105
|
* Determine which canvas root (if any) a patch should be routed to.
|
|
47
106
|
* Returns the canvas root node ID, or undefined for DOM patches.
|
|
48
107
|
*/
|
|
49
108
|
private getCanvasRouteTarget;
|
|
109
|
+
/**
|
|
110
|
+
* Record a node as a member of a canvas subtree, tracking its parent link
|
|
111
|
+
* so a later `remove` can prune the whole subtree from the routing maps.
|
|
112
|
+
*/
|
|
113
|
+
private registerCanvasMember;
|
|
114
|
+
/**
|
|
115
|
+
* Drop a removed canvas node and all of its descendants from the canvas
|
|
116
|
+
* routing maps. The engine emits `remove` only for the subtree root, so
|
|
117
|
+
* descendants are walked via the tracked child links.
|
|
118
|
+
*/
|
|
119
|
+
private pruneCanvasSubtree;
|
|
50
120
|
/**
|
|
51
121
|
* Update state and interpolate text content
|
|
52
122
|
*/
|
|
@@ -56,19 +126,35 @@ export declare class DOMRenderer {
|
|
|
56
126
|
*/
|
|
57
127
|
private mergeComponentState;
|
|
58
128
|
/**
|
|
59
|
-
* Interpolate state values in all text elements
|
|
129
|
+
* Interpolate state values in all binding-bearing text elements
|
|
60
130
|
*/
|
|
61
131
|
private interpolateAllText;
|
|
62
132
|
/**
|
|
63
|
-
*
|
|
133
|
+
* Resolve a compiled template against the current state.
|
|
64
134
|
*
|
|
65
|
-
* Bindings use the `@{state.path}` syntax (matching the engine's parser)
|
|
135
|
+
* Bindings use the `@{state.path}` syntax (matching the engine's parser);
|
|
136
|
+
* unresolvable bindings render as their original `@{...}` text.
|
|
66
137
|
*/
|
|
67
|
-
private
|
|
138
|
+
private interpolateSegments;
|
|
139
|
+
/**
|
|
140
|
+
* Keep the binding index in sync with the element's stored template:
|
|
141
|
+
* register text elements whose template contains an `@{...}` binding,
|
|
142
|
+
* drop everything else.
|
|
143
|
+
*/
|
|
144
|
+
private syncTextBinding;
|
|
68
145
|
/**
|
|
69
146
|
* Apply a single patch.
|
|
70
147
|
*/
|
|
71
148
|
private applyPatch;
|
|
149
|
+
/**
|
|
150
|
+
* Reactive accessibility update: re-run the create-time semantics
|
|
151
|
+
* translation on the live element with the node's complete re-resolved
|
|
152
|
+
* block. `applySemantics` is idempotent and clears attributes it set on a
|
|
153
|
+
* previous pass that the new block no longer produces. This is the sole
|
|
154
|
+
* ARIA writer for bound self-state — the SetProp that accompanies a bound
|
|
155
|
+
* `.expanded(@state.open)` change does not touch ARIA attributes.
|
|
156
|
+
*/
|
|
157
|
+
private onSetSemantics;
|
|
72
158
|
/**
|
|
73
159
|
* Create a new element
|
|
74
160
|
*/
|
|
@@ -111,6 +197,13 @@ export declare class DOMRenderer {
|
|
|
111
197
|
* route subtrees alive between navigations.
|
|
112
198
|
*/
|
|
113
199
|
private onDetach;
|
|
200
|
+
/**
|
|
201
|
+
* Close-side of the dialog focus contract: every open dialog at-or-under
|
|
202
|
+
* `subtree` has left the document — restore focus to its remembered
|
|
203
|
+
* trigger (when still connected) and drop the open entry so a later
|
|
204
|
+
* insert/attach counts as a fresh mount.
|
|
205
|
+
*/
|
|
206
|
+
private restoreDialogsWithin;
|
|
114
207
|
/**
|
|
115
208
|
* Remove an element from the tree
|
|
116
209
|
*/
|