@launchpad-ui/menu 0.13.56 → 0.13.58

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