@launchpad-ui/menu 0.16.17 → 0.16.19

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/dist/index.es.js CHANGED
@@ -1,368 +1,416 @@
1
1
  import './style.css';
2
- import { cx } from "classix";
3
- import { Children, cloneElement, forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
2
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
+ import { forwardRef, cloneElement, useCallback, useMemo, Children, useRef, useId, useState, useEffect } from "react";
4
4
  import { useFocusManager } from "react-aria/FocusScope";
5
5
  import { useVirtual } from "react-virtual";
6
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ import { cx } from "classix";
7
7
  import { useSeparator } from "react-aria/useSeparator";
8
- import { Tooltip } from "@launchpad-ui/tooltip";
9
- import { Slot } from "@radix-ui/react-slot";
10
8
  import { FocusRing } from "react-aria/FocusRing";
9
+ import { Slot } from "@radix-ui/react-slot";
10
+ import { Tooltip } from "@launchpad-ui/tooltip";
11
11
  import { TextField } from "@launchpad-ui/form";
12
12
  const Menu$1 = "C73R2W_Menu";
13
- var Menu_module_default = {
14
- "has-focus": "C73R2W_has-focus",
15
- "is-highlighted": "C73R2W_is-highlighted",
16
- Menu: Menu$1,
17
- "Menu--isVirtual": "C73R2W_Menu--isVirtual",
18
- "Menu-divider": "C73R2W_Menu-divider",
19
- "Menu-item": "C73R2W_Menu-item",
20
- "Menu-item--header": "C73R2W_Menu-item--header",
21
- "Menu-item--nested": "C73R2W_Menu-item--nested",
22
- "Menu-item-icon": "C73R2W_Menu-item-icon",
23
- "Menu-item-list": "C73R2W_Menu-item-list",
24
- "Menu-search": "C73R2W_Menu-search",
25
- "Menu-search-hidden-placeholder": "C73R2W_Menu-search-hidden-placeholder",
26
- "MenuSize--lg": "C73R2W_MenuSize--lg",
27
- "MenuSize--md": "C73R2W_MenuSize--md",
28
- "MenuSize--sm": "C73R2W_MenuSize--sm",
29
- "MenuSize--xl": "C73R2W_MenuSize--xl",
30
- "VirtualMenu-item": "C73R2W_VirtualMenu-item",
31
- "VirtualMenu-item-list": "C73R2W_VirtualMenu-item-list"
13
+ const styles = {
14
+ "has-focus": "C73R2W_has-focus",
15
+ "is-highlighted": "C73R2W_is-highlighted",
16
+ Menu: Menu$1,
17
+ "Menu--isVirtual": "C73R2W_Menu--isVirtual",
18
+ "Menu-divider": "C73R2W_Menu-divider",
19
+ "Menu-item": "C73R2W_Menu-item",
20
+ "Menu-item--header": "C73R2W_Menu-item--header",
21
+ "Menu-item--nested": "C73R2W_Menu-item--nested",
22
+ "Menu-item-icon": "C73R2W_Menu-item-icon",
23
+ "Menu-item-list": "C73R2W_Menu-item-list",
24
+ "Menu-search": "C73R2W_Menu-search",
25
+ "Menu-search-hidden-placeholder": "C73R2W_Menu-search-hidden-placeholder",
26
+ "MenuSize--lg": "C73R2W_MenuSize--lg",
27
+ "MenuSize--md": "C73R2W_MenuSize--md",
28
+ "MenuSize--sm": "C73R2W_MenuSize--sm",
29
+ "MenuSize--xl": "C73R2W_MenuSize--xl",
30
+ "VirtualMenu-item": "C73R2W_VirtualMenu-item",
31
+ "VirtualMenu-item-list": "C73R2W_VirtualMenu-item-list"
32
32
  };
33
- var MenuBase = /* @__PURE__ */ forwardRef(({ children, size, isVirtual, ...props }, ref) => {
34
- const classes = cx(Menu_module_default.Menu, isVirtual && Menu_module_default["Menu--isVirtual"], size && Menu_module_default[`MenuSize--${size}`]);
35
- return /* @__PURE__ */ jsx("div", {
36
- ...props,
37
- role: "menu",
38
- className: classes,
39
- ref,
40
- children
41
- });
33
+ const MenuBase = /* @__PURE__ */ forwardRef(({ children, size, isVirtual, ...props }, ref) => {
34
+ const classes = cx(styles.Menu, isVirtual && styles["Menu--isVirtual"], size && styles[`MenuSize--${size}`]);
35
+ return /* @__PURE__ */ jsx("div", { ...props, role: "menu", className: classes, ref, children });
42
36
  });
43
37
  MenuBase.displayName = "MenuBase";
44
- var MenuDivider = ({ elementType = "div", orientation, innerRef, "data-test-id": testId = "menu-divider" }) => {
45
- const { separatorProps } = useSeparator({
46
- orientation,
47
- elementType
48
- });
49
- return /* @__PURE__ */ jsx("div", {
50
- ...separatorProps,
51
- "data-test-id": testId,
52
- ref: innerRef,
53
- className: Menu_module_default["Menu-divider"]
54
- });
38
+ const MenuDivider = ({
39
+ elementType = "div",
40
+ orientation,
41
+ innerRef,
42
+ "data-test-id": testId = "menu-divider"
43
+ }) => {
44
+ const { separatorProps } = useSeparator({
45
+ orientation,
46
+ elementType
47
+ });
48
+ return /* @__PURE__ */ jsx("div", { ...separatorProps, "data-test-id": testId, ref: innerRef, className: styles["Menu-divider"] });
55
49
  };
56
- var defaultElement = "button";
57
- var MenuItem = ({ ...props }) => {
58
- const { component, children, isHighlighted, icon, nested, groupHeader, item, disabled, className, tooltip, role = "menuitem", tooltipPlacement, onKeyDown, tooltipOptions, asChild, "data-test-id": testId = "menu-item", ...rest } = props;
59
- const Component = component || (asChild ? Slot : defaultElement);
60
- const renderIcon = icon && /* @__PURE__ */ cloneElement(icon, { size: "small" });
61
- const renderedItem = /* @__PURE__ */ jsx(FocusRing, {
62
- focusRingClass: Menu_module_default["has-focus"],
63
- children: /* @__PURE__ */ jsx(Component, {
64
- ...rest,
65
- disabled,
66
- "aria-disabled": disabled ? disabled : void 0,
67
- className: cx(Menu_module_default["Menu-item"], className, isHighlighted && Menu_module_default["is-highlighted"], nested && Menu_module_default["Menu-item--nested"], groupHeader && Menu_module_default["Menu-item--header"]),
68
- "data-test-id": testId,
69
- role,
70
- onKeyDown,
71
- children: asChild ? children : /* @__PURE__ */ jsxs(Fragment, { children: [icon && /* @__PURE__ */ jsx("span", {
72
- className: Menu_module_default["Menu-item-icon"],
73
- children: renderIcon
74
- }), children] })
75
- })
76
- });
77
- if (tooltip) return /* @__PURE__ */ jsx(Tooltip, {
78
- content: tooltip,
79
- rootElementStyle: { display: "block" },
80
- allowBoundaryElementOverflow: true,
81
- placement: tooltipPlacement ? tooltipPlacement : "bottom",
82
- ...tooltipOptions || {},
83
- children: renderedItem
84
- });
85
- return renderedItem;
50
+ const defaultElement = "button";
51
+ const MenuItem = ({ ...props }) => {
52
+ const {
53
+ // TODO: remove component prop once we migrate over to asChild format
54
+ component,
55
+ children,
56
+ isHighlighted,
57
+ icon,
58
+ nested,
59
+ groupHeader,
60
+ // biome-ignore lint/correctness/noUnusedVariables: ignore
61
+ item,
62
+ disabled,
63
+ className,
64
+ tooltip,
65
+ role = "menuitem",
66
+ tooltipPlacement,
67
+ onKeyDown,
68
+ tooltipOptions,
69
+ asChild,
70
+ "data-test-id": testId = "menu-item",
71
+ ...rest
72
+ } = props;
73
+ const Component = component || (asChild ? Slot : defaultElement);
74
+ const renderIcon = icon && /* @__PURE__ */ cloneElement(icon, { size: "small" });
75
+ const renderedItem = /* @__PURE__ */ jsx(FocusRing, { focusRingClass: styles["has-focus"], children: /* @__PURE__ */ jsx(
76
+ Component,
77
+ {
78
+ ...rest,
79
+ disabled,
80
+ "aria-disabled": disabled ? disabled : void 0,
81
+ className: cx(
82
+ styles["Menu-item"],
83
+ className,
84
+ isHighlighted && styles["is-highlighted"],
85
+ nested && styles["Menu-item--nested"],
86
+ groupHeader && styles["Menu-item--header"]
87
+ ),
88
+ "data-test-id": testId,
89
+ role,
90
+ onKeyDown,
91
+ children: asChild ? children : /* @__PURE__ */ jsxs(Fragment, { children: [
92
+ icon && /* @__PURE__ */ jsx("span", { className: styles["Menu-item-icon"], children: renderIcon }),
93
+ children
94
+ ] })
95
+ }
96
+ ) });
97
+ if (tooltip) {
98
+ return /* @__PURE__ */ jsx(
99
+ Tooltip,
100
+ {
101
+ content: tooltip,
102
+ rootElementStyle: { display: "block" },
103
+ allowBoundaryElementOverflow: true,
104
+ placement: tooltipPlacement ? tooltipPlacement : "bottom",
105
+ ...tooltipOptions || {},
106
+ children: renderedItem
107
+ }
108
+ );
109
+ }
110
+ return renderedItem;
86
111
  };
87
- var MenuItemList = /* @__PURE__ */ forwardRef(({ children, ...rest }, ref) => /* @__PURE__ */ jsx("div", {
88
- ...rest,
89
- ref,
90
- "data-test-id": "menu-item-list",
91
- className: Menu_module_default["Menu-item-list"],
92
- children
93
- }));
112
+ const MenuItemList = /* @__PURE__ */ forwardRef(({ children, ...rest }, ref) => /* @__PURE__ */ jsx("div", { ...rest, ref, "data-test-id": "menu-item-list", className: styles["Menu-item-list"], children }));
94
113
  MenuItemList.displayName = "MenuItemList";
95
- var MenuSearch = /* @__PURE__ */ forwardRef((props, ref) => {
96
- const { ariaLabel, placeholder, id, "data-test-id": testId = "menu-search", ...finalProps } = props;
97
- return /* @__PURE__ */ jsx("div", {
98
- className: Menu_module_default["Menu-search"],
99
- children: /* @__PURE__ */ jsx(TextField, {
100
- ...finalProps,
101
- ref,
102
- className: Menu_module_default["Menu-search-input"],
103
- tiny: true,
104
- id,
105
- type: "search",
106
- "data-test-id": testId,
107
- autoComplete: "off",
108
- placeholder,
109
- "aria-label": ariaLabel || "Search"
110
- })
111
- });
114
+ const MenuSearch = /* @__PURE__ */ forwardRef((props, ref) => {
115
+ const { ariaLabel, placeholder, id, "data-test-id": testId = "menu-search", ...finalProps } = props;
116
+ return /* @__PURE__ */ jsx("div", { className: styles["Menu-search"], children: /* @__PURE__ */ jsx(
117
+ TextField,
118
+ {
119
+ ...finalProps,
120
+ ref,
121
+ className: styles["Menu-search-input"],
122
+ tiny: true,
123
+ id,
124
+ type: "search",
125
+ "data-test-id": testId,
126
+ autoComplete: "off",
127
+ placeholder,
128
+ "aria-label": ariaLabel || "Search"
129
+ }
130
+ ) });
112
131
  });
113
132
  MenuSearch.displayName = "MenuSearch";
114
- var createItemId = (index, id) => `${id}-item-${index}`;
115
- var getNodeForIndex = (index, menuId) => index === null ? index : document.getElementById(createItemId(index, menuId));
116
- var handleKeyboardInteractions = (event, keyHandlers) => {
117
- const ops = {
118
- ArrowUp: keyHandlers.handleUp,
119
- ArrowDown: keyHandlers.handleDown,
120
- Enter: keyHandlers.handleEnter
121
- };
122
- if (ops[event.key]) {
123
- event.preventDefault();
124
- ops[event.key]?.call(globalThis, event);
125
- }
133
+ const createItemId = (index, id) => `${id}-item-${index}`;
134
+ const getNodeForIndex = (index, menuId) => index === null ? index : document.getElementById(createItemId(index, menuId));
135
+ const handleKeyboardInteractions = (event, keyHandlers) => {
136
+ const ops = {
137
+ ArrowUp: keyHandlers.handleUp,
138
+ ArrowDown: keyHandlers.handleDown,
139
+ Enter: keyHandlers.handleEnter
140
+ // biome-ignore lint/suspicious/noConfusingVoidType: ignore
141
+ };
142
+ if (ops[event.key]) {
143
+ event.preventDefault();
144
+ ops[event.key]?.call(globalThis, event);
145
+ }
146
+ };
147
+ const chainEventHandlers = (...handlers) => (event) => {
148
+ for (const h of handlers) {
149
+ typeof h === "function" && h(event);
150
+ }
126
151
  };
127
- var chainEventHandlers = (...handlers) => (event) => {
128
- for (const h of handlers) typeof h === "function" && h(event);
152
+ const Menu = (props) => {
153
+ const {
154
+ children,
155
+ menuItemClassName,
156
+ onSelect,
157
+ enableVirtualization,
158
+ itemHeight,
159
+ size,
160
+ overscan = 1,
161
+ "data-test-id": testId = "menu"
162
+ } = props;
163
+ const focusManager = useFocusManager();
164
+ const handleArrowDown = useCallback(() => {
165
+ focusManager?.focusNext({ wrap: true });
166
+ }, [focusManager]);
167
+ const handleArrowUp = useCallback(() => {
168
+ focusManager?.focusPrevious({ wrap: true });
169
+ }, [focusManager]);
170
+ const reduceItems = useMemo(() => {
171
+ const childrenProps = Children.toArray(children);
172
+ if (enableVirtualization) {
173
+ let searchElem = null;
174
+ let elements = [];
175
+ for (const child of childrenProps) {
176
+ switch (child.type) {
177
+ case MenuSearch:
178
+ searchElem = child;
179
+ break;
180
+ case MenuItem:
181
+ case MenuDivider:
182
+ elements = elements.concat(child);
183
+ break;
184
+ }
185
+ }
186
+ return { items: elements, searchElement: searchElem };
187
+ }
188
+ return childrenProps.reduce(
189
+ ({ items, searchElement }, childElement) => {
190
+ const child = childElement;
191
+ switch (child.type) {
192
+ case MenuSearch:
193
+ return {
194
+ items,
195
+ searchElement: /* @__PURE__ */ cloneElement(child, {
196
+ onKeyDown: (e) => handleKeyboardInteractions(e, {
197
+ handleDown: handleArrowDown,
198
+ handleUp: handleArrowUp
199
+ })
200
+ })
201
+ };
202
+ case MenuItem:
203
+ return {
204
+ items: items.concat(
205
+ child.props.disabled ? /* @__PURE__ */ cloneElement(child, {
206
+ className: cx(child.props.className, menuItemClassName),
207
+ onClick: () => void 0,
208
+ onKeyDown: () => void 0,
209
+ tabIndex: -1,
210
+ disabled: true
211
+ }) : /* @__PURE__ */ cloneElement(child, {
212
+ className: cx(child.props.className, menuItemClassName),
213
+ item: child.props.item ?? items.length,
214
+ // set focus on the first menu item if there is no search input, and set in the tab order
215
+ onClick: chainEventHandlers(child.props.onClick, () => {
216
+ onSelect?.(child.props.item ?? items.length);
217
+ }),
218
+ onKeyDown: (e) => handleKeyboardInteractions(e, {
219
+ handleDown: handleArrowDown,
220
+ handleUp: handleArrowUp
221
+ })
222
+ })
223
+ ),
224
+ searchElement
225
+ };
226
+ case MenuDivider:
227
+ return { items: items.concat(child), searchElement };
228
+ default:
229
+ return { items, searchElement };
230
+ }
231
+ },
232
+ { items: [], searchElement: null }
233
+ );
234
+ }, [children, enableVirtualization, menuItemClassName, handleArrowDown, handleArrowUp, onSelect]);
235
+ if (enableVirtualization) {
236
+ return /* @__PURE__ */ jsx(MenuBase, { "data-test-id": testId, isVirtual: true, size, children: /* @__PURE__ */ jsx(
237
+ ItemVirtualizer,
238
+ {
239
+ items: Children.toArray(reduceItems.items),
240
+ searchElement: reduceItems.searchElement,
241
+ overscan,
242
+ menuItemClassName,
243
+ onSelect,
244
+ itemHeight,
245
+ focusManager
246
+ }
247
+ ) });
248
+ }
249
+ return /* @__PURE__ */ jsxs(MenuBase, { "data-test-id": testId, size, children: [
250
+ reduceItems.searchElement,
251
+ /* @__PURE__ */ jsx(MenuItemList, { role: "presentation", children: reduceItems.items })
252
+ ] });
129
253
  };
130
- var Menu = (props) => {
131
- const { children, menuItemClassName, onSelect, enableVirtualization, itemHeight, size, overscan = 1, "data-test-id": testId = "menu" } = props;
132
- const focusManager = useFocusManager();
133
- const handleArrowDown = useCallback(() => {
134
- focusManager?.focusNext({ wrap: true });
135
- }, [focusManager]);
136
- const handleArrowUp = useCallback(() => {
137
- focusManager?.focusPrevious({ wrap: true });
138
- }, [focusManager]);
139
- const reduceItems = useMemo(() => {
140
- const childrenProps = Children.toArray(children);
141
- if (enableVirtualization) {
142
- let searchElem = null;
143
- let elements = [];
144
- for (const child of childrenProps) switch (child.type) {
145
- case MenuSearch:
146
- searchElem = child;
147
- break;
148
- case MenuItem:
149
- case MenuDivider:
150
- elements = elements.concat(child);
151
- break;
152
- default: break;
153
- }
154
- return {
155
- items: elements,
156
- searchElement: searchElem
157
- };
158
- }
159
- return childrenProps.reduce(({ items, searchElement }, child) => {
160
- switch (child.type) {
161
- case MenuSearch: return {
162
- items,
163
- searchElement: /* @__PURE__ */ cloneElement(child, { onKeyDown: (e) => handleKeyboardInteractions(e, {
164
- handleDown: handleArrowDown,
165
- handleUp: handleArrowUp
166
- }) })
167
- };
168
- case MenuItem: return {
169
- items: items.concat(child.props.disabled ? /* @__PURE__ */ cloneElement(child, {
170
- className: cx(child.props.className, menuItemClassName),
171
- onClick: () => void 0,
172
- onKeyDown: () => void 0,
173
- tabIndex: -1,
174
- disabled: true
175
- }) : /* @__PURE__ */ cloneElement(child, {
176
- className: cx(child.props.className, menuItemClassName),
177
- item: child.props.item ?? items.length,
178
- onClick: chainEventHandlers(child.props.onClick, () => {
179
- onSelect?.(child.props.item ?? items.length);
180
- }),
181
- onKeyDown: (e) => handleKeyboardInteractions(e, {
182
- handleDown: handleArrowDown,
183
- handleUp: handleArrowUp
184
- })
185
- })),
186
- searchElement
187
- };
188
- case MenuDivider: return {
189
- items: items.concat(child),
190
- searchElement
191
- };
192
- default: return {
193
- items,
194
- searchElement
195
- };
196
- }
197
- }, {
198
- items: [],
199
- searchElement: null
200
- });
201
- }, [
202
- children,
203
- enableVirtualization,
204
- menuItemClassName,
205
- handleArrowDown,
206
- handleArrowUp,
207
- onSelect
208
- ]);
209
- if (enableVirtualization) return /* @__PURE__ */ jsx(MenuBase, {
210
- "data-test-id": testId,
211
- isVirtual: true,
212
- size,
213
- children: /* @__PURE__ */ jsx(ItemVirtualizer, {
214
- items: Children.toArray(reduceItems.items),
215
- searchElement: reduceItems.searchElement,
216
- overscan,
217
- menuItemClassName,
218
- onSelect,
219
- itemHeight,
220
- focusManager
221
- })
222
- });
223
- return /* @__PURE__ */ jsxs(MenuBase, {
224
- "data-test-id": testId,
225
- size,
226
- children: [reduceItems.searchElement, /* @__PURE__ */ jsx(MenuItemList, {
227
- role: "presentation",
228
- children: reduceItems.items
229
- })]
230
- });
254
+ const ItemVirtualizer = (props) => {
255
+ const { overscan, searchElement, itemHeight = 31.5, menuItemClassName, items, focusManager, onSelect } = props;
256
+ const menuId = useRef(`menu-ctrl-${useId()}`);
257
+ const focusedItemIndex = useRef(null);
258
+ const parentRef = useRef(null);
259
+ const searchRef = useRef(null);
260
+ const [nextFocusValue, setNextFocusValue] = useState(null);
261
+ const hasSearch = !!searchElement;
262
+ const lastVirtualItemIndex = items ? items.length - 1 : 0;
263
+ const rowVirtualizer = useVirtual({
264
+ size: items !== null ? items.length : 0,
265
+ parentRef,
266
+ estimateSize: useCallback(() => itemHeight, [itemHeight]),
267
+ overscan
268
+ });
269
+ const focusSearchBar = useCallback(() => {
270
+ rowVirtualizer.scrollToIndex(0);
271
+ searchRef.current?.focus?.();
272
+ }, [rowVirtualizer]);
273
+ const focusMenuItem = useCallback(
274
+ (index) => {
275
+ rowVirtualizer.scrollToIndex(index);
276
+ setNextFocusValue(index);
277
+ },
278
+ [rowVirtualizer]
279
+ );
280
+ const handleKeyboardFocusInteraction = useCallback(
281
+ (direction) => {
282
+ if (focusedItemIndex.current === null || focusedItemIndex.current === void 0) {
283
+ return;
284
+ }
285
+ const nextIndex = direction === "next" ? focusedItemIndex.current + 1 : focusedItemIndex.current - 1;
286
+ const shouldWrap = direction === "next" && focusedItemIndex.current === lastVirtualItemIndex || direction === "previous" && focusedItemIndex.current === 0;
287
+ if (shouldWrap) {
288
+ if (hasSearch) {
289
+ focusSearchBar();
290
+ } else {
291
+ focusMenuItem(direction === "next" ? 0 : lastVirtualItemIndex);
292
+ }
293
+ return;
294
+ }
295
+ switch (direction) {
296
+ case "next":
297
+ rowVirtualizer.scrollToIndex(nextIndex);
298
+ focusManager?.focusNext();
299
+ break;
300
+ case "previous":
301
+ rowVirtualizer.scrollToIndex(nextIndex);
302
+ focusManager?.focusPrevious();
303
+ break;
304
+ }
305
+ },
306
+ [focusManager, focusMenuItem, focusSearchBar, hasSearch, lastVirtualItemIndex, rowVirtualizer]
307
+ );
308
+ const getItemProps = useCallback(
309
+ (itemElem, index) => {
310
+ const childProps = itemElem.props;
311
+ switch (itemElem.type) {
312
+ case MenuItem:
313
+ return {
314
+ className: cx(childProps.className, menuItemClassName),
315
+ // set focus on the first menu item if there is no search input, and set in the tab order
316
+ onKeyDown: childProps.disabled ? () => void 0 : (e) => handleKeyboardFocusKeydown(e, {
317
+ handleFocusBackward: handleKeyboardFocusInteraction,
318
+ handleFocusForward: handleKeyboardFocusInteraction
319
+ }),
320
+ onFocus: chainEventHandlers(childProps.onFocus, () => {
321
+ focusedItemIndex.current = index;
322
+ }),
323
+ id: createItemId(index, menuId.current),
324
+ onBlur: chainEventHandlers(childProps.onBlur, () => {
325
+ focusedItemIndex.current = null;
326
+ }),
327
+ onClick: childProps.disabled ? () => void 0 : chainEventHandlers(childProps.onClick, () => {
328
+ onSelect?.(childProps.item);
329
+ })
330
+ };
331
+ default:
332
+ return {};
333
+ }
334
+ },
335
+ [handleKeyboardFocusInteraction, menuItemClassName, onSelect]
336
+ );
337
+ useEffect(() => {
338
+ if (nextFocusValue !== null) {
339
+ requestAnimationFrame(() => {
340
+ const element = getNodeForIndex(nextFocusValue, menuId.current);
341
+ element?.focus();
342
+ });
343
+ setNextFocusValue(null);
344
+ }
345
+ }, [nextFocusValue]);
346
+ const handleKeyboardFocusKeydown = (e, callbacks) => {
347
+ const keyOps = ["Tab", "ArrowUp", "ArrowDown"];
348
+ if (keyOps.includes(e.key)) {
349
+ e.preventDefault();
350
+ e.stopPropagation();
351
+ if (e.key === "Tab" && e.shiftKey || e.key === "ArrowUp") {
352
+ callbacks.handleFocusBackward?.("previous");
353
+ } else if (e.key === "ArrowDown" || e.key === "Tab") {
354
+ callbacks.handleFocusForward?.("next");
355
+ }
356
+ }
357
+ };
358
+ const renderSearch = useMemo(
359
+ () => searchElement ? /* @__PURE__ */ cloneElement(
360
+ searchElement,
361
+ {
362
+ onKeyDown: (e) => handleKeyboardFocusKeydown(e, {
363
+ handleFocusBackward: () => focusMenuItem(lastVirtualItemIndex),
364
+ handleFocusForward: () => focusMenuItem(0)
365
+ }),
366
+ ref: searchRef
367
+ }
368
+ ) : null,
369
+ [searchElement, lastVirtualItemIndex, focusMenuItem]
370
+ );
371
+ const renderItems = useMemo(
372
+ () => rowVirtualizer.virtualItems.map((virtualRow) => {
373
+ if (!items) {
374
+ return null;
375
+ }
376
+ const elem = items[virtualRow.index];
377
+ return /* @__PURE__ */ jsx(
378
+ "div",
379
+ {
380
+ ref: virtualRow.measureRef,
381
+ role: "presentation",
382
+ className: styles["VirtualMenu-item"],
383
+ style: {
384
+ transform: `translateY(${virtualRow.start}px)`
385
+ },
386
+ children: /* @__PURE__ */ cloneElement(elem, getItemProps(elem, virtualRow.index))
387
+ },
388
+ virtualRow.index
389
+ );
390
+ }),
391
+ [rowVirtualizer.virtualItems, items, getItemProps]
392
+ );
393
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
394
+ renderSearch,
395
+ /* @__PURE__ */ jsx(MenuItemList, { ref: parentRef, role: "presentation", children: /* @__PURE__ */ jsx(
396
+ "div",
397
+ {
398
+ role: "presentation",
399
+ className: styles["VirtualMenu-item-list"],
400
+ style: {
401
+ height: `${rowVirtualizer.totalSize}px`
402
+ },
403
+ children: renderItems
404
+ }
405
+ ) })
406
+ ] });
231
407
  };
232
- var ItemVirtualizer = (props) => {
233
- const { overscan, searchElement, itemHeight = 31.5, menuItemClassName, items, focusManager, onSelect } = props;
234
- const menuId = useRef(`menu-ctrl-${useId()}`);
235
- const focusedItemIndex = useRef(null);
236
- const parentRef = useRef(null);
237
- const searchRef = useRef(null);
238
- const [nextFocusValue, setNextFocusValue] = useState(null);
239
- const hasSearch = !!searchElement;
240
- const lastVirtualItemIndex = items ? items.length - 1 : 0;
241
- const rowVirtualizer = useVirtual({
242
- size: items !== null ? items.length : 0,
243
- parentRef,
244
- estimateSize: useCallback(() => itemHeight, [itemHeight]),
245
- overscan
246
- });
247
- const focusSearchBar = useCallback(() => {
248
- rowVirtualizer.scrollToIndex(0);
249
- searchRef.current?.focus?.();
250
- }, [rowVirtualizer]);
251
- const focusMenuItem = useCallback((index) => {
252
- rowVirtualizer.scrollToIndex(index);
253
- setNextFocusValue(index);
254
- }, [rowVirtualizer]);
255
- const handleKeyboardFocusInteraction = useCallback((direction) => {
256
- if (focusedItemIndex.current === null || focusedItemIndex.current === void 0) return;
257
- const nextIndex = direction === "next" ? focusedItemIndex.current + 1 : focusedItemIndex.current - 1;
258
- if (direction === "next" && focusedItemIndex.current === lastVirtualItemIndex || direction === "previous" && focusedItemIndex.current === 0) {
259
- if (hasSearch) focusSearchBar();
260
- else focusMenuItem(direction === "next" ? 0 : lastVirtualItemIndex);
261
- return;
262
- }
263
- switch (direction) {
264
- case "next":
265
- rowVirtualizer.scrollToIndex(nextIndex);
266
- focusManager?.focusNext();
267
- break;
268
- case "previous":
269
- rowVirtualizer.scrollToIndex(nextIndex);
270
- focusManager?.focusPrevious();
271
- break;
272
- default: break;
273
- }
274
- }, [
275
- focusManager,
276
- focusMenuItem,
277
- focusSearchBar,
278
- hasSearch,
279
- lastVirtualItemIndex,
280
- rowVirtualizer
281
- ]);
282
- const getItemProps = useCallback((itemElem, index) => {
283
- const childProps = itemElem.props;
284
- switch (itemElem.type) {
285
- case MenuItem: return {
286
- className: cx(childProps.className, menuItemClassName),
287
- onKeyDown: childProps.disabled ? () => void 0 : (e) => handleKeyboardFocusKeydown(e, {
288
- handleFocusBackward: handleKeyboardFocusInteraction,
289
- handleFocusForward: handleKeyboardFocusInteraction
290
- }),
291
- onFocus: chainEventHandlers(childProps.onFocus, () => {
292
- focusedItemIndex.current = index;
293
- }),
294
- id: createItemId(index, menuId.current),
295
- onBlur: chainEventHandlers(childProps.onBlur, () => {
296
- focusedItemIndex.current = null;
297
- }),
298
- onClick: childProps.disabled ? () => void 0 : chainEventHandlers(childProps.onClick, () => {
299
- onSelect?.(childProps.item);
300
- })
301
- };
302
- default: return {};
303
- }
304
- }, [
305
- handleKeyboardFocusInteraction,
306
- menuItemClassName,
307
- onSelect
308
- ]);
309
- useEffect(() => {
310
- if (nextFocusValue !== null) {
311
- requestAnimationFrame(() => {
312
- getNodeForIndex(nextFocusValue, menuId.current)?.focus();
313
- });
314
- setNextFocusValue(null);
315
- }
316
- }, [nextFocusValue]);
317
- const handleKeyboardFocusKeydown = (e, callbacks) => {
318
- if ([
319
- "Tab",
320
- "ArrowUp",
321
- "ArrowDown"
322
- ].includes(e.key)) {
323
- e.preventDefault();
324
- e.stopPropagation();
325
- if (e.key === "Tab" && e.shiftKey || e.key === "ArrowUp") callbacks.handleFocusBackward?.("previous");
326
- else if (e.key === "ArrowDown" || e.key === "Tab") callbacks.handleFocusForward?.("next");
327
- }
328
- };
329
- const renderSearch = useMemo(() => searchElement ? /* @__PURE__ */ cloneElement(searchElement, {
330
- onKeyDown: (e) => handleKeyboardFocusKeydown(e, {
331
- handleFocusBackward: () => focusMenuItem(lastVirtualItemIndex),
332
- handleFocusForward: () => focusMenuItem(0)
333
- }),
334
- ref: searchRef
335
- }) : null, [
336
- searchElement,
337
- lastVirtualItemIndex,
338
- focusMenuItem
339
- ]);
340
- const renderItems = useMemo(() => rowVirtualizer.virtualItems.map((virtualRow) => {
341
- if (!items) return null;
342
- const elem = items[virtualRow.index];
343
- return /* @__PURE__ */ jsx("div", {
344
- ref: virtualRow.measureRef,
345
- role: "presentation",
346
- className: Menu_module_default["VirtualMenu-item"],
347
- style: { transform: `translateY(${virtualRow.start}px)` },
348
- children: /* @__PURE__ */ cloneElement(elem, getItemProps(elem, virtualRow.index))
349
- }, virtualRow.index);
350
- }), [
351
- rowVirtualizer.virtualItems,
352
- items,
353
- getItemProps
354
- ]);
355
- return /* @__PURE__ */ jsxs(Fragment, { children: [renderSearch, /* @__PURE__ */ jsx(MenuItemList, {
356
- ref: parentRef,
357
- role: "presentation",
358
- children: /* @__PURE__ */ jsx("div", {
359
- role: "presentation",
360
- className: Menu_module_default["VirtualMenu-item-list"],
361
- style: { height: `${rowVirtualizer.totalSize}px` },
362
- children: renderItems
363
- })
364
- })] });
408
+ export {
409
+ Menu,
410
+ MenuBase,
411
+ MenuDivider,
412
+ MenuItem,
413
+ MenuItemList,
414
+ MenuSearch
365
415
  };
366
- export { Menu, MenuBase, MenuDivider, MenuItem, MenuItemList, MenuSearch };
367
-
368
- //# sourceMappingURL=index.es.js.map
416
+ //# sourceMappingURL=index.es.js.map