orcs-design-system 3.3.49 → 3.3.54

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.
@@ -341,4 +341,86 @@ describe("SideNavV2 Interaction Scenarios", () => {
341
341
  expect(getToggleButton()).toBeInTheDocument();
342
342
  });
343
343
  });
344
+ describe("Scenario 8: ExpandedPanel hover behavior", () => {
345
+ it("should show/hide ExpandedPanel based on hover state", async () => {
346
+ renderSideNav();
347
+
348
+ // Open expanded panel
349
+ const settingsButton = screen.getByTestId("nav-item-Settings");
350
+ fireEvent.click(settingsButton);
351
+ await waitFor(() => {
352
+ expect(screen.getByTestId("expanded-panel")).toBeInTheDocument();
353
+ });
354
+
355
+ // Hover out of wrapper should hide the panel
356
+ const wrapper = screen.getByTestId("side-nav-items").closest('[data-testid="side-nav-wrapper"]') || document.body;
357
+ fireEvent.mouseLeave(wrapper);
358
+ await waitFor(() => {
359
+ expect(screen.queryByTestId("expanded-panel")).not.toBeInTheDocument();
360
+ });
361
+
362
+ // Hover back in should show the previously opened panel
363
+ fireEvent.mouseEnter(wrapper);
364
+ await waitFor(() => {
365
+ expect(screen.getByTestId("expanded-panel")).toBeInTheDocument();
366
+ });
367
+ });
368
+ it("should not re-show panel if it was manually closed", async () => {
369
+ renderSideNav();
370
+
371
+ // Open expanded panel
372
+ const settingsButton = screen.getByTestId("nav-item-Settings");
373
+ fireEvent.click(settingsButton);
374
+ await waitFor(() => {
375
+ expect(screen.getByTestId("expanded-panel")).toBeInTheDocument();
376
+ });
377
+
378
+ // Manually close the panel
379
+ const hidePanelButton = screen.getByTestId("hide-panel-button");
380
+ fireEvent.click(hidePanelButton);
381
+ await waitFor(() => {
382
+ expect(screen.queryByTestId("expanded-panel")).not.toBeInTheDocument();
383
+ });
384
+
385
+ // Hover out and back in should not re-show the panel
386
+ const wrapper = screen.getByTestId("side-nav-items").closest('[data-testid="side-nav-wrapper"]') || document.body;
387
+ fireEvent.mouseLeave(wrapper);
388
+ fireEvent.mouseEnter(wrapper);
389
+
390
+ // Panel should still be hidden
391
+ expect(screen.queryByTestId("expanded-panel")).not.toBeInTheDocument();
392
+ });
393
+ it("should hide panel on hover out even when navigation is locked open", async () => {
394
+ renderSideNav();
395
+
396
+ // Open expanded panel
397
+ const settingsButton = screen.getByTestId("nav-item-Settings");
398
+ fireEvent.click(settingsButton);
399
+ await waitFor(() => {
400
+ expect(screen.getByTestId("expanded-panel")).toBeInTheDocument();
401
+ });
402
+
403
+ // Click "Keep Open" to lock navigation
404
+ const toggleButton = getToggleButton();
405
+ fireEvent.click(toggleButton);
406
+
407
+ // Navigation should be locked open
408
+ await waitFor(() => {
409
+ expect(toggleButton).toHaveAttribute("data-testid", "toggle-handle");
410
+ });
411
+
412
+ // Hover out of wrapper should still hide the panel
413
+ const wrapper = screen.getByTestId("side-nav-items").closest('[data-testid="side-nav-wrapper"]') || document.body;
414
+ fireEvent.mouseLeave(wrapper);
415
+ await waitFor(() => {
416
+ expect(screen.queryByTestId("expanded-panel")).not.toBeInTheDocument();
417
+ });
418
+
419
+ // Hover back in should show the panel again
420
+ fireEvent.mouseEnter(wrapper);
421
+ await waitFor(() => {
422
+ expect(screen.getByTestId("expanded-panel")).toBeInTheDocument();
423
+ });
424
+ });
425
+ });
344
426
  });
@@ -195,7 +195,7 @@ describe("SideNavTeamsSection", () => {
195
195
  teams: mockTeams,
196
196
  isExpanded: false
197
197
  }));
198
- expect(screen.queryByText("Your Teams")).not.toBeInTheDocument();
198
+ expect(screen.queryByText("My Teams")).not.toBeInTheDocument();
199
199
  });
200
200
  it("should render avatars for each team", () => {
201
201
  renderWithRouter(/*#__PURE__*/_jsx(SideNavTeamsSection, {
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import React, { useEffect, useRef } from "react";
2
2
  import PropTypes from "prop-types";
3
3
  import { SideNavExpanded, ResizeHandle, ToggleIcon } from "../styles/SideNavV2.styles";
4
4
  import PanelControlComponent from "./PanelControl";
@@ -10,6 +10,7 @@ import Icon from "../../Icon";
10
10
  *
11
11
  * Renders an expandable panel that can be resized by the user. Supports both
12
12
  * desktop (horizontal resize) and mobile (vertical resize) orientations.
13
+ * Hover behavior is handled at the parent component level.
13
14
  *
14
15
  * @param {Object} props - Component props
15
16
  * @param {Object} props.item - Navigation item data
@@ -34,6 +35,26 @@ const ExpandedPanel = _ref => {
34
35
  onResizeStart,
35
36
  onItemClick
36
37
  } = _ref;
38
+ const panelRef = useRef(null);
39
+ const isHoveringInPanelRef = useRef(false);
40
+
41
+ // Handle hover behavior for the panel
42
+ useEffect(() => {
43
+ if (!panelRef.current) return;
44
+ const panel = panelRef.current;
45
+ const handleMouseEnter = () => {
46
+ isHoveringInPanelRef.current = true;
47
+ };
48
+ const handleMouseLeave = () => {
49
+ isHoveringInPanelRef.current = false;
50
+ };
51
+ panel.addEventListener("mouseenter", handleMouseEnter);
52
+ panel.addEventListener("mouseleave", handleMouseLeave);
53
+ return () => {
54
+ panel.removeEventListener("mouseenter", handleMouseEnter);
55
+ panel.removeEventListener("mouseleave", handleMouseLeave);
56
+ };
57
+ }, []);
37
58
  if (item.actionType !== "component" || item.hide) {
38
59
  return null;
39
60
  }
@@ -41,7 +62,10 @@ const ExpandedPanel = _ref => {
41
62
  position: "relative",
42
63
  height: "100%",
43
64
  children: [/*#__PURE__*/_jsxs(SideNavExpanded, {
44
- ref: expandedRef,
65
+ ref: el => {
66
+ expandedRef.current = el;
67
+ panelRef.current = el;
68
+ },
45
69
  tabIndex: "0",
46
70
  active: expandedItem,
47
71
  large: item.large,
@@ -85,7 +109,7 @@ ExpandedPanel.propTypes = {
85
109
  onItemClick: PropTypes.func.isRequired
86
110
  };
87
111
  ExpandedPanel.__docgenInfo = {
88
- "description": "ExpandedPanel - A resizable panel component for expanded navigation items\n\nRenders an expandable panel that can be resized by the user. Supports both\ndesktop (horizontal resize) and mobile (vertical resize) orientations.\n\n@param {Object} props - Component props\n@param {Object} props.item - Navigation item data\n@param {number} [props.expandedItem] - Currently expanded item index\n@param {boolean} props.isExpanded - Whether the navigation is expanded\n@param {number} [props.expandedWidth] - Width of the expanded panel (desktop)\n@param {boolean} props.isSmallScreen - Whether currently on a small screen\n@param {React.RefObject} props.expandedRef - Ref for the expanded panel\n@param {Function} props.onResizeStart - Resize start handler\n@param {Function} props.onItemClick - Item click handler\n@returns {JSX.Element} Rendered expanded panel or null if not applicable",
112
+ "description": "ExpandedPanel - A resizable panel component for expanded navigation items\n\nRenders an expandable panel that can be resized by the user. Supports both\ndesktop (horizontal resize) and mobile (vertical resize) orientations.\nHover behavior is handled at the parent component level.\n\n@param {Object} props - Component props\n@param {Object} props.item - Navigation item data\n@param {number} [props.expandedItem] - Currently expanded item index\n@param {boolean} props.isExpanded - Whether the navigation is expanded\n@param {number} [props.expandedWidth] - Width of the expanded panel (desktop)\n@param {boolean} props.isSmallScreen - Whether currently on a small screen\n@param {React.RefObject} props.expandedRef - Ref for the expanded panel\n@param {Function} props.onResizeStart - Resize start handler\n@param {Function} props.onItemClick - Item click handler\n@returns {JSX.Element} Rendered expanded panel or null if not applicable",
89
113
  "methods": [],
90
114
  "displayName": "ExpandedPanel",
91
115
  "props": {
@@ -35,6 +35,11 @@ const useSideNavState = items => {
35
35
  const wasExpandedByHoverRef = useRef(false);
36
36
  const wasCollapsedWithExpandedPanelRef = useRef(false);
37
37
  const [isToggling, setIsToggling] = useState(false);
38
+
39
+ // New refs for ExpandedPanel hover behavior
40
+ const previouslyOpenedPanelRef = useRef(null);
41
+ const wasPanelManuallyClosedRef = useRef(false);
42
+ const isHoveringInWrapperRef = useRef(false);
38
43
  const firstExpandedItemByDefault = findFirstExpandedByDefault(items);
39
44
 
40
45
  // Initialize expanded item by default
@@ -77,43 +82,61 @@ const useSideNavState = items => {
77
82
  // Extract common mouse event logic to reduce duplication
78
83
  const createMouseEventHandlers = useCallback(() => {
79
84
  const baseHandler = shouldExpand => {
80
- if (isLocked || isToggling) {
85
+ if (isToggling) {
81
86
  return;
82
87
  }
83
88
  isHoveringRef.current = true;
89
+ isHoveringInWrapperRef.current = true;
84
90
 
85
- // Only auto-expand if the user has manually collapsed it before
91
+ // Only auto-expand navigation if the user has manually collapsed it before
86
92
  // AND it wasn't collapsed while having an expanded panel
87
- if (shouldExpand && hasBeenManuallyCollapsedRef.current && !wasCollapsedWithExpandedPanelRef.current) {
93
+ // AND the navigation is not locked
94
+ if (shouldExpand && !isLocked && hasBeenManuallyCollapsedRef.current && !wasCollapsedWithExpandedPanelRef.current) {
88
95
  setIsExpanded(true);
89
96
  wasExpandedByHoverRef.current = true;
90
97
  }
98
+
99
+ // Auto-expand previously opened panel if hovering back in
100
+ // This should work regardless of navigation lock state
101
+ if (shouldExpand && previouslyOpenedPanelRef.current !== null && !wasPanelManuallyClosedRef.current && expandedItem === null) {
102
+ setExpandedItem(previouslyOpenedPanelRef.current);
103
+ // Mark that this panel was opened by hover (not manually)
104
+ wasPanelManuallyClosedRef.current = false;
105
+ }
91
106
  };
92
107
  return {
93
108
  handleEnter: () => baseHandler(true),
94
109
  handleLeave: () => {
95
- if (isLocked) return;
96
110
  isHoveringRef.current = false;
111
+ isHoveringInWrapperRef.current = false;
97
112
 
98
- // Only auto-collapse if the user has manually collapsed it before
113
+ // Only auto-collapse navigation if the user has manually collapsed it before
99
114
  // AND it was expanded by hover (not locked)
100
115
  // AND it wasn't collapsed while having an expanded panel
101
- if (hasBeenManuallyCollapsedRef.current && wasExpandedByHoverRef.current && !wasCollapsedWithExpandedPanelRef.current) {
116
+ if (!isLocked && hasBeenManuallyCollapsedRef.current && wasExpandedByHoverRef.current && !wasCollapsedWithExpandedPanelRef.current) {
102
117
  setIsExpanded(false);
103
118
  wasExpandedByHoverRef.current = false;
104
119
  }
105
120
 
121
+ // Auto-hide ExpandedPanel when hovering out (if it was opened by hover)
122
+ // This should work regardless of navigation lock state
123
+ if (expandedItem !== null && previouslyOpenedPanelRef.current === expandedItem && !wasPanelManuallyClosedRef.current) {
124
+ // Store the panel that was open before hiding it
125
+ previouslyOpenedPanelRef.current = expandedItem;
126
+ setExpandedItem(null);
127
+ }
128
+
106
129
  // Reset the wasCollapsedWithExpandedPanelRef flag when hovering out of the entire wrapper
107
130
  // This allows hover behavior to work again after hovering out and back in
108
131
  wasCollapsedWithExpandedPanelRef.current = false;
109
132
  // Don't reset hasBeenManuallyCollapsedRef here - it should persist until user manually expands again
110
133
  }
111
134
  };
112
- }, [isLocked, setIsExpanded, isToggling]);
135
+ }, [isLocked, setIsExpanded, isToggling, expandedItem, setExpandedItem]);
113
136
 
114
137
  // Mouse event handlers for hover state
115
138
  useEffect(() => {
116
- if (!wrapperRef.current || isLocked) return;
139
+ if (!wrapperRef.current) return;
117
140
  const wrapper = wrapperRef.current;
118
141
  const sideNavItems = wrapper.querySelector('[data-testid="side-nav-items"]');
119
142
  const toggleHandle = wrapper.querySelector(".toggle-popover");
@@ -126,12 +149,12 @@ const useSideNavState = items => {
126
149
  const handleSideNavItemsMouseEnter = handleEnter;
127
150
  const handleToggleMouseEnter = handleEnter;
128
151
  const handleSideNavItemsMouseLeave = () => {
129
- if (isLocked || isToggling) return;
152
+ if (isToggling) return;
130
153
  // Don't collapse immediately, let the wrapper handle it
131
154
  // This allows hovering over ExpandedPanel to keep it expanded
132
155
  };
133
156
  const handleToggleMouseLeave = () => {
134
- if (isLocked || isToggling) return;
157
+ if (isToggling) return;
135
158
  // Don't collapse immediately, let the wrapper handle it
136
159
  // This allows hovering over the toggle handle to keep it expanded
137
160
  };
@@ -159,7 +182,7 @@ const useSideNavState = items => {
159
182
  toggleHandle.removeEventListener("mouseleave", handleToggleMouseLeave);
160
183
  }
161
184
  };
162
- }, [isLocked, createMouseEventHandlers, isToggling]);
185
+ }, [createMouseEventHandlers, isToggling]);
163
186
  const handleItemClick = useCallback(item => {
164
187
  const {
165
188
  index: itemIndex,
@@ -175,26 +198,51 @@ const useSideNavState = items => {
175
198
  // If we're already on this panel, toggle it closed
176
199
  if (expandedItem === relatedPanelIndex) {
177
200
  setExpandedItem(null);
201
+ // Mark that this panel was manually closed
202
+ if (previouslyOpenedPanelRef.current === relatedPanelIndex) {
203
+ wasPanelManuallyClosedRef.current = true;
204
+ }
178
205
  } else {
179
206
  // Otherwise, open the related panel
180
207
  setExpandedItem(relatedPanelIndex);
208
+ // Reset manual close flag when opening a panel
209
+ wasPanelManuallyClosedRef.current = false;
181
210
  }
182
211
  } else {
183
212
  // No related panel, close any open panel
184
213
  setExpandedItem(null);
214
+ // Mark that any open panel was manually closed
215
+ if (expandedItem !== null) {
216
+ wasPanelManuallyClosedRef.current = true;
217
+ }
185
218
  }
186
219
  } else {
187
220
  // For button items, just close any open panel
221
+ if (expandedItem !== null) {
222
+ wasPanelManuallyClosedRef.current = true;
223
+ }
188
224
  setExpandedItem(null);
189
225
  }
190
226
  onButtonClick && onButtonClick(item);
191
227
  } else {
192
228
  const wasExpanded = expandedItem !== null;
193
- setExpandedItem(itemIndex === expandedItem ? null : itemIndex);
229
+ const wasClosingPanel = itemIndex === expandedItem;
230
+ if (wasClosingPanel) {
231
+ // Closing a panel - mark it as manually closed
232
+ wasPanelManuallyClosedRef.current = true;
233
+ // Store reference to the panel that was closed
234
+ previouslyOpenedPanelRef.current = itemIndex;
235
+ } else {
236
+ // Opening a panel - reset manual close flag and track that it was opened manually
237
+ wasPanelManuallyClosedRef.current = false;
238
+ // Store reference to the panel that was opened
239
+ previouslyOpenedPanelRef.current = itemIndex;
240
+ }
241
+ setExpandedItem(wasClosingPanel ? null : itemIndex);
194
242
  onButtonClick && onButtonClick(item);
195
243
 
196
244
  // If we just closed an expanded panel, reset the flags to enable hover behavior
197
- if (wasExpanded && itemIndex === expandedItem) {
245
+ if (wasExpanded && wasClosingPanel) {
198
246
  // Reset the wasCollapsedWithExpandedPanelRef flag to enable hover behavior again
199
247
  wasCollapsedWithExpandedPanelRef.current = false;
200
248
  // Don't reset hasBeenManuallyCollapsedRef - the user has still manually collapsed the nav
@@ -278,7 +326,10 @@ const useSideNavState = items => {
278
326
  handleExpandToggle,
279
327
  isLocked,
280
328
  wasExpandedByHover: wasExpandedByHoverRef.current,
281
- hasBeenManuallyCollapsed: hasBeenManuallyCollapsedRef.current
329
+ hasBeenManuallyCollapsed: hasBeenManuallyCollapsedRef.current,
330
+ previouslyOpenedPanel: previouslyOpenedPanelRef.current,
331
+ wasPanelManuallyClosed: wasPanelManuallyClosedRef.current,
332
+ isHoveringInWrapper: isHoveringInWrapperRef.current
282
333
  };
283
334
  };
284
335
  export default useSideNavState;
@@ -54,7 +54,8 @@ SideNavTeamsSection.propTypes = {
54
54
  teams: PropTypes.arrayOf(PropTypes.shape({
55
55
  avatar: PropTypes.string,
56
56
  name: PropTypes.string.isRequired,
57
- link: PropTypes.string.isRequired
57
+ link: PropTypes.string.isRequired,
58
+ gradient: PropTypes.string
58
59
  })),
59
60
  isExpanded: PropTypes.bool
60
61
  };
@@ -81,6 +82,10 @@ SideNavTeamsSection.__docgenInfo = {
81
82
  "link": {
82
83
  "name": "string",
83
84
  "required": true
85
+ },
86
+ "gradient": {
87
+ "name": "string",
88
+ "required": false
84
89
  }
85
90
  }
86
91
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orcs-design-system",
3
- "version": "3.3.49",
3
+ "version": "3.3.54",
4
4
  "engines": {
5
5
  "node": "20.12.2"
6
6
  },