@launchpad-ui/menu 0.16.16 → 0.16.18

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