@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
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
@value (
|
|
10
10
|
colorBackgroundSecondary,
|
|
11
11
|
colorFillSecondary,
|
|
12
|
+
colorFillPrimary,
|
|
12
13
|
colorTextDisabled,
|
|
13
14
|
colorTextPrimary,
|
|
14
15
|
colorTextSecondary,
|
|
@@ -60,6 +61,17 @@
|
|
|
60
61
|
z-index: elevationMenu;
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
/*
|
|
65
|
+
* The options carry their own scroll container, so the card must not scroll as
|
|
66
|
+
* well — otherwise both would show a scrollbar. `hidden` also keeps a pinned
|
|
67
|
+
* header / footer inside the border radius.
|
|
68
|
+
*/
|
|
69
|
+
.menuCardTopPaddingZero,
|
|
70
|
+
.menuCardBottomPaddingZero,
|
|
71
|
+
.menuCardNoScroll {
|
|
72
|
+
overflow: hidden;
|
|
73
|
+
}
|
|
74
|
+
|
|
63
75
|
.menuCardTopPaddingZero {
|
|
64
76
|
padding-top: spaceNone;
|
|
65
77
|
}
|
|
@@ -68,7 +80,28 @@
|
|
|
68
80
|
padding-bottom: spaceNone;
|
|
69
81
|
}
|
|
70
82
|
|
|
71
|
-
|
|
83
|
+
/*
|
|
84
|
+
* Scrolls the options only. A header / footer is a flex sibling of this element
|
|
85
|
+
* rather than something sticky inside it, so the scrollbar spans just the option
|
|
86
|
+
* list — it no longer runs the whole height of the card behind the bars — and no
|
|
87
|
+
* option can be clipped under them. `min-height: 0` is what lets a flex item
|
|
88
|
+
* actually shrink and scroll instead of growing to fit its content.
|
|
89
|
+
*/
|
|
90
|
+
.optionsScroll {
|
|
91
|
+
display: flex;
|
|
92
|
+
flex-flow: column;
|
|
93
|
+
flex: 1 1 auto;
|
|
94
|
+
min-height: 0;
|
|
95
|
+
overflow-y: auto;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/*
|
|
99
|
+
* Collapse a menu that renders nothing. `:empty` alone no longer suffices: the
|
|
100
|
+
* card always holds the (possibly empty) role="listbox" element so the trigger's
|
|
101
|
+
* aria-controls resolves, so Menu also sets `.menuCardEmpty` explicitly.
|
|
102
|
+
*/
|
|
103
|
+
.menuCard:empty,
|
|
104
|
+
.menuCard.menuCardEmpty {
|
|
72
105
|
padding: spaceNone;
|
|
73
106
|
border: none;
|
|
74
107
|
}
|
|
@@ -165,25 +198,58 @@
|
|
|
165
198
|
color: colorTextTertiary;
|
|
166
199
|
}
|
|
167
200
|
|
|
201
|
+
/*
|
|
202
|
+
* Background for the highlighted option (the "cursor"). There is only ever one —
|
|
203
|
+
* the Menu keeps it in sync across pointer and keyboard. `.active` covers the
|
|
204
|
+
* combobox active-descendant case (focus stays on the input); :hover covers a
|
|
205
|
+
* plain mouse-over.
|
|
206
|
+
*/
|
|
168
207
|
.option:hover,
|
|
169
|
-
.option:active
|
|
170
|
-
|
|
208
|
+
.option:active,
|
|
209
|
+
.option.active {
|
|
210
|
+
outline: none;
|
|
171
211
|
color: colorTextPrimary;
|
|
212
|
+
background: colorBackgroundSecondary;
|
|
172
213
|
}
|
|
173
214
|
|
|
174
|
-
.option:hover .optionTextSecondaryLabel
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.option:active .optionTextSecondaryLabel {
|
|
215
|
+
.option:hover .optionTextSecondaryLabel,
|
|
216
|
+
.option:active .optionTextSecondaryLabel,
|
|
217
|
+
.option.active .optionTextSecondaryLabel,
|
|
218
|
+
.option:focus .optionTextSecondaryLabel {
|
|
179
219
|
color: colorTextSecondary;
|
|
180
220
|
}
|
|
181
221
|
|
|
182
|
-
|
|
222
|
+
/*
|
|
223
|
+
* Selected value (committed), shown as a stronger fill than the active highlight.
|
|
224
|
+
*
|
|
225
|
+
* Deliberately no left accent bar: a 2px inset box-shadow gets tapered by the
|
|
226
|
+
* option's 8px border-radius, so it renders as a small curved sliver at the edge
|
|
227
|
+
* that reads as a stray focus artefact rather than an intentional indicator. The
|
|
228
|
+
* checkbox / radio glyph carries the state in those variants, and forced-colors
|
|
229
|
+
* mode below re-expresses it as a full outline, which the radius follows cleanly.
|
|
230
|
+
*/
|
|
231
|
+
.option.selected {
|
|
232
|
+
background: colorFillSecondary;
|
|
183
233
|
outline: none;
|
|
184
234
|
color: colorTextPrimary;
|
|
185
235
|
}
|
|
186
236
|
|
|
237
|
+
/*
|
|
238
|
+
* Focus ring — KEYBOARD ONLY. A focus indicator has to mean "focus is here", so
|
|
239
|
+
* it must not appear under the mouse pointer:
|
|
240
|
+
*
|
|
241
|
+
* - `.activeRing` is set by Menu for the combobox modes (Dropdown / Typeahead),
|
|
242
|
+
* where DOM focus stays on the input and the active option is virtual. Menu
|
|
243
|
+
* withholds it when the highlight came from the pointer.
|
|
244
|
+
* - `:focus-visible` covers the roving menus, which move real DOM focus. It is
|
|
245
|
+
* exactly the right selector here: arrow-key navigation calls .focus() after a
|
|
246
|
+
* key press so the ring shows, while hover-driven .focus() follows a pointer
|
|
247
|
+
* interaction so it does not.
|
|
248
|
+
*
|
|
249
|
+
* Declared after `.selected`, which sets no box-shadow of its own, so a selected
|
|
250
|
+
* option that also has focus shows the ring without anything to fight over.
|
|
251
|
+
*/
|
|
252
|
+
.option.activeRing,
|
|
187
253
|
.option:focus-visible {
|
|
188
254
|
outline: none;
|
|
189
255
|
color: colorTextPrimary;
|
|
@@ -191,20 +257,34 @@
|
|
|
191
257
|
borderWidthTertiary colorFocusSecondary;
|
|
192
258
|
}
|
|
193
259
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
.option
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
.option
|
|
207
|
-
|
|
260
|
+
/*
|
|
261
|
+
* Windows High Contrast / forced-colors mode drops box-shadow and flattens
|
|
262
|
+
* background, so the bar + tint indicators above vanish. Re-express the active
|
|
263
|
+
* highlight and selected state with system-color outlines (honored in HCM).
|
|
264
|
+
*/
|
|
265
|
+
@media (forced-colors: active) {
|
|
266
|
+
.option:hover,
|
|
267
|
+
.option.active {
|
|
268
|
+
outline: borderWidthTertiary solid Highlight;
|
|
269
|
+
outline-offset: calc(-1 * borderWidthTertiary);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.option.selected {
|
|
273
|
+
outline: borderWidthTertiary solid SelectedItem;
|
|
274
|
+
outline-offset: calc(-1 * borderWidthTertiary);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/*
|
|
278
|
+
* Keyboard focus must stay distinguishable from BOTH the hover highlight and
|
|
279
|
+
* the selected state, and Highlight / SelectedItem commonly resolve to the same
|
|
280
|
+
* system color — so change the outline STYLE rather than add a third color.
|
|
281
|
+
* Declared last so a selected option that also has focus still shows it.
|
|
282
|
+
*/
|
|
283
|
+
.option.activeRing,
|
|
284
|
+
.option:focus-visible {
|
|
285
|
+
outline: borderWidthTertiary dashed Highlight;
|
|
286
|
+
outline-offset: calc(-1 * borderWidthTertiary);
|
|
287
|
+
}
|
|
208
288
|
}
|
|
209
289
|
|
|
210
290
|
.disabled {
|
|
@@ -244,6 +324,41 @@
|
|
|
244
324
|
flex-flow: column;
|
|
245
325
|
}
|
|
246
326
|
|
|
327
|
+
/*
|
|
328
|
+
* Wraps only the option list so the listbox role contains option children
|
|
329
|
+
* exclusively (search/header/footer stay outside it). display:contents keeps
|
|
330
|
+
* the existing flex layout of .menuCard unchanged.
|
|
331
|
+
*/
|
|
332
|
+
.optionsListbox {
|
|
333
|
+
display: contents;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/*
|
|
337
|
+
* The listbox as the element that actually scrolls. Under virtualization
|
|
338
|
+
* react-window's own scroll box carries the role (see ListboxOuter); this is the
|
|
339
|
+
* same arrangement for a plain option list, and it matters beyond tidiness: the
|
|
340
|
+
* options are not focusable themselves whenever a combobox trigger drives the
|
|
341
|
+
* menu, so a
|
|
342
|
+
* scroll container that is NOT the combobox's popup reads as an unreachable
|
|
343
|
+
* scroll area (axe `scrollable-region-focusable`). Making the listbox the
|
|
344
|
+
* scroller means the region IS the popup, which that rule exempts.
|
|
345
|
+
*/
|
|
346
|
+
.optionsListboxScroll {
|
|
347
|
+
display: flex;
|
|
348
|
+
flex-flow: column;
|
|
349
|
+
flex: 1 1 auto;
|
|
350
|
+
min-height: 0;
|
|
351
|
+
overflow-y: auto;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/*
|
|
355
|
+
* role="group" wrapper around each titled option group. display:contents keeps
|
|
356
|
+
* the existing flex layout (title + options) unchanged.
|
|
357
|
+
*/
|
|
358
|
+
.optionGroup {
|
|
359
|
+
display: contents;
|
|
360
|
+
}
|
|
361
|
+
|
|
247
362
|
.groupTitleWrapper {
|
|
248
363
|
composes: formLabelSmall from '../../styles/typography.module.css';
|
|
249
364
|
display: flex;
|
|
@@ -260,11 +375,16 @@
|
|
|
260
375
|
margin-left: spaceSmall;
|
|
261
376
|
}
|
|
262
377
|
|
|
378
|
+
/*
|
|
379
|
+
* Pinned by living OUTSIDE the options scroll container rather than by sticking
|
|
380
|
+
* inside it, so it never has the option list's scrollbar running behind it and
|
|
381
|
+
* never has an option tucked under it. `relative` positions the divider below.
|
|
382
|
+
*/
|
|
263
383
|
.menuHeader {
|
|
264
|
-
position:
|
|
384
|
+
position: relative;
|
|
385
|
+
flex: 0 0 auto;
|
|
265
386
|
background-color: colorBackgroundTertiary;
|
|
266
387
|
padding: spaceXSmall spaceNone;
|
|
267
|
-
top: spaceNone;
|
|
268
388
|
margin-bottom: spaceXSmall;
|
|
269
389
|
z-index: calc(elevationCard/4);
|
|
270
390
|
min-height: size50;
|
|
@@ -280,11 +400,12 @@
|
|
|
280
400
|
border-bottom: borderWidthPrimary solid colorBorderPrimary;
|
|
281
401
|
}
|
|
282
402
|
|
|
403
|
+
/* As `.menuHeader`, pinned by sitting outside the options scroll container. */
|
|
283
404
|
.menuFooter {
|
|
284
|
-
position:
|
|
405
|
+
position: relative;
|
|
406
|
+
flex: 0 0 auto;
|
|
285
407
|
background-color: colorBackgroundTertiary;
|
|
286
408
|
padding: spaceXSmall spaceNone;
|
|
287
|
-
bottom: spaceNone;
|
|
288
409
|
margin-top: spaceXSmall;
|
|
289
410
|
min-height: size50;
|
|
290
411
|
max-height: size50;
|
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import type { CSSProperties } from 'react';
|
|
3
3
|
import type { BaseMenuProps, MenuOption } from '../../components/Menu/Menu';
|
|
4
|
-
export interface MenuOptionProps extends Omit<BaseMenuProps, 'option' | '
|
|
4
|
+
export interface MenuOptionProps extends Omit<BaseMenuProps, 'option' | 'style'> {
|
|
5
5
|
option: MenuOption;
|
|
6
|
-
isLastItem?: boolean;
|
|
7
6
|
style?: CSSProperties | undefined;
|
|
7
|
+
/** Total option count — set for virtualized lists so AT announces "N of total". */
|
|
8
|
+
setSize?: number;
|
|
9
|
+
/** 1-based position — set for virtualized lists (windowed DOM hides the rest). */
|
|
10
|
+
posInSet?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Make this option the active highlight when the pointer moves over it, so
|
|
13
|
+
* mouse and keyboard share one cursor (and roving focus follows the pointer).
|
|
14
|
+
* `clientX`/`clientY` let Menu drop no-op moves (scroll under a still cursor).
|
|
15
|
+
*/
|
|
16
|
+
onPointerActivate?: (key: string, el: HTMLElement, clientX: number, clientY: number) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Key of the option the POINTER most recently highlighted. Used to keep the
|
|
19
|
+
* focus ring off a highlight the mouse created, while the background tint still
|
|
20
|
+
* follows the pointer.
|
|
21
|
+
*/
|
|
22
|
+
pointerActiveKey?: string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Withhold the highlight the menu opened with. Set when the menu was opened by
|
|
25
|
+
* mouse, so it does not appear to have a cursor the user never placed; cleared
|
|
26
|
+
* as soon as they move the active option themselves.
|
|
27
|
+
*/
|
|
28
|
+
suppressInitialHighlight?: boolean;
|
|
8
29
|
testId?: string;
|
|
9
30
|
}
|
|
10
31
|
export declare const MenuOptionButton: (props: MenuOptionProps) => React.JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MenuOptionButton.d.ts","sourceRoot":"","sources":["../../../src/components/Menu/MenuOptionButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,OAAO,CAAC;AASzC,OAAO,KAAK,EAAC,aAAa,EAAE,UAAU,EAAC,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"MenuOptionButton.d.ts","sourceRoot":"","sources":["../../../src/components/Menu/MenuOptionButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,OAAO,CAAC;AASzC,OAAO,KAAK,EAAC,aAAa,EAAE,UAAU,EAAC,MAAM,0BAA0B,CAAC;AASxE,MAAM,WAAW,eACf,SAAQ,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,OAAO,CAAC;IAC/C,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAClC,mFAAmF;IACnF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,CAClB,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,WAAW,EACf,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,KACZ,IAAI,CAAC;IACV;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC;;;;OAIG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,eAAO,MAAM,gBAAgB,GAAI,OAAO,eAAe,sBAkQtD,CAAC"}
|
|
@@ -10,6 +10,7 @@ var _qa = require("../../utils/qa");
|
|
|
10
10
|
var _Button = require("../Button");
|
|
11
11
|
var _Checkbox = require("../Checkbox");
|
|
12
12
|
var _Icon = require("../Icon");
|
|
13
|
+
var _menuOptionId = require("./menuOptionId");
|
|
13
14
|
var _RadioButton = require("../RadioButton");
|
|
14
15
|
var _Truncate = require("../Truncate");
|
|
15
16
|
var _TruncatedTextWithTooltip = require("../TruncatedTextWithTooltip");
|
|
@@ -18,7 +19,6 @@ var _jsxRuntime = require("react/jsx-runtime");
|
|
|
18
19
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
19
20
|
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); }
|
|
20
21
|
const MenuOptionButton = props => {
|
|
21
|
-
const lastMenuItemRef = React.useRef(null);
|
|
22
22
|
const {
|
|
23
23
|
option,
|
|
24
24
|
size = 'medium',
|
|
@@ -28,13 +28,20 @@ const MenuOptionButton = props => {
|
|
|
28
28
|
classNames,
|
|
29
29
|
optionsVariant = 'normal',
|
|
30
30
|
selectedKeys,
|
|
31
|
-
isLastItem,
|
|
32
|
-
onTabOut,
|
|
33
31
|
resolveLabel,
|
|
34
32
|
resolveSecondaryLabel,
|
|
35
33
|
style,
|
|
36
34
|
showLabelTooltip,
|
|
37
35
|
allowWrap = false,
|
|
36
|
+
allowSearch = false,
|
|
37
|
+
menuId,
|
|
38
|
+
activeOptionKey,
|
|
39
|
+
externalKeyboardNav = false,
|
|
40
|
+
setSize,
|
|
41
|
+
posInSet,
|
|
42
|
+
onPointerActivate,
|
|
43
|
+
pointerActiveKey,
|
|
44
|
+
suppressInitialHighlight = false,
|
|
38
45
|
testId
|
|
39
46
|
} = props;
|
|
40
47
|
const {
|
|
@@ -62,24 +69,45 @@ const MenuOptionButton = props => {
|
|
|
62
69
|
}
|
|
63
70
|
return selectedKeys.includes(option.key);
|
|
64
71
|
};
|
|
72
|
+
const isOptionSelected = isSelected() || key === selectedOption?.key;
|
|
73
|
+
const isOptionDisabled = Boolean(menuDisabled || disabled);
|
|
74
|
+
// Roving tabindex: Menu always passes activeOptionKey (string | null), so a
|
|
75
|
+
// single option owns the tab stop. The `undefined` fallback only matters if a
|
|
76
|
+
// MenuOptionButton is rendered outside Menu — there every option stays
|
|
77
|
+
// tabbable (default tabIndex), preserving the pre-roving behavior.
|
|
78
|
+
const isRoving = activeOptionKey !== undefined;
|
|
79
|
+
// `isActive` still drives the roving tab stop and aria-activedescendant — the
|
|
80
|
+
// menu always has an active option so the keyboard has somewhere to start.
|
|
81
|
+
// `showHighlight` is the visible half of it, withheld while the only reason an
|
|
82
|
+
// option is active is that the menu just opened under the mouse.
|
|
83
|
+
const isActive = isRoving && activeOptionKey === key;
|
|
84
|
+
const showHighlight = isActive && !suppressInitialHighlight;
|
|
85
|
+
// Focus ring. Only the combobox modes need a CLASS for it: there, DOM focus
|
|
86
|
+
// stays on the trigger input and this ring is the only cursor the user has, so
|
|
87
|
+
// it is drawn from `activeOptionKey` — minus any highlight the pointer created,
|
|
88
|
+
// since a focus indicator must not follow the mouse.
|
|
89
|
+
//
|
|
90
|
+
// Roving menus move real DOM focus, so they are left to `:focus-visible` in CSS,
|
|
91
|
+
// which the browser only matches for keyboard-driven focus. That is why merely
|
|
92
|
+
// opening a roving menu with the mouse no longer paints a ring.
|
|
93
|
+
const isVirtualFocusMode = allowSearch || externalKeyboardNav;
|
|
94
|
+
const showActiveRing = showHighlight && isVirtualFocusMode && pointerActiveKey !== key;
|
|
95
|
+
// The option is a real tab stop only under true roving focus. Under search or
|
|
96
|
+
// external combobox nav, focus stays on the input — every option is -1 and the
|
|
97
|
+
// active one is surfaced through the input's aria-activedescendant instead.
|
|
98
|
+
const optionTabIndex = externalKeyboardNav ? -1 : isRoving ? isActive && !allowSearch ? 0 : -1 : undefined;
|
|
99
|
+
const optionId = menuId ? (0, _menuOptionId.menuOptionId)(menuId, key) : undefined;
|
|
65
100
|
React.useEffect(() => {
|
|
66
101
|
setButtonSize(optionSize || size);
|
|
67
102
|
}, [optionSize, size]);
|
|
68
|
-
React.useEffect(() => {
|
|
69
|
-
const handleKeyDown = event => {
|
|
70
|
-
if (event.key === 'Tab' && !event.shiftKey) {
|
|
71
|
-
// Tab pressed without shift key, calling tab out callback
|
|
72
|
-
onTabOut?.();
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
lastMenuItemRef.current?.addEventListener('keydown', handleKeyDown);
|
|
76
|
-
return () => {
|
|
77
|
-
lastMenuItemRef.current?.removeEventListener('keydown', handleKeyDown);
|
|
78
|
-
};
|
|
79
|
-
}, [isLastItem]);
|
|
80
103
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_Button.UnstyledButton, {
|
|
81
104
|
className: (0, _classify.classify)(_MenuModule.default.option, {
|
|
82
105
|
[_MenuModule.default.selected]: isSelected() || key === selectedOption?.key,
|
|
106
|
+
// Background highlight for the single active option, from either input
|
|
107
|
+
// source, so pointer and keyboard never show two cursors.
|
|
108
|
+
[_MenuModule.default.active]: showHighlight,
|
|
109
|
+
// Focus ring, keyboard-driven highlights only.
|
|
110
|
+
[_MenuModule.default.activeRing]: showActiveRing,
|
|
83
111
|
[_MenuModule.default.optionSmall]: buttonSize === 'small',
|
|
84
112
|
[_MenuModule.default.optionMedium]: buttonSize === 'medium',
|
|
85
113
|
[_MenuModule.default.disabled]: menuDisabled || disabled,
|
|
@@ -87,36 +115,57 @@ const MenuOptionButton = props => {
|
|
|
87
115
|
[_MenuModule.default.withIconRight]: !!iconRight
|
|
88
116
|
}, classNames?.option, optionClassNames?.wrapper),
|
|
89
117
|
style: style,
|
|
90
|
-
disabled:
|
|
118
|
+
disabled: isOptionDisabled,
|
|
119
|
+
role: "option",
|
|
120
|
+
"aria-selected": isOptionSelected,
|
|
121
|
+
"aria-disabled": isOptionDisabled || undefined,
|
|
122
|
+
"aria-setsize": setSize,
|
|
123
|
+
"aria-posinset": posInSet,
|
|
124
|
+
tabIndex: optionTabIndex,
|
|
125
|
+
id: optionId,
|
|
126
|
+
"data-menu-option-key": key,
|
|
91
127
|
onClick: e => onSelect && onSelect(option, e),
|
|
92
|
-
|
|
128
|
+
onMouseMove: e => {
|
|
129
|
+
// Pointer drives the same single active highlight as the keyboard.
|
|
130
|
+
// Guarded so it only fires when crossing into a different option. The
|
|
131
|
+
// cursor coordinates are forwarded so Menu can ignore moves with no real
|
|
132
|
+
// pointer delta (the list scrolling under a stationary cursor during
|
|
133
|
+
// keyboard nav), which would otherwise steal focus from the keyboard.
|
|
134
|
+
if (isOptionDisabled || isActive) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
onPointerActivate?.(key, e.currentTarget, e.clientX, e.clientY);
|
|
138
|
+
},
|
|
93
139
|
testId: (0, _qa.generateTestId)({
|
|
94
140
|
base: optionTestId || testId,
|
|
95
141
|
slot: 'option-button',
|
|
96
142
|
index: key
|
|
97
143
|
}),
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
144
|
+
children: [optionVariant === 'checkbox' && /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
|
|
145
|
+
"aria-hidden": "true",
|
|
146
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Checkbox.Checkbox, {
|
|
147
|
+
tabIndex: -1,
|
|
148
|
+
disabled: menuDisabled || disabled,
|
|
149
|
+
checked: isSelected(),
|
|
150
|
+
indeterminate: indeterminate,
|
|
151
|
+
testId: (0, _qa.generateTestId)({
|
|
152
|
+
base: optionTestId || testId,
|
|
153
|
+
slot: 'checkbox',
|
|
154
|
+
index: key
|
|
155
|
+
})
|
|
110
156
|
})
|
|
111
|
-
}), optionVariant === 'radio' && /*#__PURE__*/(0, _jsxRuntime.jsx)(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
157
|
+
}), optionVariant === 'radio' && /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
|
|
158
|
+
"aria-hidden": "true",
|
|
159
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_RadioButton.RadioButton, {
|
|
160
|
+
disabled: menuDisabled || disabled,
|
|
161
|
+
value: option.key,
|
|
162
|
+
selectedValue: selectedKeys?.[0],
|
|
163
|
+
tabIndex: -1,
|
|
164
|
+
testId: (0, _qa.generateTestId)({
|
|
165
|
+
base: optionTestId || testId,
|
|
166
|
+
slot: 'radio-button',
|
|
167
|
+
index: key
|
|
168
|
+
})
|
|
120
169
|
})
|
|
121
170
|
}), !!iconLeft && /*#__PURE__*/(0, _jsxRuntime.jsx)(_Icon.Icon, {
|
|
122
171
|
name: iconLeft,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Menu/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,sCAAsC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Menu/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,sCAAsC,CAAC;AAKrD,cAAc,oCAAoC,CAAC;AACnD,cAAc,+CAA+C,CAAC"}
|
|
@@ -24,4 +24,26 @@ Object.keys(_MenuOptionButton).forEach(function (key) {
|
|
|
24
24
|
return _MenuOptionButton[key];
|
|
25
25
|
}
|
|
26
26
|
});
|
|
27
|
+
});
|
|
28
|
+
var _useMenuTrigger = require("./useMenuTrigger");
|
|
29
|
+
Object.keys(_useMenuTrigger).forEach(function (key) {
|
|
30
|
+
if (key === "default" || key === "__esModule") return;
|
|
31
|
+
if (key in exports && exports[key] === _useMenuTrigger[key]) return;
|
|
32
|
+
Object.defineProperty(exports, key, {
|
|
33
|
+
enumerable: true,
|
|
34
|
+
get: function () {
|
|
35
|
+
return _useMenuTrigger[key];
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
var _useMenuKeyboardNavigation = require("./useMenuKeyboardNavigation");
|
|
40
|
+
Object.keys(_useMenuKeyboardNavigation).forEach(function (key) {
|
|
41
|
+
if (key === "default" || key === "__esModule") return;
|
|
42
|
+
if (key in exports && exports[key] === _useMenuKeyboardNavigation[key]) return;
|
|
43
|
+
Object.defineProperty(exports, key, {
|
|
44
|
+
enumerable: true,
|
|
45
|
+
get: function () {
|
|
46
|
+
return _useMenuKeyboardNavigation[key];
|
|
47
|
+
}
|
|
48
|
+
});
|
|
27
49
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DOM id of an option row — the one thing the row itself, the listbox's
|
|
3
|
+
* `aria-activedescendant`, and a combobox trigger's `aria-activedescendant`
|
|
4
|
+
* must all agree on, so it is built in exactly one place.
|
|
5
|
+
*
|
|
6
|
+
* Option keys are consumer data and may contain whitespace ("New York"), which
|
|
7
|
+
* an id can hold but an IDREF cannot: `aria-activedescendant="m-option-New
|
|
8
|
+
* York"` is a list of two references, neither of which resolves. Encoding the
|
|
9
|
+
* key keeps the id a single token.
|
|
10
|
+
*/
|
|
11
|
+
export declare const menuOptionId: (menuId: string, key: string) => string;
|
|
12
|
+
//# sourceMappingURL=menuOptionId.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menuOptionId.d.ts","sourceRoot":"","sources":["../../../src/components/Menu/menuOptionId.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,MAAM,EAAE,KAAK,MAAM,KAAG,MACZ,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.menuOptionId = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* The DOM id of an option row — the one thing the row itself, the listbox's
|
|
9
|
+
* `aria-activedescendant`, and a combobox trigger's `aria-activedescendant`
|
|
10
|
+
* must all agree on, so it is built in exactly one place.
|
|
11
|
+
*
|
|
12
|
+
* Option keys are consumer data and may contain whitespace ("New York"), which
|
|
13
|
+
* an id can hold but an IDREF cannot: `aria-activedescendant="m-option-New
|
|
14
|
+
* York"` is a list of two references, neither of which resolves. Encoding the
|
|
15
|
+
* key keeps the id a single token.
|
|
16
|
+
*/
|
|
17
|
+
const menuOptionId = (menuId, key) => `${menuId}-option-${encodeURIComponent(key)}`;
|
|
18
|
+
exports.menuOptionId = menuOptionId;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Keyboard navigation for the Menu option list. Three interaction models,
|
|
4
|
+
* selected via `mode`:
|
|
5
|
+
*
|
|
6
|
+
* 1. `'roving'` — ROVING FOCUS (default; menu-button popups). Options are native
|
|
7
|
+
* <button>s, so ArrowUp/ArrowDown move real DOM focus between the enabled
|
|
8
|
+
* options and Space/Enter activate the focused option natively. Home/End jump
|
|
9
|
+
* to the first/last option, PageUp/PageDown move by a page, and typing a
|
|
10
|
+
* printable character jumps to the next matching label (type-ahead).
|
|
11
|
+
*
|
|
12
|
+
* 2. `'activedescendant'` — SELECT-ONLY COMBOBOX. DOM focus stays on the
|
|
13
|
+
* combobox input; the "active" option is a virtual highlight reported via
|
|
14
|
+
* `setActiveOptionKey` (surfaced as the input's aria-activedescendant) and
|
|
15
|
+
* NEVER takes real focus. Arrows/Home/End/PageUp/PageDown move the highlight,
|
|
16
|
+
* printable characters type-ahead, and Enter selects the active option.
|
|
17
|
+
*
|
|
18
|
+
* 3. `'search'` — EDITABLE COMBOBOX. Like `'activedescendant'`, but the input is
|
|
19
|
+
* a free-text field: printable keys, Left/Right and Home/End are left to the
|
|
20
|
+
* input for text editing (no type-ahead). Arrows/PageUp/PageDown move the
|
|
21
|
+
* highlight and Enter selects.
|
|
22
|
+
*
|
|
23
|
+
* Navigation wraps around at the ends. Options are discovered from the DOM
|
|
24
|
+
* (`[data-menu-option-key]`) so it always matches what is rendered after search
|
|
25
|
+
* filtering / grouping.
|
|
26
|
+
*
|
|
27
|
+
* Tab / Shift+Tab keep focus moving through the menu's own focus zones in DOM
|
|
28
|
+
* order — header → search → options → footer — and only close the menu (via
|
|
29
|
+
* `onTabOut`) when focus would leave the menu entirely (Tab past the last
|
|
30
|
+
* focusable, Shift+Tab before the first). This keeps focus from silently
|
|
31
|
+
* dropping to <body> when the popup renders in a portal with nothing after it.
|
|
32
|
+
*/
|
|
33
|
+
export type MenuNavigationMode = 'roving' | 'activedescendant' | 'search';
|
|
34
|
+
/** One navigable option, for menus whose option DOM is windowed. */
|
|
35
|
+
export interface MenuNavigationOption {
|
|
36
|
+
key: string;
|
|
37
|
+
label: string;
|
|
38
|
+
}
|
|
39
|
+
interface UseMenuKeyboardNavigationArgs {
|
|
40
|
+
/** Ref to the element that contains the option buttons (the Menu listbox). */
|
|
41
|
+
containerRef: React.RefObject<HTMLElement | null>;
|
|
42
|
+
/** Disable all handling (e.g. when the menu is disabled or closed). */
|
|
43
|
+
enabled?: boolean;
|
|
44
|
+
/** Interaction model — see the file header. Defaults to `'roving'`. */
|
|
45
|
+
mode?: MenuNavigationMode;
|
|
46
|
+
/** Key of the option currently highlighted (needed for the non-roving modes). */
|
|
47
|
+
activeOptionKey?: string | null;
|
|
48
|
+
/** Select the option with the given key (used for Enter in the combobox modes). */
|
|
49
|
+
onSelectKey?: (key: string) => void;
|
|
50
|
+
/** Report the option key that owns the roving tab stop / active descendant. */
|
|
51
|
+
setActiveOptionKey: (key: string | null) => void;
|
|
52
|
+
/** Close the menu when Tab / Shift+Tab would move focus out of it. */
|
|
53
|
+
onTabOut?: () => void;
|
|
54
|
+
/**
|
|
55
|
+
* Combobox menus only: return focus to the trigger input (keeping the menu
|
|
56
|
+
* open) when Tab / Shift+Tab moves off a header/footer control back toward the
|
|
57
|
+
* input — which sits OUTSIDE the menu DOM, so the input is the logical centre
|
|
58
|
+
* of the tab order (header ← input → footer), not an edge. When omitted (the
|
|
59
|
+
* roving menus) the input lives inside the menu, so the simple boundary close
|
|
60
|
+
* applies instead.
|
|
61
|
+
*/
|
|
62
|
+
onReturnFocusToInput?: () => void;
|
|
63
|
+
/**
|
|
64
|
+
* Enabled options in render order. Supply this when the option DOM is WINDOWED
|
|
65
|
+
* (react-window virtualization): only a slice of the rows exists at any moment,
|
|
66
|
+
* so DOM discovery would confine Home/End, PageUp/PageDown, wrap-around and
|
|
67
|
+
* type-ahead to that slice — e.g. End on a 100-page Pagination would stop at
|
|
68
|
+
* page 10. When supplied, all index maths resolve against this list and the
|
|
69
|
+
* consumer is responsible for scrolling the active option into view (Menu does
|
|
70
|
+
* this from `activeOptionKey`). Omit it for plain menus: the DOM is then the
|
|
71
|
+
* source of truth, which keeps grouped/compose layouts and `resolveLabel`
|
|
72
|
+
* type-ahead working off the text actually rendered.
|
|
73
|
+
*/
|
|
74
|
+
options?: MenuNavigationOption[];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Focusable controls inside the menu's sticky header / footer region, in DOM
|
|
78
|
+
* order. Used by combobox triggers (Dropdown / Typeahead) so Tab / Shift+Tab can
|
|
79
|
+
* move focus into the footer / header instead of closing the menu when one
|
|
80
|
+
* holds focusable controls (e.g. Cancel / Save buttons).
|
|
81
|
+
*/
|
|
82
|
+
export declare const getMenuRegionFocusables: (menu: HTMLElement | null, region: "header" | "footer") => HTMLElement[];
|
|
83
|
+
/**
|
|
84
|
+
* Move focus into the menu's footer (Tab) or header (Shift+Tab) edge if that
|
|
85
|
+
* region has focusable controls. Returns true when it moved focus, so a combobox
|
|
86
|
+
* trigger can close the menu only when there was no chrome to land on.
|
|
87
|
+
*/
|
|
88
|
+
export declare const moveFocusToMenuChrome: (menu: HTMLElement | null, shiftKey: boolean) => boolean;
|
|
89
|
+
export declare const useMenuKeyboardNavigation: ({ containerRef, enabled, mode, activeOptionKey, onSelectKey, setActiveOptionKey, onTabOut, onReturnFocusToInput, options, }: UseMenuKeyboardNavigationArgs) => {
|
|
90
|
+
onKeyDown: (event: React.KeyboardEvent<HTMLElement>) => void;
|
|
91
|
+
};
|
|
92
|
+
export {};
|
|
93
|
+
//# sourceMappingURL=useMenuKeyboardNavigation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useMenuKeyboardNavigation.d.ts","sourceRoot":"","sources":["../../../src/components/Menu/useMenuKeyboardNavigation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,kBAAkB,GAAG,QAAQ,CAAC;AAE1E,oEAAoE;AACpE,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAaD,UAAU,6BAA6B;IACrC,8EAA8E;IAC9E,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAClD,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,uEAAuE;IACvE,IAAI,CAAC,EAAE,kBAAkB,CAAC;IAC1B,iFAAiF;IACjF,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,mFAAmF;IACnF,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,+EAA+E;IAC/E,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACjD,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,EAAE,oBAAoB,EAAE,CAAC;CAClC;AAkDD;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAClC,MAAM,WAAW,GAAG,IAAI,EACxB,QAAQ,QAAQ,GAAG,QAAQ,KAC1B,WAAW,EAUb,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GAChC,MAAM,WAAW,GAAG,IAAI,EACxB,UAAU,OAAO,KAChB,OAYF,CAAC;AASF,eAAO,MAAM,yBAAyB,GAAI,6HAUvC,6BAA6B,KAAG;IACjC,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;CAiY9D,CAAC"}
|