@spaced-out/ui-design-system 0.6.36 → 0.6.37
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 +9 -0
- package/lib/components/ButtonDropdown/ButtonDropdown.d.ts +10 -0
- package/lib/components/ButtonDropdown/ButtonDropdown.d.ts.map +1 -1
- package/lib/components/ButtonDropdown/ButtonDropdown.js +26 -3
- package/lib/components/ButtonTabs/ButtonTab/ButtonTab.d.ts +9 -1
- package/lib/components/ButtonTabs/ButtonTab/ButtonTab.d.ts.map +1 -1
- package/lib/components/ButtonTabs/ButtonTab/ButtonTab.js +8 -0
- package/lib/components/ButtonTabs/ButtonTabDropdown.d.ts +10 -1
- package/lib/components/ButtonTabs/ButtonTabDropdown.d.ts.map +1 -1
- package/lib/components/ButtonTabs/ButtonTabDropdown.js +31 -3
- package/lib/components/ButtonTabs/ButtonTabs.d.ts +6 -0
- package/lib/components/ButtonTabs/ButtonTabs.d.ts.map +1 -1
- package/lib/components/ButtonTabs/ButtonTabs.js +2 -0
- package/lib/components/Combobox/Combobox.d.ts +9 -0
- package/lib/components/Combobox/Combobox.d.ts.map +1 -1
- package/lib/components/Combobox/Combobox.js +21 -0
- package/lib/components/Combobox/helper.d.ts.map +1 -1
- package/lib/components/Combobox/helper.js +0 -4
- package/lib/components/Dropdown/Dropdown.d.ts +9 -0
- package/lib/components/Dropdown/Dropdown.d.ts.map +1 -1
- package/lib/components/Dropdown/Dropdown.js +92 -24
- package/lib/components/Dropdown/Dropdown.module.css +24 -0
- package/lib/components/InlineDropdown/InlineDropdown.d.ts +10 -0
- package/lib/components/InlineDropdown/InlineDropdown.d.ts.map +1 -1
- package/lib/components/InlineDropdown/InlineDropdown.js +23 -3
- package/lib/components/Input/Input.module.css +38 -0
- package/lib/components/Menu/Menu.d.ts +99 -0
- package/lib/components/Menu/Menu.d.ts.map +1 -1
- package/lib/components/Menu/Menu.js +1152 -187
- package/lib/components/Menu/Menu.module.css +191 -7
- package/lib/components/Menu/MenuFocusScope.d.ts +21 -0
- package/lib/components/Menu/MenuFocusScope.d.ts.map +1 -0
- package/lib/components/Menu/MenuFocusScope.js +37 -0
- package/lib/components/Menu/MenuOptionButton.d.ts +36 -0
- package/lib/components/Menu/MenuOptionButton.d.ts.map +1 -1
- package/lib/components/Menu/MenuOptionButton.js +137 -29
- 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 +207 -0
- package/lib/components/Menu/useMenuTrigger.d.ts.map +1 -0
- package/lib/components/Menu/useMenuTrigger.js +389 -0
- package/lib/components/OptionButton/OptionButton.d.ts +16 -0
- package/lib/components/OptionButton/OptionButton.d.ts.map +1 -1
- package/lib/components/OptionButton/OptionButton.js +4 -1
- package/lib/components/Tabs/Tab/Tab.d.ts +8 -1
- package/lib/components/Tabs/Tab/Tab.d.ts.map +1 -1
- package/lib/components/Tabs/Tab/Tab.js +8 -0
- package/lib/components/Tabs/TabList/TabDropdown.d.ts +10 -1
- package/lib/components/Tabs/TabList/TabDropdown.d.ts.map +1 -1
- package/lib/components/Tabs/TabList/TabDropdown.js +23 -3
- package/lib/components/Tabs/TabList/TabList.d.ts +14 -0
- package/lib/components/Tabs/TabList/TabList.d.ts.map +1 -1
- package/lib/components/Tabs/TabList/TabList.js +6 -0
- package/lib/components/Typeahead/Typeahead.d.ts +9 -0
- package/lib/components/Typeahead/Typeahead.d.ts.map +1 -1
- package/lib/components/Typeahead/Typeahead.js +107 -13
- package/lib/components/Typeahead/Typeahead.module.css +13 -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 +39 -0
- package/lib/hooks/useInteractionModality/useInteractionModality.d.ts.map +1 -0
- package/lib/hooks/useInteractionModality/useInteractionModality.js +111 -0
- package/lib/utils/click-away/click-away.d.ts +16 -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
|
@@ -3,19 +3,28 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.Menu = void 0;
|
|
6
|
+
exports.getInitialActiveOptionKey = exports.getFlatOptions = exports.Menu = void 0;
|
|
7
7
|
var React = _interopRequireWildcard(require("react"));
|
|
8
8
|
var _reactWindow = require("react-window");
|
|
9
|
+
var _hooks = require("../../hooks");
|
|
9
10
|
var _classify = require("../../utils/classify");
|
|
10
11
|
var _menu = require("../../utils/menu");
|
|
12
|
+
var _mergeRefs = require("../../utils/merge-refs");
|
|
11
13
|
var _qa = require("../../utils/qa");
|
|
12
14
|
var _MenuOptionButton = require("./MenuOptionButton");
|
|
15
|
+
var _menuOptionId = require("./menuOptionId");
|
|
16
|
+
var _useMenuKeyboardNavigation = require("./useMenuKeyboardNavigation");
|
|
13
17
|
var _SearchInput = require("../SearchInput");
|
|
14
18
|
var _Text = require("../Text");
|
|
15
19
|
var _MenuModule = _interopRequireDefault(require("./Menu.module.css"));
|
|
16
20
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
17
21
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
18
22
|
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); }
|
|
23
|
+
/**
|
|
24
|
+
* Either react-window list Menu may render. Both expose the `scrollToItem`
|
|
25
|
+
* used to keep the keyboard's active option mounted.
|
|
26
|
+
*/
|
|
27
|
+
|
|
19
28
|
// Render first available option set
|
|
20
29
|
|
|
21
30
|
const menuSizeMedium = 276,
|
|
@@ -25,49 +34,369 @@ const buttonSizeMedium = 40,
|
|
|
25
34
|
const searchInputSizeMedium = 42,
|
|
26
35
|
searchInputSizeSmall = 34;
|
|
27
36
|
const cardPadding = 8;
|
|
37
|
+
// default group title row height when virtualizing groupTitleOptions
|
|
28
38
|
const groupTitleSize = 32;
|
|
29
39
|
const resultTextSize = 41;
|
|
30
40
|
const headerFooterSize = 58;
|
|
31
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Live region announcing the filtered result count. Rendered as a SIBLING of
|
|
43
|
+
* the listbox (not inside it) so the listbox owns only option children.
|
|
44
|
+
* `role="status"` already implies `aria-live="polite"`.
|
|
45
|
+
*/
|
|
46
|
+
const ResultStatus = _ref => {
|
|
47
|
+
let {
|
|
48
|
+
enableA11y,
|
|
49
|
+
text,
|
|
50
|
+
testId
|
|
51
|
+
} = _ref;
|
|
52
|
+
return enableA11y ? /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
|
|
53
|
+
role: "status",
|
|
54
|
+
className: _MenuModule.default.filterOptionsResultText,
|
|
55
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Text.FormLabelSmall, {
|
|
56
|
+
color: "tertiary",
|
|
57
|
+
testId: (0, _qa.generateTestId)({
|
|
58
|
+
base: testId,
|
|
59
|
+
slot: 'result-text'
|
|
60
|
+
}),
|
|
61
|
+
children: text
|
|
62
|
+
})
|
|
63
|
+
}) :
|
|
64
|
+
/*#__PURE__*/
|
|
65
|
+
// No live region without the opt-in: the count is the same label element it
|
|
66
|
+
// has always been, carrying the class itself rather than a wrapper.
|
|
67
|
+
(0, _jsxRuntime.jsx)(_Text.FormLabelSmall, {
|
|
68
|
+
className: _MenuModule.default.filterOptionsResultText,
|
|
69
|
+
color: "tertiary",
|
|
70
|
+
testId: (0, _qa.generateTestId)({
|
|
71
|
+
base: testId,
|
|
72
|
+
slot: 'result-text'
|
|
73
|
+
}),
|
|
74
|
+
children: text
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* role="listbox" around the option children, and — for every shape but a
|
|
79
|
+
* virtualized one — the element that scrolls them (`.optionsListboxScroll`).
|
|
80
|
+
* Under virtualization react-window owns the scroll box and carries the role
|
|
81
|
+
* instead (ListboxOuter), so this falls back to `display: contents` and leaves
|
|
82
|
+
* the card's flex layout untouched.
|
|
83
|
+
*
|
|
84
|
+
* Keeping the role on the element that scrolls is what lets it hold a tab stop
|
|
85
|
+
* when the menu drives its own filter field, and what earns the scroll area
|
|
86
|
+
* axe's combobox-popup exemption when a trigger drives it.
|
|
87
|
+
*/
|
|
88
|
+
const OptionsListbox = _ref2 => {
|
|
89
|
+
let {
|
|
90
|
+
enableA11y,
|
|
91
|
+
menuId,
|
|
92
|
+
ariaLabelledBy,
|
|
93
|
+
ariaLabel,
|
|
94
|
+
optionsVariant,
|
|
95
|
+
scrolls,
|
|
96
|
+
tabIndex,
|
|
97
|
+
activeDescendantId,
|
|
98
|
+
children
|
|
99
|
+
} = _ref2;
|
|
100
|
+
return !enableA11y ?
|
|
101
|
+
/*#__PURE__*/
|
|
102
|
+
// No opt-in: no element. The options sit directly in the card and the card
|
|
103
|
+
// scrolls them, exactly as before.
|
|
104
|
+
(0, _jsxRuntime.jsx)(_jsxRuntime.Fragment, {
|
|
105
|
+
children: children
|
|
106
|
+
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
|
|
107
|
+
role: "listbox",
|
|
108
|
+
id: menuId,
|
|
109
|
+
tabIndex: tabIndex,
|
|
110
|
+
"aria-activedescendant": activeDescendantId
|
|
111
|
+
// The element that actually scrolls, for consumers that need to scroll the
|
|
112
|
+
// menu programmatically (Dropdown's `scrollMenuToBottom`).
|
|
113
|
+
,
|
|
114
|
+
"data-menu-scroll-container": scrolls ? '' : undefined,
|
|
115
|
+
"aria-labelledby": ariaLabelledBy
|
|
116
|
+
// `listbox` is an accessible-name-required role, and Menu is used directly
|
|
117
|
+
// (not only behind a trigger that can name it), so default the name here
|
|
118
|
+
// rather than leaving every inline consumer with an unnamed listbox.
|
|
119
|
+
// Consumers should still pass something meaningful via `ariaLabelledBy` /
|
|
120
|
+
// `ariaLabel`; this only stops the unnamed case from existing at all.
|
|
121
|
+
,
|
|
122
|
+
"aria-label": ariaLabelledBy ? undefined : ariaLabel ?? 'Options',
|
|
123
|
+
"aria-multiselectable": optionsVariant === 'checkbox' || undefined,
|
|
124
|
+
className: (0, _classify.classify)(scrolls ? _MenuModule.default.optionsListboxScroll : _MenuModule.default.optionsListbox, _MenuModule.default.listboxFocusContainer),
|
|
125
|
+
children: children
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
const ListboxAttrsContext = /*#__PURE__*/React.createContext({});
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Groups a title with its options so the listbox owns only `group` and `option`
|
|
132
|
+
* children and the group name is announced. Without the opt-in it adds no
|
|
133
|
+
* element: the title and the options are siblings in the card, as before.
|
|
134
|
+
*/
|
|
135
|
+
const OptionGroup = _ref3 => {
|
|
136
|
+
let {
|
|
137
|
+
enableA11y,
|
|
138
|
+
labelledBy,
|
|
139
|
+
children
|
|
140
|
+
} = _ref3;
|
|
141
|
+
return enableA11y ? /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
|
|
142
|
+
role: "group",
|
|
143
|
+
"aria-labelledby": labelledBy,
|
|
144
|
+
className: _MenuModule.default.optionGroup,
|
|
145
|
+
children: children
|
|
146
|
+
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_jsxRuntime.Fragment, {
|
|
147
|
+
children: children
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/*
|
|
152
|
+
* Wrapper element type for react-window's inner scroll container. Marks it
|
|
153
|
+
* role="presentation" so it doesn't sit as a generic node between the
|
|
154
|
+
* role="listbox" and its role="option" children (valid listbox ownership).
|
|
155
|
+
*/
|
|
156
|
+
const PresentationDiv = /*#__PURE__*/React.forwardRef((props, ref) => {
|
|
157
|
+
// Reads the flag from context rather than taking it as a prop: react-window
|
|
158
|
+
// renders this as a component TYPE, so a wrapper closing over the flag would
|
|
159
|
+
// be a new type every render and remount every row.
|
|
160
|
+
const {
|
|
161
|
+
enableA11y
|
|
162
|
+
} = React.useContext(ListboxAttrsContext);
|
|
163
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
|
|
164
|
+
ref: ref,
|
|
165
|
+
role: enableA11y ? 'presentation' : undefined,
|
|
166
|
+
...props
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
PresentationDiv.displayName = 'PresentationDiv';
|
|
170
|
+
|
|
171
|
+
/*
|
|
172
|
+
* react-window owns the element that scrolls, so the listbox has to BE that
|
|
173
|
+
* element rather than a wrapper around it. A listbox wrapped around the
|
|
174
|
+
* scroller leaves a scrollable region that holds no focusable content — the
|
|
175
|
+
* options are `tabindex="-1"` under `aria-activedescendant` — which reads as an
|
|
176
|
+
* unreachable scroll area (axe `scrollable-region-focusable`) even though the
|
|
177
|
+
* combobox driving it scrolls the list with the arrow keys. Sitting on the
|
|
178
|
+
* scroller, it is the combobox's own popup, which is what that rule exempts.
|
|
179
|
+
*
|
|
180
|
+
* The attributes arrive through context because react-window renders
|
|
181
|
+
* `outerElementType` as a component TYPE: an inline wrapper closing over them
|
|
182
|
+
* would be a new type on every render and remount every row (see the row
|
|
183
|
+
* renderers below).
|
|
184
|
+
*/
|
|
185
|
+
|
|
186
|
+
const ListboxOuter = /*#__PURE__*/React.forwardRef((props, ref) => {
|
|
187
|
+
const {
|
|
188
|
+
enableA11y,
|
|
189
|
+
menuId,
|
|
190
|
+
ariaLabelledBy,
|
|
191
|
+
ariaLabel,
|
|
192
|
+
optionsVariant,
|
|
193
|
+
tabIndex,
|
|
194
|
+
activeDescendantId
|
|
195
|
+
} = React.useContext(ListboxAttrsContext);
|
|
196
|
+
if (!enableA11y) {
|
|
197
|
+
// react-window still needs its scroll box; it just carries nothing extra.
|
|
198
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
|
|
199
|
+
ref: ref,
|
|
200
|
+
...props
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
const {
|
|
204
|
+
className,
|
|
205
|
+
...rest
|
|
206
|
+
} = props;
|
|
207
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
|
|
208
|
+
ref: ref,
|
|
209
|
+
role: "listbox",
|
|
210
|
+
id: menuId
|
|
211
|
+
// The element that actually scrolls, for consumers that need to scroll
|
|
212
|
+
// the menu programmatically (Dropdown's `scrollMenuToBottom`).
|
|
213
|
+
,
|
|
214
|
+
"data-menu-scroll-container": "",
|
|
215
|
+
"aria-labelledby": ariaLabelledBy
|
|
216
|
+
// `listbox` is an accessible-name-required role — see OptionsListbox.
|
|
217
|
+
,
|
|
218
|
+
"aria-label": ariaLabelledBy ? undefined : ariaLabel ?? 'Options',
|
|
219
|
+
"aria-multiselectable": optionsVariant === 'checkbox' || undefined,
|
|
220
|
+
tabIndex: tabIndex,
|
|
221
|
+
"aria-activedescendant": activeDescendantId,
|
|
222
|
+
...rest,
|
|
223
|
+
className: (0, _classify.classify)(className, _MenuModule.default.listboxFocusContainer)
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
ListboxOuter.displayName = 'ListboxOuter';
|
|
227
|
+
const getMenuChromeHeight = _ref4 => {
|
|
32
228
|
let {
|
|
33
229
|
size,
|
|
34
230
|
allowSearch,
|
|
35
231
|
showResultText,
|
|
36
232
|
hasHeader,
|
|
37
233
|
hasFooter
|
|
38
|
-
} =
|
|
234
|
+
} = _ref4;
|
|
39
235
|
return (hasHeader ? headerFooterSize : cardPadding) + (hasFooter ? headerFooterSize : cardPadding) + (allowSearch ? size === 'medium' ? searchInputSizeMedium : searchInputSizeSmall : 0) + (allowSearch && showResultText ? resultTextSize : 0);
|
|
40
236
|
};
|
|
41
237
|
const makeItemHeightResolver = (itemHeight, size) => (option, index) => typeof itemHeight === 'function' ? itemHeight(option, index) : itemHeight || (size === 'medium' ? buttonSizeMedium : buttonSizeSmall);
|
|
42
238
|
const makeGroupTitleHeightResolver = groupTitleHeight => (groupTitle, groupIndex) => typeof groupTitleHeight === 'function' ? groupTitleHeight(groupTitle, groupIndex) : groupTitleHeight || groupTitleSize;
|
|
239
|
+
/**
|
|
240
|
+
* Flatten groups into the single row list a virtualized grouped menu renders.
|
|
241
|
+
* Shared with Menu itself, which needs the same row order to translate an
|
|
242
|
+
* option key into the row index `scrollToItem` expects.
|
|
243
|
+
*/
|
|
244
|
+
const buildGroupVirtualRows = groups => {
|
|
245
|
+
const rows = [];
|
|
246
|
+
let flatOptionIndex = 0;
|
|
247
|
+
groups.forEach((optionsGroup, groupIndex) => {
|
|
248
|
+
if (optionsGroup.groupTitle) {
|
|
249
|
+
rows.push({
|
|
250
|
+
kind: 'title',
|
|
251
|
+
node: optionsGroup.groupTitle,
|
|
252
|
+
groupIndex
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
optionsGroup.options?.forEach((option, optionIndex) => {
|
|
256
|
+
rows.push({
|
|
257
|
+
kind: 'option',
|
|
258
|
+
option,
|
|
259
|
+
groupIndex,
|
|
260
|
+
optionIndex,
|
|
261
|
+
flatOptionIndex
|
|
262
|
+
});
|
|
263
|
+
flatOptionIndex += 1;
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
return rows;
|
|
267
|
+
};
|
|
268
|
+
|
|
43
269
|
// VariableSizeList caches row offsets, so the cache must be flushed whenever
|
|
44
270
|
// the rows or their heights change. `heightSignature` encodes every row's
|
|
45
271
|
// key and height; when it changes, resetAfterIndex invalidates the cache in
|
|
46
272
|
// place without remounting the list (a remount would drop scroll position).
|
|
47
|
-
|
|
273
|
+
|
|
274
|
+
function MenuVariableSizeList(_ref5) {
|
|
48
275
|
let {
|
|
49
276
|
heightSignature,
|
|
50
277
|
height,
|
|
51
278
|
itemCount,
|
|
52
279
|
itemSize,
|
|
53
280
|
itemKey,
|
|
281
|
+
itemData,
|
|
282
|
+
listRef,
|
|
54
283
|
children
|
|
55
|
-
} =
|
|
56
|
-
const
|
|
284
|
+
} = _ref5;
|
|
285
|
+
const innerListRef = React.useRef(null);
|
|
57
286
|
React.useEffect(() => {
|
|
58
|
-
|
|
287
|
+
innerListRef.current?.resetAfterIndex(0, true);
|
|
59
288
|
}, [heightSignature]);
|
|
60
289
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactWindow.VariableSizeList, {
|
|
61
|
-
ref: listRef,
|
|
290
|
+
ref: (0, _mergeRefs.mergeRefs)([innerListRef, listRef]),
|
|
62
291
|
height: height,
|
|
63
292
|
itemSize: itemSize,
|
|
64
293
|
itemCount: itemCount,
|
|
65
294
|
itemKey: itemKey,
|
|
295
|
+
itemData: itemData,
|
|
66
296
|
width: "initial",
|
|
297
|
+
outerElementType: ListboxOuter,
|
|
298
|
+
innerElementType: PresentationDiv,
|
|
67
299
|
children: children
|
|
68
300
|
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/*
|
|
304
|
+
* Row renderers for the virtualized lists, and the reason they live out here.
|
|
305
|
+
*
|
|
306
|
+
* react-window renders its `children` as a COMPONENT TYPE, so an inline arrow
|
|
307
|
+
* function is a new type on every render and React unmounts and remounts every
|
|
308
|
+
* row. Under roving focus that destroys the row the user is standing on — focus
|
|
309
|
+
* lands on <body>, the menu stops receiving keys, and arrows, Home/End and
|
|
310
|
+
* type-ahead all go dead. (In the combobox and search modes focus lives on the
|
|
311
|
+
* input, which is why the remount went unnoticed there.) Keeping the renderers
|
|
312
|
+
* stable and passing the per-render values through `itemData` keeps the rows
|
|
313
|
+
* mounted across re-renders.
|
|
314
|
+
*/
|
|
315
|
+
|
|
316
|
+
const FlatRow = _ref6 => {
|
|
317
|
+
let {
|
|
318
|
+
index,
|
|
319
|
+
style,
|
|
320
|
+
data
|
|
321
|
+
} = _ref6;
|
|
322
|
+
const {
|
|
323
|
+
options,
|
|
324
|
+
classNames,
|
|
325
|
+
testId,
|
|
326
|
+
optionProps
|
|
327
|
+
} = data;
|
|
328
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_MenuOptionButton.MenuOptionButton, {
|
|
329
|
+
option: options[index],
|
|
330
|
+
classNames: classNames,
|
|
331
|
+
style: style,
|
|
332
|
+
...optionProps,
|
|
333
|
+
isLastItem: index === options.length - 1,
|
|
334
|
+
setSize: options.length,
|
|
335
|
+
posInSet: index + 1,
|
|
336
|
+
testId: (0, _qa.generateTestId)({
|
|
337
|
+
base: testId,
|
|
338
|
+
slot: 'option',
|
|
339
|
+
index: index.toString()
|
|
340
|
+
})
|
|
341
|
+
});
|
|
69
342
|
};
|
|
70
|
-
const
|
|
343
|
+
const GroupRow = _ref7 => {
|
|
344
|
+
let {
|
|
345
|
+
index,
|
|
346
|
+
style,
|
|
347
|
+
data
|
|
348
|
+
} = _ref7;
|
|
349
|
+
const {
|
|
350
|
+
rows,
|
|
351
|
+
optionCount,
|
|
352
|
+
enableA11y,
|
|
353
|
+
menuId,
|
|
354
|
+
classNames,
|
|
355
|
+
testId,
|
|
356
|
+
optionProps
|
|
357
|
+
} = data;
|
|
358
|
+
const row = rows[index];
|
|
359
|
+
if (row.kind === 'title') {
|
|
360
|
+
// A flattened row cannot contain the options that follow it — react-window
|
|
361
|
+
// positions every row absolutely — so the title is its own `group` naming
|
|
362
|
+
// itself from its content, rather than a presentation row. That keeps the
|
|
363
|
+
// listbox owning only option and group children (a group title passed as a
|
|
364
|
+
// custom node would otherwise expose that node as generic content inside
|
|
365
|
+
// the listbox) and keeps the group NAME in the tree, which a presentation
|
|
366
|
+
// row dropped. It is a real limitation all the same: the options sit beside
|
|
367
|
+
// the group rather than inside it, so AT reads the group as a row of its own
|
|
368
|
+
// and does not announce it as context for the options that follow.
|
|
369
|
+
const titleId = `${menuId}-group-${row.groupIndex}`;
|
|
370
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
|
|
371
|
+
style: style,
|
|
372
|
+
id: enableA11y ? titleId : undefined,
|
|
373
|
+
role: enableA11y ? 'group' : undefined,
|
|
374
|
+
"aria-labelledby": enableA11y ? titleId : undefined,
|
|
375
|
+
className: (0, _classify.classify)(_MenuModule.default.groupTitleWrapper, classNames?.groupTitle),
|
|
376
|
+
"data-testid": (0, _qa.generateTestId)({
|
|
377
|
+
base: testId,
|
|
378
|
+
slot: 'group-title',
|
|
379
|
+
index: row.groupIndex.toString()
|
|
380
|
+
}),
|
|
381
|
+
children: row.node
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_MenuOptionButton.MenuOptionButton, {
|
|
385
|
+
option: row.option,
|
|
386
|
+
classNames: classNames,
|
|
387
|
+
style: style,
|
|
388
|
+
...optionProps,
|
|
389
|
+
isLastItem: index === rows.length - 1,
|
|
390
|
+
setSize: optionCount,
|
|
391
|
+
posInSet: row.flatOptionIndex + 1,
|
|
392
|
+
testId: (0, _qa.generateTestId)({
|
|
393
|
+
base: testId,
|
|
394
|
+
slot: 'option',
|
|
395
|
+
index: `${row.groupIndex}-${row.optionIndex}`
|
|
396
|
+
})
|
|
397
|
+
});
|
|
398
|
+
};
|
|
399
|
+
const RenderOption = _ref8 => {
|
|
71
400
|
let {
|
|
72
401
|
options,
|
|
73
402
|
composeOptions,
|
|
@@ -76,17 +405,25 @@ const RenderOption = _ref3 => {
|
|
|
76
405
|
searchText = '',
|
|
77
406
|
showResultText = true,
|
|
78
407
|
testId,
|
|
408
|
+
listRef,
|
|
79
409
|
staticLabels = {
|
|
80
410
|
RESULT: 'result',
|
|
81
411
|
RESULTS: 'results',
|
|
82
412
|
SEARCH_PLACEHOLDER: 'Search...'
|
|
83
413
|
},
|
|
84
414
|
...restProps
|
|
85
|
-
} =
|
|
415
|
+
} = _ref8;
|
|
86
416
|
const {
|
|
87
417
|
allowSearch,
|
|
88
418
|
size,
|
|
89
|
-
|
|
419
|
+
enableA11y,
|
|
420
|
+
menuId,
|
|
421
|
+
ariaLabelledBy,
|
|
422
|
+
ariaLabel,
|
|
423
|
+
optionsVariant,
|
|
424
|
+
virtualization,
|
|
425
|
+
externalKeyboardNav,
|
|
426
|
+
activeOptionKey
|
|
90
427
|
} = restProps;
|
|
91
428
|
const {
|
|
92
429
|
enable: isVirtualizationEnabled = false,
|
|
@@ -99,135 +436,151 @@ const RenderOption = _ref3 => {
|
|
|
99
436
|
const listHeight = menuHeight || (size === 'medium' ? menuSizeMedium : menuSizeSmall);
|
|
100
437
|
const resolveItemHeight = makeItemHeightResolver(itemHeight, size);
|
|
101
438
|
const resolveGroupTitleHeight = makeGroupTitleHeightResolver(groupTitleHeight);
|
|
439
|
+
|
|
440
|
+
/*
|
|
441
|
+
* When the menu owns its filter field and the list is virtualized, the
|
|
442
|
+
* scrolling list is a standalone listbox, and APG makes that focusable with
|
|
443
|
+
* the active option reported through `aria-activedescendant` — the field and
|
|
444
|
+
* the list each carry it, so whichever holds focus reports the same option. It
|
|
445
|
+
* also stops the virtualized scroll area reading as unreachable: the options
|
|
446
|
+
* are `tabindex="-1"`, so without a tab stop of its own the region has no
|
|
447
|
+
* focusable content at all (axe `scrollable-region-focusable`).
|
|
448
|
+
*
|
|
449
|
+
* Only the virtualized scroller can take that tab stop; the non-virtualized
|
|
450
|
+
* listbox is a `display: contents` wrapper with no box at all, and such an
|
|
451
|
+
* element is skipped by sequential focus navigation (see OptionsListbox).
|
|
452
|
+
* Behind a
|
|
453
|
+
* combobox trigger none of this applies — the trigger is the single tab stop
|
|
454
|
+
* and owns `aria-activedescendant`, and axe exempts a combobox's own popup —
|
|
455
|
+
* and a roving menu needs no tab stop here either, since its options are real
|
|
456
|
+
* focusable buttons.
|
|
457
|
+
*/
|
|
458
|
+
const listboxIsFocusable = Boolean(enableA11y && allowSearch && !externalKeyboardNav);
|
|
459
|
+
const listboxAttrs = {
|
|
460
|
+
enableA11y,
|
|
461
|
+
menuId,
|
|
462
|
+
ariaLabelledBy,
|
|
463
|
+
ariaLabel,
|
|
464
|
+
optionsVariant,
|
|
465
|
+
// Under virtualization react-window's outer box scrolls and carries the
|
|
466
|
+
// role; every other shape scrolls on the listbox itself.
|
|
467
|
+
scrolls: !isVirtualizationEnabled,
|
|
468
|
+
tabIndex: listboxIsFocusable ? 0 : undefined,
|
|
469
|
+
activeDescendantId: listboxIsFocusable && activeOptionKey && menuId ? (0, _menuOptionId.menuOptionId)(menuId, activeOptionKey) : undefined
|
|
470
|
+
};
|
|
102
471
|
if (options && Array.isArray(options) && options.length) {
|
|
103
472
|
const optionsFiltered = !allowSearch ? options : (0, _menu.getFilteredOptionsFromSearchText)(options, searchText);
|
|
104
473
|
const finalResultText = !allowSearch ? '' : (0, _menu.getFilteredOptionsResultText)(optionsFiltered, staticLabels);
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const buttonOption = optionsFiltered[idx];
|
|
111
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_MenuOptionButton.MenuOptionButton, {
|
|
112
|
-
option: buttonOption,
|
|
113
|
-
classNames: classNames,
|
|
114
|
-
style: style,
|
|
115
|
-
...restProps,
|
|
116
|
-
isLastItem: idx === optionsFiltered.length - 1,
|
|
117
|
-
testId: (0, _qa.generateTestId)({
|
|
118
|
-
base: testId,
|
|
119
|
-
slot: 'option',
|
|
120
|
-
index: idx.toString()
|
|
121
|
-
})
|
|
122
|
-
});
|
|
474
|
+
const flatRowData = {
|
|
475
|
+
options: optionsFiltered,
|
|
476
|
+
classNames,
|
|
477
|
+
testId,
|
|
478
|
+
optionProps: restProps
|
|
123
479
|
};
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
isLastItem: idx === optionsFiltered.length - 1,
|
|
153
|
-
testId: (0, _qa.generateTestId)({
|
|
154
|
-
base: testId,
|
|
155
|
-
slot: 'option',
|
|
156
|
-
index: idx.toString()
|
|
480
|
+
if (isVirtualizationEnabled) {
|
|
481
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
482
|
+
children: [allowSearch && showResultText && /*#__PURE__*/(0, _jsxRuntime.jsx)(ResultStatus, {
|
|
483
|
+
enableA11y: enableA11y,
|
|
484
|
+
text: finalResultText,
|
|
485
|
+
testId: testId
|
|
486
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(ListboxAttrsContext.Provider, {
|
|
487
|
+
value: listboxAttrs,
|
|
488
|
+
children: typeof itemHeight === 'function' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(MenuVariableSizeList, {
|
|
489
|
+
listRef: listRef,
|
|
490
|
+
heightSignature: optionsFiltered.map((option, idx) => `${option.key}:${resolveItemHeight(option, idx)}`).join('|'),
|
|
491
|
+
height: listHeight,
|
|
492
|
+
itemSize: idx => resolveItemHeight(optionsFiltered[idx], idx),
|
|
493
|
+
itemCount: optionsFiltered.length,
|
|
494
|
+
itemKey: idx => optionsFiltered[idx].key,
|
|
495
|
+
itemData: flatRowData,
|
|
496
|
+
children: FlatRow
|
|
497
|
+
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactWindow.FixedSizeList, {
|
|
498
|
+
ref: listRef,
|
|
499
|
+
height: listHeight,
|
|
500
|
+
itemSize: itemHeight || (size === 'medium' ? buttonSizeMedium : buttonSizeSmall),
|
|
501
|
+
itemCount: optionsFiltered.length,
|
|
502
|
+
itemKey: idx => optionsFiltered[idx].key,
|
|
503
|
+
itemData: flatRowData,
|
|
504
|
+
width: "initial",
|
|
505
|
+
outerElementType: ListboxOuter,
|
|
506
|
+
innerElementType: PresentationDiv,
|
|
507
|
+
children: FlatRow
|
|
157
508
|
})
|
|
158
|
-
})
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
if (composeOptions && Array.isArray(composeOptions) && composeOptions.length) {
|
|
163
|
-
const optionsFiltered = !allowSearch ? composeOptions : (0, _menu.getFilteredComposeOptionsFromSearchText)(composeOptions, searchText);
|
|
164
|
-
const finalResultText = !allowSearch ? '' : (0, _menu.getFilteredComposeOptionsResultText)(optionsFiltered, staticLabels);
|
|
509
|
+
})]
|
|
510
|
+
});
|
|
511
|
+
}
|
|
165
512
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
166
|
-
children: [allowSearch && showResultText && /*#__PURE__*/(0, _jsxRuntime.jsx)(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
testId:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
children: finalResultText
|
|
174
|
-
}), optionsFiltered.map((composeMenuOptions, index) =>
|
|
175
|
-
/*#__PURE__*/
|
|
176
|
-
// eslint-disable-next-line react/no-array-index-key
|
|
177
|
-
(0, _jsxRuntime.jsx)("span", {
|
|
178
|
-
className: _MenuModule.default.menuDivider,
|
|
179
|
-
children: composeMenuOptions.map((option, idx) => /*#__PURE__*/(0, _jsxRuntime.jsx)(React.Fragment, {
|
|
513
|
+
children: [allowSearch && showResultText && /*#__PURE__*/(0, _jsxRuntime.jsx)(ResultStatus, {
|
|
514
|
+
enableA11y: enableA11y,
|
|
515
|
+
text: finalResultText,
|
|
516
|
+
testId: testId
|
|
517
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(OptionsListbox, {
|
|
518
|
+
...listboxAttrs,
|
|
519
|
+
children: optionsFiltered.map((option, idx) => /*#__PURE__*/(0, _jsxRuntime.jsx)(React.Fragment, {
|
|
180
520
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_MenuOptionButton.MenuOptionButton, {
|
|
181
521
|
option: option,
|
|
182
522
|
classNames: classNames,
|
|
183
523
|
...restProps,
|
|
184
|
-
isLastItem:
|
|
524
|
+
isLastItem: idx === optionsFiltered.length - 1,
|
|
185
525
|
testId: (0, _qa.generateTestId)({
|
|
186
526
|
base: testId,
|
|
187
527
|
slot: 'option',
|
|
188
|
-
index:
|
|
528
|
+
index: idx.toString()
|
|
189
529
|
})
|
|
190
530
|
})
|
|
191
531
|
}, option.key))
|
|
192
|
-
}
|
|
532
|
+
})]
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
if (composeOptions && Array.isArray(composeOptions) && composeOptions.length) {
|
|
536
|
+
const optionsFiltered = !allowSearch ? composeOptions : (0, _menu.getFilteredComposeOptionsFromSearchText)(composeOptions, searchText);
|
|
537
|
+
const finalResultText = !allowSearch ? '' : (0, _menu.getFilteredComposeOptionsResultText)(optionsFiltered, staticLabels);
|
|
538
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
539
|
+
children: [allowSearch && showResultText && /*#__PURE__*/(0, _jsxRuntime.jsx)(ResultStatus, {
|
|
540
|
+
enableA11y: enableA11y,
|
|
541
|
+
text: finalResultText,
|
|
542
|
+
testId: testId
|
|
543
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(OptionsListbox, {
|
|
544
|
+
...listboxAttrs,
|
|
545
|
+
children: optionsFiltered.map((composeMenuOptions, index) => /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
|
|
546
|
+
role: enableA11y ? 'presentation' : undefined,
|
|
547
|
+
className: _MenuModule.default.menuDivider,
|
|
548
|
+
children: composeMenuOptions.map((option, idx) => /*#__PURE__*/(0, _jsxRuntime.jsx)(React.Fragment, {
|
|
549
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_MenuOptionButton.MenuOptionButton, {
|
|
550
|
+
option: option,
|
|
551
|
+
classNames: classNames,
|
|
552
|
+
...restProps,
|
|
553
|
+
isLastItem: index === optionsFiltered.length - 1 && idx === composeMenuOptions.length - 1,
|
|
554
|
+
testId: (0, _qa.generateTestId)({
|
|
555
|
+
base: testId,
|
|
556
|
+
slot: 'option',
|
|
557
|
+
index: `${index}-${idx}`
|
|
558
|
+
})
|
|
559
|
+
})
|
|
560
|
+
}, option.key))
|
|
561
|
+
}, index))
|
|
562
|
+
})]
|
|
193
563
|
});
|
|
194
564
|
}
|
|
195
565
|
if (groupTitleOptions && Array.isArray(groupTitleOptions) && groupTitleOptions.length) {
|
|
196
566
|
const optionsFiltered = !allowSearch ? groupTitleOptions : (0, _menu.getFilteredGroupTitleOptionsFromSearchText)(groupTitleOptions, searchText);
|
|
197
567
|
const finalResultText = !allowSearch ? '' : (0, _menu.getFilteredGroupTitleOptionsResultText)(optionsFiltered, staticLabels);
|
|
198
|
-
const resultText = allowSearch && showResultText && /*#__PURE__*/(0, _jsxRuntime.jsx)(_Text.FormLabelSmall, {
|
|
199
|
-
className: _MenuModule.default.filterOptionsResultText,
|
|
200
|
-
color: "tertiary",
|
|
201
|
-
testId: (0, _qa.generateTestId)({
|
|
202
|
-
base: testId,
|
|
203
|
-
slot: 'result-text'
|
|
204
|
-
}),
|
|
205
|
-
children: finalResultText
|
|
206
|
-
});
|
|
207
568
|
if (isVirtualizationEnabled) {
|
|
208
|
-
//
|
|
209
|
-
// flatten the groups into one row list and size each row
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
option,
|
|
224
|
-
groupIndex,
|
|
225
|
-
optionIndex,
|
|
226
|
-
flatOptionIndex
|
|
227
|
-
});
|
|
228
|
-
flatOptionIndex += 1;
|
|
229
|
-
});
|
|
230
|
-
});
|
|
569
|
+
// Group titles and options have different heights, which rules out
|
|
570
|
+
// FixedSizeList — flatten the groups into one row list and size each row
|
|
571
|
+
// individually.
|
|
572
|
+
//
|
|
573
|
+
// Flattening also costs the role="group" WRAPPERS the non-virtualized
|
|
574
|
+
// path renders: react-window positions every row absolutely, so a title
|
|
575
|
+
// cannot contain the options that follow it. Each title is instead its
|
|
576
|
+
// own `group` row naming itself (see GroupRow), so the listbox still owns
|
|
577
|
+
// only option/group children and the group name stays in the tree — but
|
|
578
|
+
// the options sit beside that group rather than inside it, so AT will not
|
|
579
|
+
// announce the group as context while arrowing through them. Every option
|
|
580
|
+
// carries aria-setsize / aria-posinset so its place in the whole list is
|
|
581
|
+
// still reported rather than its place in the mounted window.
|
|
582
|
+
const rows = buildGroupVirtualRows(optionsFiltered);
|
|
583
|
+
const optionCount = rows.filter(row => row.kind === 'option').length;
|
|
231
584
|
const rowHeight = idx => {
|
|
232
585
|
const row = rows[idx];
|
|
233
586
|
return row.kind === 'title' ? resolveGroupTitleHeight(row.node, row.groupIndex) : resolveItemHeight(row.option, row.flatOptionIndex);
|
|
@@ -236,78 +589,129 @@ const RenderOption = _ref3 => {
|
|
|
236
589
|
const row = rows[idx];
|
|
237
590
|
return row.kind === 'title' ? `group-title-${row.groupIndex}` : row.option.key;
|
|
238
591
|
};
|
|
592
|
+
const groupRowData = {
|
|
593
|
+
rows,
|
|
594
|
+
optionCount,
|
|
595
|
+
enableA11y,
|
|
596
|
+
menuId,
|
|
597
|
+
classNames,
|
|
598
|
+
testId,
|
|
599
|
+
optionProps: restProps
|
|
600
|
+
};
|
|
239
601
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
240
|
-
children: [
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
children:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
"data-testid": (0, _qa.generateTestId)({
|
|
257
|
-
base: testId,
|
|
258
|
-
slot: 'group-title',
|
|
259
|
-
index: row.groupIndex.toString()
|
|
260
|
-
}),
|
|
261
|
-
children: row.node
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_MenuOptionButton.MenuOptionButton, {
|
|
265
|
-
option: row.option,
|
|
266
|
-
classNames: classNames,
|
|
267
|
-
style: style,
|
|
268
|
-
...restProps,
|
|
269
|
-
isLastItem: idx === rows.length - 1,
|
|
270
|
-
testId: (0, _qa.generateTestId)({
|
|
271
|
-
base: testId,
|
|
272
|
-
slot: 'option',
|
|
273
|
-
index: `${row.groupIndex}-${row.optionIndex}`
|
|
274
|
-
})
|
|
275
|
-
});
|
|
276
|
-
}
|
|
602
|
+
children: [allowSearch && showResultText && /*#__PURE__*/(0, _jsxRuntime.jsx)(ResultStatus, {
|
|
603
|
+
enableA11y: enableA11y,
|
|
604
|
+
text: finalResultText,
|
|
605
|
+
testId: testId
|
|
606
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(ListboxAttrsContext.Provider, {
|
|
607
|
+
value: listboxAttrs,
|
|
608
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(MenuVariableSizeList, {
|
|
609
|
+
listRef: listRef,
|
|
610
|
+
heightSignature: rows.map((_row, idx) => `${rowKey(idx)}:${rowHeight(idx)}`).join('|'),
|
|
611
|
+
height: listHeight,
|
|
612
|
+
itemSize: rowHeight,
|
|
613
|
+
itemCount: rows.length,
|
|
614
|
+
itemKey: rowKey,
|
|
615
|
+
itemData: groupRowData,
|
|
616
|
+
children: GroupRow
|
|
617
|
+
})
|
|
277
618
|
})]
|
|
278
619
|
});
|
|
279
620
|
}
|
|
280
621
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
281
|
-
children: [
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
622
|
+
children: [allowSearch && showResultText && /*#__PURE__*/(0, _jsxRuntime.jsx)(ResultStatus, {
|
|
623
|
+
enableA11y: enableA11y,
|
|
624
|
+
text: finalResultText,
|
|
625
|
+
testId: testId
|
|
626
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(OptionsListbox, {
|
|
627
|
+
...listboxAttrs,
|
|
628
|
+
children: optionsFiltered.map((optionsGroup, index) => {
|
|
629
|
+
// Link each group's options to its visible title via role="group"
|
|
630
|
+
// so the listbox owns only group/option children and AT announces
|
|
631
|
+
// group boundaries + names.
|
|
632
|
+
const groupTitleId = optionsGroup.groupTitle ? `${menuId}-group-${index}` : undefined;
|
|
633
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(OptionGroup
|
|
634
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
635
|
+
, {
|
|
636
|
+
enableA11y: enableA11y,
|
|
637
|
+
labelledBy: groupTitleId,
|
|
638
|
+
children: [!!optionsGroup.groupTitle && /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
|
|
639
|
+
id: enableA11y ? groupTitleId : undefined,
|
|
640
|
+
className: (0, _classify.classify)(_MenuModule.default.groupTitleWrapper, classNames?.groupTitle),
|
|
641
|
+
"data-testid": (0, _qa.generateTestId)({
|
|
642
|
+
base: testId,
|
|
643
|
+
slot: 'group-title',
|
|
644
|
+
index: index.toString()
|
|
645
|
+
}),
|
|
646
|
+
children: optionsGroup.groupTitle
|
|
647
|
+
}), optionsGroup.options?.map((option, idx) => /*#__PURE__*/(0, _jsxRuntime.jsx)(React.Fragment, {
|
|
648
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_MenuOptionButton.MenuOptionButton, {
|
|
649
|
+
option: option,
|
|
650
|
+
classNames: classNames,
|
|
651
|
+
...restProps,
|
|
652
|
+
isLastItem: index === optionsFiltered.length - 1 && idx === (optionsGroup.options && optionsGroup.options.length - 1),
|
|
653
|
+
testId: (0, _qa.generateTestId)({
|
|
654
|
+
base: testId,
|
|
655
|
+
slot: 'option',
|
|
656
|
+
index: `${index}-${idx}`
|
|
657
|
+
})
|
|
658
|
+
})
|
|
659
|
+
}, option.key))]
|
|
660
|
+
}, index);
|
|
661
|
+
})
|
|
662
|
+
})]
|
|
307
663
|
});
|
|
308
664
|
}
|
|
309
|
-
|
|
665
|
+
// No option source yet (e.g. async options still loading). Render the listbox
|
|
666
|
+
// element anyway: the trigger advertises aria-expanded + aria-controls={menuId}
|
|
667
|
+
// for as long as the menu is mounted, and an IDREF that resolves to nothing is
|
|
668
|
+
// an invalid-value violation. An empty listbox is honest and valid.
|
|
669
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(OptionsListbox, {
|
|
670
|
+
enableA11y: enableA11y,
|
|
671
|
+
menuId: menuId,
|
|
672
|
+
ariaLabelledBy: ariaLabelledBy,
|
|
673
|
+
ariaLabel: ariaLabel,
|
|
674
|
+
optionsVariant: optionsVariant
|
|
675
|
+
});
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Flatten the active option source (options | compose | groups) into one list.
|
|
680
|
+
* Exported because a combobox trigger drives the highlight from outside Menu
|
|
681
|
+
* and needs the same list Menu itself navigates — see `useMenuTrigger`.
|
|
682
|
+
*/
|
|
683
|
+
const getFlatOptions = props => {
|
|
684
|
+
const {
|
|
685
|
+
options,
|
|
686
|
+
composeOptions,
|
|
687
|
+
groupTitleOptions
|
|
688
|
+
} = props;
|
|
689
|
+
if (options && options.length) {
|
|
690
|
+
return options;
|
|
691
|
+
}
|
|
692
|
+
if (composeOptions && composeOptions.length) {
|
|
693
|
+
return composeOptions.flat();
|
|
694
|
+
}
|
|
695
|
+
if (groupTitleOptions && groupTitleOptions.length) {
|
|
696
|
+
return groupTitleOptions.flatMap(group => group.options ?? []);
|
|
697
|
+
}
|
|
698
|
+
return [];
|
|
310
699
|
};
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Key of the option that should own the roving tab stop when the menu opens:
|
|
703
|
+
* the selected option if present, otherwise the first enabled option.
|
|
704
|
+
*/
|
|
705
|
+
exports.getFlatOptions = getFlatOptions;
|
|
706
|
+
const getInitialActiveOptionKey = props => {
|
|
707
|
+
const enabled = getFlatOptions(props).filter(option => !option.disabled);
|
|
708
|
+
if (!enabled.length) {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
const selectedKey = enabled.find(option => props.selectedKeys?.includes(option.key) || option.key === props.selectedOption?.key)?.key;
|
|
712
|
+
return selectedKey ?? enabled[0].key;
|
|
713
|
+
};
|
|
714
|
+
exports.getInitialActiveOptionKey = getInitialActiveOptionKey;
|
|
311
715
|
const Menu = exports.Menu = /*#__PURE__*/React.forwardRef((props, ref) => {
|
|
312
716
|
const {
|
|
313
717
|
classNames,
|
|
@@ -325,6 +729,15 @@ const Menu = exports.Menu = /*#__PURE__*/React.forwardRef((props, ref) => {
|
|
|
325
729
|
header,
|
|
326
730
|
footer,
|
|
327
731
|
staticLabels,
|
|
732
|
+
menuId,
|
|
733
|
+
menuDisabled,
|
|
734
|
+
onSelect,
|
|
735
|
+
onTabOut,
|
|
736
|
+
onReturnFocusToInput,
|
|
737
|
+
initialFocusRef,
|
|
738
|
+
externalKeyboardNav = false,
|
|
739
|
+
onActiveOptionKeyChange,
|
|
740
|
+
activeOptionKey: controlledActiveOptionKey,
|
|
328
741
|
testId
|
|
329
742
|
} = props;
|
|
330
743
|
const [searchText, setSearchText] = React.useState('');
|
|
@@ -333,8 +746,494 @@ const Menu = exports.Menu = /*#__PURE__*/React.forwardRef((props, ref) => {
|
|
|
333
746
|
itemHeight,
|
|
334
747
|
groupTitleHeight
|
|
335
748
|
} = virtualization;
|
|
749
|
+
// Everything the accessibility work added hangs off this. Unset, the
|
|
750
|
+
// component behaves exactly as it did before any of it existed.
|
|
751
|
+
const enableA11y = props.enableA11y === true;
|
|
752
|
+
|
|
753
|
+
// A virtualized list only mounts a window of rows, so the DOM cannot be the
|
|
754
|
+
// source of truth for keyboard navigation — Home/End and friends would stop
|
|
755
|
+
// at the edge of the window. Build the real option list and hand it to the
|
|
756
|
+
// hook. The plain `options` and the grouped `groupTitleOptions` branches
|
|
757
|
+
// both virtualize (see RenderOption); compose menus never do, so they keep
|
|
758
|
+
// the DOM-driven path.
|
|
759
|
+
const isVirtualized = Boolean(virtualization.enable && (props.options?.length || props.groupTitleOptions?.length));
|
|
760
|
+
const virtualListRef = React.useRef(null);
|
|
761
|
+
// A callback ref, because which of the two react-window list types is
|
|
762
|
+
// mounted depends on the virtualization mode.
|
|
763
|
+
const setVirtualList = React.useCallback(instance => {
|
|
764
|
+
virtualListRef.current = instance;
|
|
765
|
+
}, []);
|
|
766
|
+
// Last `key:rowIndex` the virtual list was scrolled to. Guards against
|
|
767
|
+
// re-scrolling on unrelated re-renders, and is pre-set by the pointer path so
|
|
768
|
+
// hovering a partially clipped row never scrolls the list under the cursor.
|
|
769
|
+
// The row index is part of the stamp because filtering can keep the active
|
|
770
|
+
// key while moving its row — a key-only guard would leave the highlight
|
|
771
|
+
// stranded off-screen with `aria-activedescendant` pointing at nothing.
|
|
772
|
+
const lastScrolledKeyRef = React.useRef(null);
|
|
773
|
+
// Which option the pointer last highlighted. State (not a ref) because the
|
|
774
|
+
// focus ring is rendered from it and has to repaint when it changes.
|
|
775
|
+
const [pointerActiveKey, setPointerActiveKey] = React.useState(null);
|
|
776
|
+
// Exactly the rows react-window renders, in its order. Disabled options ARE
|
|
777
|
+
// included, because they still occupy a row.
|
|
778
|
+
//
|
|
779
|
+
// `scrollToItem` counts ROWS, and a virtualized grouped menu interleaves
|
|
780
|
+
// title rows among the options, so an option's row index is not its index
|
|
781
|
+
// among the options. Keep both: `options` drives navigation, `rowIndexByKey`
|
|
782
|
+
// translates the active option into the index the list expects.
|
|
783
|
+
const virtualLayout = React.useMemo(() => {
|
|
784
|
+
if (!isVirtualized) {
|
|
785
|
+
return undefined;
|
|
786
|
+
}
|
|
787
|
+
const rowIndexByKey = new Map();
|
|
788
|
+
if (props.options?.length) {
|
|
789
|
+
const options = allowSearch ? (0, _menu.getFilteredOptionsFromSearchText)(props.options, searchText) : props.options;
|
|
790
|
+
options.forEach((option, index) => {
|
|
791
|
+
rowIndexByKey.set(option.key, index);
|
|
792
|
+
});
|
|
793
|
+
return {
|
|
794
|
+
options,
|
|
795
|
+
rowIndexByKey
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
if (props.groupTitleOptions?.length) {
|
|
799
|
+
const groups = allowSearch ? (0, _menu.getFilteredGroupTitleOptionsFromSearchText)(props.groupTitleOptions, searchText) : props.groupTitleOptions;
|
|
800
|
+
const options = [];
|
|
801
|
+
buildGroupVirtualRows(groups).forEach((row, index) => {
|
|
802
|
+
if (row.kind === 'option') {
|
|
803
|
+
rowIndexByKey.set(row.option.key, index);
|
|
804
|
+
options.push(row.option);
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
return {
|
|
808
|
+
options,
|
|
809
|
+
rowIndexByKey
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
return undefined;
|
|
813
|
+
}, [isVirtualized, props.options, props.groupTitleOptions, allowSearch, searchText]);
|
|
814
|
+
const virtualRows = virtualLayout?.options;
|
|
815
|
+
// The navigable subset (arrow keys skip disabled options). Kept separate from
|
|
816
|
+
// the row layout because mixing the two would make `scrollToItem` land on
|
|
817
|
+
// the wrong row whenever the list contains a disabled option or a title.
|
|
818
|
+
const navigationOptions = React.useMemo(() => virtualRows?.filter(option => !option.disabled && !menuDisabled).map(option => {
|
|
819
|
+
// Type-ahead must match what the user can read, so prefer the
|
|
820
|
+
// resolved label when the consumer supplies a string one.
|
|
821
|
+
const resolved = props.resolveLabel?.(option);
|
|
822
|
+
return {
|
|
823
|
+
key: option.key,
|
|
824
|
+
label: typeof resolved === 'string' ? resolved : option.label ?? ''
|
|
825
|
+
};
|
|
826
|
+
}), [virtualRows, menuDisabled, props.resolveLabel]);
|
|
827
|
+
const generatedMenuId = React.useId();
|
|
828
|
+
const resolvedMenuId = menuId ?? generatedMenuId;
|
|
829
|
+
const containerRef = React.useRef(null);
|
|
830
|
+
// Last pointer position seen by an option's mousemove — used to ignore
|
|
831
|
+
// scroll-under-stationary-cursor moves so hover doesn't fight the keyboard.
|
|
832
|
+
const lastPointerRef = React.useRef(null);
|
|
833
|
+
const [internalActiveKey, setInternalActiveKey] = React.useState(() => getInitialActiveOptionKey(props));
|
|
834
|
+
|
|
835
|
+
/*
|
|
836
|
+
* The menu opens with an option already active so the keyboard has somewhere
|
|
837
|
+
* to start, but painting that highlight after a MOUSE open makes the menu
|
|
838
|
+
* look like it has a cursor the user never asked for. Suppress it until the
|
|
839
|
+
* keyboard or the pointer actually picks an option.
|
|
840
|
+
*
|
|
841
|
+
* Read imperatively, not as reactive state: what matters is how the menu was
|
|
842
|
+
* OPENED. Re-rendering when the user later switches to the mouse would undo
|
|
843
|
+
* the highlight they just navigated to with the keyboard.
|
|
844
|
+
*/
|
|
845
|
+
/*
|
|
846
|
+
* Whether focus is inside the menu right now.
|
|
847
|
+
*
|
|
848
|
+
* A roving menu's highlight IS its DOM focus, so showing it while focus has
|
|
849
|
+
* moved elsewhere on the page paints a cursor in a widget the keyboard is no
|
|
850
|
+
* longer driving. The active option is still remembered — arrowing back in
|
|
851
|
+
* resumes where it was — it just stops being drawn.
|
|
852
|
+
*
|
|
853
|
+
* The combobox modes are exempt: there the trigger input outside this DOM
|
|
854
|
+
* holds focus by design, and this highlight is the only cursor the user has.
|
|
855
|
+
*/
|
|
856
|
+
const [menuHasFocus, setMenuHasFocus] = React.useState(false);
|
|
857
|
+
const [suppressInitialHighlight, setSuppressInitialHighlight] = React.useState(() => (0, _hooks.getInteractionModality)() !== 'keyboard');
|
|
858
|
+
// Primed by the trigger hooks before the popup exists; primed here too for a
|
|
859
|
+
// Menu opened by a trigger that does not use them.
|
|
860
|
+
React.useEffect(() => {
|
|
861
|
+
if (enableA11y) {
|
|
862
|
+
(0, _hooks.startInteractionModalityTracking)();
|
|
863
|
+
}
|
|
864
|
+
}, [enableA11y]);
|
|
865
|
+
|
|
866
|
+
// When an external combobox input drives navigation the active option is a
|
|
867
|
+
// controlled prop; otherwise the Menu owns it (roving / internal search).
|
|
868
|
+
const activeOptionKey = externalKeyboardNav ? controlledActiveOptionKey ?? null : internalActiveKey;
|
|
869
|
+
const setActiveOptionKey = externalKeyboardNav ? onActiveOptionKeyChange ?? (() => undefined) : setInternalActiveKey;
|
|
870
|
+
|
|
871
|
+
// Once the user reaches for the keyboard the highlight is wanted, wherever
|
|
872
|
+
// they pressed. This is what covers the combobox consumers, whose key
|
|
873
|
+
// handling lives on the trigger OUTSIDE this component and so never reaches
|
|
874
|
+
// the keydown handler below.
|
|
875
|
+
//
|
|
876
|
+
// Deliberately keyed off real interaction rather than off `activeOptionKey`
|
|
877
|
+
// changing: async options arriving after a mouse open also change that key,
|
|
878
|
+
// and treating it as intent would paint the very highlight this suppresses.
|
|
879
|
+
//
|
|
880
|
+
// Popups only. An inline Menu has no trigger outside itself, so the only
|
|
881
|
+
// keyboard that should reveal its highlight is the one pressed inside it
|
|
882
|
+
// (the card's own onKeyDown below) — not typing in some unrelated field on
|
|
883
|
+
// the same page, which also flips the session-wide modality.
|
|
884
|
+
const modality = (0, _hooks.useInteractionModality)(enableA11y);
|
|
885
|
+
const isPopup = Boolean(initialFocusRef || externalKeyboardNav);
|
|
886
|
+
React.useEffect(() => {
|
|
887
|
+
if (isPopup && modality === 'keyboard') {
|
|
888
|
+
setSuppressInitialHighlight(false);
|
|
889
|
+
}
|
|
890
|
+
}, [isPopup, modality]);
|
|
891
|
+
|
|
892
|
+
// Keep the roving tab stop valid as the rendered option set changes
|
|
893
|
+
// (search filtering, async options, etc.). Skipped under external nav —
|
|
894
|
+
// the parent owns the active option there.
|
|
895
|
+
React.useLayoutEffect(() => {
|
|
896
|
+
if (!enableA11y || externalKeyboardNav) {
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
const container = containerRef.current;
|
|
900
|
+
// Under virtualization the DOM holds only a window of rows, so validating
|
|
901
|
+
// against it would discard a perfectly good active key the moment it
|
|
902
|
+
// scrolled out and snap the highlight back to the top of the window.
|
|
903
|
+
const keys = navigationOptions ? navigationOptions.map(option => option.key) : container ? Array.from(container.querySelectorAll('[data-menu-option-key]')).filter(el => el.getAttribute('aria-disabled') !== 'true' && !el.disabled).map(el => el.getAttribute('data-menu-option-key')).filter(key => key !== null) : [];
|
|
904
|
+
setInternalActiveKey(prev => {
|
|
905
|
+
if (!keys.length) {
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
908
|
+
if (prev && keys.includes(prev)) {
|
|
909
|
+
return prev;
|
|
910
|
+
}
|
|
911
|
+
const selectedKey = keys.find(key => props.selectedKeys?.includes(key) || key === props.selectedOption?.key);
|
|
912
|
+
return selectedKey ?? keys[0];
|
|
913
|
+
});
|
|
914
|
+
}, [enableA11y, searchText, props.options, props.composeOptions, props.groupTitleOptions, props.selectedKeys, props.selectedOption, navigationOptions]);
|
|
915
|
+
|
|
916
|
+
// Keep the active option MOUNTED in a virtualized list: the row the keyboard
|
|
917
|
+
// just moved to may be outside the render window, which would leave roving
|
|
918
|
+
// focus with no target and aria-activedescendant pointing at nothing.
|
|
919
|
+
React.useLayoutEffect(() => {
|
|
920
|
+
if (!enableA11y || !isVirtualized || !activeOptionKey || !virtualLayout) {
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
// Only act on an actual CHANGE of active option. The row layout is memoized
|
|
924
|
+
// on the consumer's own array, so a parent that rebuilds it every render (e.g.
|
|
925
|
+
// Pagination's inline `menu={{options: allPages.map(...)}}`) would
|
|
926
|
+
// otherwise re-run this on every render and yank the list back to the
|
|
927
|
+
// active row, throwing away wherever the user had scrolled to.
|
|
928
|
+
// Index within the RENDERED rows — scrollToItem counts disabled options
|
|
929
|
+
// and group titles too. A key that is not in the layout yet (async options
|
|
930
|
+
// still arriving) is left unstamped, so it is scrolled to once it exists.
|
|
931
|
+
const index = virtualLayout.rowIndexByKey.get(activeOptionKey);
|
|
932
|
+
if (index === undefined) {
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
const stamp = `${activeOptionKey}:${index}`;
|
|
936
|
+
if (lastScrolledKeyRef.current === stamp) {
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
lastScrolledKeyRef.current = stamp;
|
|
940
|
+
// Whether the menu owns focus RIGHT NOW, captured before the scroll can
|
|
941
|
+
// unmount the row holding it. Focus is only ever reclaimed below when the
|
|
942
|
+
// answer is yes: on the very first run of this effect (the menu mounting
|
|
943
|
+
// with an option already seeded) focus is still wherever the user left it,
|
|
944
|
+
// and an inline Menu must not pull it out of the page.
|
|
945
|
+
const hadFocusInside = Boolean(typeof document !== 'undefined' && containerRef.current?.contains(document.activeElement));
|
|
946
|
+
virtualListRef.current?.scrollToItem(index, 'smart');
|
|
947
|
+
// react-window positions using the `height` it was handed, which under this
|
|
948
|
+
// flex layout can be taller than the scroll box actually ends up, leaving
|
|
949
|
+
// the last rows a few pixels clipped. Once the row has mounted, nudge it
|
|
950
|
+
// with the real box so the highlight is never cut off, and — for roving
|
|
951
|
+
// focus — put real focus on it, since the row the open-time effect wanted
|
|
952
|
+
// may not have existed until this scroll rendered it.
|
|
953
|
+
const frame = requestAnimationFrame(() => {
|
|
954
|
+
const row = containerRef.current?.querySelector(`[data-menu-option-key="${CSS.escape(activeOptionKey)}"]`);
|
|
955
|
+
if (!row) {
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
row.scrollIntoView({
|
|
959
|
+
block: 'nearest'
|
|
960
|
+
});
|
|
961
|
+
// Reclaim focus only if the scroll actually cost us it (the previously
|
|
962
|
+
// focused row was unmounted, dropping focus to <body>). Never steal it
|
|
963
|
+
// from the search field, the chrome, a combobox trigger outside — or
|
|
964
|
+
// from whatever owned it before this menu existed.
|
|
965
|
+
if (hadFocusInside && !allowSearch && !externalKeyboardNav && typeof document !== 'undefined' && (document.activeElement === document.body || document.activeElement === null)) {
|
|
966
|
+
row.focus({
|
|
967
|
+
preventScroll: true
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
return () => cancelAnimationFrame(frame);
|
|
972
|
+
}, [enableA11y, activeOptionKey, isVirtualized, virtualLayout, allowSearch, externalKeyboardNav]);
|
|
973
|
+
|
|
974
|
+
// A virtualized row can be unmounted underneath the focus it holds: the user
|
|
975
|
+
// scrolls the options with the wheel, the scrollbar, or a key the browser
|
|
976
|
+
// scrolls with, and react-window drops the row they were standing on. Focus
|
|
977
|
+
// then falls to <body>, which takes the menu's key handling with it — and
|
|
978
|
+
// because the roving tab stop lives on the active row, that row is no longer
|
|
979
|
+
// rendered either, so there is nothing left to Tab back to. The options are
|
|
980
|
+
// unreachable by keyboard until the user clicks one.
|
|
981
|
+
//
|
|
982
|
+
// The removal cannot be caught on blur: React's onBlur needs the event to
|
|
983
|
+
// bubble up through this container, and the row is already detached by then.
|
|
984
|
+
// The scroll is the live signal. Park focus on the card — always mounted,
|
|
985
|
+
// and it carries the key handler — and the next arrow key puts real focus
|
|
986
|
+
// back on a row, resuming from `activeOptionKey`.
|
|
987
|
+
React.useLayoutEffect(() => {
|
|
988
|
+
const container = containerRef.current;
|
|
989
|
+
if (!enableA11y || !isVirtualized || allowSearch || externalKeyboardNav || !container) {
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
let frame = 0;
|
|
993
|
+
// The row is not removed during the scroll event — the commit that drops it
|
|
994
|
+
// lands a frame or two later, and focus only falls to <body> then. So the
|
|
995
|
+
// check runs for a few frames after a scroll rather than once.
|
|
996
|
+
const CHECK_FRAMES = 4;
|
|
997
|
+
const check = remaining => {
|
|
998
|
+
frame = 0;
|
|
999
|
+
if (typeof document === 'undefined' || !container.isConnected) {
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
if (document.activeElement === document.body || document.activeElement === null) {
|
|
1003
|
+
container.focus({
|
|
1004
|
+
preventScroll: true
|
|
1005
|
+
});
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
if (remaining > 0) {
|
|
1009
|
+
frame = requestAnimationFrame(() => check(remaining - 1));
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
const onScroll = () => {
|
|
1013
|
+
// Sample ownership NOW, while the doomed row is still mounted: only a
|
|
1014
|
+
// scroll that starts with focus inside the menu can be the one that
|
|
1015
|
+
// unmounts the focused row. Someone who clicked the page away from the
|
|
1016
|
+
// menu and then scrolled it with the wheel is not asking for focus back.
|
|
1017
|
+
if (typeof document === 'undefined' || !container.contains(document.activeElement)) {
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
if (!frame) {
|
|
1021
|
+
frame = requestAnimationFrame(() => check(CHECK_FRAMES));
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
// `scroll` does not bubble, so listen for it in the capture phase rather
|
|
1025
|
+
// than reaching into react-window's own scroll container.
|
|
1026
|
+
container.addEventListener('scroll', onScroll, true);
|
|
1027
|
+
return () => {
|
|
1028
|
+
container.removeEventListener('scroll', onScroll, true);
|
|
1029
|
+
if (frame) {
|
|
1030
|
+
cancelAnimationFrame(frame);
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
}, [enableA11y, isVirtualized, allowSearch, externalKeyboardNav]);
|
|
1034
|
+
|
|
1035
|
+
// Move focus into the menu when it opens — the search input (so typing
|
|
1036
|
+
// works) or the active option (so arrow-key roving works) — never a
|
|
1037
|
+
// focusable header that happens to come first in the DOM. This is what a
|
|
1038
|
+
// trigger relies on instead of wrapping the menu in FloatingFocusManager.
|
|
1039
|
+
// Gated on `initialFocusRef` so only popup consumers (the dropdown family)
|
|
1040
|
+
// auto-focus on open; an inline Menu without that prop never steals focus.
|
|
1041
|
+
React.useLayoutEffect(() => {
|
|
1042
|
+
if (!enableA11y || !initialFocusRef || externalKeyboardNav) {
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
const container = containerRef.current;
|
|
1046
|
+
// A virtualized list mounts only a window of rows, so the active option may
|
|
1047
|
+
// not exist yet on open. Focusing whatever IS mounted would be undone a
|
|
1048
|
+
// moment later when the scroll effect renders the real row and unmounts the
|
|
1049
|
+
// one we focused, dropping focus to <body> and leaving the popup with no
|
|
1050
|
+
// keyboard owner. Scroll first and let the scroll effect place focus.
|
|
1051
|
+
if (isVirtualized && !allowSearch && activeOptionKey && virtualLayout) {
|
|
1052
|
+
const index = virtualLayout.rowIndexByKey.get(activeOptionKey);
|
|
1053
|
+
if (index !== undefined) {
|
|
1054
|
+
// Park focus on the card so it is inside the popup straight away (Tab
|
|
1055
|
+
// and Escape behave, nothing falls to <body>), scroll the real row in,
|
|
1056
|
+
// then hand focus over once it has mounted.
|
|
1057
|
+
initialFocusRef.current = container;
|
|
1058
|
+
container?.focus();
|
|
1059
|
+
lastScrolledKeyRef.current = `${activeOptionKey}:${index}`;
|
|
1060
|
+
virtualListRef.current?.scrollToItem(index, 'smart');
|
|
1061
|
+
const openFrame = requestAnimationFrame(() => {
|
|
1062
|
+
const row = containerRef.current?.querySelector(`[data-menu-option-key="${CSS.escape(activeOptionKey)}"]`);
|
|
1063
|
+
if (!row) {
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
initialFocusRef.current = row;
|
|
1067
|
+
row.focus({
|
|
1068
|
+
preventScroll: true
|
|
1069
|
+
});
|
|
1070
|
+
row.scrollIntoView({
|
|
1071
|
+
block: 'nearest'
|
|
1072
|
+
});
|
|
1073
|
+
});
|
|
1074
|
+
return () => cancelAnimationFrame(openFrame);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const target = container ? allowSearch ? container.querySelector('input[data-menu-search]') :
|
|
1078
|
+
// The roving tab stop, unless `menuDisabled` has rendered it disabled —
|
|
1079
|
+
// focusing a disabled <button> is a no-op that would leave focus on
|
|
1080
|
+
// the trigger with the popup open and keyboard-dead.
|
|
1081
|
+
container.querySelector('[data-menu-option-key][tabindex="0"]:not(:disabled):not([aria-disabled="true"])') ??
|
|
1082
|
+
// Fallback must skip disabled options — focusing a disabled <button>
|
|
1083
|
+
// is a no-op that would drop focus to <body> (all-disabled menus).
|
|
1084
|
+
container.querySelector('[data-menu-option-key]:not([aria-disabled="true"]):not(:disabled)') : null;
|
|
1085
|
+
// When no focusable option exists (every option disabled / menuDisabled),
|
|
1086
|
+
// focus the menu container itself (it has tabIndex={-1}) so focus still
|
|
1087
|
+
// enters the popup instead of falling to <body>.
|
|
1088
|
+
const focusTarget = target ?? container;
|
|
1089
|
+
initialFocusRef.current = focusTarget;
|
|
1090
|
+
focusTarget?.focus();
|
|
1091
|
+
// Only needs to resolve once when the menu mounts (opens).
|
|
1092
|
+
}, []);
|
|
1093
|
+
const selectByKey = key => {
|
|
1094
|
+
const option = getFlatOptions(props).find(opt => opt.key === key);
|
|
1095
|
+
if (option) {
|
|
1096
|
+
onSelect?.(option, null);
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
const {
|
|
1100
|
+
onKeyDown
|
|
1101
|
+
} = (0, _useMenuKeyboardNavigation.useMenuKeyboardNavigation)({
|
|
1102
|
+
containerRef,
|
|
1103
|
+
enabled: enableA11y && !menuDisabled && !externalKeyboardNav,
|
|
1104
|
+
mode: allowSearch ? 'search' : 'roving',
|
|
1105
|
+
activeOptionKey,
|
|
1106
|
+
onSelectKey: selectByKey,
|
|
1107
|
+
setActiveOptionKey,
|
|
1108
|
+
onTabOut,
|
|
1109
|
+
onReturnFocusToInput,
|
|
1110
|
+
options: navigationOptions
|
|
1111
|
+
});
|
|
1112
|
+
const activeDescendantId = allowSearch && activeOptionKey ? (0, _menuOptionId.menuOptionId)(resolvedMenuId, activeOptionKey) : undefined;
|
|
1113
|
+
|
|
1114
|
+
// Hovering an option makes it the single active highlight, so pointer and
|
|
1115
|
+
// keyboard never diverge. In roving mode focus follows the pointer too, so
|
|
1116
|
+
// arrow-key navigation resumes from wherever the mouse last was and the
|
|
1117
|
+
// previously focused option's highlight clears. Search / external-combobox
|
|
1118
|
+
// modes keep DOM focus on the input and only move the active descendant.
|
|
1119
|
+
const handleOptionPointerActivate = (key, el, clientX, clientY) => {
|
|
1120
|
+
// Ignore moves with no real pointer delta — e.g. the list scrolling under
|
|
1121
|
+
// a stationary cursor during keyboard nav fires mousemove with unchanged
|
|
1122
|
+
// viewport coordinates. Acting on those would steal focus from the
|
|
1123
|
+
// keyboard / fight the arrow keys.
|
|
1124
|
+
const last = lastPointerRef.current;
|
|
1125
|
+
if (last && last.x === clientX && last.y === clientY) {
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
lastPointerRef.current = {
|
|
1129
|
+
x: clientX,
|
|
1130
|
+
y: clientY
|
|
1131
|
+
};
|
|
1132
|
+
// The row is already under the cursor, so it must not be scrolled to.
|
|
1133
|
+
// Claiming it here makes the virtual-scroll effect treat it as handled —
|
|
1134
|
+
// otherwise hovering a partially clipped edge row would nudge the list
|
|
1135
|
+
// under a stationary mouse and shift every row out from under it.
|
|
1136
|
+
lastScrolledKeyRef.current = `${key}:${virtualLayout?.rowIndexByKey.get(key) ?? '?'}`;
|
|
1137
|
+
setPointerActiveKey(key);
|
|
1138
|
+
// Hovering an option IS the user picking it out, so show the highlight
|
|
1139
|
+
// even though the menu was opened by mouse.
|
|
1140
|
+
setSuppressInitialHighlight(false);
|
|
1141
|
+
setActiveOptionKey(key);
|
|
1142
|
+
// Roving mode only: move real focus so the keyboard cursor / tab stop
|
|
1143
|
+
// follows the pointer. Search & external-combobox keep focus on the input.
|
|
1144
|
+
// Gate on focus ALREADY being inside the menu so an inline menu (or a
|
|
1145
|
+
// consumer like TokenListInput, where the user types in an external input
|
|
1146
|
+
// and the menu is just a mouse target) is never robbed of its focus by a
|
|
1147
|
+
// stray hover.
|
|
1148
|
+
if (!allowSearch && !externalKeyboardNav) {
|
|
1149
|
+
const container = containerRef.current;
|
|
1150
|
+
if (container && typeof document !== 'undefined' && container.contains(document.activeElement)) {
|
|
1151
|
+
el.focus({
|
|
1152
|
+
preventScroll: true
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
// Close when focus leaves the menu entirely (e.g. Tab past the footer or
|
|
1159
|
+
// Shift+Tab before the header). Internal moves between the search input,
|
|
1160
|
+
// options, header and footer keep focus inside, so they don't close.
|
|
1161
|
+
const handleMenuBlur = event => {
|
|
1162
|
+
const next = event.relatedTarget;
|
|
1163
|
+
if (next && event.currentTarget.contains(next)) {
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
// Focus fell out of the menu without landing anywhere — a click on
|
|
1167
|
+
// non-focusable page content, or a virtualized row being unmounted under
|
|
1168
|
+
// it. The highlight stops being drawn either way; if it was the row
|
|
1169
|
+
// churning, the reclaim below focuses the card again and brings it back.
|
|
1170
|
+
if (!next) {
|
|
1171
|
+
setMenuHasFocus(false);
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
// Focus moving into ANOTHER floating-ui portal is a nested popup opening
|
|
1175
|
+
// from this menu's own chrome (a Dropdown in the footer, say), not the user
|
|
1176
|
+
// leaving — ClickAway tolerates the click for the same reason, so the blur
|
|
1177
|
+
// has to as well or the parent closes under the child.
|
|
1178
|
+
const ownPortal = event.currentTarget.closest('[data-floating-ui-portal]');
|
|
1179
|
+
const nextPortal = next.closest?.('[data-floating-ui-portal]') ?? null;
|
|
1180
|
+
if (nextPortal && nextPortal !== ownPortal) {
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
// Focus has genuinely gone somewhere else on the page. Stop drawing the
|
|
1184
|
+
// highlight before deciding whether this is a tab-out: moving to the
|
|
1185
|
+
// trigger is not one, but it is still focus leaving the menu.
|
|
1186
|
+
setMenuHasFocus(false);
|
|
1187
|
+
// Focus landing on the trigger that controls this menu is never a tab-out.
|
|
1188
|
+
// For combobox menus it is a deliberate return to the input (Shift+Tab off
|
|
1189
|
+
// a footer control). For roving menus it is the user pressing the trigger
|
|
1190
|
+
// to dismiss — and mousedown moves focus BEFORE the click, so closing here
|
|
1191
|
+
// would let the trigger's own toggle re-open the menu a moment later.
|
|
1192
|
+
// Leaving the menu by keyboard is handled explicitly in handleTab, and
|
|
1193
|
+
// clicking elsewhere is handled by ClickAway, so neither needs this path.
|
|
1194
|
+
if (next.getAttribute?.('aria-controls') === resolvedMenuId) {
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
onTabOut?.();
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
// The card is tabIndex={-1} so it can take focus programmatically, but that
|
|
1201
|
+
// also makes it CLICK-focusable. Without this, clicking its padding, a group
|
|
1202
|
+
// title, the result-count row or its scrollbar pulls focus off the combobox
|
|
1203
|
+
// input (or the roving option) onto the card, leaving the menu open but
|
|
1204
|
+
// keyboard-dead: the trigger's key handler can't fire and Menu's own is off
|
|
1205
|
+
// under externalKeyboardNav. Suppressing the default only for non-control
|
|
1206
|
+
// targets keeps focus where it was; real controls still focus normally.
|
|
1207
|
+
const handleMenuMouseDown = event => {
|
|
1208
|
+
const target = event.target;
|
|
1209
|
+
// A press on a scrollbar reports its scroll container as the target but
|
|
1210
|
+
// lands OUTSIDE that element's client box; preventing the default there
|
|
1211
|
+
// would cancel the drag. Test the target itself, not just the card — under
|
|
1212
|
+
// virtualization the scroll container is react-window's own wrapper, so a
|
|
1213
|
+
// card-only check would leave that scrollbar unguarded.
|
|
1214
|
+
if (target) {
|
|
1215
|
+
const scrolls = target.scrollHeight > target.clientHeight || target.scrollWidth > target.clientWidth;
|
|
1216
|
+
const rect = target.getBoundingClientRect();
|
|
1217
|
+
if (scrolls && (event.clientX > rect.left + target.clientWidth || event.clientY > rect.top + target.clientHeight)) {
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
if (!target?.closest('a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])')) {
|
|
1222
|
+
event.preventDefault();
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
336
1225
|
const hasHeader = header ? true : false;
|
|
337
1226
|
const hasFooter = footer ? true : false;
|
|
1227
|
+
|
|
1228
|
+
// The card used to collapse via `:empty` when it rendered nothing at all.
|
|
1229
|
+
// It now always contains the (possibly empty) listbox element so the
|
|
1230
|
+
// trigger's aria-controls resolves, which means `:empty` can never match —
|
|
1231
|
+
// so decide emptiness here instead and keep the collapsed styling.
|
|
1232
|
+
// Mirror RenderOption's own branch conditions rather than using the FLATTENED
|
|
1233
|
+
// option count: `groupTitleOptions: [{groupTitle: 'Recent', options: []}]`
|
|
1234
|
+
// still renders a visible group title, so collapsing the card there would
|
|
1235
|
+
// strip its border and padding while leaving that title on screen.
|
|
1236
|
+
const isCardEmpty = !hasHeader && !hasFooter && !allowSearch && !props.options?.length && !props.composeOptions?.length && !props.groupTitleOptions?.length;
|
|
338
1237
|
const firstOption = options?.[0] ?? composeOptions?.[0]?.[0] ?? groupTitleOptions?.[0]?.options?.[0];
|
|
339
1238
|
const firstRowHeight = (groupTitleOptions?.length ? makeGroupTitleHeightResolver(groupTitleHeight)(groupTitleOptions[0].groupTitle, 0) : 0) + (firstOption ? makeItemHeightResolver(itemHeight, size)(firstOption, 0) : 0);
|
|
340
1239
|
const minCardHeight = getMenuChromeHeight({
|
|
@@ -346,6 +1245,24 @@ const Menu = exports.Menu = /*#__PURE__*/React.forwardRef((props, ref) => {
|
|
|
346
1245
|
}) + firstRowHeight;
|
|
347
1246
|
const cardMaxHeight = menuHeight ? Math.max(menuHeight, minCardHeight) : null;
|
|
348
1247
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
|
|
1248
|
+
// -1 so it is never in the tab order but CAN receive programmatic focus
|
|
1249
|
+
// as the open-time fallback when no option is focusable (all disabled).
|
|
1250
|
+
tabIndex: enableA11y ? -1 : undefined,
|
|
1251
|
+
onKeyDown: enableA11y ? event => {
|
|
1252
|
+
// Any key press means the keyboard is driving again, so a stale
|
|
1253
|
+
// pointer highlight must stop suppressing the focus ring —
|
|
1254
|
+
// otherwise arrowing back onto the row the mouse last touched
|
|
1255
|
+
// would show no focus.
|
|
1256
|
+
setPointerActiveKey(null);
|
|
1257
|
+
// A key press means the keyboard is driving, so the opening
|
|
1258
|
+
// highlight is now wanted even if the key did not move the
|
|
1259
|
+
// active option (Home on the first option, say).
|
|
1260
|
+
setSuppressInitialHighlight(false);
|
|
1261
|
+
onKeyDown(event);
|
|
1262
|
+
} : undefined,
|
|
1263
|
+
onMouseDown: enableA11y ? handleMenuMouseDown : undefined,
|
|
1264
|
+
onFocus: enableA11y ? () => setMenuHasFocus(true) : undefined,
|
|
1265
|
+
onBlur: enableA11y ? handleMenuBlur : undefined,
|
|
349
1266
|
className: (0, _classify.classify)(_MenuModule.default.menuCard, {
|
|
350
1267
|
[_MenuModule.default.fluid]: isFluid,
|
|
351
1268
|
[_MenuModule.default.medium]: size === 'medium',
|
|
@@ -356,26 +1273,46 @@ const Menu = exports.Menu = /*#__PURE__*/React.forwardRef((props, ref) => {
|
|
|
356
1273
|
[_MenuModule.default.smallWithHeader]: size === 'small' && hasHeader && !hasFooter,
|
|
357
1274
|
[_MenuModule.default.smallWithFooter]: size === 'small' && !hasHeader && hasFooter,
|
|
358
1275
|
[_MenuModule.default.smallWithHeaderAndFooter]: size === 'small' && hasFooter && hasHeader,
|
|
1276
|
+
// Scopes the CSS the opt-in changes — the pinned header/footer
|
|
1277
|
+
// layout and the card's overflow — so nothing restyles a menu that
|
|
1278
|
+
// has not opted in.
|
|
1279
|
+
[_MenuModule.default.menuCardA11y]: enableA11y,
|
|
359
1280
|
[_MenuModule.default.menuCardTopPaddingZero]: header,
|
|
360
|
-
[_MenuModule.default.menuCardBottomPaddingZero]: footer
|
|
1281
|
+
[_MenuModule.default.menuCardBottomPaddingZero]: footer,
|
|
1282
|
+
// Whatever scrolls the options, the card must not scroll too —
|
|
1283
|
+
// but only the opt-in moves the scroll onto the listbox, so without
|
|
1284
|
+
// it the card stays the scroll box it has always been.
|
|
1285
|
+
[_MenuModule.default.menuCardNoScroll]: enableA11y && !isVirtualized,
|
|
1286
|
+
[_MenuModule.default.menuCardEmpty]: enableA11y && isCardEmpty
|
|
361
1287
|
}, classNames?.wrapper),
|
|
362
1288
|
style: {
|
|
363
1289
|
width,
|
|
364
1290
|
maxHeight: cardMaxHeight ? cardMaxHeight + 'px' : ''
|
|
365
1291
|
},
|
|
366
|
-
ref: ref,
|
|
1292
|
+
ref: (0, _mergeRefs.mergeRefs)([ref, containerRef]),
|
|
367
1293
|
"data-testid": (0, _qa.generateTestId)({
|
|
368
1294
|
base: testId,
|
|
369
1295
|
slot: 'wrapper'
|
|
370
1296
|
}),
|
|
371
1297
|
children: [hasHeader && /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
|
|
372
1298
|
className: (0, _classify.classify)(_MenuModule.default.menuHeader, classNames?.header),
|
|
1299
|
+
"data-menu-region": enableA11y ? 'header' : undefined,
|
|
373
1300
|
"data-testid": (0, _qa.generateTestId)({
|
|
374
1301
|
base: testId,
|
|
375
1302
|
slot: 'header'
|
|
376
1303
|
}),
|
|
377
1304
|
children: header
|
|
378
|
-
}), allowSearch && /*#__PURE__*/(0, _jsxRuntime.jsx)(_SearchInput.SearchInput
|
|
1305
|
+
}), allowSearch && /*#__PURE__*/(0, _jsxRuntime.jsx)(_SearchInput.SearchInput
|
|
1306
|
+
// A filter field, NOT a combobox: an always-expanded combobox is
|
|
1307
|
+
// meaningless, and a second combobox pointing aria-controls at the
|
|
1308
|
+
// same listbox as the trigger that opened the popup has no defined
|
|
1309
|
+
// mapping. `searchbox` describes what it actually is; the trigger
|
|
1310
|
+
// outside the menu remains the single combobox owning the listbox.
|
|
1311
|
+
, {
|
|
1312
|
+
role: enableA11y ? 'searchbox' : undefined,
|
|
1313
|
+
"data-menu-search": enableA11y ? 'true' : undefined,
|
|
1314
|
+
"aria-controls": enableA11y ? resolvedMenuId : undefined,
|
|
1315
|
+
"aria-activedescendant": enableA11y ? activeDescendantId : undefined,
|
|
379
1316
|
value: searchText,
|
|
380
1317
|
onChange: e => setSearchText(e.target.value),
|
|
381
1318
|
onClear: () => setSearchText(''),
|
|
@@ -385,12 +1322,40 @@ const Menu = exports.Menu = /*#__PURE__*/React.forwardRef((props, ref) => {
|
|
|
385
1322
|
base: testId,
|
|
386
1323
|
slot: 'search'
|
|
387
1324
|
})
|
|
388
|
-
}),
|
|
1325
|
+
}), enableA11y && (hasHeader || hasFooter) && isVirtualized ?
|
|
1326
|
+
/*#__PURE__*/
|
|
1327
|
+
// Only a virtualized list needs this wrapper: it is sized by
|
|
1328
|
+
// `menuHeight` rather than by the flex box, so it needs something to
|
|
1329
|
+
// sit between the pinned bars. A plain option list scrolls on the
|
|
1330
|
+
// listbox itself.
|
|
1331
|
+
(0, _jsxRuntime.jsx)("div", {
|
|
1332
|
+
className: _MenuModule.default.optionsScroll,
|
|
1333
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(RenderOption, {
|
|
1334
|
+
...props,
|
|
1335
|
+
searchText: searchText,
|
|
1336
|
+
testId: testId,
|
|
1337
|
+
menuId: resolvedMenuId,
|
|
1338
|
+
activeOptionKey: activeOptionKey,
|
|
1339
|
+
onPointerActivate: handleOptionPointerActivate,
|
|
1340
|
+
pointerActiveKey: pointerActiveKey,
|
|
1341
|
+
suppressInitialHighlight: suppressInitialHighlight,
|
|
1342
|
+
menuHasFocus: menuHasFocus,
|
|
1343
|
+
listRef: setVirtualList
|
|
1344
|
+
})
|
|
1345
|
+
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(RenderOption, {
|
|
389
1346
|
...props,
|
|
390
1347
|
searchText: searchText,
|
|
391
|
-
testId: testId
|
|
1348
|
+
testId: testId,
|
|
1349
|
+
menuId: resolvedMenuId,
|
|
1350
|
+
activeOptionKey: activeOptionKey,
|
|
1351
|
+
onPointerActivate: handleOptionPointerActivate,
|
|
1352
|
+
pointerActiveKey: pointerActiveKey,
|
|
1353
|
+
suppressInitialHighlight: suppressInitialHighlight,
|
|
1354
|
+
menuHasFocus: menuHasFocus,
|
|
1355
|
+
listRef: setVirtualList
|
|
392
1356
|
}), hasFooter && /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
|
|
393
1357
|
className: (0, _classify.classify)(_MenuModule.default.menuFooter, classNames?.footer),
|
|
1358
|
+
"data-menu-region": enableA11y ? 'footer' : undefined,
|
|
394
1359
|
"data-testid": (0, _qa.generateTestId)({
|
|
395
1360
|
base: testId,
|
|
396
1361
|
slot: 'footer'
|