@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
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useMenuListKeyboardNav = void 0;
4
+ const react_1 = require("react");
5
+ const findFirstFocusable_1 = require("./findFirstFocusable");
6
+ const findLastFocusable_1 = require("./findLastFocusable");
7
+ const findNextFocusable_1 = require("./findNextFocusable");
8
+ const findPrevFocusable_1 = require("./findPrevFocusable");
9
+ const isButton_1 = require("./isButton");
10
+ const isLink_1 = require("./isLink");
11
+ const resolveKeys_1 = require("./resolveKeys");
12
+ const useMenuListKeyboardNav = (keyboardActiveRef, navigatedViaKeyboardRef, isHorizontal) => {
13
+ const handleNavigation = (0, react_1.useCallback)(
14
+ // eslint-disable-next-line complexity
15
+ (e, keyMap) => {
16
+ var _a, _b, _c, _d, _e, _f;
17
+ keyboardActiveRef.current = true;
18
+ const target = e.target;
19
+ const currentTarget = e.currentTarget;
20
+ if (!(0, isButton_1.isButton)(target) && !(0, isLink_1.isLink)(target)) {
21
+ if (target === currentTarget) {
22
+ const menuList = currentTarget.querySelector("ul");
23
+ if (menuList) {
24
+ if (e.key === keyMap.keyNext || e.key === "Home") {
25
+ e.preventDefault();
26
+ (_a = (0, findFirstFocusable_1.findFirstFocusable)(menuList)) === null || _a === void 0 ? void 0 : _a.focus();
27
+ }
28
+ else if (e.key === keyMap.keyPrev || e.key === "End") {
29
+ e.preventDefault();
30
+ (_b = (0, findLastFocusable_1.findLastFocusable)(menuList)) === null || _b === void 0 ? void 0 : _b.focus();
31
+ }
32
+ }
33
+ }
34
+ return;
35
+ }
36
+ const isItemWithChildren = (e.key === keyMap.keyOpenNested || e.key === "Enter" || e.key === " ") &&
37
+ target.classList.contains("zen-menu-button__action--has-children");
38
+ const isBackButton = (e.key === keyMap.keyBack || e.key === "Enter" || e.key === " ") && target.dataset.id === "root";
39
+ if (e.key === keyMap.keyNext) {
40
+ e.preventDefault();
41
+ (_c = (0, findNextFocusable_1.findNextFocusable)(target)) === null || _c === void 0 ? void 0 : _c.focus();
42
+ return;
43
+ }
44
+ if (e.key === keyMap.keyPrev) {
45
+ e.preventDefault();
46
+ (_d = (0, findPrevFocusable_1.findPrevFocusable)(target)) === null || _d === void 0 ? void 0 : _d.focus();
47
+ return;
48
+ }
49
+ if (e.key === "Home") {
50
+ e.preventDefault();
51
+ (_e = (0, findFirstFocusable_1.findFirstFocusable)(target)) === null || _e === void 0 ? void 0 : _e.focus();
52
+ return;
53
+ }
54
+ if (e.key === "End") {
55
+ e.preventDefault();
56
+ (_f = (0, findLastFocusable_1.findLastFocusable)(target)) === null || _f === void 0 ? void 0 : _f.focus();
57
+ return;
58
+ }
59
+ if ((0, isButton_1.isButton)(target) && (isItemWithChildren || isBackButton)) {
60
+ e.preventDefault();
61
+ navigatedViaKeyboardRef.current = true;
62
+ target.click();
63
+ }
64
+ }, [keyboardActiveRef, navigatedViaKeyboardRef]);
65
+ const onKeyDown = (0, react_1.useCallback)((e) => {
66
+ handleNavigation(e, (0, resolveKeys_1.resolveKeys)(e.target, isHorizontal));
67
+ }, [isHorizontal, handleNavigation]);
68
+ const onKeyDownVertical = (0, react_1.useCallback)((e) => {
69
+ handleNavigation(e, resolveKeys_1.verticalKeys);
70
+ }, [handleNavigation]);
71
+ const onMouseDown = (0, react_1.useCallback)(() => {
72
+ keyboardActiveRef.current = false;
73
+ // eslint-disable-next-line react-hooks/exhaustive-deps
74
+ }, []);
75
+ return { onKeyDown, onKeyDownVertical, onMouseDown };
76
+ };
77
+ exports.useMenuListKeyboardNav = useMenuListKeyboardNav;
@@ -0,0 +1,6 @@
1
+ export declare const useMenuPath: (isOpen?: boolean) => {
2
+ path: string[];
3
+ onOpenBranch: (branchId?: string) => void;
4
+ closeBranch: () => void;
5
+ closeAll: () => void;
6
+ };
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useMenuPath = void 0;
4
+ const react_1 = require("react");
5
+ const useMenuPath = (isOpen) => {
6
+ const [path, setPath] = (0, react_1.useState)([]);
7
+ (0, react_1.useEffect)(() => {
8
+ if (isOpen === false) {
9
+ setPath(v => (v.length ? [] : v));
10
+ }
11
+ }, [isOpen]);
12
+ const onOpenBranch = (0, react_1.useCallback)((branchId) => {
13
+ if (!branchId) {
14
+ return;
15
+ }
16
+ setPath(v => {
17
+ if (!v.includes(branchId)) {
18
+ return [...v, branchId];
19
+ }
20
+ const newPath = [...v];
21
+ newPath.pop();
22
+ return newPath;
23
+ });
24
+ }, []);
25
+ const closeBranch = (0, react_1.useCallback)(() => {
26
+ setPath(v => {
27
+ const newPath = [...v];
28
+ newPath.pop();
29
+ return newPath;
30
+ });
31
+ }, []);
32
+ const closeAll = (0, react_1.useCallback)(() => setPath([]), []);
33
+ return { path, onOpenBranch, closeBranch, closeAll };
34
+ };
35
+ exports.useMenuPath = useMenuPath;
@@ -94,17 +94,19 @@ const ButtonNavItem = (_a) => {
94
94
  }
95
95
  };
96
96
  const handleKeyPress = evt => {
97
- if (evt.key !== "ArrowRight") {
97
+ if (hasNestedItems && (evt.key === "Enter" || evt.key === " ")) {
98
+ evt.preventDefault();
99
+ setMenuOpen(true);
98
100
  return;
99
101
  }
100
- if (hasNestedItems) {
101
- setMenuOpen(prevOpen => !prevOpen);
102
+ if (hasNestedItems && evt.key === "ArrowRight") {
103
+ setMenuOpen(true);
102
104
  }
103
105
  };
104
106
  const isMobile = (0, useMobile_1.useMobile)();
105
107
  const isActive = active || (hasNestedItems && menuOpen);
106
108
  const triggerId = (0, react_1.useId)();
107
- const buttonElement = ((0, jsx_runtime_1.jsx)("button", { id: triggerId, ref: triggerRef, "aria-label": title, title: title, tabIndex: tabIndex, role: isMenuItem ? "menuitem" : undefined, className: "zen-nav-item__main", onClick: handleClick, onKeyDown: handleKeyPress, children: (0, jsx_runtime_1.jsx)(NavItemContent, Object.assign({ title: title, collapsed: collapsed, hasSubmenu: hasNestedItems, level: currentLevel }, rest, { children: (0, jsx_runtime_1.jsx)("span", { className: "zen-nav-item__title-text", children: title }) })) }));
109
+ const buttonElement = ((0, jsx_runtime_1.jsx)("button", { id: triggerId, ref: triggerRef, "aria-label": title, title: title, tabIndex: tabIndex, role: isMenuItem ? "menuitem" : undefined, "aria-haspopup": hasNestedItems ? "menu" : undefined, "aria-expanded": hasNestedItems ? menuOpen : undefined, className: "zen-nav-item__main", onClick: handleClick, onKeyDown: handleKeyPress, children: (0, jsx_runtime_1.jsx)(NavItemContent, Object.assign({ title: title, collapsed: collapsed, hasSubmenu: hasNestedItems, level: currentLevel }, rest, { children: (0, jsx_runtime_1.jsx)("span", { className: "zen-nav-item__title-text", children: title }) })) }));
108
110
  const trigger = collapsed ? ((0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { trigger: buttonElement, alignment: tooltipAlignment, children: title })) : (buttonElement);
109
111
  if (hasNestedItems) {
110
112
  // Process children to add appropriate level classes while allowing unlimited nesting
@@ -6,6 +6,7 @@ const classNames_1 = require("../../commonHelpers/classNames/classNames");
6
6
  const react_1 = require("react");
7
7
  const nav_context_1 = require("../context/nav.context");
8
8
  const navItem_1 = require("../navItem/navItem");
9
+ const getNavItemMain = (wrapper) => { var _a; return (_a = wrapper === null || wrapper === void 0 ? void 0 : wrapper.querySelector(".zen-nav-item__main")) !== null && _a !== void 0 ? _a : wrapper; };
9
10
  /**
10
11
  * @beta This component is not fully ready yet and may change in future releases.
11
12
  */
@@ -26,11 +27,11 @@ const NavSection = ({ children, className }) => {
26
27
  if (!sectionRef.current) {
27
28
  return;
28
29
  }
29
- const navItemElements = sectionRef.current.querySelectorAll(".zen-nav-item");
30
+ const navItemElements = sectionRef.current.querySelectorAll(":scope > .zen-nav-item");
30
31
  childRefs.current = Array.from(navItemElements);
31
32
  }, [children]);
32
33
  const arrowClickHandler = (0, react_1.useCallback)(evt => {
33
- var _a, _b, _c;
34
+ var _a;
34
35
  const key = evt.key;
35
36
  if ((key !== "ArrowDown" && key !== "ArrowUp" && key !== "Home" && key !== "End" && key !== "PageUp" && key !== "PageDown") || // non-handled keys
36
37
  !children || // section is empty
@@ -39,7 +40,7 @@ const NavSection = ({ children, className }) => {
39
40
  return;
40
41
  }
41
42
  evt.preventDefault();
42
- (_a = childRefs.current[focusedIndex.current]) === null || _a === void 0 ? void 0 : _a.setAttribute("tabindex", "-1");
43
+ (_a = getNavItemMain(childRefs.current[focusedIndex.current])) === null || _a === void 0 ? void 0 : _a.setAttribute("tabindex", "-1");
43
44
  if (key === "ArrowDown") {
44
45
  // get next in a loop
45
46
  focusedIndex.current = focusedIndex.current === react_1.Children.count(children) - 1 ? 0 : focusedIndex.current + 1;
@@ -54,8 +55,9 @@ const NavSection = ({ children, className }) => {
54
55
  if (key === "End" || key === "PageDown") {
55
56
  focusedIndex.current = react_1.Children.count(children) - 1;
56
57
  }
57
- (_b = childRefs.current[focusedIndex.current]) === null || _b === void 0 ? void 0 : _b.setAttribute("tabindex", "0");
58
- (_c = childRefs.current[focusedIndex.current]) === null || _c === void 0 ? void 0 : _c.focus();
58
+ const nextMain = getNavItemMain(childRefs.current[focusedIndex.current]);
59
+ nextMain === null || nextMain === void 0 ? void 0 : nextMain.setAttribute("tabindex", "0");
60
+ nextMain === null || nextMain === void 0 ? void 0 : nextMain.focus();
59
61
  }, [children]);
60
62
  const cssClasses = (0, classNames_1.classNames)(["zen-nav-section", className || "", collapsed ? "zen-nav-section--collapsed" : ""]);
61
63
  return ((0, jsx_runtime_1.jsx)("div", { ref: sectionRef, role: "menu", className: cssClasses, onKeyDown: arrowClickHandler, children: focusableChildren }));
@@ -0,0 +1,27 @@
1
+ import { FC, ForwardRefExoticComponent, RefAttributes, ReactNode } from "react";
2
+ import { IMenuControlledItem } from "../menuItem";
3
+ import { IZenComponentProps } from "../../../commonHelpers/zenComponent";
4
+ import { IMenuSeparator } from "../menuSeparator";
5
+ import { TAlignment } from "../../../absolute/absolute";
6
+ export type TMenuListDirection = "vertical" | "horizontal";
7
+ export interface IControlledMenuList extends IZenComponentProps {
8
+ listClassName?: string;
9
+ ariaLabel?: string;
10
+ /** Default tooltip alignment injected into MenuItem children that don't specify their own. */
11
+ defaultTooltipAlignment?: TAlignment;
12
+ /** Default submenu alignment injected into MenuItem children that don't specify their own. */
13
+ defaultAlignment?: TAlignment;
14
+ /** Layout direction of the menu list. Defaults to "vertical". */
15
+ direction?: TMenuListDirection;
16
+ }
17
+ /** Internal props injected by ControlledMenu — not part of the public API. */
18
+ export interface IControlledMenuListInternal extends IControlledMenuList {
19
+ isOpen?: boolean;
20
+ setIsOpen?: (v: boolean) => void;
21
+ }
22
+ export declare const ControlledMenuList: ForwardRefExoticComponent<IControlledMenuList & {
23
+ children?: ReactNode;
24
+ } & RefAttributes<HTMLDivElement>> & {
25
+ Item: FC<IMenuControlledItem>;
26
+ Separator: FC<IMenuSeparator>;
27
+ };
@@ -0,0 +1,120 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /* eslint-disable @typescript-eslint/naming-convention */
3
+ import { Children, cloneElement, isValidElement, useState, useMemo, useCallback, useRef, forwardRef, Fragment } from "react";
4
+ import { MenuItem, isMenuItem } from "../menuItem";
5
+ import { classNames } from "../../../commonHelpers/classNames/classNames";
6
+ import { findContent } from "../../utils/findContent";
7
+ import { getItemLabel } from "../../utils/getItemLabel";
8
+ import { normalizeSeparators } from "../../utils/normalizeSeparators";
9
+ import { useMenuPath } from "../../utils/useMenuPath";
10
+ import { useLastValidSheet } from "../../utils/useLastValidSheet";
11
+ import { useMenuListKeyboardNav } from "../../utils/useMenuListKeyboardNav";
12
+ import { MenuButton } from "../menuButton";
13
+ import { IconArrowLeft } from "../../../icons/iconArrowLeft";
14
+ import { DeviceType } from "../../../commonHelpers/hooks/deviceType";
15
+ import { useDeviceType } from "../../../commonHelpers/hooks/useDeviceType";
16
+ import { generateId } from "../../../commonHelpers/generateId";
17
+ import { PathProvider } from "../../contexts/pathProvider";
18
+ import { MobileSheet } from "../../../mobileSheet/mobileSheet";
19
+ import { isSeparator, MenuSeparator } from "../menuSeparator";
20
+ const ControlledMenuListBase = forwardRef(({ children, setIsOpen, isOpen, className = "", listClassName = "", ariaLabel, defaultTooltipAlignment, defaultAlignment, direction = "vertical" }, ref) => {
21
+ const [deviceType, setDeviceType] = useState(DeviceType.Desktop);
22
+ const isMobile = deviceType === DeviceType.Mobile;
23
+ useDeviceType(setDeviceType);
24
+ // True when mounted inside ControlledMenu, which injects setIsOpen and isOpen.
25
+ // Standalone usage leaves both undefined.
26
+ const isEmbedded = setIsOpen !== undefined;
27
+ const { path, onOpenBranch, closeBranch, closeAll } = useMenuPath(isOpen);
28
+ const navigatedViaKeyboardRef = useRef(false);
29
+ const keyboardActiveRef = useRef(false);
30
+ const internalRef = useRef(null);
31
+ const divRefCallback = useCallback((node) => {
32
+ internalRef.current = node;
33
+ if (typeof ref === "function") {
34
+ ref(node);
35
+ }
36
+ else if (ref) {
37
+ ref.current = node;
38
+ }
39
+ }, [ref]);
40
+ const effectiveSetIsOpen = useCallback((v) => {
41
+ setIsOpen === null || setIsOpen === void 0 ? void 0 : setIsOpen(v);
42
+ if (!v && !isEmbedded) {
43
+ closeAll();
44
+ }
45
+ }, [setIsOpen, closeAll, isEmbedded]);
46
+ const buildListItems = useCallback((childrenToProcess) => {
47
+ var _a;
48
+ const cont = [];
49
+ // If children is a Fragment, extract its children
50
+ let actualChildren = childrenToProcess;
51
+ if (isValidElement(childrenToProcess) && childrenToProcess.type === Fragment) {
52
+ actualChildren = (_a = childrenToProcess.props.children) !== null && _a !== void 0 ? _a : [];
53
+ }
54
+ Children.map(actualChildren, (child) => {
55
+ var _a, _b;
56
+ if (!child)
57
+ return;
58
+ if (typeof child === "string") {
59
+ cont.push(_jsx("li", { role: "presentation", className: classNames(["zen-menu-item__content"]), children: child }, generateId()));
60
+ return;
61
+ }
62
+ if (isValidElement(child) && isSeparator(child)) {
63
+ cont.push(cloneElement(child, { key: (_a = child.key) !== null && _a !== void 0 ? _a : generateId() }));
64
+ return;
65
+ }
66
+ if (isValidElement(child) && isMenuItem(child)) {
67
+ const childProps = child.props;
68
+ cont.push(cloneElement(child, {
69
+ isMobile,
70
+ key: (_b = childProps.id) !== null && _b !== void 0 ? _b : generateId(),
71
+ setIsOpen: effectiveSetIsOpen,
72
+ onClick: childProps.onClick,
73
+ tooltipAlignment: childProps.tooltipAlignment || defaultTooltipAlignment || undefined,
74
+ alignment: childProps.alignment || defaultAlignment || undefined
75
+ }));
76
+ return;
77
+ }
78
+ const childProps = child.props;
79
+ cont.push(_jsx("li", { className: classNames(["zen-menu-item__content"]), role: "presentation", children: child }, childProps.id || childProps["data-id"] || generateId()));
80
+ });
81
+ return normalizeSeparators(cont);
82
+ }, [isMobile, effectiveSetIsOpen, defaultTooltipAlignment, defaultAlignment]);
83
+ // Embedded (ControlledMenu): switch content based on path so the outer MobileSheet
84
+ // shows nested items with the back button as a list row.
85
+ // Standalone: always show top-level content; nested navigation uses its own MobileSheet.
86
+ const [content, parent] = useMemo(() => {
87
+ if (isEmbedded && isMobile && path.length > 0) {
88
+ const el = findContent(children, isMenuItem, path[path.length - 1]);
89
+ if (el && isValidElement(el)) {
90
+ return [buildListItems(el.props.children), el];
91
+ }
92
+ }
93
+ return [buildListItems(children), null];
94
+ }, [children, isMobile, path, isEmbedded, buildListItems]);
95
+ // Standalone mobile only: compute nested content for the inline MobileSheet.
96
+ const [nestedContent, nestedParent] = useMemo(() => {
97
+ if (isEmbedded || !isMobile || path.length === 0) {
98
+ return [null, null];
99
+ }
100
+ const el = findContent(children, isMenuItem, path[path.length - 1]);
101
+ if (!el || !isValidElement(el)) {
102
+ return [null, null];
103
+ }
104
+ return [buildListItems(el.props.children), el];
105
+ }, [children, isMobile, path, isEmbedded, buildListItems]);
106
+ const isHorizontal = direction === "horizontal";
107
+ const { onKeyDown, onKeyDownVertical, onMouseDown } = useMenuListKeyboardNav(keyboardActiveRef, navigatedViaKeyboardRef, isHorizontal);
108
+ const renderList = (listContent, backParent, listRef, applyDirection = true, keyDownHandler = onKeyDown) => (_jsx("div", { ref: listRef, tabIndex: -1, onKeyDown: keyDownHandler, onMouseDown: onMouseDown, className: classNames(["zen-action-list", className]), children: _jsxs("ul", { role: "menu", "aria-label": ariaLabel, className: classNames([
109
+ "zen-menu-item",
110
+ className,
111
+ listClassName,
112
+ applyDirection && direction === "horizontal" ? "zen-menu-item--horizontal" : ""
113
+ ]), children: [backParent ? (_jsx(MenuButton, { id: "root", name: getItemLabel(backParent), icon: IconArrowLeft, onClick: closeBranch, hasChildren: false, disabled: false }, "root")) : null, listContent] }) }));
114
+ const { sheetContent, sheetParent, sheetPathLength, sheetParentName } = useLastValidSheet(nestedContent, nestedParent, path, children);
115
+ return (_jsxs(PathProvider, { path: path, onOpenBranch: onOpenBranch, closeBranch: closeBranch, navigatedViaKeyboardRef: navigatedViaKeyboardRef, keyboardActiveRef: keyboardActiveRef, children: [renderList(content, parent, divRefCallback), !isEmbedded && isMobile && (_jsxs(MobileSheet, { label: sheetParentName, isOpen: path.length > 0, triggerRef: internalRef, onHidePanel: closeAll, onCloseClick: closeAll, children: [_jsx(MobileSheet.Title, { children: sheetParentName }), _jsx(MobileSheet.Content, { children: renderList(sheetContent, sheetPathLength > 1 ? sheetParent : null, undefined, false, onKeyDownVertical) })] }))] }));
116
+ });
117
+ ControlledMenuListBase.displayName = "ControlledMenuList";
118
+ export const ControlledMenuList = ControlledMenuListBase;
119
+ ControlledMenuList.Item = MenuItem;
120
+ ControlledMenuList.Separator = MenuSeparator;
@@ -0,0 +1,37 @@
1
+ import { FC, ReactNode } from "react";
2
+ import { IControlledMenuListInternal } from "./controlledMenuList/controlledMenuList";
3
+ import { IMenuControlledItem } from "./menuItem";
4
+ import { IMenuSeparator } from "./menuSeparator";
5
+ export interface ICreateControlledMenuListRenderProps {
6
+ children: ReactNode;
7
+ }
8
+ type CreatedListComponent<T> = FC<T & IControlledMenuListInternal & {
9
+ children?: ReactNode;
10
+ }> & {
11
+ Item: FC<IMenuControlledItem>;
12
+ Separator: FC<IMenuSeparator>;
13
+ };
14
+ /**
15
+ * Factory that creates a typed ControlledMenuList component with custom props.
16
+ *
17
+ * The `renderContent` function receives all consumer props plus `children` (the built ControlledMenuList element),
18
+ * allowing the consumer to wrap the list with custom markup.
19
+ *
20
+ * @example
21
+ * interface INavSection { sectionTitle: string }
22
+ *
23
+ * const NavSection = createControlledMenuList<INavSection>(({ sectionTitle, children }) => (
24
+ * <div>
25
+ * <div className="section-header">{sectionTitle}</div>
26
+ * {children}
27
+ * </div>
28
+ * ));
29
+ *
30
+ * // Usage — accepts INavSection + IControlledMenuList props:
31
+ * <NavSection sectionTitle="Reports" setIsOpen={setIsOpen} isOpen={isOpen}>
32
+ * <NavSection.Item id="daily" name="Daily Reports" />
33
+ * <NavSection.Item id="weekly" name="Weekly Reports" />
34
+ * </NavSection>
35
+ */
36
+ export declare function createControlledMenuList<T extends object>(renderContent: (props: T & ICreateControlledMenuListRenderProps) => ReactNode): CreatedListComponent<T>;
37
+ export {};
@@ -0,0 +1,51 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { jsx as _jsx } from "react/jsx-runtime";
13
+ import { ControlledMenuList } from "./controlledMenuList/controlledMenuList";
14
+ import { MenuItem } from "./menuItem";
15
+ import { MenuSeparator } from "./menuSeparator";
16
+ /**
17
+ * Factory that creates a typed ControlledMenuList component with custom props.
18
+ *
19
+ * The `renderContent` function receives all consumer props plus `children` (the built ControlledMenuList element),
20
+ * allowing the consumer to wrap the list with custom markup.
21
+ *
22
+ * @example
23
+ * interface INavSection { sectionTitle: string }
24
+ *
25
+ * const NavSection = createControlledMenuList<INavSection>(({ sectionTitle, children }) => (
26
+ * <div>
27
+ * <div className="section-header">{sectionTitle}</div>
28
+ * {children}
29
+ * </div>
30
+ * ));
31
+ *
32
+ * // Usage — accepts INavSection + IControlledMenuList props:
33
+ * <NavSection sectionTitle="Reports" setIsOpen={setIsOpen} isOpen={isOpen}>
34
+ * <NavSection.Item id="daily" name="Daily Reports" />
35
+ * <NavSection.Item id="weekly" name="Weekly Reports" />
36
+ * </NavSection>
37
+ */
38
+ export function createControlledMenuList(renderContent) {
39
+ const CreatedList = allProps => {
40
+ const { setIsOpen, listClassName, isOpen, defaultTooltipAlignment, defaultAlignment, direction, className, children } = allProps, rest = __rest(allProps, ["setIsOpen", "listClassName", "isOpen", "defaultTooltipAlignment", "defaultAlignment", "direction", "className", "children"]);
41
+ // Cast required to pass internal props (isOpen, setIsOpen) not in the public interface.
42
+ const InternalList = ControlledMenuList;
43
+ const list = (_jsx(InternalList, { setIsOpen: setIsOpen, listClassName: listClassName, isOpen: isOpen, defaultTooltipAlignment: defaultTooltipAlignment, defaultAlignment: defaultAlignment, direction: direction, className: className, children: children }));
44
+ return renderContent(Object.assign(Object.assign({}, rest), { children: list }));
45
+ };
46
+ CreatedList.displayName = "ControlledMenuList";
47
+ const result = CreatedList;
48
+ result.Item = MenuItem;
49
+ result.Separator = MenuSeparator;
50
+ return result;
51
+ }
@@ -0,0 +1,67 @@
1
+ import { ReactNode, ForwardRefExoticComponent, RefAttributes, ForwardedRef } from "react";
2
+ import { TAlignment } from "../../absolute/absolute";
3
+ import { IMenuItem } from "./menuItem";
4
+ import "./menuButton.less";
5
+ export interface ICreateMenuItemRenderProps {
6
+ hasChildren: boolean;
7
+ isOpen: boolean;
8
+ isMobile: boolean;
9
+ }
10
+ export interface ICreateMenuItemWrapperProps {
11
+ role: "presentation";
12
+ className: string;
13
+ tabIndex?: number;
14
+ }
15
+ /**
16
+ * Factory that creates a typed Menu item component with full MenuItem functionality
17
+ * (nested submenus, keyboard navigation, mobile sheet support), but with a
18
+ * consumer-defined typed interface instead of the fixed MenuItem props.
19
+ *
20
+ * The `renderTrigger` function receives all consumer props plus `hasChildren` and
21
+ * `isOpen` so the trigger content can reflect submenu state.
22
+ *
23
+ * @example
24
+ * interface INavItem { label: string; count?: number }
25
+ *
26
+ * const NavMenuItem = createMenuItem<INavItem>(({ label, count, hasChildren, isOpen }) => (
27
+ * <>
28
+ * <span>{label}</span>
29
+ * {count !== undefined && <Badge>{count}</Badge>}
30
+ * {hasChildren && <ChevronRight rotated={isOpen} />}
31
+ * </>
32
+ * ));
33
+ *
34
+ * // Usage — supports nesting exactly like Menu.Item:
35
+ * <Menu title="Nav">
36
+ * <NavMenuItem id="reports" label="Reports" count={3}>
37
+ * <Menu.Item id="charts" name="Charts" />
38
+ * <Menu.Item id="tables" name="Tables" />
39
+ * </NavMenuItem>
40
+ * </Menu>
41
+ *
42
+ * @example
43
+ * // renderWrapper: replace the default <li> with a custom host element
44
+ * <NavMenuItem
45
+ * id="settings"
46
+ * label="Settings"
47
+ * renderWrapper={(children, ref, defaultProps) => (
48
+ * <li ref={ref} {...defaultProps} data-testid="nav-settings">
49
+ * {children}
50
+ * </li>
51
+ * )}
52
+ * />
53
+ */
54
+ type PublicMenuItemProps<T> = T & Pick<IMenuItem, "id" | "children" | "className" | "alignment" | "disabled" | "onClick" | "link" | "target" | "rel"> & {
55
+ ariaLabel?: string;
56
+ onOpenChange?: (isOpen: boolean) => void;
57
+ renderWrapper?: (children: ReactNode, ref: ForwardedRef<HTMLLIElement>, defaultProps: ICreateMenuItemWrapperProps) => ReactNode;
58
+ tabIndex?: number;
59
+ tooltip?: ReactNode;
60
+ tooltipAlignment?: TAlignment;
61
+ };
62
+ export interface ICreateMenuItemOptions<T> {
63
+ /** Hook-compatible function called during render to derive a default tooltip from item props. Can call useContext. */
64
+ renderTooltip?: (props: T) => ReactNode | undefined;
65
+ }
66
+ export declare function createMenuItem<T extends object>(renderTrigger: (props: T & ICreateMenuItemRenderProps) => ReactNode, options?: ICreateMenuItemOptions<T>): ForwardRefExoticComponent<PublicMenuItemProps<T> & RefAttributes<HTMLLIElement>>;
67
+ export {};
@@ -0,0 +1,93 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ import { Fragment, forwardRef, useMemo } from "react";
14
+ import { ControlledPopup } from "../../controlledPopup/controlledPopup";
15
+ import { classNames } from "../../commonHelpers/classNames/classNames";
16
+ import { useMenuItemCore } from "../utils/useMenuItemCore";
17
+ import { Tooltip } from "../../tooltip/tooltip";
18
+ import { isSafeHref } from "../utils/isSafeHref";
19
+ import { getSafeRel } from "../utils/getSafeRel";
20
+ import { useDriveClassName } from "../../utils/theme/useDriveClassName";
21
+ export function createMenuItem(renderTrigger, options) {
22
+ var _a;
23
+ // Defined at factory level so it is always the same function reference —
24
+ // safe to call unconditionally inside the component (rules of hooks).
25
+ const resolveDefaultTooltip = (_a = options === null || options === void 0 ? void 0 : options.renderTooltip) !== null && _a !== void 0 ? _a : (() => undefined);
26
+ const CreatedItem = forwardRef((allProps, containerRef) => {
27
+ const { id, children, className, alignment, disabled, isMobile = false, setIsOpen, ariaLabel, onClick, onOpenChange, link, target, rel, renderWrapper, tabIndex, tooltip, tooltipAlignment } = allProps, rest = __rest(allProps, ["id", "children", "className", "alignment", "disabled", "isMobile", "setIsOpen", "ariaLabel", "onClick", "onOpenChange", "link", "target", "rel", "renderWrapper", "tabIndex", "tooltip", "tooltipAlignment"]);
28
+ const linkEvents = useMemo(() => ({
29
+ onKeyDown: (e) => {
30
+ if (e.key === " ") {
31
+ e.preventDefault();
32
+ const linkEl = e.currentTarget;
33
+ linkEl.dataset.spaceDown = "1";
34
+ linkEl.classList.add("zen-menu-button__action--active");
35
+ }
36
+ },
37
+ onKeyUp: (e) => {
38
+ if (e.key === " ") {
39
+ e.preventDefault();
40
+ const linkEl = e.currentTarget;
41
+ linkEl.classList.remove("zen-menu-button__action--active");
42
+ if (linkEl.dataset.spaceDown) {
43
+ delete linkEl.dataset.spaceDown;
44
+ linkEl.click();
45
+ }
46
+ }
47
+ }
48
+ }), []);
49
+ const { ref, isOpen, hasChildren, content, openedViaKeyboard, contentAlignment, path, handleDesktopActionClick, handleTriggerClick, handleOpenChange } = useMenuItemCore({ id, children, className, alignment, isMobile, setIsOpen, onClick, onOpenChange });
50
+ const driveClass = useDriveClassName("zen-menu-button__action");
51
+ const liDefaultProps = Object.assign({ role: "presentation", className: classNames(["zen-menu-button", className !== null && className !== void 0 ? className : ""]) }, (tabIndex !== undefined && { tabIndex }));
52
+ // Resolved at component top level so renderTooltip can safely call hooks (e.g. useContext).
53
+ const defaultTooltip = resolveDefaultTooltip(Object.assign(Object.assign({}, rest), { isMobile }));
54
+ const effectiveTooltip = tooltip !== null && tooltip !== void 0 ? tooltip : defaultTooltip;
55
+ const effectiveTooltipAlignment = tooltipAlignment;
56
+ const wrapButton = (button) => {
57
+ // On mobile, tooltip wraps the <li> instead of the button so Tooltip doesn't
58
+ // intercept the button's onClick and shows on tap instead.
59
+ // Items with children are excluded — wrapping <li> would fire both the tooltip and
60
+ // the submenu drill-in on the same tap.
61
+ const isMobileTooltip = isMobile && !!effectiveTooltip && !hasChildren;
62
+ const wrappedButton = effectiveTooltip && !isMobileTooltip ? (_jsx(Tooltip, { trigger: button, alignment: effectiveTooltipAlignment, children: effectiveTooltip })) : (button);
63
+ const liElement = renderWrapper ? (renderWrapper(wrappedButton, containerRef, liDefaultProps)) : (_jsx("li", Object.assign({ ref: containerRef }, liDefaultProps, { tabIndex: tabIndex, children: wrappedButton })));
64
+ // Wrap li with Tooltip on mobile
65
+ return isMobileTooltip ? (_jsx(Tooltip, { trigger: liElement, alignment: effectiveTooltipAlignment, children: effectiveTooltip })) : (liElement);
66
+ };
67
+ // Leaf node — no submenu
68
+ if (!hasChildren) {
69
+ const triggerContent = renderTrigger(Object.assign(Object.assign({}, rest), { hasChildren: false, isOpen: false, isMobile }));
70
+ return wrapButton(link ? (_jsx("a", Object.assign({ role: "menuitem", "aria-label": ariaLabel, "aria-disabled": disabled, href: disabled || !isSafeHref(link) ? undefined : link, target: target, rel: getSafeRel(rel, target), className: classNames(["zen-menu-button__action", "zen-caption", driveClass !== null && driveClass !== void 0 ? driveClass : ""]), onClick: handleDesktopActionClick }, linkEvents, { children: triggerContent }))) : (_jsx("button", { ref: ref, type: "button", role: "menuitem", "aria-label": ariaLabel, disabled: !!disabled, className: classNames(["zen-menu-button__action", "zen-button", "zen-caption", driveClass !== null && driveClass !== void 0 ? driveClass : ""]), onClick: handleDesktopActionClick, children: triggerContent })));
71
+ }
72
+ // Mobile — button drills into sub-level via PathContext
73
+ if (isMobile) {
74
+ return wrapButton(_jsx("button", { ref: ref, type: "button", role: "menuitem", "aria-label": ariaLabel, disabled: !!disabled, className: classNames([
75
+ "zen-menu-button__action",
76
+ "zen-button",
77
+ "zen-caption",
78
+ "zen-menu-button__action--has-children",
79
+ driveClass !== null && driveClass !== void 0 ? driveClass : ""
80
+ ]), onClick: handleTriggerClick, children: renderTrigger(Object.assign(Object.assign({}, rest), { hasChildren: true, isOpen, isMobile })) }));
81
+ }
82
+ // Desktop with submenu
83
+ return (_jsxs(Fragment, { children: [wrapButton(_jsx("button", { ref: ref, type: "button", role: "menuitem", "aria-label": ariaLabel, disabled: !!disabled, "aria-haspopup": "menu", "aria-expanded": isOpen, className: classNames([
84
+ "zen-menu-button__action",
85
+ "zen-button",
86
+ "zen-caption",
87
+ "zen-menu-button__action--has-children",
88
+ driveClass !== null && driveClass !== void 0 ? driveClass : ""
89
+ ]), onClick: handleTriggerClick, children: renderTrigger(Object.assign(Object.assign({}, rest), { hasChildren: true, isOpen, isMobile: false })) })), _jsx(ControlledPopup, { className: classNames([`zen-controlled-menu-submenu--${path.length}`]), useTrapFocusWithTrigger: openedViaKeyboard ? "on" : "withTrigger", alignment: contentAlignment, triggerRef: ref, isOpen: isOpen, onOpenChange: handleOpenChange, ariaLabel: ariaLabel, recalculateOnScroll: true, children: _jsx("ul", { role: "menu", className: "zen-menu-item", children: content }) })] }));
90
+ });
91
+ CreatedItem.displayName = "MenuItem";
92
+ return CreatedItem;
93
+ }
@@ -5,6 +5,8 @@ import { IconChevronRight } from "../../icons/iconChevronRight";
5
5
  import { useDriveClassName } from "../../utils/theme/useDriveClassName";
6
6
  import { useDrive } from "../../utils/theme/useDrive";
7
7
  import { getMenuButtonState } from "../utils/getMenuButtonState";
8
+ import { isSafeHref } from "../utils/isSafeHref";
9
+ import { getSafeRel } from "../utils/getSafeRel";
8
10
  export const MenuButton = ({ id, onClick, hasChildren, disabled, icon, name, link, target, rel, className = "", active = null, ref }) => {
9
11
  const { hasState, isActive } = getMenuButtonState(active, disabled);
10
12
  const driveMenuButtonActionClasses = useDriveClassName("zen-menu-button__action");
@@ -14,6 +16,7 @@ export const MenuButton = ({ id, onClick, hasChildren, disabled, icon, name, lin
14
16
  if (e.key === " ") {
15
17
  e.preventDefault();
16
18
  const linkEl = e.target;
19
+ linkEl.dataset.spaceDown = "1";
17
20
  linkEl.classList.add("zen-menu-button__action--active");
18
21
  }
19
22
  },
@@ -22,7 +25,10 @@ export const MenuButton = ({ id, onClick, hasChildren, disabled, icon, name, lin
22
25
  e.preventDefault();
23
26
  const linkEl = e.target;
24
27
  linkEl.classList.remove("zen-menu-button__action--active");
25
- linkEl.click();
28
+ if (linkEl.dataset.spaceDown) {
29
+ delete linkEl.dataset.spaceDown;
30
+ linkEl.click();
31
+ }
26
32
  }
27
33
  }
28
34
  }), []);
@@ -46,7 +52,7 @@ export const MenuButton = ({ id, onClick, hasChildren, disabled, icon, name, lin
46
52
  "zen-caption",
47
53
  disabled ? "zen-menu-button__action--disabled" : "",
48
54
  driveMenuButtonActionClasses || ""
49
- ]), href: disabled ? undefined : link, "aria-disabled": disabled, onClick: onClickHandler, target: target, rel: rel }, linkEvents, { children: [!!icon &&
55
+ ]), href: disabled || !isSafeHref(link) ? undefined : link, "aria-disabled": disabled, onClick: onClickHandler, target: target, rel: getSafeRel(rel, target) }, linkEvents, { children: [!!icon &&
50
56
  createElement(icon, {
51
57
  size: isDrive ? "huge" : "large",
52
58
  className: "zen-caption__pre-content"
@@ -2,6 +2,7 @@ import { FC, ReactElement } from "react";
2
2
  import { IMenuButton } from "./menuButton";
3
3
  import "./menuItem.less";
4
4
  import { TAlignment } from "../../absolute/absolute";
5
+ export { isMenuItem } from "../utils/isMenuItem";
5
6
  interface IMenuItemInternal {
6
7
  isMobile?: boolean;
7
8
  setIsOpen?: (v: boolean) => void;
@@ -12,6 +13,4 @@ export interface IMenuControlledItem extends IMenuItem {
12
13
  export interface IMenuItem extends IMenuButton {
13
14
  alignment?: TAlignment;
14
15
  }
15
- export declare const isMenuItem: (element: ReactElement | undefined) => boolean;
16
16
  export declare const MenuItem: FC<Omit<IMenuItem & IMenuControlledItem & IMenuItemInternal, "hasChildren">>;
17
- export {};