@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.
- package/README.md +6 -0
- package/dist/index.css +8 -1
- package/dist/menu/components/controlledMenuList/controlledMenuList.d.ts +27 -0
- package/dist/menu/components/controlledMenuList/controlledMenuList.js +123 -0
- package/dist/menu/components/createControlledMenuList.d.ts +37 -0
- package/dist/menu/components/createControlledMenuList.js +55 -0
- package/dist/menu/components/createMenuItem.d.ts +67 -0
- package/dist/menu/components/createMenuItem.js +97 -0
- package/dist/menu/components/menuButton.js +8 -2
- package/dist/menu/components/menuItem.d.ts +1 -2
- package/dist/menu/components/menuItem.js +20 -74
- package/dist/menu/contexts/usePathContext.d.ts +2 -0
- package/dist/menu/contexts/usePathContext.js +9 -0
- package/dist/menu/controlledMenu.js +8 -175
- package/dist/menu/utils/buildMenuContent.d.ts +2 -0
- package/dist/menu/utils/buildMenuContent.js +38 -0
- package/dist/menu/utils/findContent.d.ts +2 -2
- package/dist/menu/utils/findContent.js +4 -3
- package/dist/menu/utils/getItemLabel.d.ts +2 -0
- package/dist/menu/utils/getItemLabel.js +8 -0
- package/dist/menu/utils/getSafeRel.d.ts +1 -0
- package/dist/menu/utils/getSafeRel.js +14 -0
- package/dist/menu/utils/isMenuItem.d.ts +2 -0
- package/dist/menu/utils/isMenuItem.js +13 -0
- package/dist/menu/utils/isSafeHref.d.ts +1 -0
- package/dist/menu/utils/isSafeHref.js +10 -0
- package/dist/menu/utils/normalizeSeparators.d.ts +2 -0
- package/dist/menu/utils/normalizeSeparators.js +23 -0
- package/dist/menu/utils/resolveKeys.d.ts +12 -0
- package/dist/menu/utils/resolveKeys.js +22 -0
- package/dist/menu/utils/useLastValidSheet.d.ts +7 -0
- package/dist/menu/utils/useLastValidSheet.js +30 -0
- package/dist/menu/utils/useMenuItemCore.d.ts +31 -0
- package/dist/menu/utils/useMenuItemCore.js +51 -0
- package/dist/menu/utils/useMenuItemKeyboardNav.d.ts +2 -0
- package/dist/menu/utils/useMenuItemKeyboardNav.js +15 -0
- package/dist/menu/utils/useMenuListKeyboardNav.d.ts +12 -0
- package/dist/menu/utils/useMenuListKeyboardNav.js +77 -0
- package/dist/menu/utils/useMenuPath.d.ts +6 -0
- package/dist/menu/utils/useMenuPath.js +35 -0
- package/dist/nav/navItem/navItem.js +6 -4
- package/dist/nav/navSection/navSection.js +7 -5
- package/esm/menu/components/controlledMenuList/controlledMenuList.d.ts +27 -0
- package/esm/menu/components/controlledMenuList/controlledMenuList.js +120 -0
- package/esm/menu/components/createControlledMenuList.d.ts +37 -0
- package/esm/menu/components/createControlledMenuList.js +51 -0
- package/esm/menu/components/createMenuItem.d.ts +67 -0
- package/esm/menu/components/createMenuItem.js +93 -0
- package/esm/menu/components/menuButton.js +8 -2
- package/esm/menu/components/menuItem.d.ts +1 -2
- package/esm/menu/components/menuItem.js +20 -74
- package/esm/menu/contexts/usePathContext.d.ts +2 -0
- package/esm/menu/contexts/usePathContext.js +5 -0
- package/esm/menu/controlledMenu.js +10 -177
- package/esm/menu/utils/buildMenuContent.d.ts +2 -0
- package/esm/menu/utils/buildMenuContent.js +34 -0
- package/esm/menu/utils/findContent.d.ts +2 -2
- package/esm/menu/utils/findContent.js +4 -3
- package/esm/menu/utils/getItemLabel.d.ts +2 -0
- package/esm/menu/utils/getItemLabel.js +4 -0
- package/esm/menu/utils/getSafeRel.d.ts +1 -0
- package/esm/menu/utils/getSafeRel.js +10 -0
- package/esm/menu/utils/isMenuItem.d.ts +2 -0
- package/esm/menu/utils/isMenuItem.js +9 -0
- package/esm/menu/utils/isSafeHref.d.ts +1 -0
- package/esm/menu/utils/isSafeHref.js +6 -0
- package/esm/menu/utils/normalizeSeparators.d.ts +2 -0
- package/esm/menu/utils/normalizeSeparators.js +19 -0
- package/esm/menu/utils/resolveKeys.d.ts +12 -0
- package/esm/menu/utils/resolveKeys.js +18 -0
- package/esm/menu/utils/useLastValidSheet.d.ts +7 -0
- package/esm/menu/utils/useLastValidSheet.js +26 -0
- package/esm/menu/utils/useMenuItemCore.d.ts +31 -0
- package/esm/menu/utils/useMenuItemCore.js +47 -0
- package/esm/menu/utils/useMenuItemKeyboardNav.d.ts +2 -0
- package/esm/menu/utils/useMenuItemKeyboardNav.js +11 -0
- package/esm/menu/utils/useMenuListKeyboardNav.d.ts +12 -0
- package/esm/menu/utils/useMenuListKeyboardNav.js +73 -0
- package/esm/menu/utils/useMenuPath.d.ts +6 -0
- package/esm/menu/utils/useMenuPath.js +31 -0
- package/esm/nav/navItem/navItem.js +6 -4
- package/esm/nav/navSection/navSection.js +7 -5
- 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
|
|
3
|
-
import {
|
|
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 {
|
|
10
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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";
|
|
@@ -1,45 +1,26 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
2
|
-
|
|
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
|
-
}, [
|
|
50
|
+
}, [setIsOpen]);
|
|
215
51
|
if (isMobile) {
|
|
216
|
-
return (
|
|
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(
|
|
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 =
|
|
224
|
-
ControlledMenu.Separator =
|
|
56
|
+
ControlledMenu.Item = ControlledMenuList.Item;
|
|
57
|
+
ControlledMenu.Separator = ControlledMenuList.Separator;
|
|
225
58
|
export const TRANSLATIONS = ["Back"];
|
|
@@ -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
|
|
2
|
-
export declare const findContent: (children: ReactNode,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
15
|
+
found = findContent(childProps.children, matcher, id);
|
|
15
16
|
}
|
|
16
17
|
});
|
|
17
18
|
return found;
|
|
@@ -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,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,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
|
+
};
|