@geotab/zenith 3.6.3 → 3.7.0

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 (149) hide show
  1. package/README.md +15 -0
  2. package/dist/absolute/absolute.d.ts +3 -1
  3. package/dist/absolute/absolute.js +4 -4
  4. package/dist/checkbox/checkbox.d.ts +3 -1
  5. package/dist/checkbox/checkbox.js +5 -8
  6. package/dist/checkboxListWithAction/checkboxListWithAction.d.ts +3 -1
  7. package/dist/checkboxListWithAction/checkboxListWithAction.js +185 -3
  8. package/dist/controlledPopup/controlledPopup.d.ts +3 -1
  9. package/dist/controlledPopup/controlledPopup.js +2 -2
  10. package/dist/dropdown/dropdownPopup.d.ts +1 -1
  11. package/dist/dropdown/dropdownTrigger.d.ts +1 -1
  12. package/dist/dropdownRaw/dropdownList.d.ts +2 -1
  13. package/dist/dropdownRaw/dropdownList.js +15 -4
  14. package/dist/dropdownRaw/dropdownPopup.d.ts +3 -1
  15. package/dist/dropdownRaw/dropdownPopup.js +35 -5
  16. package/dist/dropdownRaw/dropdownRaw.js +82 -7
  17. package/dist/dropdownRaw/dropdownSearchableTrigger.d.ts +2 -1
  18. package/dist/dropdownRaw/dropdownSearchableTrigger.js +7 -2
  19. package/dist/dropdownRaw/dropdownTrigger.d.ts +2 -1
  20. package/dist/dropdownRaw/dropdownTrigger.js +3 -5
  21. package/dist/footerButtons/footerButtons.d.ts +6 -4
  22. package/dist/footerButtons/footerButtons.js +3 -8
  23. package/dist/groupsFilterRaw/groupsFilterCurrentlySelectedState.js +8 -2
  24. package/dist/groupsFilterRaw/groupsFilterInitialState.js +16 -0
  25. package/dist/groupsFilterRaw/groupsFilterRaw.js +43 -9
  26. package/dist/groupsFilterRaw/groupsFilterTrigger.d.ts +2 -1
  27. package/dist/groupsFilterRaw/groupsFilterTrigger.js +13 -2
  28. package/dist/index.css +222 -18
  29. package/dist/index.d.ts +1 -1
  30. package/dist/list/hooks/useDragAndDrop.d.ts +3 -1
  31. package/dist/list/hooks/useDragAndDrop.js +11 -4
  32. package/dist/menu/components/menuItem.js +12 -2
  33. package/dist/menu/contexts/pathContext.d.ts +3 -1
  34. package/dist/menu/contexts/pathProvider.d.ts +1 -1
  35. package/dist/menu/contexts/pathProvider.js +1 -1
  36. package/dist/menu/controlledMenu.js +97 -29
  37. package/dist/nav/nav.d.ts +2 -1
  38. package/dist/nav/nav.js +3 -2
  39. package/dist/nav/navEditList/navEditList.js +2 -1
  40. package/dist/nav/navItem/navItem.d.ts +1 -0
  41. package/dist/nav/navItem/navItem.js +4 -1
  42. package/dist/pillExpandable/pillContent.d.ts +2 -1
  43. package/dist/pillExpandable/pillContent.js +8 -3
  44. package/dist/pillExpandable/pillExpandable.d.ts +27 -4
  45. package/dist/pillExpandable/pillExpandable.js +426 -196
  46. package/dist/pillExpandable/pillExpandablePopoverContent.d.ts +19 -0
  47. package/dist/pillExpandable/pillExpandablePopoverContent.js +7 -0
  48. package/dist/pillExpandable/pillExpandableSimple.d.ts +5 -3
  49. package/dist/pillExpandable/pillExpandableSimple.js +12 -5
  50. package/dist/summary/summary.js +29 -1
  51. package/dist/summaryTile/summaryTile.d.ts +2 -1
  52. package/dist/summaryTile/summaryTile.js +175 -65
  53. package/dist/summaryTile/summaryTileTrigger.d.ts +1 -1
  54. package/dist/summaryTile/summaryTileTrigger.js +1 -1
  55. package/dist/toggleButtonRaw/toggleButtonRaw.js +2 -1
  56. package/dist/utils/localization/getSupportedLanguage.d.ts +2 -2
  57. package/dist/utils/localization/getSupportedLanguage.js +28 -8
  58. package/dist/utils/localization/languageContext.d.ts +1 -1
  59. package/dist/utils/localization/translations/cs.json +2 -1
  60. package/dist/utils/localization/translations/de.json +2 -1
  61. package/dist/utils/localization/translations/en.json +2 -1
  62. package/dist/utils/localization/translations/es.json +2 -1
  63. package/dist/utils/localization/translations/fr-FR.json +2 -1
  64. package/dist/utils/localization/translations/fr.json +2 -1
  65. package/dist/utils/localization/translations/id.json +2 -1
  66. package/dist/utils/localization/translations/it.json +2 -1
  67. package/dist/utils/localization/translations/ja.json +2 -1
  68. package/dist/utils/localization/translations/ms.json +2 -1
  69. package/dist/utils/localization/translations/nl.json +2 -1
  70. package/dist/utils/localization/translations/pl.json +2 -1
  71. package/dist/utils/localization/translations/pt-BR.json +2 -1
  72. package/dist/utils/localization/translations/sv.json +2 -1
  73. package/dist/utils/localization/translations/th.json +2 -1
  74. package/dist/utils/localization/translations/tr.json +2 -1
  75. package/dist/utils/localization/translations/zh-Hans.json +2 -1
  76. package/esm/absolute/absolute.d.ts +3 -1
  77. package/esm/absolute/absolute.js +4 -4
  78. package/esm/checkbox/checkbox.d.ts +3 -1
  79. package/esm/checkbox/checkbox.js +5 -8
  80. package/esm/checkboxListWithAction/checkboxListWithAction.d.ts +3 -1
  81. package/esm/checkboxListWithAction/checkboxListWithAction.js +186 -4
  82. package/esm/controlledPopup/controlledPopup.d.ts +3 -1
  83. package/esm/controlledPopup/controlledPopup.js +2 -2
  84. package/esm/dropdown/dropdownPopup.d.ts +1 -1
  85. package/esm/dropdown/dropdownTrigger.d.ts +1 -1
  86. package/esm/dropdownRaw/dropdownList.d.ts +2 -1
  87. package/esm/dropdownRaw/dropdownList.js +16 -5
  88. package/esm/dropdownRaw/dropdownPopup.d.ts +3 -1
  89. package/esm/dropdownRaw/dropdownPopup.js +37 -7
  90. package/esm/dropdownRaw/dropdownRaw.js +82 -7
  91. package/esm/dropdownRaw/dropdownSearchableTrigger.d.ts +2 -1
  92. package/esm/dropdownRaw/dropdownSearchableTrigger.js +7 -2
  93. package/esm/dropdownRaw/dropdownTrigger.d.ts +2 -1
  94. package/esm/dropdownRaw/dropdownTrigger.js +3 -5
  95. package/esm/footerButtons/footerButtons.d.ts +6 -4
  96. package/esm/footerButtons/footerButtons.js +3 -8
  97. package/esm/groupsFilterRaw/groupsFilterCurrentlySelectedState.js +9 -3
  98. package/esm/groupsFilterRaw/groupsFilterInitialState.js +16 -0
  99. package/esm/groupsFilterRaw/groupsFilterRaw.js +43 -9
  100. package/esm/groupsFilterRaw/groupsFilterTrigger.d.ts +2 -1
  101. package/esm/groupsFilterRaw/groupsFilterTrigger.js +13 -2
  102. package/esm/index.d.ts +1 -1
  103. package/esm/list/hooks/useDragAndDrop.d.ts +3 -1
  104. package/esm/list/hooks/useDragAndDrop.js +11 -4
  105. package/esm/menu/components/menuItem.js +12 -2
  106. package/esm/menu/contexts/pathContext.d.ts +3 -1
  107. package/esm/menu/contexts/pathProvider.d.ts +1 -1
  108. package/esm/menu/contexts/pathProvider.js +1 -1
  109. package/esm/menu/controlledMenu.js +98 -30
  110. package/esm/nav/nav.d.ts +2 -1
  111. package/esm/nav/nav.js +3 -2
  112. package/esm/nav/navEditList/navEditList.js +2 -1
  113. package/esm/nav/navItem/navItem.d.ts +1 -0
  114. package/esm/nav/navItem/navItem.js +5 -2
  115. package/esm/pillExpandable/pillContent.d.ts +2 -1
  116. package/esm/pillExpandable/pillContent.js +8 -3
  117. package/esm/pillExpandable/pillExpandable.d.ts +27 -4
  118. package/esm/pillExpandable/pillExpandable.js +427 -197
  119. package/esm/pillExpandable/pillExpandablePopoverContent.d.ts +19 -0
  120. package/esm/pillExpandable/pillExpandablePopoverContent.js +3 -0
  121. package/esm/pillExpandable/pillExpandableSimple.d.ts +5 -3
  122. package/esm/pillExpandable/pillExpandableSimple.js +12 -5
  123. package/esm/summary/summary.js +29 -1
  124. package/esm/summaryTile/summaryTile.d.ts +2 -1
  125. package/esm/summaryTile/summaryTile.js +140 -43
  126. package/esm/summaryTile/summaryTileTrigger.d.ts +1 -1
  127. package/esm/summaryTile/summaryTileTrigger.js +1 -1
  128. package/esm/toggleButtonRaw/toggleButtonRaw.js +2 -1
  129. package/esm/utils/localization/getSupportedLanguage.d.ts +2 -2
  130. package/esm/utils/localization/getSupportedLanguage.js +28 -8
  131. package/esm/utils/localization/languageContext.d.ts +1 -1
  132. package/esm/utils/localization/translations/cs.json +2 -1
  133. package/esm/utils/localization/translations/de.json +2 -1
  134. package/esm/utils/localization/translations/en.json +2 -1
  135. package/esm/utils/localization/translations/es.json +2 -1
  136. package/esm/utils/localization/translations/fr-FR.json +2 -1
  137. package/esm/utils/localization/translations/fr.json +2 -1
  138. package/esm/utils/localization/translations/id.json +2 -1
  139. package/esm/utils/localization/translations/it.json +2 -1
  140. package/esm/utils/localization/translations/ja.json +2 -1
  141. package/esm/utils/localization/translations/ms.json +2 -1
  142. package/esm/utils/localization/translations/nl.json +2 -1
  143. package/esm/utils/localization/translations/pl.json +2 -1
  144. package/esm/utils/localization/translations/pt-BR.json +2 -1
  145. package/esm/utils/localization/translations/sv.json +2 -1
  146. package/esm/utils/localization/translations/th.json +2 -1
  147. package/esm/utils/localization/translations/tr.json +2 -1
  148. package/esm/utils/localization/translations/zh-Hans.json +2 -1
  149. package/package.json +4 -7
@@ -24,7 +24,7 @@ export const isMenuItem = (element) => {
24
24
  export const MenuItem = ({ id, children, name, icon, disabled, onClick, link, target, rel, isMobile = false, setIsOpen, trigger, className, active, alignment }) => {
25
25
  const aligmentContext = useContext(MenuAlignmentContext);
26
26
  const contentAlignment = alignment || aligmentContext.alignment || "right-top";
27
- const { path, onOpenBranch, closeBranch } = useContext(PathContext);
27
+ const { path, onOpenBranch, closeBranch, navigatedViaKeyboardRef } = useContext(PathContext);
28
28
  const memoizedDesktopActionOnClick = useCallback((itemId, e) => {
29
29
  setIsOpen === null || setIsOpen === void 0 ? void 0 : setIsOpen(false);
30
30
  onClick === null || onClick === void 0 ? void 0 : onClick(itemId, e);
@@ -83,6 +83,16 @@ export const MenuItem = ({ id, children, name, icon, disabled, onClick, link, ta
83
83
  return cont;
84
84
  }, [children, isMobile, setIsOpen, className]);
85
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;
86
96
  if (content.length === 0) {
87
97
  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);
88
98
  }
@@ -99,6 +109,6 @@ export const MenuItem = ({ id, children, name, icon, disabled, onClick, link, ta
99
109
  else {
100
110
  popupTrigger = _jsx(MenuButton, { id: id, ref: ref, name: name, icon: icon, disabled: disabled, hasChildren: true, onClick: memoizedTriggerOnClick, active: active }, id);
101
111
  }
102
- return _jsxs(Fragment, { children: [popupTrigger, _jsx(ControlledPopup, { className: classNames([`zen-controlled-menu-submenu--${path.length}`]), useTrapFocusWithTrigger: "on", 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);
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);
103
113
  };
104
114
  MenuItem.displayName = "MenuItem";
@@ -1,7 +1,9 @@
1
- /// <reference types="react" />
1
+ import { RefObject } from "react";
2
2
  export interface IPathContext {
3
3
  path: string[];
4
4
  onOpenBranch: (id: string) => void;
5
5
  closeBranch: () => void;
6
+ navigatedViaKeyboardRef?: RefObject<boolean>;
7
+ keyboardActiveRef?: RefObject<boolean>;
6
8
  }
7
9
  export declare const PathContext: import("react").Context<IPathContext>;
@@ -2,5 +2,5 @@ import { PropsWithChildren } from "react";
2
2
  import { IPathContext } from "./pathContext";
3
3
  interface IPathProvider extends IPathContext, PropsWithChildren {
4
4
  }
5
- export declare const PathProvider: ({ children, path, onOpenBranch, closeBranch }: IPathProvider) => import("react/jsx-runtime").JSX.Element;
5
+ export declare const PathProvider: ({ children, path, onOpenBranch, closeBranch, navigatedViaKeyboardRef, keyboardActiveRef }: IPathProvider) => import("react/jsx-runtime").JSX.Element;
6
6
  export {};
@@ -1,3 +1,3 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { PathContext } from "./pathContext";
3
- export const PathProvider = ({ children, path, onOpenBranch, closeBranch }) => (_jsx(PathContext.Provider, { value: { path, onOpenBranch, closeBranch }, children: children }));
3
+ export const PathProvider = ({ children, path, onOpenBranch, closeBranch, navigatedViaKeyboardRef, keyboardActiveRef }) => (_jsx(PathContext.Provider, { value: { path, onOpenBranch, closeBranch, navigatedViaKeyboardRef, keyboardActiveRef }, children: children }));
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
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 } from "react";
3
+ import { Children, cloneElement, isValidElement, useState, useMemo, useCallback, useEffect, useRef } from "react";
4
4
  import { MenuItem, isMenuItem } from "./components/menuItem";
5
5
  import { classNames } from "../commonHelpers/classNames/classNames";
6
6
  import { findContent } from "./utils/findContent";
@@ -19,17 +19,57 @@ import { useDeviceType } from "../commonHelpers/hooks/useDeviceType";
19
19
  import { generateId } from "../commonHelpers/utils";
20
20
  import { PathProvider } from "./contexts/pathProvider";
21
21
  import { isSeparator, MenuSeparator } from "./components/menuSeparator";
22
+ import { FOCUSABLE_SELECTOR } from "../utils/focusableSelector";
22
23
  export const ControlledMenu = ({ children, isOpen, setIsOpen, triggerRef, ariaLabel, ariaLabelledby, id, title, className = "", listClassName = "", paddingX = 0, paddingY = 0, alignment }) => {
23
24
  const [deviceType, setDeviceType] = useState(DeviceType.Desktop);
24
25
  const isMobile = deviceType === DeviceType.Mobile;
25
26
  const memoizedOnChange = useCallback(setIsOpen, [setIsOpen]);
26
27
  useDeviceType(setDeviceType);
28
+ const menuListRef = useRef(null);
27
29
  const [path, setPath] = useState([]);
30
+ // Track if the trigger was activated via keyboard
31
+ 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
+ useEffect(() => {
38
+ const trigger = triggerRef.current;
39
+ if (!trigger)
40
+ return undefined;
41
+ const handleKeyDown = (e) => {
42
+ if (e.key === "Enter" || e.key === " ") {
43
+ openedViaKeyboardRef.current = true;
44
+ }
45
+ };
46
+ const handleMouseDown = () => {
47
+ openedViaKeyboardRef.current = false;
48
+ };
49
+ trigger.addEventListener("keydown", handleKeyDown);
50
+ trigger.addEventListener("mousedown", handleMouseDown);
51
+ return () => {
52
+ trigger.removeEventListener("keydown", handleKeyDown);
53
+ trigger.removeEventListener("mousedown", handleMouseDown);
54
+ };
55
+ }, [triggerRef]);
28
56
  useEffect(() => {
29
57
  if (path.length && !isOpen) {
30
58
  setPath([]);
31
59
  }
32
60
  }, [isOpen, path, setPath]);
61
+ // Focus the menu list container or first item when menu opens (for keyboard navigation)
62
+ useEffect(() => {
63
+ var _a;
64
+ if (isOpen && !isMobile && menuListRef.current) {
65
+ if (openedViaKeyboardRef.current) {
66
+ (_a = menuListRef.current.querySelector(FOCUSABLE_SELECTOR)) === null || _a === void 0 ? void 0 : _a.focus();
67
+ }
68
+ else {
69
+ menuListRef.current.focus();
70
+ }
71
+ }
72
+ }, [isOpen, isMobile]);
33
73
  const onOpenBranch = useCallback((branchId) => {
34
74
  if (!branchId) {
35
75
  return;
@@ -107,48 +147,76 @@ export const ControlledMenu = ({ children, isOpen, setIsOpen, triggerRef, ariaLa
107
147
  });
108
148
  return [cont, par];
109
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
+ };
110
168
  const onKeyDown = (e) => {
111
169
  var _a, _b, _c, _d;
170
+ keyboardActiveRef.current = true;
112
171
  const target = e.target;
113
- if (isButton(target) || isLink(target)) {
114
- if (e.key === "ArrowDown") {
115
- e.preventDefault();
116
- (_a = findNextFocusable(target)) === null || _a === void 0 ? void 0 : _a.focus();
117
- return;
118
- }
119
- if (e.key === "ArrowUp") {
120
- e.preventDefault();
121
- (_b = findPrevFocusable(target)) === null || _b === void 0 ? void 0 : _b.focus();
122
- return;
123
- }
124
- if (e.key === "Home") {
125
- e.preventDefault();
126
- (_c = findFirstFocusable(target)) === null || _c === void 0 ? void 0 : _c.focus();
127
- return;
128
- }
129
- if (e.key === "End") {
130
- e.preventDefault();
131
- (_d = findLastFocusable(target)) === null || _d === void 0 ? void 0 : _d.focus();
132
- return;
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
+ }
133
179
  }
180
+ return;
134
181
  }
135
- if (isButton(target)) {
136
- if (e.key === "ArrowRight" && target.classList.contains("zen-menu-button__action--has-children")) {
137
- e.preventDefault();
138
- target.click();
139
- return;
140
- }
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) && (e.key === "ArrowRight" || e.key === "Enter" || e.key === " ") && target.classList.contains("zen-menu-button__action--has-children")) {
203
+ e.preventDefault();
204
+ navigatedViaKeyboardRef.current = true;
205
+ target.click();
141
206
  }
142
207
  };
143
- const renderMenuList = () => _jsx("div", { onKeyDown: onKeyDown, 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] }) });
208
+ 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] }) });
144
209
  const hideMenu = useCallback(() => {
145
210
  closeBranch();
146
211
  setIsOpen(false);
147
212
  }, [closeBranch, setIsOpen]);
148
213
  if (isMobile) {
149
- return _jsx(_Fragment, { children: _jsx(PathProvider, { path: path, onOpenBranch: onOpenBranch, closeBranch: closeBranch, children: _jsxs(MobileSheet, { label: title, isOpen: isOpen, triggerRef: triggerRef, onHidePanel: hideMenu, onCloseClick: hideMenu, children: [_jsx(MobileSheet.Title, { children: title }), _jsx(MobileSheet.Content, { children: renderMenuList() })] }) }) });
214
+ 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() })] }) }) });
150
215
  }
151
- return _jsx(_Fragment, { children: _jsx(PathProvider, { path: path, onOpenBranch: onOpenBranch, closeBranch: closeBranch, 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, recalculateOnScroll: true, children: renderMenuList() }) }) });
216
+ 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, recalculateOnScroll: true,
217
+ // focusOnOpen is false - ControlledMenu handles focus based on input method
218
+ // (keyboard vs mouse) in its own useEffect
219
+ focusOnOpen: false, children: renderMenuList() }) }) });
152
220
  };
153
221
  ControlledMenu.Item = MenuItem;
154
222
  ControlledMenu.Separator = MenuSeparator;
package/esm/nav/nav.d.ts CHANGED
@@ -7,8 +7,9 @@ export interface INav extends IZenComponentProps {
7
7
  onMenuVisibilityToggle?: (isOpen: boolean) => void;
8
8
  isMobileMenuOpen?: boolean;
9
9
  onIsMobileMenuOpenToggle?: (isOpen: boolean) => void;
10
+ onEditModeToggle?: (isEdit: boolean) => void;
10
11
  }
11
12
  /**
12
13
  * @beta This component is not fully ready yet and may change in future releases.
13
14
  */
14
- export declare const Nav: ({ children, className, collapsed, onCollapseToggle, onMenuVisibilityToggle, mobileBarContainer, isMobileMenuOpen, onIsMobileMenuOpenToggle }: INav) => import("react/jsx-runtime").JSX.Element;
15
+ export declare const Nav: ({ children, className, collapsed, onCollapseToggle, onMenuVisibilityToggle, mobileBarContainer, isMobileMenuOpen, onIsMobileMenuOpenToggle, onEditModeToggle }: INav) => import("react/jsx-runtime").JSX.Element;
package/esm/nav/nav.js CHANGED
@@ -51,7 +51,7 @@ const NavContent = ({ children }) => {
51
51
  /**
52
52
  * @beta This component is not fully ready yet and may change in future releases.
53
53
  */
54
- export const Nav = ({ children, className, collapsed = false, onCollapseToggle, onMenuVisibilityToggle, mobileBarContainer, isMobileMenuOpen = false, onIsMobileMenuOpenToggle
54
+ export const Nav = ({ children, className, collapsed = false, onCollapseToggle, onMenuVisibilityToggle, mobileBarContainer, isMobileMenuOpen = false, onIsMobileMenuOpenToggle, onEditModeToggle
55
55
  // eslint-disable-next-line complexity
56
56
  }) => {
57
57
  const menuRef = useRef(null);
@@ -94,7 +94,8 @@ export const Nav = ({ children, className, collapsed = false, onCollapseToggle,
94
94
  const onEditStateToggle = useCallback((isEdit) => {
95
95
  isEdit && !isMobile && onCollapseToggle && onCollapseToggle(false);
96
96
  editListItems && editFooter && setIsEditState(isEdit);
97
- }, [onCollapseToggle, setIsEditState, editListItems, editFooter, isMobile]);
97
+ onEditModeToggle && onEditModeToggle(isEdit);
98
+ }, [onCollapseToggle, setIsEditState, editListItems, editFooter, isMobile, onEditModeToggle]);
98
99
  useEffect(() => {
99
100
  const wasMobile = prevIsMobileRef.current;
100
101
  prevIsMobileRef.current = isMobile;
@@ -9,10 +9,11 @@ const marksClassNames = {
9
9
  dragging: "zen-nav-edit-list__item--dragging",
10
10
  dragged: "zen-nav-edit-list__item--dragged"
11
11
  };
12
+ const EDIT_ITEM_GAP = 10;
12
13
  export const NavEditList = ({ children, className, onItemsChangeOrder, onBeforeItemDrag }) => {
13
14
  const appsListRef = useRef(null);
14
15
  const dragHandler = onItemsChangeOrder ? onItemsChangeOrder : () => { };
15
- const { onDragStart } = useDragAndDrop(appsListRef, dragHandler, marksClassNames, onBeforeItemDrag);
16
+ const { onDragStart } = useDragAndDrop(appsListRef, dragHandler, marksClassNames, onBeforeItemDrag, EDIT_ITEM_GAP);
16
17
  const childrenArray = useMemo(() => onItemsChangeOrder ? Children.map(children, child => attachDndHandler(child, onDragStart)) : children, [children, onItemsChangeOrder, onDragStart]);
17
18
  return _jsx("div", { className: classNames(["zen-nav-edit-list", className || ""]), ref: appsListRef, children: childrenArray });
18
19
  };
@@ -27,6 +27,7 @@ export interface ILinkNavItem extends INavItemCommon {
27
27
  export interface IButtonNavItem extends INavItemCommon {
28
28
  onClick?: () => void;
29
29
  menuAlignment?: TAlignment;
30
+ onDropdownStateChange?: (isOpen: boolean) => void;
30
31
  }
31
32
  interface INavItemContext {
32
33
  parentLevel?: number;
@@ -11,7 +11,7 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  };
12
12
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
13
13
  import { classNames } from "../../commonHelpers/classNames/classNames";
14
- import { useRef, useState, Children, cloneElement, createContext, useContext } from "react";
14
+ import { useRef, useState, useEffect, Children, cloneElement, createContext, useContext } from "react";
15
15
  import { IconBeta } from "../../icons/iconBeta";
16
16
  import { IconChevronRight } from "../../icons/iconChevronRight";
17
17
  import { IconOpen2 } from "../../icons/iconOpen2";
@@ -70,9 +70,12 @@ const LinkNavItem = (_a) => {
70
70
  };
71
71
  LinkNavItem.displayName = "LinkNavItem";
72
72
  const ButtonNavItem = (_a) => {
73
- var { className, title, onClick, collapsed, active, children, tabIndex, menuAlignment = "right-top", isMenuItem = true, tooltipAlignment = defaultTooltipAlignment, actionIcon, actionIconTitle, onActionIconClick } = _a, rest = __rest(_a, ["className", "title", "onClick", "collapsed", "active", "children", "tabIndex", "menuAlignment", "isMenuItem", "tooltipAlignment", "actionIcon", "actionIconTitle", "onActionIconClick"]);
73
+ var { className, title, onClick, collapsed, active, children, tabIndex, menuAlignment = "right-top", isMenuItem = true, tooltipAlignment = defaultTooltipAlignment, actionIcon, actionIconTitle, onActionIconClick, onDropdownStateChange } = _a, rest = __rest(_a, ["className", "title", "onClick", "collapsed", "active", "children", "tabIndex", "menuAlignment", "isMenuItem", "tooltipAlignment", "actionIcon", "actionIconTitle", "onActionIconClick", "onDropdownStateChange"]);
74
74
  const [menuOpen, setMenuOpen] = useState(false);
75
75
  const { parentLevel = 0, closeParentMenu } = useContext(NavItemContext);
76
+ useEffect(() => {
77
+ onDropdownStateChange === null || onDropdownStateChange === void 0 ? void 0 : onDropdownStateChange(menuOpen);
78
+ }, [menuOpen, onDropdownStateChange]);
76
79
  const triggerRef = useRef(null);
77
80
  const currentLevel = parentLevel + 1;
78
81
  const validChildren = Children.toArray(children).filter(child => {
@@ -7,10 +7,11 @@ interface IPillContent {
7
7
  descriptionText?: TLazyContent<ReactNode>;
8
8
  mainAction?: TLazyContent<IPillExpandableAction>;
9
9
  secondaryAction?: TLazyContent<IPillExpandableAction>;
10
+ tertiaryAction?: TLazyContent<IPillExpandableAction>;
10
11
  onActionClick: () => void;
11
12
  isVisible?: boolean;
12
13
  errorHandler: (err: any) => ReactNode | undefined;
13
14
  descriptionId?: string;
14
15
  }
15
- export declare const PillContent: ({ date, count, descriptionText, mainAction, secondaryAction, onActionClick, isVisible, errorHandler, descriptionId }: IPillContent) => import("react/jsx-runtime").JSX.Element;
16
+ export declare const PillContent: ({ date, count, descriptionText, mainAction, secondaryAction, tertiaryAction, onActionClick, isVisible, errorHandler, descriptionId }: IPillContent) => import("react/jsx-runtime").JSX.Element;
16
17
  export {};
@@ -4,7 +4,7 @@ import { Button } from "../button/button";
4
4
  import { classNames } from "../commonHelpers/classNames/classNames";
5
5
  import { useDriveClassName } from "../utils/theme/useDriveClassName";
6
6
  import { LazyContent } from "../skeleton/lazyContent";
7
- export const PillContent = ({ date, count, descriptionText, mainAction, secondaryAction, onActionClick, isVisible = true, errorHandler, descriptionId }) => {
7
+ export const PillContent = ({ date, count, descriptionText, mainAction, secondaryAction, tertiaryAction, onActionClick, isVisible = true, errorHandler, descriptionId }) => {
8
8
  const headerDriveClassName = useDriveClassName("zen-status-pill-popup__header-text");
9
9
  const descriptionDriveClassName = useDriveClassName("zen-status-pill-popup__description");
10
10
  const mainActionClick = useCallback((onClick) => () => {
@@ -15,11 +15,16 @@ export const PillContent = ({ date, count, descriptionText, mainAction, secondar
15
15
  onClick && onClick();
16
16
  onActionClick();
17
17
  }, [onActionClick]);
18
+ const tertiaryActionClick = useCallback((onClick) => () => {
19
+ onClick && onClick();
20
+ onActionClick();
21
+ }, [onActionClick]);
18
22
  const renderMainButton = useCallback((data) => _jsx(Button, Object.assign({ link: data.link }, data.linkOptions, { onClick: mainActionClick(data.onClick), type: "primary", children: data.text })), [mainActionClick]);
19
- const renderSecondaryButton = useCallback((data) => _jsx(Button, Object.assign({ link: data.link }, data.linkOptions, { onClick: secondaryActionClick(data.onClick), type: "tertiary", children: data.text })), [secondaryActionClick]);
23
+ const renderSecondaryButton = useCallback((data) => _jsx(Button, Object.assign({ link: data.link }, data.linkOptions, { onClick: secondaryActionClick(data.onClick), type: "secondary", children: data.text })), [secondaryActionClick]);
24
+ const renderTertiaryButton = useCallback((data) => _jsx(Button, Object.assign({ link: data.link }, data.linkOptions, { onClick: tertiaryActionClick(data.onClick), type: "tertiary", children: data.text })), [tertiaryActionClick]);
20
25
  const renderDescription = useCallback(data => _jsx("div", { id: descriptionId, className: classNames(["zen-status-pill-popup__description", descriptionDriveClassName || ""]), children: data }), [descriptionDriveClassName, descriptionId]);
21
26
  const renderCount = useCallback((data) => data ? _jsx("div", { className: classNames(["zen-status-pill-popup__header-text zen-status-pill-popup__header-text--left", headerDriveClassName || ""]), children: data }) : null, [headerDriveClassName]);
22
27
  const renderDate = useCallback((data) => data ? _jsx("div", { className: classNames(["zen-status-pill-popup__header-text", headerDriveClassName || ""]), children: data }) : null, [headerDriveClassName]);
23
28
  return _jsxs(_Fragment, { children: [(date || count) && isVisible && _jsxs("div", { className: "zen-status-pill-popup__content-header", children: [date && _jsx(LazyContent, { width: 80, type: "text", content: date, isVisible: isVisible, errorHandler: errorHandler, render: renderDate }), count && _jsx(LazyContent, { width: 80, type: "text", content: count, isVisible: isVisible, errorHandler: errorHandler, render: renderCount })] }), descriptionText &&
24
- _jsx(LazyContent, { width: 200, type: "text", content: descriptionText, isVisible: isVisible, errorHandler: errorHandler, render: renderDescription }), (mainAction || secondaryAction) && isVisible && _jsxs("div", { className: "zen-status-pill-popup__actions", children: [mainAction && _jsx(LazyContent, { type: "button", content: mainAction, errorHandler: errorHandler, render: renderMainButton, isVisible: isVisible }), secondaryAction && _jsx(LazyContent, { type: "button", content: secondaryAction, errorHandler: errorHandler, render: renderSecondaryButton, isVisible: isVisible })] })] });
29
+ _jsx(LazyContent, { width: 200, type: "text", content: descriptionText, isVisible: isVisible, errorHandler: errorHandler, render: renderDescription }), (mainAction || secondaryAction || tertiaryAction) && isVisible && _jsxs("div", { className: "zen-status-pill-popup__actions", children: [mainAction && _jsx(LazyContent, { type: "button", content: mainAction, errorHandler: errorHandler, render: renderMainButton, isVisible: isVisible }), secondaryAction && _jsx(LazyContent, { type: "button", content: secondaryAction, errorHandler: errorHandler, render: renderSecondaryButton, isVisible: isVisible }), tertiaryAction && _jsx(LazyContent, { type: "button", content: tertiaryAction, errorHandler: errorHandler, render: renderTertiaryButton, isVisible: isVisible })] })] });
25
30
  };
@@ -1,5 +1,6 @@
1
1
  import "./pillExpandable.less";
2
2
  import { IZenComponentProps } from "../commonHelpers/zenComponent";
3
+ import { Exclusive } from "../commonHelpers/types/exclusive";
3
4
  import React, { FC, ReactNode } from "react";
4
5
  import { TViewMoreButton } from "./getProps";
5
6
  import { IIcon } from "../icons/icon";
@@ -20,6 +21,7 @@ export interface IBaseData {
20
21
  count?: TLazyContent<number>;
21
22
  mainAction?: TLazyContent<IPillExpandableAction>;
22
23
  secondaryAction?: TLazyContent<IPillExpandableAction>;
24
+ tertiaryAction?: TLazyContent<IPillExpandableAction>;
23
25
  getData?: undefined;
24
26
  }
25
27
  export interface IPromisedBaseData {
@@ -29,16 +31,20 @@ export interface IPromisedBaseData {
29
31
  count?: undefined;
30
32
  mainAction?: undefined;
31
33
  secondaryAction?: undefined;
34
+ tertiaryAction?: undefined;
32
35
  }
33
36
  type TProps = IBaseData | IPromisedBaseData;
34
- export interface IPillExpandable extends IZenComponentProps {
37
+ export type TPopoverVariant = "default" | "placeholder";
38
+ export type TPopoverSize = "small" | "medium" | "large";
39
+ type TPillTypeBase = "error" | "success" | "warning" | "info" | "default";
40
+ export type TPillType = TPillTypeBase | "upsell";
41
+ /** Base props shared by all PillExpandable variants */
42
+ export interface IPillExpandableBase extends IZenComponentProps {
35
43
  uniquePills?: number;
36
44
  text: string;
37
- type?: "error" | "success" | "warning" | "info" | "default";
38
45
  viewMoreAction?: TViewMoreButton;
39
46
  className?: string;
40
47
  popupClassName?: string;
41
- icon?: FC<IIcon> | false;
42
48
  secondaryIcon?: FC<IIcon> | false;
43
49
  isFlat?: boolean;
44
50
  isLoading?: boolean;
@@ -47,8 +53,25 @@ export interface IPillExpandable extends IZenComponentProps {
47
53
  isBeta?: boolean;
48
54
  hideCounterNumber?: boolean;
49
55
  autoExpandChildren?: boolean;
56
+ /** Popover size: small (220px), medium (320px), large (420px). Default: "medium" */
57
+ popoverSize?: TPopoverSize;
58
+ /** Title displayed in the popover title bar. If provided, the title bar will be shown. */
59
+ title?: string;
60
+ }
61
+ /** Props for standard PillExpandable (non-upsell) */
62
+ interface IPillExpandableStandard extends IPillExpandableBase {
63
+ type?: TPillTypeBase;
64
+ icon?: FC<IIcon> | false;
65
+ }
66
+ /** Props for upsell PillExpandable - icon and children are not allowed */
67
+ interface IPillExpandableUpsell extends IPillExpandableBase {
68
+ type: "upsell";
69
+ icon?: never;
70
+ children?: never;
50
71
  }
72
+ /** Combined type using Exclusive to prevent mixing upsell with standard props */
73
+ export type IPillExpandable = Exclusive<IPillExpandableStandard, IPillExpandableUpsell>;
51
74
  type TPillExpandable = IPillExpandable & TProps;
52
- export declare const PillExpandable: React.MemoExoticComponent<({ children, errorHandler, getData, text, description, date, count, mainAction, secondaryAction, viewMoreAction, uniquePills, type, className, popupClassName, isLoading, isFlat, icon, secondaryIcon, isBeta, loadingWidth, hideCounterNumber, autoExpandChildren }: TPillExpandable) => import("react/jsx-runtime").JSX.Element>;
75
+ export declare const PillExpandable: React.MemoExoticComponent<({ children, errorHandler, getData, text, description, date, count, mainAction, secondaryAction, tertiaryAction, viewMoreAction, uniquePills, type, className, popupClassName, isLoading, isFlat, icon, secondaryIcon, isBeta, loadingWidth, hideCounterNumber, autoExpandChildren, popoverSize, title }: TPillExpandable) => import("react/jsx-runtime").JSX.Element>;
53
76
  export declare const TRANSLATIONS: string[];
54
77
  export {};