@geotab/zenith 3.10.0 → 3.11.0-beta.1

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.
Files changed (83) hide show
  1. package/README.md +6 -0
  2. package/dist/index.css +8 -1
  3. package/dist/menu/components/controlledMenuList/controlledMenuList.d.ts +27 -0
  4. package/dist/menu/components/controlledMenuList/controlledMenuList.js +123 -0
  5. package/dist/menu/components/createControlledMenuList.d.ts +37 -0
  6. package/dist/menu/components/createControlledMenuList.js +55 -0
  7. package/dist/menu/components/createMenuItem.d.ts +67 -0
  8. package/dist/menu/components/createMenuItem.js +97 -0
  9. package/dist/menu/components/menuButton.js +8 -2
  10. package/dist/menu/components/menuItem.d.ts +1 -2
  11. package/dist/menu/components/menuItem.js +20 -74
  12. package/dist/menu/contexts/usePathContext.d.ts +2 -0
  13. package/dist/menu/contexts/usePathContext.js +9 -0
  14. package/dist/menu/controlledMenu.js +8 -175
  15. package/dist/menu/utils/buildMenuContent.d.ts +2 -0
  16. package/dist/menu/utils/buildMenuContent.js +38 -0
  17. package/dist/menu/utils/findContent.d.ts +2 -2
  18. package/dist/menu/utils/findContent.js +4 -3
  19. package/dist/menu/utils/getItemLabel.d.ts +2 -0
  20. package/dist/menu/utils/getItemLabel.js +8 -0
  21. package/dist/menu/utils/getSafeRel.d.ts +1 -0
  22. package/dist/menu/utils/getSafeRel.js +14 -0
  23. package/dist/menu/utils/isMenuItem.d.ts +2 -0
  24. package/dist/menu/utils/isMenuItem.js +13 -0
  25. package/dist/menu/utils/isSafeHref.d.ts +1 -0
  26. package/dist/menu/utils/isSafeHref.js +10 -0
  27. package/dist/menu/utils/normalizeSeparators.d.ts +2 -0
  28. package/dist/menu/utils/normalizeSeparators.js +23 -0
  29. package/dist/menu/utils/resolveKeys.d.ts +12 -0
  30. package/dist/menu/utils/resolveKeys.js +22 -0
  31. package/dist/menu/utils/useLastValidSheet.d.ts +7 -0
  32. package/dist/menu/utils/useLastValidSheet.js +30 -0
  33. package/dist/menu/utils/useMenuItemCore.d.ts +31 -0
  34. package/dist/menu/utils/useMenuItemCore.js +51 -0
  35. package/dist/menu/utils/useMenuItemKeyboardNav.d.ts +2 -0
  36. package/dist/menu/utils/useMenuItemKeyboardNav.js +15 -0
  37. package/dist/menu/utils/useMenuListKeyboardNav.d.ts +12 -0
  38. package/dist/menu/utils/useMenuListKeyboardNav.js +77 -0
  39. package/dist/menu/utils/useMenuPath.d.ts +6 -0
  40. package/dist/menu/utils/useMenuPath.js +35 -0
  41. package/dist/nav/navItem/navItem.js +6 -4
  42. package/dist/nav/navSection/navSection.js +7 -5
  43. package/esm/menu/components/controlledMenuList/controlledMenuList.d.ts +27 -0
  44. package/esm/menu/components/controlledMenuList/controlledMenuList.js +120 -0
  45. package/esm/menu/components/createControlledMenuList.d.ts +37 -0
  46. package/esm/menu/components/createControlledMenuList.js +51 -0
  47. package/esm/menu/components/createMenuItem.d.ts +67 -0
  48. package/esm/menu/components/createMenuItem.js +93 -0
  49. package/esm/menu/components/menuButton.js +8 -2
  50. package/esm/menu/components/menuItem.d.ts +1 -2
  51. package/esm/menu/components/menuItem.js +20 -74
  52. package/esm/menu/contexts/usePathContext.d.ts +2 -0
  53. package/esm/menu/contexts/usePathContext.js +5 -0
  54. package/esm/menu/controlledMenu.js +10 -177
  55. package/esm/menu/utils/buildMenuContent.d.ts +2 -0
  56. package/esm/menu/utils/buildMenuContent.js +34 -0
  57. package/esm/menu/utils/findContent.d.ts +2 -2
  58. package/esm/menu/utils/findContent.js +4 -3
  59. package/esm/menu/utils/getItemLabel.d.ts +2 -0
  60. package/esm/menu/utils/getItemLabel.js +4 -0
  61. package/esm/menu/utils/getSafeRel.d.ts +1 -0
  62. package/esm/menu/utils/getSafeRel.js +10 -0
  63. package/esm/menu/utils/isMenuItem.d.ts +2 -0
  64. package/esm/menu/utils/isMenuItem.js +9 -0
  65. package/esm/menu/utils/isSafeHref.d.ts +1 -0
  66. package/esm/menu/utils/isSafeHref.js +6 -0
  67. package/esm/menu/utils/normalizeSeparators.d.ts +2 -0
  68. package/esm/menu/utils/normalizeSeparators.js +19 -0
  69. package/esm/menu/utils/resolveKeys.d.ts +12 -0
  70. package/esm/menu/utils/resolveKeys.js +18 -0
  71. package/esm/menu/utils/useLastValidSheet.d.ts +7 -0
  72. package/esm/menu/utils/useLastValidSheet.js +26 -0
  73. package/esm/menu/utils/useMenuItemCore.d.ts +31 -0
  74. package/esm/menu/utils/useMenuItemCore.js +47 -0
  75. package/esm/menu/utils/useMenuItemKeyboardNav.d.ts +2 -0
  76. package/esm/menu/utils/useMenuItemKeyboardNav.js +11 -0
  77. package/esm/menu/utils/useMenuListKeyboardNav.d.ts +12 -0
  78. package/esm/menu/utils/useMenuListKeyboardNav.js +73 -0
  79. package/esm/menu/utils/useMenuPath.d.ts +6 -0
  80. package/esm/menu/utils/useMenuPath.js +31 -0
  81. package/esm/nav/navItem/navItem.js +6 -4
  82. package/esm/nav/navSection/navSection.js +7 -5
  83. package/package.json +1 -1
@@ -1,34 +1,27 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument, @typescript-eslint/naming-convention */
3
- import { Children, Fragment, cloneElement, useMemo, useRef, useContext, useCallback, isValidElement } from "react";
2
+ /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument */
3
+ import { Fragment, cloneElement, useCallback } from "react";
4
4
  import { MenuButton } from "./menuButton";
5
- import { ControlledMenu } from "../controlledMenu";
6
5
  import { ControlledPopup } from "../../controlledPopup/controlledPopup";
7
- import { PathContext } from "../contexts/pathContext";
8
6
  import { classNames } from "../../commonHelpers/classNames/classNames";
9
- import { generateId } from "../../commonHelpers/generateId";
10
- import { MenuAlignmentContext } from "../../header/headerContext";
11
- import { isSeparator } from "./menuSeparator";
12
- export const isMenuItem = (element) => {
13
- if (!element || !element.type) {
14
- return false;
15
- }
16
- if (element.type === ControlledMenu.Item) {
17
- return true;
18
- }
19
- if ((typeof element.type === "object" || typeof element.type === "function") && "displayName" in element.type) {
20
- return element.type.displayName === "MenuItem";
21
- }
22
- return false;
23
- };
7
+ import { useMenuItemCore } from "../utils/useMenuItemCore";
8
+ export { isMenuItem } from "../utils/isMenuItem";
24
9
  export const MenuItem = ({ id, children, name, icon, disabled, onClick, link, target, rel, isMobile = false, setIsOpen, trigger, className, active, alignment }) => {
25
- const aligmentContext = useContext(MenuAlignmentContext);
26
- const contentAlignment = alignment || aligmentContext.alignment || "right-top";
27
- const { path, onOpenBranch, closeBranch, navigatedViaKeyboardRef } = useContext(PathContext);
28
- const memoizedDesktopActionOnClick = useCallback((itemId, e) => {
10
+ const { ref, isOpen, hasChildren, content, openedViaKeyboard, contentAlignment, path, onOpenBranch, handleOpenChange } = useMenuItemCore({
11
+ id,
12
+ children,
13
+ className,
14
+ alignment,
15
+ isMobile,
16
+ setIsOpen,
17
+ onClick
18
+ });
19
+ // MenuButton.onClick signature is (id, e) — adapt the hook's (e)-only handler
20
+ const memoizedDesktopActionOnClick = useCallback((_, e) => {
29
21
  setIsOpen === null || setIsOpen === void 0 ? void 0 : setIsOpen(false);
30
- onClick === null || onClick === void 0 ? void 0 : onClick(itemId, e);
31
- }, [setIsOpen, onClick]);
22
+ onClick === null || onClick === void 0 ? void 0 : onClick(id, e);
23
+ }, [setIsOpen, onClick, id]);
24
+ // MenuItem-specific callbacks (not provided by hook)
32
25
  const memoizedMobileActionOnClick = useCallback((itemId, e) => {
33
26
  onOpenBranch(id);
34
27
  !link && (onClick === null || onClick === void 0 ? void 0 : onClick(itemId, e));
@@ -46,54 +39,7 @@ export const MenuItem = ({ id, children, name, icon, disabled, onClick, link, ta
46
39
  }
47
40
  onClick === null || onClick === void 0 ? void 0 : onClick(id, e);
48
41
  }, [onClick, onOpenBranch, id, trigger]);
49
- const memoizedOnOpenChange = useCallback(() => {
50
- closeBranch();
51
- }, [closeBranch]);
52
- const ref = useRef(null);
53
- const content = useMemo(() => {
54
- const cont = [];
55
- Children.map(children, (child) => {
56
- if (!child) {
57
- return;
58
- }
59
- if (typeof child === "string") {
60
- cont.push(_jsx("li", { className: classNames(["zen-menu-item__content", className !== null && className !== void 0 ? className : ""]), role: "presentation", children: child }, generateId()));
61
- return;
62
- }
63
- if (isValidElement(child) && isSeparator(child)) {
64
- const clone = cloneElement(child, {
65
- key: child.props.key || generateId()
66
- });
67
- cont.push(clone);
68
- return;
69
- }
70
- if (isMenuItem(child)) {
71
- const childProps = child.props;
72
- const clone = cloneElement(child, {
73
- isMobile,
74
- key: childProps.id,
75
- setIsOpen
76
- });
77
- cont.push(clone);
78
- return;
79
- }
80
- const childProps = child.props;
81
- cont.push(_jsx("li", { className: classNames(["zen-menu-item__content", className !== null && className !== void 0 ? className : ""]), role: "presentation", children: child }, childProps.id || childProps["data-id"] || generateId()));
82
- });
83
- return cont;
84
- }, [children, isMobile, setIsOpen, className]);
85
- const isOpen = useMemo(() => path.includes(id), [path, id]);
86
- // Track previous isOpen state to detect when submenu opens
87
- const wasOpenRef = useRef(false);
88
- const localOpenedViaKeyboardRef = useRef(false);
89
- // Capture keyboard navigation state synchronously when isOpen transitions to true
90
- if (isOpen && !wasOpenRef.current && navigatedViaKeyboardRef) {
91
- localOpenedViaKeyboardRef.current = navigatedViaKeyboardRef.current;
92
- navigatedViaKeyboardRef.current = false; // Reset for next navigation
93
- }
94
- wasOpenRef.current = isOpen;
95
- const openedViaKeyboard = localOpenedViaKeyboardRef.current;
96
- if (content.length === 0) {
42
+ if (!hasChildren) {
97
43
  return (_jsx(MenuButton, { id: id, name: name, icon: icon, disabled: disabled, link: link, target: target, rel: rel, onClick: memoizedDesktopActionOnClick, className: className, active: active, hasChildren: false }, id));
98
44
  }
99
45
  if (isMobile) {
@@ -109,6 +55,6 @@ export const MenuItem = ({ id, children, name, icon, disabled, onClick, link, ta
109
55
  else {
110
56
  popupTrigger = (_jsx(MenuButton, { id: id, ref: ref, name: name, icon: icon, disabled: disabled, hasChildren: true, onClick: memoizedTriggerOnClick, active: active }, id));
111
57
  }
112
- return (_jsxs(Fragment, { children: [popupTrigger, _jsx(ControlledPopup, { className: classNames([`zen-controlled-menu-submenu--${path.length}`]), useTrapFocusWithTrigger: openedViaKeyboard ? "on" : "withTrigger", alignment: contentAlignment, triggerRef: ref, isOpen: isOpen, onOpenChange: memoizedOnOpenChange, ariaLabel: popupTrigger.props.name, recalculateOnScroll: true, children: _jsx("ul", { role: "menu", className: "zen-menu-item", children: content }) })] }, id));
58
+ return (_jsxs(Fragment, { children: [popupTrigger, _jsx(ControlledPopup, { className: classNames([`zen-controlled-menu-submenu--${path.length}`]), useTrapFocusWithTrigger: openedViaKeyboard ? "on" : "withTrigger", alignment: contentAlignment, triggerRef: ref, isOpen: isOpen, onOpenChange: handleOpenChange, ariaLabel: popupTrigger.props.name, recalculateOnScroll: true, children: _jsx("ul", { role: "menu", className: "zen-menu-item", children: content }) })] }, id));
113
59
  };
114
60
  MenuItem.displayName = "MenuItem";
@@ -0,0 +1,2 @@
1
+ import { IPathContext } from "./pathContext";
2
+ export declare function usePathContext(): IPathContext;
@@ -0,0 +1,5 @@
1
+ import { useContext } from "react";
2
+ import { PathContext } from "./pathContext";
3
+ export function usePathContext() {
4
+ return useContext(PathContext);
5
+ }
@@ -1,45 +1,26 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- /* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument */
3
- import { Children, cloneElement, isValidElement, useState, useMemo, useCallback, useEffect, useRef } from "react";
4
- import { MenuItem, isMenuItem } from "./components/menuItem";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useCallback, useEffect, useRef } from "react";
5
3
  import { classNames } from "../commonHelpers/classNames/classNames";
6
- import { findContent } from "./utils/findContent";
7
- import { findFirstFocusable } from "./utils/findFirstFocusable";
8
- import { findLastFocusable } from "./utils/findLastFocusable";
9
- import { findNextFocusable } from "./utils/findNextFocusable";
10
- import { isButton } from "./utils/isButton";
11
- import { isLink } from "./utils/isLink";
12
- import { findPrevFocusable } from "./utils/findPrevFocusable";
13
- import { MenuButton } from "./components/menuButton";
14
- import { IconArrowLeft } from "../icons/iconArrowLeft";
15
4
  import { ControlledPopup } from "../controlledPopup/controlledPopup";
16
5
  import { MobileSheet } from "../mobileSheet/mobileSheet";
17
6
  import { DeviceType } from "../commonHelpers/hooks/deviceType";
18
7
  import { useDeviceType } from "../commonHelpers/hooks/useDeviceType";
19
- import { generateId } from "../commonHelpers/generateId";
20
- import { PathProvider } from "./contexts/pathProvider";
21
- import { isSeparator, MenuSeparator } from "./components/menuSeparator";
22
8
  import { FOCUSABLE_SELECTOR } from "../utils/focusableSelector";
9
+ import { ControlledMenuList } from "./components/controlledMenuList/controlledMenuList";
10
+ const EmbeddedMenuList = ControlledMenuList;
23
11
  export const ControlledMenu = ({ children, isOpen, setIsOpen, triggerRef, ariaLabel, ariaLabelledby, id, title, className = "", listClassName = "", paddingX = 0, paddingY = 0, alignment, closeOnScroll = true }) => {
24
12
  const [deviceType, setDeviceType] = useState(DeviceType.Desktop);
25
13
  const isMobile = deviceType === DeviceType.Mobile;
26
14
  const memoizedOnChange = useCallback(setIsOpen, [setIsOpen]);
27
15
  useDeviceType(setDeviceType);
28
16
  const menuListRef = useRef(null);
29
- const [path, setPath] = useState([]);
30
- // Track if the trigger was activated via keyboard
31
17
  const openedViaKeyboardRef = useRef(false);
32
- // Track if submenu was navigated via keyboard (ArrowRight)
33
- const navigatedViaKeyboardRef = useRef(false);
34
- // Track if keyboard is actively being used for navigation (vs mouse/touch)
35
- const keyboardActiveRef = useRef(false);
36
- // Listen for keyboard activation on trigger
37
18
  useEffect(() => {
38
19
  const trigger = triggerRef.current;
39
20
  if (!trigger)
40
21
  return undefined;
41
22
  const handleKeyDown = (e) => {
42
- if (e.key === "Enter" || e.key === " ") {
23
+ if (e.key === "Enter" || e.key === " " || e.key === "ArrowRight") {
43
24
  openedViaKeyboardRef.current = true;
44
25
  }
45
26
  };
@@ -53,12 +34,6 @@ export const ControlledMenu = ({ children, isOpen, setIsOpen, triggerRef, ariaLa
53
34
  trigger.removeEventListener("mousedown", handleMouseDown);
54
35
  };
55
36
  }, [triggerRef]);
56
- useEffect(() => {
57
- if (path.length && !isOpen) {
58
- setPath([]);
59
- }
60
- }, [isOpen, path, setPath]);
61
- // Focus the menu list container or first item when menu opens (for keyboard navigation)
62
37
  useEffect(() => {
63
38
  var _a;
64
39
  if (isOpen && !isMobile && menuListRef.current) {
@@ -70,156 +45,14 @@ export const ControlledMenu = ({ children, isOpen, setIsOpen, triggerRef, ariaLa
70
45
  }
71
46
  }
72
47
  }, [isOpen, isMobile]);
73
- const onOpenBranch = useCallback((branchId) => {
74
- if (!branchId) {
75
- return;
76
- }
77
- if (!path.includes(branchId)) {
78
- setPath([...path, branchId]);
79
- return;
80
- }
81
- if (path.includes(branchId)) {
82
- setPath(v => {
83
- const newPath = [...v];
84
- newPath.pop();
85
- return newPath;
86
- });
87
- }
88
- }, [setPath, path]);
89
- const closeBranch = useCallback(() => {
90
- setPath(v => {
91
- const newPath = [...v];
92
- newPath.pop();
93
- return newPath;
94
- });
95
- }, [setPath]);
96
- const [content, parent] = useMemo(() => {
97
- let par = null;
98
- let currentChildren = children;
99
- if (isMobile && path.length > 0) {
100
- const el = findContent(children, ControlledMenu.Item, path[path.length - 1]);
101
- if (el || isValidElement(el)) {
102
- const elProps = el.props;
103
- currentChildren = elProps.children;
104
- par = el;
105
- }
106
- }
107
- let cont = [];
108
- Children.map(currentChildren, (child) => {
109
- if (!child) {
110
- return;
111
- }
112
- if (typeof child === "string") {
113
- cont.push(_jsx("li", { role: "presentation", className: classNames(["zen-menu-item__content"]), children: child }, generateId()));
114
- return;
115
- }
116
- if (isValidElement(child) && isSeparator(child)) {
117
- const clone = cloneElement(child, {
118
- key: child.props.key || generateId()
119
- });
120
- cont.push(clone);
121
- return;
122
- }
123
- if (isValidElement(child) && isMenuItem(child)) {
124
- const clone = cloneElement(child, {
125
- isMobile,
126
- key: child.props.id,
127
- setIsOpen,
128
- onClick: child.props.onClick
129
- });
130
- cont.push(clone);
131
- return;
132
- }
133
- const childProps = child.props;
134
- cont.push(_jsx("li", { className: classNames(["zen-menu-item__content"]), role: "presentation", children: child }, childProps.id || childProps["data-id"] || generateId()));
135
- });
136
- while (cont[0] && isSeparator(cont[0])) {
137
- cont.shift();
138
- }
139
- while (cont[cont.length - 1] && isSeparator(cont[cont.length - 1])) {
140
- cont.pop();
141
- }
142
- cont = cont.filter((el, indx, arr) => {
143
- if (isSeparator(el) && arr[indx - 1] && isSeparator(arr[indx - 1])) {
144
- return false;
145
- }
146
- return true;
147
- });
148
- return [cont, par];
149
- }, [children, isMobile, path, setIsOpen]);
150
- // Handle keyboard navigation when no menu item is focused (e.g., when menu just opened)
151
- const handleUnfocusedKeyDown = (e, menuList) => {
152
- var _a, _b;
153
- if (e.key === "ArrowDown" || e.key === "Home") {
154
- e.preventDefault();
155
- (_a = findFirstFocusable(menuList)) === null || _a === void 0 ? void 0 : _a.focus();
156
- return true;
157
- }
158
- if (e.key === "ArrowUp" || e.key === "End") {
159
- e.preventDefault();
160
- (_b = findLastFocusable(menuList)) === null || _b === void 0 ? void 0 : _b.focus();
161
- return true;
162
- }
163
- return false;
164
- };
165
- const onMouseDown = () => {
166
- keyboardActiveRef.current = false;
167
- };
168
- const onKeyDown = e => {
169
- var _a, _b, _c, _d;
170
- keyboardActiveRef.current = true;
171
- const target = e.target;
172
- const currentTarget = e.currentTarget;
173
- if (!isButton(target) && !isLink(target)) {
174
- if (target === currentTarget) {
175
- const menuList = currentTarget.querySelector("ul");
176
- if (menuList) {
177
- handleUnfocusedKeyDown(e, menuList);
178
- }
179
- }
180
- return;
181
- }
182
- if (e.key === "ArrowDown") {
183
- e.preventDefault();
184
- (_a = findNextFocusable(target)) === null || _a === void 0 ? void 0 : _a.focus();
185
- return;
186
- }
187
- if (e.key === "ArrowUp") {
188
- e.preventDefault();
189
- (_b = findPrevFocusable(target)) === null || _b === void 0 ? void 0 : _b.focus();
190
- return;
191
- }
192
- if (e.key === "Home") {
193
- e.preventDefault();
194
- (_c = findFirstFocusable(target)) === null || _c === void 0 ? void 0 : _c.focus();
195
- return;
196
- }
197
- if (e.key === "End") {
198
- e.preventDefault();
199
- (_d = findLastFocusable(target)) === null || _d === void 0 ? void 0 : _d.focus();
200
- return;
201
- }
202
- if (isButton(target) &&
203
- (e.key === "ArrowRight" || e.key === "Enter" || e.key === " ") &&
204
- target.classList.contains("zen-menu-button__action--has-children")) {
205
- e.preventDefault();
206
- navigatedViaKeyboardRef.current = true;
207
- target.click();
208
- }
209
- };
210
- const renderMenuList = () => (_jsx("div", { ref: menuListRef, tabIndex: -1, onKeyDown: onKeyDown, onMouseDown: onMouseDown, className: classNames(["zen-action-list", className]), children: _jsxs("ul", { role: "menu", className: classNames(["zen-menu-item", className, listClassName]), children: [parent ? (_jsx(MenuButton, { id: "root", name: parent.props.name || "", icon: IconArrowLeft, onClick: closeBranch, hasChildren: false, disabled: false }, "root")) : null, content] }) }));
211
48
  const hideMenu = useCallback(() => {
212
- closeBranch();
213
49
  setIsOpen(false);
214
- }, [closeBranch, setIsOpen]);
50
+ }, [setIsOpen]);
215
51
  if (isMobile) {
216
- return (_jsx(_Fragment, { children: _jsx(PathProvider, { path: path, onOpenBranch: onOpenBranch, closeBranch: closeBranch, navigatedViaKeyboardRef: navigatedViaKeyboardRef, keyboardActiveRef: keyboardActiveRef, children: _jsxs(MobileSheet, { label: title, isOpen: isOpen, triggerRef: triggerRef, onHidePanel: hideMenu, onCloseClick: hideMenu, children: [_jsx(MobileSheet.Title, { children: title }), _jsx(MobileSheet.Content, { children: renderMenuList() })] }) }) }));
52
+ return (_jsxs(MobileSheet, { label: title, isOpen: isOpen, triggerRef: triggerRef, onHidePanel: hideMenu, onCloseClick: hideMenu, children: [_jsx(MobileSheet.Title, { children: title }), _jsx(MobileSheet.Content, { children: _jsx(EmbeddedMenuList, { ref: menuListRef, setIsOpen: setIsOpen, isOpen: isOpen, className: className, listClassName: listClassName, children: children }) })] }));
217
53
  }
218
- return (_jsx(_Fragment, { children: _jsx(PathProvider, { path: path, onOpenBranch: onOpenBranch, closeBranch: closeBranch, navigatedViaKeyboardRef: navigatedViaKeyboardRef, keyboardActiveRef: keyboardActiveRef, children: _jsx(ControlledPopup, { id: id, useTrapFocusWithTrigger: "on", className: classNames(["zen-controlled-menu", className]), onOpenChange: memoizedOnChange, isOpen: isOpen, triggerRef: triggerRef, paddingX: paddingX, paddingY: paddingY, alignment: alignment, ariaLabelledby: ariaLabelledby, ariaLabel: ariaLabel || title, closeOnScroll: closeOnScroll,
219
- // focusOnOpen is false - ControlledMenu handles focus based on input method
220
- // (keyboard vs mouse) in its own useEffect
221
- focusOnOpen: false, children: renderMenuList() }) }) }));
54
+ return (_jsx(ControlledPopup, { id: id, useTrapFocusWithTrigger: "on", className: classNames(["zen-controlled-menu", className]), onOpenChange: memoizedOnChange, isOpen: isOpen, triggerRef: triggerRef, paddingX: paddingX, paddingY: paddingY, alignment: alignment, ariaLabelledby: ariaLabelledby, ariaLabel: ariaLabel || title, recalculateOnScroll: true, focusOnOpen: false, closeOnScroll: closeOnScroll, children: _jsx(EmbeddedMenuList, { ref: menuListRef, setIsOpen: setIsOpen, isOpen: isOpen, className: className, listClassName: listClassName, children: children }) }));
222
55
  };
223
- ControlledMenu.Item = MenuItem;
224
- ControlledMenu.Separator = MenuSeparator;
56
+ ControlledMenu.Item = ControlledMenuList.Item;
57
+ ControlledMenu.Separator = ControlledMenuList.Separator;
225
58
  export const TRANSLATIONS = ["Back"];
@@ -0,0 +1,2 @@
1
+ import { ReactNode } from "react";
2
+ export declare function buildMenuContent(children: ReactNode, isMobile: boolean, setIsOpen: ((v: boolean) => void) | undefined, className: string | undefined): ReactNode[];
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument */
3
+ import { Children, cloneElement, isValidElement } from "react";
4
+ import { classNames } from "../../commonHelpers/classNames/classNames";
5
+ import { isSeparator } from "../components/menuSeparator";
6
+ import { isMenuItem } from "./isMenuItem";
7
+ import { generateId } from "../../commonHelpers/generateId";
8
+ export function buildMenuContent(children, isMobile, setIsOpen, className) {
9
+ const cont = [];
10
+ Children.map(children, (child) => {
11
+ if (!child) {
12
+ return;
13
+ }
14
+ if (typeof child === "string") {
15
+ cont.push(_jsx("li", { className: classNames(["zen-menu-item__content", className !== null && className !== void 0 ? className : ""]), role: "presentation", children: child }, generateId()));
16
+ return;
17
+ }
18
+ if (isValidElement(child) && isSeparator(child)) {
19
+ const clone = cloneElement(child, { key: child.props.key || generateId() });
20
+ cont.push(clone);
21
+ return;
22
+ }
23
+ if (isMenuItem(child)) {
24
+ const childProps = child.props;
25
+ const clone = cloneElement(child, { isMobile, key: childProps.id, setIsOpen });
26
+ cont.push(clone);
27
+ return;
28
+ }
29
+ // eslint-disable-next-line @typescript-eslint/naming-convention
30
+ const childProps = child.props;
31
+ cont.push(_jsx("li", { className: classNames(["zen-menu-item__content", className !== null && className !== void 0 ? className : ""]), role: "presentation", children: child }, childProps.id || childProps["data-id"] || generateId()));
32
+ });
33
+ return cont;
34
+ }
@@ -1,2 +1,2 @@
1
- import React, { ReactNode, ReactElement } from "react";
2
- export declare const findContent: (children: ReactNode, targetComponent: React.FC | React.ComponentClass, id?: string) => ReactElement | null;
1
+ import { ReactNode, ReactElement } from "react";
2
+ export declare const findContent: (children: ReactNode, matcher: (child: ReactElement) => boolean, id?: string) => ReactElement | null;
@@ -1,17 +1,18 @@
1
1
  import React from "react";
2
- export const findContent = (children, targetComponent, id) => {
2
+ export const findContent = (children, matcher, id) => {
3
3
  let found = null;
4
4
  React.Children.forEach(children, (child) => {
5
5
  if (found || !React.isValidElement(child)) {
6
6
  return;
7
7
  }
8
8
  const childProps = child.props;
9
- if (id ? child.type === targetComponent && childProps.id === id : child.type === targetComponent) {
9
+ const matches = matcher(child);
10
+ if (id ? matches && childProps.id === id : matches) {
10
11
  found = child;
11
12
  return;
12
13
  }
13
14
  if (childProps.children) {
14
- found = findContent(childProps.children, targetComponent, id);
15
+ found = findContent(childProps.children, matcher, id);
15
16
  }
16
17
  });
17
18
  return found;
@@ -0,0 +1,2 @@
1
+ import { ReactElement } from "react";
2
+ export declare const getItemLabel: (el: ReactElement | null) => string;
@@ -0,0 +1,4 @@
1
+ export const getItemLabel = (el) => {
2
+ const props = el === null || el === void 0 ? void 0 : el.props;
3
+ return (props === null || props === void 0 ? void 0 : props.name) || (props === null || props === void 0 ? void 0 : props.title) || "";
4
+ };
@@ -0,0 +1 @@
1
+ export declare const getSafeRel: (rel?: string, target?: string) => string | undefined;
@@ -0,0 +1,10 @@
1
+ export const getSafeRel = (rel, target) => {
2
+ if (target !== "_blank")
3
+ return rel;
4
+ const parts = rel ? rel.split(/\s+/) : [];
5
+ if (!parts.includes("noopener"))
6
+ parts.push("noopener");
7
+ if (!parts.includes("noreferrer"))
8
+ parts.push("noreferrer");
9
+ return parts.join(" ");
10
+ };
@@ -0,0 +1,2 @@
1
+ import { ReactElement } from "react";
2
+ export declare const isMenuItem: (element: ReactElement | undefined) => boolean;
@@ -0,0 +1,9 @@
1
+ export const isMenuItem = (element) => {
2
+ if (!element || !element.type) {
3
+ return false;
4
+ }
5
+ if ((typeof element.type === "object" || typeof element.type === "function") && "displayName" in element.type) {
6
+ return element.type.displayName === "MenuItem";
7
+ }
8
+ return false;
9
+ };
@@ -0,0 +1 @@
1
+ export declare const isSafeHref: (href?: string) => boolean;
@@ -0,0 +1,6 @@
1
+ export const isSafeHref = (href) => {
2
+ if (!href)
3
+ return false;
4
+ const lower = href.toLowerCase().trimStart();
5
+ return !lower.startsWith("javascript:") && !lower.startsWith("data:") && !lower.startsWith("vbscript:");
6
+ };
@@ -0,0 +1,2 @@
1
+ import { ReactElement } from "react";
2
+ export declare const normalizeSeparators: (items: ReactElement[]) => ReactElement[];
@@ -0,0 +1,19 @@
1
+ import { isSeparator } from "../components/menuSeparator";
2
+ export const normalizeSeparators = (items) => {
3
+ // Find the first non-separator index
4
+ let start = 0;
5
+ while (start < items.length && isSeparator(items[start])) {
6
+ start++;
7
+ }
8
+ // Find the last non-separator index
9
+ let end = items.length - 1;
10
+ while (end >= start && isSeparator(items[end])) {
11
+ end--;
12
+ }
13
+ // If all items are separators or array is empty
14
+ if (start > end) {
15
+ return [];
16
+ }
17
+ // Slice to remove leading/trailing separators and filter consecutive duplicates
18
+ return items.slice(start, end + 1).filter((el, i, arr) => !(isSeparator(el) && arr[i - 1] && isSeparator(arr[i - 1])));
19
+ };
@@ -0,0 +1,12 @@
1
+ export declare const verticalKeys: {
2
+ keyNext: string;
3
+ keyPrev: string;
4
+ keyOpenNested: string;
5
+ keyBack: string;
6
+ };
7
+ export declare const resolveKeys: (target: HTMLElement, isHorizontal: boolean) => {
8
+ keyNext: string;
9
+ keyPrev: string;
10
+ keyOpenNested: string;
11
+ keyBack: string;
12
+ };
@@ -0,0 +1,18 @@
1
+ export const verticalKeys = {
2
+ keyNext: "ArrowDown",
3
+ keyPrev: "ArrowUp",
4
+ keyOpenNested: "ArrowRight",
5
+ keyBack: "ArrowLeft"
6
+ };
7
+ export const resolveKeys = (target, isHorizontal) => {
8
+ const inSubMenu = isHorizontal && !!target.closest('[class*="zen-controlled-menu-submenu"]');
9
+ if (inSubMenu) {
10
+ return verticalKeys;
11
+ }
12
+ return {
13
+ keyNext: isHorizontal ? "ArrowRight" : "ArrowDown",
14
+ keyPrev: isHorizontal ? "ArrowLeft" : "ArrowUp",
15
+ keyOpenNested: isHorizontal ? "ArrowUp" : "ArrowRight",
16
+ keyBack: isHorizontal ? "ArrowDown" : "ArrowLeft"
17
+ };
18
+ };
@@ -0,0 +1,7 @@
1
+ import { ReactElement, ReactNode } from "react";
2
+ export declare const useLastValidSheet: (nestedContent: ReactElement[] | null, nestedParent: ReactElement | null, path: string[], children: ReactNode) => {
3
+ sheetContent: ReactElement<unknown, string | import("react").JSXElementConstructor<any>>[];
4
+ sheetParent: ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | null;
5
+ sheetPathLength: number;
6
+ sheetParentName: string;
7
+ };
@@ -0,0 +1,26 @@
1
+ import { useRef } from "react";
2
+ import { findContent } from "./findContent";
3
+ import { getItemLabel } from "./getItemLabel";
4
+ import { isMenuItem } from "../components/menuItem";
5
+ // Preserves the last non-null nested sheet state so the MobileSheet has content
6
+ // to render during its closing animation after `nestedContent` goes null.
7
+ export const useLastValidSheet = (nestedContent, nestedParent, path, children) => {
8
+ const lastNestedContentRef = useRef([]);
9
+ const lastNestedParentRef = useRef(null);
10
+ const lastPathLengthRef = useRef(0);
11
+ const lastSheetTitleRef = useRef("");
12
+ const rootEl = path.length > 0 ? findContent(children, isMenuItem, path[0]) : null;
13
+ const currentSheetTitle = rootEl ? getItemLabel(rootEl) : "";
14
+ if (nestedContent !== null) {
15
+ lastNestedContentRef.current = nestedContent;
16
+ lastNestedParentRef.current = nestedParent;
17
+ lastPathLengthRef.current = path.length;
18
+ lastSheetTitleRef.current = currentSheetTitle;
19
+ }
20
+ return {
21
+ sheetContent: nestedContent !== null ? nestedContent : lastNestedContentRef.current,
22
+ sheetParent: nestedContent !== null ? nestedParent : lastNestedParentRef.current,
23
+ sheetPathLength: nestedContent !== null ? path.length : lastPathLengthRef.current,
24
+ sheetParentName: nestedContent !== null ? currentSheetTitle : lastSheetTitleRef.current
25
+ };
26
+ };
@@ -0,0 +1,31 @@
1
+ import { ReactNode } from "react";
2
+ import { TAlignment } from "../../absolute/absolute";
3
+ export interface IUseMenuItemCoreParams {
4
+ id: string;
5
+ children?: ReactNode;
6
+ className?: string;
7
+ alignment?: TAlignment;
8
+ isMobile?: boolean;
9
+ setIsOpen?: (v: boolean) => void;
10
+ onClick?: (id: string, e: React.MouseEvent) => void;
11
+ onOpenChange?: (isOpen: boolean) => void;
12
+ }
13
+ /**
14
+ * Core menu item logic shared between MenuItem and createMenuItem.
15
+ * Consolidates context access, state computation, and callback memoization.
16
+ */
17
+ export declare const useMenuItemCore: ({ id, children, className, alignment, isMobile, setIsOpen, onClick, onOpenChange }: IUseMenuItemCoreParams) => {
18
+ ref: import("react").RefObject<HTMLButtonElement | null>;
19
+ isOpen: boolean;
20
+ hasChildren: boolean;
21
+ content: ReactNode[];
22
+ openedViaKeyboard: boolean;
23
+ contentAlignment: TAlignment;
24
+ path: string[];
25
+ onOpenBranch: (id: string) => void;
26
+ closeBranch: () => void;
27
+ navigatedViaKeyboardRef: import("react").RefObject<boolean> | undefined;
28
+ handleDesktopActionClick: (e: React.MouseEvent) => void;
29
+ handleTriggerClick: () => void;
30
+ handleOpenChange: () => void;
31
+ };
@@ -0,0 +1,47 @@
1
+ import { useRef, useContext, useMemo, useCallback, useEffect } from "react";
2
+ import { PathContext } from "../contexts/pathContext";
3
+ import { MenuAlignmentContext } from "../../header/headerContext";
4
+ import { buildMenuContent } from "./buildMenuContent";
5
+ import { useMenuItemKeyboardNav } from "./useMenuItemKeyboardNav";
6
+ /**
7
+ * Core menu item logic shared between MenuItem and createMenuItem.
8
+ * Consolidates context access, state computation, and callback memoization.
9
+ */
10
+ export const useMenuItemCore = ({ id, children, className, alignment, isMobile = false, setIsOpen, onClick, onOpenChange }) => {
11
+ const alignmentContext = useContext(MenuAlignmentContext);
12
+ const contentAlignment = alignment || alignmentContext.alignment || "right-top";
13
+ const { path, onOpenBranch, closeBranch, navigatedViaKeyboardRef } = useContext(PathContext);
14
+ const ref = useRef(null);
15
+ const content = useMemo(() => buildMenuContent(children, isMobile, setIsOpen, className), [children, isMobile, setIsOpen, className]);
16
+ const isOpen = useMemo(() => path.includes(id), [path, id]);
17
+ const hasChildren = content.length > 0;
18
+ const openedViaKeyboard = useMenuItemKeyboardNav(isOpen, navigatedViaKeyboardRef);
19
+ const handleDesktopActionClick = useCallback((e) => {
20
+ setIsOpen === null || setIsOpen === void 0 ? void 0 : setIsOpen(false);
21
+ onClick === null || onClick === void 0 ? void 0 : onClick(id, e);
22
+ }, [setIsOpen, onClick, id]);
23
+ const handleTriggerClick = useCallback(() => {
24
+ onOpenBranch(id);
25
+ }, [id, onOpenBranch]);
26
+ const handleOpenChange = useCallback(() => {
27
+ closeBranch();
28
+ }, [closeBranch]);
29
+ useEffect(() => {
30
+ onOpenChange === null || onOpenChange === void 0 ? void 0 : onOpenChange(isOpen);
31
+ }, [isOpen, onOpenChange]);
32
+ return {
33
+ ref,
34
+ isOpen,
35
+ hasChildren,
36
+ content,
37
+ openedViaKeyboard,
38
+ contentAlignment,
39
+ path,
40
+ onOpenBranch,
41
+ closeBranch,
42
+ navigatedViaKeyboardRef,
43
+ handleDesktopActionClick,
44
+ handleTriggerClick,
45
+ handleOpenChange
46
+ };
47
+ };