@spaced-out/ui-design-system 0.6.34 → 0.6.35-beta.0
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/.cspell/custom-words.txt +9 -0
- package/CHANGELOG.md +7 -0
- package/lib/components/Menu/Menu.d.ts +71 -0
- package/lib/components/Menu/Menu.d.ts.map +1 -1
- package/lib/components/Menu/Menu.js +1028 -185
- package/lib/components/Menu/Menu.module.css +148 -27
- package/lib/components/Menu/MenuOptionButton.d.ts +23 -2
- package/lib/components/Menu/MenuOptionButton.d.ts.map +1 -1
- package/lib/components/Menu/MenuOptionButton.js +87 -38
- package/lib/components/Menu/index.d.ts +2 -0
- package/lib/components/Menu/index.d.ts.map +1 -1
- package/lib/components/Menu/index.js +22 -0
- package/lib/components/Menu/menuOptionId.d.ts +12 -0
- package/lib/components/Menu/menuOptionId.d.ts.map +1 -0
- package/lib/components/Menu/menuOptionId.js +18 -0
- package/lib/components/Menu/useMenuKeyboardNavigation.d.ts +93 -0
- package/lib/components/Menu/useMenuKeyboardNavigation.d.ts.map +1 -0
- package/lib/components/Menu/useMenuKeyboardNavigation.js +471 -0
- package/lib/components/Menu/useMenuTrigger.d.ts +183 -0
- package/lib/components/Menu/useMenuTrigger.d.ts.map +1 -0
- package/lib/components/Menu/useMenuTrigger.js +347 -0
- package/lib/hooks/index.d.ts +1 -0
- package/lib/hooks/index.d.ts.map +1 -1
- package/lib/hooks/index.js +11 -0
- package/lib/hooks/useInteractionModality/index.d.ts +2 -0
- package/lib/hooks/useInteractionModality/index.d.ts.map +1 -0
- package/lib/hooks/useInteractionModality/index.js +16 -0
- package/lib/hooks/useInteractionModality/useInteractionModality.d.ts +15 -0
- package/lib/hooks/useInteractionModality/useInteractionModality.d.ts.map +1 -0
- package/lib/hooks/useInteractionModality/useInteractionModality.js +103 -0
- package/lib/utils/click-away/click-away.d.ts +3 -0
- package/lib/utils/click-away/click-away.d.ts.map +1 -1
- package/lib/utils/click-away/click-away.js +49 -0
- package/mcp/package.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useMenuKeyboardNavigation = exports.moveFocusToMenuChrome = exports.getMenuRegionFocusables = void 0;
|
|
7
|
+
var React = _interopRequireWildcard(require("react"));
|
|
8
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
9
|
+
/**
|
|
10
|
+
* Keyboard navigation for the Menu option list. Three interaction models,
|
|
11
|
+
* selected via `mode`:
|
|
12
|
+
*
|
|
13
|
+
* 1. `'roving'` — ROVING FOCUS (default; menu-button popups). Options are native
|
|
14
|
+
* <button>s, so ArrowUp/ArrowDown move real DOM focus between the enabled
|
|
15
|
+
* options and Space/Enter activate the focused option natively. Home/End jump
|
|
16
|
+
* to the first/last option, PageUp/PageDown move by a page, and typing a
|
|
17
|
+
* printable character jumps to the next matching label (type-ahead).
|
|
18
|
+
*
|
|
19
|
+
* 2. `'activedescendant'` — SELECT-ONLY COMBOBOX. DOM focus stays on the
|
|
20
|
+
* combobox input; the "active" option is a virtual highlight reported via
|
|
21
|
+
* `setActiveOptionKey` (surfaced as the input's aria-activedescendant) and
|
|
22
|
+
* NEVER takes real focus. Arrows/Home/End/PageUp/PageDown move the highlight,
|
|
23
|
+
* printable characters type-ahead, and Enter selects the active option.
|
|
24
|
+
*
|
|
25
|
+
* 3. `'search'` — EDITABLE COMBOBOX. Like `'activedescendant'`, but the input is
|
|
26
|
+
* a free-text field: printable keys, Left/Right and Home/End are left to the
|
|
27
|
+
* input for text editing (no type-ahead). Arrows/PageUp/PageDown move the
|
|
28
|
+
* highlight and Enter selects.
|
|
29
|
+
*
|
|
30
|
+
* Navigation wraps around at the ends. Options are discovered from the DOM
|
|
31
|
+
* (`[data-menu-option-key]`) so it always matches what is rendered after search
|
|
32
|
+
* filtering / grouping.
|
|
33
|
+
*
|
|
34
|
+
* Tab / Shift+Tab keep focus moving through the menu's own focus zones in DOM
|
|
35
|
+
* order — header → search → options → footer — and only close the menu (via
|
|
36
|
+
* `onTabOut`) when focus would leave the menu entirely (Tab past the last
|
|
37
|
+
* focusable, Shift+Tab before the first). This keeps focus from silently
|
|
38
|
+
* dropping to <body> when the popup renders in a portal with nothing after it.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/** One navigable option, for menus whose option DOM is windowed. */
|
|
42
|
+
|
|
43
|
+
const OPTION_SELECTOR = '[data-menu-option-key]';
|
|
44
|
+
// Elements that can hold a tab stop inside the menu (header/footer controls,
|
|
45
|
+
// the search input, and the single roving option whose tabIndex is 0).
|
|
46
|
+
const TABBABLE_SELECTOR = 'a[href], button, input, select, textarea, [tabindex]';
|
|
47
|
+
const PAGE_SIZE = 10;
|
|
48
|
+
const TYPEAHEAD_RESET_MS = 600;
|
|
49
|
+
// Frames to wait for a windowed list to mount the row we just moved to before
|
|
50
|
+
// giving up on placing real focus on it.
|
|
51
|
+
const MOUNT_RETRY_FRAMES = 3;
|
|
52
|
+
const getEnabledOptionEls = container => {
|
|
53
|
+
if (!container) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
return Array.from(container.querySelectorAll(OPTION_SELECTOR)).filter(el => el.getAttribute('aria-disabled') !== 'true' && !el.disabled);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Enabled + visible. Used for elements we focus PROGRAMMATICALLY (footer/header
|
|
60
|
+
// controls via moveFocusToMenuChrome), so the natural tab order (tabIndex) is
|
|
61
|
+
// irrelevant — el.focus() works regardless. This matters because a combobox
|
|
62
|
+
// menu's footer/header controls are observed to sit at tabIndex -1 while the
|
|
63
|
+
// input holds focus, which a tabIndex-based filter (isTabbable) would wrongly
|
|
64
|
+
// skip.
|
|
65
|
+
const isFocusableControl = el => {
|
|
66
|
+
if (el.disabled) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
if (el.getAttribute('aria-disabled') === 'true') {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
// Skip hidden elements (no layout box).
|
|
73
|
+
return el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// As above, but also in the natural tab order right now (tabIndex >= 0). Used
|
|
77
|
+
// for the roving boundary check, where focus actually rests on the element.
|
|
78
|
+
const isTabbable = el => el.tabIndex >= 0 && isFocusableControl(el);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Focusable elements inside the menu, in DOM order. With roving tabindex only
|
|
82
|
+
* the active option is tabbable (tabIndex 0); the others and the menu container
|
|
83
|
+
* are tabIndex -1, so they are excluded — leaving header/footer controls, the
|
|
84
|
+
* search input and the single active option.
|
|
85
|
+
*/
|
|
86
|
+
const getTabbables = container => Array.from(container.querySelectorAll(TABBABLE_SELECTOR)).filter(isTabbable);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Focusable controls inside the menu's sticky header / footer region, in DOM
|
|
90
|
+
* order. Used by combobox triggers (Dropdown / Typeahead) so Tab / Shift+Tab can
|
|
91
|
+
* move focus into the footer / header instead of closing the menu when one
|
|
92
|
+
* holds focusable controls (e.g. Cancel / Save buttons).
|
|
93
|
+
*/
|
|
94
|
+
const getMenuRegionFocusables = (menu, region) => {
|
|
95
|
+
const regionEl = menu?.querySelector(`[data-menu-region="${region}"]`);
|
|
96
|
+
if (!regionEl) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
return Array.from(regionEl.querySelectorAll(TABBABLE_SELECTOR)).filter(isFocusableControl);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Move focus into the menu's footer (Tab) or header (Shift+Tab) edge if that
|
|
104
|
+
* region has focusable controls. Returns true when it moved focus, so a combobox
|
|
105
|
+
* trigger can close the menu only when there was no chrome to land on.
|
|
106
|
+
*/
|
|
107
|
+
exports.getMenuRegionFocusables = getMenuRegionFocusables;
|
|
108
|
+
const moveFocusToMenuChrome = (menu, shiftKey) => {
|
|
109
|
+
const focusables = getMenuRegionFocusables(menu, shiftKey ? 'header' : 'footer');
|
|
110
|
+
if (!focusables.length) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
// Tab → first control in the footer; Shift+Tab → last control in the header.
|
|
114
|
+
const target = shiftKey ? focusables[focusables.length - 1] : focusables[0];
|
|
115
|
+
target.focus();
|
|
116
|
+
return true;
|
|
117
|
+
};
|
|
118
|
+
exports.moveFocusToMenuChrome = moveFocusToMenuChrome;
|
|
119
|
+
const isPrintableCharacter = event => event.key.length === 1 && event.key !== ' ' && !event.altKey && !event.ctrlKey && !event.metaKey;
|
|
120
|
+
const useMenuKeyboardNavigation = _ref => {
|
|
121
|
+
let {
|
|
122
|
+
containerRef,
|
|
123
|
+
enabled = true,
|
|
124
|
+
mode = 'roving',
|
|
125
|
+
activeOptionKey,
|
|
126
|
+
onSelectKey,
|
|
127
|
+
setActiveOptionKey,
|
|
128
|
+
onTabOut,
|
|
129
|
+
onReturnFocusToInput,
|
|
130
|
+
options
|
|
131
|
+
} = _ref;
|
|
132
|
+
const typeahead = React.useRef({
|
|
133
|
+
query: '',
|
|
134
|
+
at: 0
|
|
135
|
+
});
|
|
136
|
+
const keyOf = el => el?.getAttribute('data-menu-option-key') ?? null;
|
|
137
|
+
|
|
138
|
+
// Move the highlight without moving focus (combobox / active-descendant model).
|
|
139
|
+
const highlightAt = (els, index) => {
|
|
140
|
+
const el = els[index];
|
|
141
|
+
if (!el) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
el.scrollIntoView({
|
|
145
|
+
block: 'nearest'
|
|
146
|
+
});
|
|
147
|
+
setActiveOptionKey(keyOf(el));
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Move real DOM focus (roving model).
|
|
151
|
+
const focusAt = (els, index) => {
|
|
152
|
+
const el = els[index];
|
|
153
|
+
if (!el) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
el.focus();
|
|
157
|
+
el.scrollIntoView({
|
|
158
|
+
block: 'nearest'
|
|
159
|
+
});
|
|
160
|
+
setActiveOptionKey(keyOf(el));
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/** Type-ahead: jump to the next option whose label matches the typed text. */
|
|
164
|
+
const typeaheadTo = _ref2 => {
|
|
165
|
+
let {
|
|
166
|
+
count,
|
|
167
|
+
labelAt,
|
|
168
|
+
currentIndex,
|
|
169
|
+
char,
|
|
170
|
+
move
|
|
171
|
+
} = _ref2;
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
const state = typeahead.current;
|
|
174
|
+
if (now - state.at > TYPEAHEAD_RESET_MS) {
|
|
175
|
+
state.query = '';
|
|
176
|
+
}
|
|
177
|
+
state.at = now;
|
|
178
|
+
state.query += char.toLowerCase();
|
|
179
|
+
const isRepeat = state.query.length > 1 && state.query.split('').every(c => c === state.query[0]);
|
|
180
|
+
const query = isRepeat ? state.query[0] : state.query;
|
|
181
|
+
// First keystroke or a repeated character advances past the current option
|
|
182
|
+
// so repeats cycle; accumulating distinct characters refines in place.
|
|
183
|
+
const offset = state.query.length === 1 || isRepeat ? 1 : 0;
|
|
184
|
+
const base = currentIndex < 0 ? -offset : currentIndex;
|
|
185
|
+
for (let i = 0; i < count; i++) {
|
|
186
|
+
const idx = (base + offset + i + count) % count;
|
|
187
|
+
if (labelAt(idx).startsWith(query)) {
|
|
188
|
+
move(idx);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Search mode only: a printable key pressed while focus is somewhere in the
|
|
196
|
+
* menu OTHER than the search input (in practice the menu container — the
|
|
197
|
+
* fallback target when every option is disabled) redirects focus to the input
|
|
198
|
+
* and forwards the character, so typing still lands in the search box.
|
|
199
|
+
* Header/footer controls never reach here: onKeyDown's `[data-menu-region]`
|
|
200
|
+
* guard returns before the printable branch when focus is in that chrome.
|
|
201
|
+
*/
|
|
202
|
+
const redirectPrintableToInput = event => {
|
|
203
|
+
const container = containerRef.current;
|
|
204
|
+
// Scope to the Menu's OWN search field via the marker it renders, not to a
|
|
205
|
+
// role — a consumer header/footer could contain an unrelated search input,
|
|
206
|
+
// and matching that would replay the user's keystrokes into the wrong field.
|
|
207
|
+
const searchInput = container?.querySelector('input[data-menu-search]');
|
|
208
|
+
if (!searchInput || typeof document === 'undefined' || document.activeElement === searchInput) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
event.preventDefault();
|
|
212
|
+
searchInput.focus();
|
|
213
|
+
searchInput.scrollIntoView({
|
|
214
|
+
block: 'nearest'
|
|
215
|
+
});
|
|
216
|
+
if (typeof window !== 'undefined') {
|
|
217
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
|
|
218
|
+
if (nativeSetter) {
|
|
219
|
+
nativeSetter.call(searchInput, searchInput.value + event.key);
|
|
220
|
+
searchInput.dispatchEvent(new Event('input', {
|
|
221
|
+
bubbles: true
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Keep Tab / Shift+Tab inside the menu's focus zones (header → search →
|
|
230
|
+
* options → footer) and close the menu when focus would leave it entirely,
|
|
231
|
+
* instead of letting it fall to <body> below the portal. Internal moves
|
|
232
|
+
* (e.g. option → footer button) fall through to the browser default.
|
|
233
|
+
*
|
|
234
|
+
* Combobox menus (when `onReturnFocusToInput` is set) treat the trigger input
|
|
235
|
+
* — which lives OUTSIDE the menu DOM — as the logical centre of the tab order
|
|
236
|
+
* (header ← input → footer). So Tabbing toward the input (Shift+Tab off the
|
|
237
|
+
* footer's first control, or Tab off the header's last control) returns focus
|
|
238
|
+
* to the input with the menu open, and only the OUTER edges (Tab past the last
|
|
239
|
+
* footer control, Shift+Tab before the first header control) close the menu.
|
|
240
|
+
*/
|
|
241
|
+
const handleTab = event => {
|
|
242
|
+
const container = containerRef.current;
|
|
243
|
+
if (!container || !onTabOut) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const focused = typeof document !== 'undefined' ? document.activeElement : null;
|
|
247
|
+
if (onReturnFocusToInput) {
|
|
248
|
+
const footers = getMenuRegionFocusables(container, 'footer');
|
|
249
|
+
const headers = getMenuRegionFocusables(container, 'header');
|
|
250
|
+
// Moving off a region back toward the input (Shift+Tab off the footer's
|
|
251
|
+
// first control, or Tab off the header's last control) returns focus to
|
|
252
|
+
// the input with the menu open — the input is the centre of the tab order.
|
|
253
|
+
const returningToInput = event.shiftKey ? footers.length > 0 && focused === footers[0] : headers.length > 0 && focused === headers[headers.length - 1];
|
|
254
|
+
if (returningToInput) {
|
|
255
|
+
event.preventDefault();
|
|
256
|
+
onReturnFocusToInput();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
// Anything else (footer/header outer edge, or focus not on a region edge)
|
|
260
|
+
// falls through to the boundary close logic below, so leaving the menu's
|
|
261
|
+
// outer edge closes it and never drops focus to <body>.
|
|
262
|
+
}
|
|
263
|
+
const tabbables = getTabbables(container);
|
|
264
|
+
const index = focused ? tabbables.indexOf(focused) : -1;
|
|
265
|
+
// Focus is inside the menu but not on one of its tab stops — in practice the
|
|
266
|
+
// menu container itself, which is the documented fallback target when every
|
|
267
|
+
// option is disabled or the option list is still empty. Treat it as sitting
|
|
268
|
+
// BEFORE the first tab stop: Tab moves forward into the menu's own stops
|
|
269
|
+
// (header / footer controls) instead of closing and leaving them unreachable,
|
|
270
|
+
// while Shift+Tab keeps moving backwards and falls through to the close
|
|
271
|
+
// below — jumping forward to the last control would reverse the user's
|
|
272
|
+
// direction of travel.
|
|
273
|
+
if (!event.shiftKey && index === -1 && tabbables.length) {
|
|
274
|
+
event.preventDefault();
|
|
275
|
+
tabbables[0].focus();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const leavingMenu = event.shiftKey ? index <= 0 : index === tabbables.length - 1;
|
|
279
|
+
if (leavingMenu) {
|
|
280
|
+
// Stop the browser moving focus out (to <body>); close instead so the
|
|
281
|
+
// consumer's ClickAway returns focus to the trigger.
|
|
282
|
+
event.preventDefault();
|
|
283
|
+
onTabOut();
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
const onKeyDown = event => {
|
|
287
|
+
// Tab management runs even when option navigation is disabled (external
|
|
288
|
+
// combobox menus) so that, once focus is inside the footer / header, Tabbing
|
|
289
|
+
// off the edge still closes the menu instead of dropping focus to <body>.
|
|
290
|
+
if (event.key === 'Tab') {
|
|
291
|
+
handleTab(event);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (!enabled) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
// While focus sits in the header/footer chrome (e.g. after Tabbing to a
|
|
298
|
+
// footer button), the menu's key handler must stay out of the way: arrows
|
|
299
|
+
// must not move or steal the option highlight, and printable keys must not
|
|
300
|
+
// be redirected into the search input. Let the focused control own its keys.
|
|
301
|
+
const focused = typeof document !== 'undefined' ? document.activeElement : null;
|
|
302
|
+
if (focused?.closest('[data-menu-region]')) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const els = getEnabledOptionEls(containerRef.current);
|
|
306
|
+
// A windowed list only has a slice of its rows in the DOM, so the caller
|
|
307
|
+
// supplies the real option list and it wins over what we can see.
|
|
308
|
+
const windowed = Boolean(options?.length);
|
|
309
|
+
const count = windowed ? options.length : els.length;
|
|
310
|
+
if (!count) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const movesFocus = mode === 'roving';
|
|
314
|
+
const hasSearchField = mode === 'search';
|
|
315
|
+
// Home/End (and Cmd+Arrow) belong to the caret only while the caret HAS
|
|
316
|
+
// focus. A searchable menu's virtualized listbox is itself a tab stop; once
|
|
317
|
+
// focus is on the list, those keys move the highlight like any listbox.
|
|
318
|
+
const caretOwnsKeys = hasSearchField && focused instanceof HTMLInputElement && focused.getAttribute('data-menu-search') !== null;
|
|
319
|
+
const labelAt = index => windowed ? (options[index].label ?? '').trim().toLowerCase() : (els[index].textContent ?? '').trim().toLowerCase();
|
|
320
|
+
const moveWindowed = index => {
|
|
321
|
+
const key = options[index].key;
|
|
322
|
+
// Publishing the key is the whole move: the consumer keys its virtual list
|
|
323
|
+
// off `activeOptionKey`, so this both scrolls the row into view and makes
|
|
324
|
+
// aria-activedescendant resolve once that row mounts. Deliberately no
|
|
325
|
+
// scrollIntoView here — that would fight the consumer's own scrolling.
|
|
326
|
+
setActiveOptionKey(key);
|
|
327
|
+
if (!movesFocus) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
// Roving mode needs REAL focus, and the row may not be mounted yet when the
|
|
331
|
+
// list is windowed. Retry for a few frames until the consumer's scroll has
|
|
332
|
+
// rendered it, rather than assuming a single frame is enough.
|
|
333
|
+
let attempts = 0;
|
|
334
|
+
const focusRow = () => {
|
|
335
|
+
const el = containerRef.current?.querySelector(`[data-menu-option-key="${CSS.escape(key)}"]`);
|
|
336
|
+
if (el) {
|
|
337
|
+
el.focus({
|
|
338
|
+
preventScroll: true
|
|
339
|
+
});
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (attempts < MOUNT_RETRY_FRAMES) {
|
|
343
|
+
attempts += 1;
|
|
344
|
+
requestAnimationFrame(focusRow);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
focusRow();
|
|
348
|
+
};
|
|
349
|
+
const move = windowed ? moveWindowed : movesFocus ? index => focusAt(els, index) : index => highlightAt(els, index);
|
|
350
|
+
const currentIndex = windowed ? activeOptionKey ? options.findIndex(option => option.key === activeOptionKey) : -1 : movesFocus ? els.findIndex(el => typeof document !== 'undefined' && el === document.activeElement) : activeOptionKey ? els.map(el => keyOf(el)).indexOf(activeOptionKey) : -1;
|
|
351
|
+
const fromIndex = currentIndex < 0 ? 0 : currentIndex;
|
|
352
|
+
|
|
353
|
+
// Mac keyboards have no End / Home key — those are Fn+Right / Fn+Left — so
|
|
354
|
+
// Cmd+Down / Cmd+Up stand in for them, which is the "jump to the end"
|
|
355
|
+
// gesture macOS uses everywhere else. Subject to the same search-input
|
|
356
|
+
// exemption as End / Home below, because in a text field the platform binds
|
|
357
|
+
// them to the caret. Option and Control leave the key to the browser
|
|
358
|
+
// instead of being silently treated as an unmodified arrow.
|
|
359
|
+
const jumpsToEdge = event.metaKey && !event.altKey && !event.ctrlKey;
|
|
360
|
+
if ((event.altKey || event.ctrlKey) && !event.metaKey) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
switch (event.key) {
|
|
364
|
+
case 'ArrowDown':
|
|
365
|
+
{
|
|
366
|
+
if (jumpsToEdge) {
|
|
367
|
+
if (!caretOwnsKeys) {
|
|
368
|
+
event.preventDefault();
|
|
369
|
+
move(count - 1);
|
|
370
|
+
}
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
// Wraps from the last option back to the first.
|
|
374
|
+
event.preventDefault();
|
|
375
|
+
move(currentIndex < 0 ? 0 : (currentIndex + 1) % count);
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
case 'ArrowUp':
|
|
379
|
+
{
|
|
380
|
+
if (jumpsToEdge) {
|
|
381
|
+
if (!caretOwnsKeys) {
|
|
382
|
+
event.preventDefault();
|
|
383
|
+
move(0);
|
|
384
|
+
}
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
// Wraps from the first option back to the last.
|
|
388
|
+
event.preventDefault();
|
|
389
|
+
move(currentIndex < 0 ? count - 1 : (currentIndex - 1 + count) % count);
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
case 'PageDown':
|
|
393
|
+
{
|
|
394
|
+
event.preventDefault();
|
|
395
|
+
move(Math.min(fromIndex + PAGE_SIZE, count - 1));
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
case 'PageUp':
|
|
399
|
+
{
|
|
400
|
+
event.preventDefault();
|
|
401
|
+
move(Math.max(fromIndex - PAGE_SIZE, 0));
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
case 'Home':
|
|
405
|
+
{
|
|
406
|
+
// Editable (search) input keeps Home/End for text-cursor movement.
|
|
407
|
+
if (!caretOwnsKeys) {
|
|
408
|
+
event.preventDefault();
|
|
409
|
+
move(0);
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
case 'End':
|
|
414
|
+
{
|
|
415
|
+
if (!caretOwnsKeys) {
|
|
416
|
+
event.preventDefault();
|
|
417
|
+
move(count - 1);
|
|
418
|
+
}
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
case ' ':
|
|
422
|
+
{
|
|
423
|
+
// In the text field Space types; on a roving option the <button> handles
|
|
424
|
+
// it natively. With focus on the list itself (the focusable listbox of a
|
|
425
|
+
// searchable menu) Space selects the active option, as it does for Enter.
|
|
426
|
+
if (!movesFocus && !caretOwnsKeys && activeOptionKey && onSelectKey) {
|
|
427
|
+
event.preventDefault();
|
|
428
|
+
onSelectKey(activeOptionKey);
|
|
429
|
+
}
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
case 'Enter':
|
|
433
|
+
{
|
|
434
|
+
// Roving mode lets the focused <button> handle Enter natively. The
|
|
435
|
+
// combobox modes keep focus on the input, so select explicitly.
|
|
436
|
+
if (!movesFocus && activeOptionKey && onSelectKey) {
|
|
437
|
+
event.preventDefault();
|
|
438
|
+
onSelectKey(activeOptionKey);
|
|
439
|
+
}
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
default:
|
|
443
|
+
{
|
|
444
|
+
// Space/Enter in roving mode are handled natively by the focused option.
|
|
445
|
+
if (!isPrintableCharacter(event)) {
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
if (hasSearchField) {
|
|
449
|
+
// Editable input: let the character type into the field (it filters).
|
|
450
|
+
// Only step in when focus is somewhere else in the menu.
|
|
451
|
+
redirectPrintableToInput(event);
|
|
452
|
+
} else {
|
|
453
|
+
// Roving / select-only combobox: printable jumps to a matching label.
|
|
454
|
+
event.preventDefault();
|
|
455
|
+
typeaheadTo({
|
|
456
|
+
count,
|
|
457
|
+
labelAt,
|
|
458
|
+
currentIndex,
|
|
459
|
+
char: event.key,
|
|
460
|
+
move
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
return {
|
|
468
|
+
onKeyDown
|
|
469
|
+
};
|
|
470
|
+
};
|
|
471
|
+
exports.useMenuKeyboardNavigation = useMenuKeyboardNavigation;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { MenuOption, MenuProps } from '../../components/Menu/Menu';
|
|
3
|
+
/**
|
|
4
|
+
* Wires a trigger to the Menu it opens.
|
|
5
|
+
*
|
|
6
|
+
* Three things have to live on the trigger rather than in Menu, because they
|
|
7
|
+
* exist while the menu does NOT: `aria-haspopup`, `aria-expanded="false"`, and
|
|
8
|
+
* the ArrowDown/ArrowUp that opens the popup in the first place. That is the
|
|
9
|
+
* floor — no Menu prop can supply them — so these hooks hand them over as props
|
|
10
|
+
* to spread instead, and own everything else: the ids, the tie between the
|
|
11
|
+
* trigger's name and the listbox, and the ref Menu focuses into on open.
|
|
12
|
+
*
|
|
13
|
+
* `useMenuTrigger` is for triggers whose popup options own focus — a button, a
|
|
14
|
+
* tab overflow chevron, an inline label — so arrows move real DOM focus.
|
|
15
|
+
* `useComboboxMenuTrigger` is for triggers that are text inputs, where focus
|
|
16
|
+
* stays put and the highlight is virtual (`aria-activedescendant`).
|
|
17
|
+
*
|
|
18
|
+
* `getTriggerProps` is a getter rather than a static object because open state
|
|
19
|
+
* belongs to the consumer's `ClickAway`, which exposes it through a render
|
|
20
|
+
* callback — where a hook cannot be called.
|
|
21
|
+
*
|
|
22
|
+
* const menuTrigger = useMenuTrigger({id: props.id, ariaLabel: menu?.ariaLabel});
|
|
23
|
+
* <ClickAway>
|
|
24
|
+
* {({isOpen, onOpen, clickAway}) => (
|
|
25
|
+
* <>
|
|
26
|
+
* <Button {...props} {...menuTrigger.getTriggerProps({isOpen, onOpen})} />
|
|
27
|
+
* {isOpen && <Menu {...menu} {...menuTrigger.menuProps} onTabOut={clickAway} />}
|
|
28
|
+
* </>
|
|
29
|
+
* )}
|
|
30
|
+
* </ClickAway>
|
|
31
|
+
*/
|
|
32
|
+
export interface UseMenuTriggerArgs {
|
|
33
|
+
/**
|
|
34
|
+
* The trigger's own `id`, when the consumer supplied one. It is kept — the
|
|
35
|
+
* hook only mints an id when there is none, since the listbox is named after
|
|
36
|
+
* the trigger through it.
|
|
37
|
+
*/
|
|
38
|
+
id?: string;
|
|
39
|
+
/** The listbox id, when the consumer set `menu.menuId`. Minted otherwise. */
|
|
40
|
+
menuId?: string;
|
|
41
|
+
/** Literal name for the listbox. Wins over naming it after the trigger. */
|
|
42
|
+
ariaLabel?: string;
|
|
43
|
+
/** Id of the element naming the listbox. Defaults to the trigger itself. */
|
|
44
|
+
ariaLabelledBy?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Generic in the trigger element so the returned handler matches whatever it is
|
|
48
|
+
* spread onto — inferred from the trigger's own `onKeyDown` where it has one.
|
|
49
|
+
*/
|
|
50
|
+
export interface GetTriggerPropsArgs<T extends HTMLElement = HTMLElement> {
|
|
51
|
+
/** The consumer's popup state — what ClickAway reports. */
|
|
52
|
+
isOpen: boolean;
|
|
53
|
+
onOpen: () => void;
|
|
54
|
+
/** The trigger's own key handler, if it has one. Runs first. */
|
|
55
|
+
onKeyDown?: React.KeyboardEventHandler<T>;
|
|
56
|
+
}
|
|
57
|
+
export interface MenuTriggerProps<T extends HTMLElement = HTMLElement> {
|
|
58
|
+
id: string;
|
|
59
|
+
'aria-haspopup': 'listbox';
|
|
60
|
+
'aria-expanded': boolean;
|
|
61
|
+
'aria-controls': string | undefined;
|
|
62
|
+
onKeyDown: React.KeyboardEventHandler<T>;
|
|
63
|
+
}
|
|
64
|
+
export interface MenuTriggerMenuProps {
|
|
65
|
+
menuId: string;
|
|
66
|
+
ariaLabelledBy?: string;
|
|
67
|
+
ariaLabel?: string;
|
|
68
|
+
initialFocusRef: React.RefObject<HTMLElement | null>;
|
|
69
|
+
}
|
|
70
|
+
export declare const useMenuTrigger: ({ id, menuId: consumerMenuId, ariaLabel, ariaLabelledBy, }?: UseMenuTriggerArgs) => {
|
|
71
|
+
getTriggerProps: <T extends HTMLElement = HTMLElement>(args: GetTriggerPropsArgs<T>) => MenuTriggerProps<T>;
|
|
72
|
+
menuProps: MenuTriggerMenuProps;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Which of ARIA's two combobox patterns the trigger implements.
|
|
76
|
+
*
|
|
77
|
+
* - `select` — a read-only field standing in for a `<select>`. An option is
|
|
78
|
+
* highlighted as soon as the list opens, Space selects it (the field has no
|
|
79
|
+
* typing role), and Enter or Space opens the list in the first place.
|
|
80
|
+
* - `autocomplete` — an editable field that filters the list as you type.
|
|
81
|
+
* Nothing is highlighted until the user arrows to it (APG calls this manual
|
|
82
|
+
* selection: auto-highlighting would let Enter select something they never
|
|
83
|
+
* chose), Space types a space, and only the arrow keys open the list.
|
|
84
|
+
*/
|
|
85
|
+
export type ComboboxPattern = 'select' | 'autocomplete';
|
|
86
|
+
export interface UseComboboxMenuTriggerArgs {
|
|
87
|
+
/** Which pattern this trigger implements. Defaults to `select`. */
|
|
88
|
+
pattern?: ComboboxPattern;
|
|
89
|
+
/**
|
|
90
|
+
* The menu this trigger opens. Read for the option source (unless `options`
|
|
91
|
+
* is given), the selection, `virtualization`, `menuDisabled`, `resolveLabel`,
|
|
92
|
+
* `allowSearch` and the consumer's own `menuId` / `ariaLabel` /
|
|
93
|
+
* `ariaLabelledBy`, all of which win over what the hook would derive.
|
|
94
|
+
*/
|
|
95
|
+
menu?: MenuProps;
|
|
96
|
+
/**
|
|
97
|
+
* Option source, when it is not `menu` — a consumer that owns filtering and
|
|
98
|
+
* hands the already-filtered options to Menu supplies them here instead. Every
|
|
99
|
+
* derivation (navigation list, highlight seed, selection lookup) uses it.
|
|
100
|
+
*/
|
|
101
|
+
options?: Array<MenuOption>;
|
|
102
|
+
/**
|
|
103
|
+
* Refuse to open, e.g. until enough has been typed. Consulted only for the
|
|
104
|
+
* keys that would otherwise open the list.
|
|
105
|
+
*/
|
|
106
|
+
canOpen?: () => boolean;
|
|
107
|
+
/** Ref on the rendered Menu, for reaching its header/footer controls. */
|
|
108
|
+
menuRef: React.RefObject<HTMLElement | null>;
|
|
109
|
+
/**
|
|
110
|
+
* The consumer's popup state — what ClickAway reports. Drives which keys the
|
|
111
|
+
* trigger handles (open-state navigation vs. closed-state opening) and the
|
|
112
|
+
* highlight's lifecycle. If the listbox can be WITHHELD while the popup is
|
|
113
|
+
* open (still loading, no matches), keep passing the open state here and hand
|
|
114
|
+
* `isExpanded` to `getTriggerProps` for the ARIA — routing keys off the
|
|
115
|
+
* rendered state instead would make an arrow key call `onOpen` on an
|
|
116
|
+
* already-open popup, and `onOpen` toggles.
|
|
117
|
+
*/
|
|
118
|
+
isOpen: boolean;
|
|
119
|
+
/** Apply a selection. Called with the option the keyboard acted on. */
|
|
120
|
+
onSelectOption: (option: MenuOption) => void;
|
|
121
|
+
/** Close the popup — used when Tab leaves a menu with no chrome to land on. */
|
|
122
|
+
onClose: () => void;
|
|
123
|
+
/** Put focus back on the trigger input, keeping the menu open. */
|
|
124
|
+
focusTrigger: () => void;
|
|
125
|
+
/** What already labels the trigger, used to name the listbox after it. */
|
|
126
|
+
triggerAriaLabelledBy?: string;
|
|
127
|
+
triggerAriaLabel?: string;
|
|
128
|
+
/**
|
|
129
|
+
* Literal name for the listbox when nothing else supplies one. Defaults to
|
|
130
|
+
* `'Options'`; a suggestion list wants something truer to what it holds.
|
|
131
|
+
*/
|
|
132
|
+
defaultAriaLabel?: string;
|
|
133
|
+
}
|
|
134
|
+
export interface GetComboboxTriggerPropsArgs<T extends HTMLElement = HTMLElement> extends GetTriggerPropsArgs<T> {
|
|
135
|
+
/**
|
|
136
|
+
* Whether the listbox is actually rendered right now, when that can differ
|
|
137
|
+
* from `isOpen` (a popup withheld while loading or with no matches). Drives
|
|
138
|
+
* `aria-expanded`, `aria-controls` and `aria-activedescendant`, so the trigger
|
|
139
|
+
* never claims an expanded listbox that is not in the DOM. Defaults to
|
|
140
|
+
* `isOpen`.
|
|
141
|
+
*/
|
|
142
|
+
isExpanded?: boolean;
|
|
143
|
+
}
|
|
144
|
+
export interface ComboboxTriggerProps<T extends HTMLElement = HTMLElement> {
|
|
145
|
+
role: 'combobox';
|
|
146
|
+
'aria-haspopup': 'listbox';
|
|
147
|
+
'aria-expanded': boolean;
|
|
148
|
+
'aria-controls': string | undefined;
|
|
149
|
+
'aria-autocomplete': 'none' | 'list';
|
|
150
|
+
'aria-activedescendant': string | undefined;
|
|
151
|
+
onKeyDown: React.KeyboardEventHandler<T>;
|
|
152
|
+
}
|
|
153
|
+
export interface ComboboxMenuProps {
|
|
154
|
+
menuId: string;
|
|
155
|
+
ariaLabelledBy?: string;
|
|
156
|
+
ariaLabel?: string;
|
|
157
|
+
externalKeyboardNav: boolean;
|
|
158
|
+
activeOptionKey?: string | null;
|
|
159
|
+
onActiveOptionKeyChange?: (key: string | null) => void;
|
|
160
|
+
onReturnFocusToInput?: () => void;
|
|
161
|
+
initialFocusRef?: React.RefObject<HTMLElement | null>;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Wires a COMBOBOX trigger — a text input — to the Menu it opens. Focus stays
|
|
165
|
+
* on the input and the highlight is virtual, reported through
|
|
166
|
+
* `aria-activedescendant`, because the input has to keep receiving the keys it
|
|
167
|
+
* filters with.
|
|
168
|
+
*
|
|
169
|
+
* Driving the highlight from outside Menu means owning the active option as
|
|
170
|
+
* state, and owning it means deriving the same option list, initial highlight
|
|
171
|
+
* and navigation list that Menu derives internally. Both combobox consumers had
|
|
172
|
+
* grown their own copies of all three; they come from Menu itself now.
|
|
173
|
+
*
|
|
174
|
+
* A menu that carries its own filter field (`menu.allowSearch`) inverts the
|
|
175
|
+
* model: navigation lives inside Menu, so the trigger stops driving it and
|
|
176
|
+
* hands over an `initialFocusRef` instead. Both shapes come out of this hook.
|
|
177
|
+
*/
|
|
178
|
+
export declare const useComboboxMenuTrigger: ({ pattern, menu, options, canOpen, menuRef, isOpen, onSelectOption, onClose, focusTrigger, triggerAriaLabelledBy, triggerAriaLabel, defaultAriaLabel, }: UseComboboxMenuTriggerArgs) => {
|
|
179
|
+
getTriggerProps: <T extends HTMLElement = HTMLElement>(args: GetComboboxTriggerPropsArgs<T>) => ComboboxTriggerProps<T>;
|
|
180
|
+
menuProps: ComboboxMenuProps;
|
|
181
|
+
activeOptionKey: string | null;
|
|
182
|
+
};
|
|
183
|
+
//# sourceMappingURL=useMenuTrigger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useMenuTrigger.d.ts","sourceRoot":"","sources":["../../../src/components/Menu/useMenuTrigger.ts"],"names":[],"mappings":"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"}
|