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.
- package/es/components/SideNavV2/__tests__/interaction-scenarios.test.js +82 -0
- package/es/components/SideNavV2/__tests__/sections.test.js +1 -1
- package/es/components/SideNavV2/components/ExpandedPanel.js +27 -3
- package/es/components/SideNavV2/hooks/useSideNavState.js +65 -14
- package/es/components/SideNavV2/sections/SideNavTeamsSection.js +6 -1
- package/package.json +1 -1
|
@@ -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("
|
|
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:
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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 &&
|
|
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
|
}
|