orcs-design-system 3.3.49 → 3.3.55

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.
@@ -0,0 +1,66 @@
1
+ import React from "react";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ const AiIcon = () => /*#__PURE__*/_jsxs("svg", {
4
+ width: "32",
5
+ height: "30",
6
+ viewBox: "0 0 32 30",
7
+ fill: "none",
8
+ xmlns: "http://www.w3.org/2000/svg",
9
+ children: [/*#__PURE__*/_jsx("path", {
10
+ d: "M12.713 5.32658C12.975 4.33326 14.3849 4.33326 14.6469 5.32658L15.8314 9.8179C16.1932 11.1899 17.2548 12.2675 18.6212 12.6499L23.2851 13.955C24.28 14.2334 24.2509 15.6539 23.2454 15.8913L18.7801 16.9454C17.3272 17.2884 16.1862 18.412 15.8208 19.8594L14.6496 24.4992C14.3954 25.5062 12.9646 25.5062 12.7104 24.4992L11.5391 19.8594C11.1737 18.412 10.0327 17.2884 8.57984 16.9454L4.11454 15.8913C3.10903 15.6539 3.07989 14.2334 4.07482 13.955L8.73872 12.6499C10.1051 12.2675 11.1667 11.1899 11.5285 9.8179L12.713 5.32658Z",
11
+ fill: "url(#paint0_linear_104_237)"
12
+ }), /*#__PURE__*/_jsx("path", {
13
+ d: "M24.7966 2.33321C24.9275 1.83655 25.6325 1.83655 25.7635 2.33321L26.2963 4.35338C26.4772 5.03938 27.008 5.57819 27.6912 5.76938L29.7926 6.35743C30.2901 6.49663 30.2755 7.20686 29.7727 7.32555L27.7706 7.79821C27.0442 7.96971 26.4737 8.53147 26.291 9.25519L25.7648 11.3395C25.6377 11.843 24.9223 11.843 24.7952 11.3395L24.2691 9.25519C24.0864 8.53147 23.5159 7.96971 22.7894 7.79821L20.7873 7.32555C20.2846 7.20686 20.27 6.49664 20.7675 6.35743L22.8689 5.76938C23.5521 5.57819 24.0829 5.03938 24.2638 4.35338L24.7966 2.33321Z",
14
+ fill: "url(#paint1_linear_104_237)"
15
+ }), /*#__PURE__*/_jsx("path", {
16
+ d: "M24.7966 18.5734C24.9275 18.0768 25.6325 18.0768 25.7635 18.5734L26.2963 20.5936C26.4772 21.2796 27.008 21.8184 27.6912 22.0096L29.7926 22.5977C30.2901 22.7369 30.2755 23.4471 29.7727 23.5658L27.7706 24.0384C27.0442 24.2099 26.4737 24.7717 26.291 25.4954L25.7648 27.5798C25.6377 28.0833 24.9223 28.0833 24.7952 27.5798L24.2691 25.4954C24.0864 24.7717 23.5159 24.2099 22.7894 24.0384L20.7873 23.5658C20.2846 23.4471 20.27 22.7369 20.7675 22.5977L22.8689 22.0096C23.5521 21.8184 24.0829 21.2796 24.2638 20.5936L24.7966 18.5734Z",
17
+ fill: "url(#paint2_linear_104_237)"
18
+ }), /*#__PURE__*/_jsxs("defs", {
19
+ children: [/*#__PURE__*/_jsxs("linearGradient", {
20
+ id: "paint0_linear_104_237",
21
+ x1: "8.24515",
22
+ y1: "6.46256",
23
+ x2: "24.5658",
24
+ y2: "19.6244",
25
+ gradientUnits: "userSpaceOnUse",
26
+ children: [/*#__PURE__*/_jsx("stop", {
27
+ stopColor: "#0F8BE9"
28
+ }), /*#__PURE__*/_jsx("stop", {
29
+ offset: "0.701923",
30
+ stopColor: "#9853E0"
31
+ })]
32
+ }), /*#__PURE__*/_jsxs("linearGradient", {
33
+ id: "paint1_linear_104_237",
34
+ x1: "22.6808",
35
+ y1: "2.7968",
36
+ x2: "30.4863",
37
+ y2: "9.09158",
38
+ gradientUnits: "userSpaceOnUse",
39
+ children: [/*#__PURE__*/_jsx("stop", {
40
+ stopColor: "#0F8BE9"
41
+ }), /*#__PURE__*/_jsx("stop", {
42
+ offset: "0.701923",
43
+ stopColor: "#9853E0"
44
+ })]
45
+ }), /*#__PURE__*/_jsxs("linearGradient", {
46
+ id: "paint2_linear_104_237",
47
+ x1: "22.6808",
48
+ y1: "19.037",
49
+ x2: "30.4863",
50
+ y2: "25.3318",
51
+ gradientUnits: "userSpaceOnUse",
52
+ children: [/*#__PURE__*/_jsx("stop", {
53
+ stopColor: "#0F8BE9"
54
+ }), /*#__PURE__*/_jsx("stop", {
55
+ offset: "0.701923",
56
+ stopColor: "#9853E0"
57
+ })]
58
+ })]
59
+ })]
60
+ });
61
+ AiIcon.__docgenInfo = {
62
+ "description": "",
63
+ "methods": [],
64
+ "displayName": "AiIcon"
65
+ };
66
+ export default AiIcon;
@@ -7,6 +7,7 @@ import { Small } from "../Typography";
7
7
  import Toggle from "../Toggle";
8
8
  import Divider from "../Divider";
9
9
  import Icon from "../Icon";
10
+ import { action } from "@storybook/addon-actions";
10
11
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
11
12
  export default {
12
13
  title: "Components/Header",
@@ -80,8 +81,34 @@ export const defaultHeader = () => /*#__PURE__*/_jsx(Header, {
80
81
  userMenuContent: /*#__PURE__*/_jsx(UserMenuContent, {})
81
82
  });
82
83
  defaultHeader.storyName = "Default Header";
84
+ export const headerWithExtras = () => /*#__PURE__*/_jsx(Header, {
85
+ appName: "Powercorp directory",
86
+ logo: dummyLogo,
87
+ showLogoSeparator: true,
88
+ userName: "Michael Jenkins (michael.jenkins@powercorp.com)",
89
+ avatarSource: "https://api.dicebear.com/7.x/personas/svg?seed=mike",
90
+ avatarAlt: "Avatar for Michael",
91
+ avatarInitials: "MJ",
92
+ currentWorkspace: "Org Design Workspace",
93
+ searchComponent: /*#__PURE__*/_jsx(TextInput, {
94
+ fullWidth: true,
95
+ id: "search",
96
+ height: "100%",
97
+ type: "text",
98
+ placeholder: "Search for...",
99
+ iconRight: ["fas", "search"]
100
+ }, "search"),
101
+ userMenuContent: /*#__PURE__*/_jsx(UserMenuContent, {}),
102
+ onAIClick: action("AI button clicked")
103
+ });
104
+ headerWithExtras.storyName = "Header with Extras";
83
105
  defaultHeader.__docgenInfo = {
84
106
  "description": "",
85
107
  "methods": [],
86
108
  "displayName": "defaultHeader"
109
+ };
110
+ headerWithExtras.__docgenInfo = {
111
+ "description": "",
112
+ "methods": [],
113
+ "displayName": "headerWithExtras"
87
114
  };
@@ -80,22 +80,30 @@ export const UserMenuToggle = styled("button").withConfig({
80
80
  alignItems: "center",
81
81
  justifyContent: "center",
82
82
  padding: "0",
83
- cursor: "pointer",
84
83
  border: "none",
84
+ cursor: "pointer",
85
85
  appearance: "none",
86
86
  bg: "transparent",
87
87
  color: themeGet("colors.greyDark")(props),
88
88
  fontSize: themeGet("fontSizes.1")(props),
89
89
  fontWeight: themeGet("fontWeights.2")(props),
90
90
  "&:focus, &:hover": {
91
- outline: "none"
91
+ outline: "none",
92
+ ".avatar-border": {
93
+ borderColor: themeGet("colors.primaryLight")(props)
94
+ },
95
+ ".icon-container": {
96
+ background: themeGet("colors.primaryLight")(props)
97
+ }
92
98
  }
93
99
  }));
94
100
  export const AvatarBorder = styled(Box).withConfig({
95
101
  displayName: "Headerstyles__AvatarBorder",
96
102
  componentId: "sc-xs8ba0-5"
97
- })(css({
98
- background: "linear-gradient(135deg, rgba(0,145,234,1) 0%, rgba(155,81,224,1) 100%)"
103
+ })(props => css({
104
+ transition: themeGet("transition.transitionDefault")(props),
105
+ border: "solid 2px transparent",
106
+ borderColor: themeGet("colors.greyDark")(props)
99
107
  }));
100
108
  export const UserMenuContainer = styled(Box).withConfig({
101
109
  displayName: "Headerstyles__UserMenuContainer",
@@ -106,13 +114,14 @@ export const UserMenuContainer = styled(Box).withConfig({
106
114
  export const IconContainer = styled(Flex).withConfig({
107
115
  displayName: "Headerstyles__IconContainer",
108
116
  componentId: "sc-xs8ba0-7"
109
- })(css({
117
+ })(props => css({
110
118
  position: "absolute",
111
119
  bottom: "0",
112
120
  right: "0",
113
121
  height: "15px",
114
122
  width: "15px",
115
- background: "linear-gradient(135deg, rgba(0,145,234,1) 0%, rgba(155,81,224,1) 100%)"
123
+ background: themeGet("colors.greyDark")(props),
124
+ transition: themeGet("transition.transitionDefault")(props)
116
125
  }));
117
126
  export const UserMenuContent = styled(Box).withConfig({
118
127
  displayName: "Headerstyles__UserMenuContent",
@@ -127,4 +136,29 @@ export const UserMenuContent = styled(Box).withConfig({
127
136
  transform: props.isOpen ? "translateY(0)" : "translateY(-10px)",
128
137
  pointerEvents: props.isOpen ? "all" : "none",
129
138
  transition: "opacity 0.3s ease, transform 0.3s ease"
139
+ }));
140
+ export const AiButton = styled("button").withConfig({
141
+ displayName: "Headerstyles__AiButton",
142
+ componentId: "sc-xs8ba0-9"
143
+ })(props => css({
144
+ background: themeGet("colors.white")(props),
145
+ border: "solid 2px transparent",
146
+ display: "flex",
147
+ flex: "0 0 auto",
148
+ alignItems: "center",
149
+ justifyContent: "center",
150
+ appearance: "none",
151
+ textDecoration: "none",
152
+ textAlign: "center",
153
+ cursor: "pointer",
154
+ borderRadius: "50%",
155
+ width: `calc(${themeGet("avatarScale.navBarAvatarSize")(props)} + 4px)`,
156
+ height: `calc(${themeGet("avatarScale.navBarAvatarSize")(props)} + 4px)`,
157
+ padding: "1px 4px 0 0",
158
+ transition: themeGet("transition.transitionDefault")(props),
159
+ "&:hover, &:focus": {
160
+ background: themeGet("colors.primaryLightest")(props),
161
+ borderColor: themeGet("colors.primaryLighter")(props),
162
+ outline: "none"
163
+ }
130
164
  }));
@@ -35,8 +35,8 @@ const UserMenu = _ref => {
35
35
  children: [/*#__PURE__*/_jsxs(UserMenuToggle, {
36
36
  onClick: toggleMenu,
37
37
  children: [/*#__PURE__*/_jsx(AvatarBorder, {
38
+ className: "avatar-border",
38
39
  borderRadius: "50%",
39
- p: "2px",
40
40
  children: /*#__PURE__*/_jsx(Avatar, {
41
41
  customSize: "navBarAvatarSize",
42
42
  image: avatarSource,
@@ -44,6 +44,7 @@ const UserMenu = _ref => {
44
44
  initials: avatarInitials
45
45
  })
46
46
  }), /*#__PURE__*/_jsx(IconContainer, {
47
+ className: "icon-container",
47
48
  alignItems: "center",
48
49
  justifyContent: "center",
49
50
  borderRadius: "50%",
@@ -2,8 +2,10 @@ import React from "react";
2
2
  import PropTypes from "prop-types";
3
3
  import Flex, { FlexItem } from "../Flex";
4
4
  import { Small } from "../Typography";
5
- import { Bar, AppName, SearchContainer, Separator } from "./Header.styles";
5
+ import { Bar, AppName, SearchContainer, Separator, AiButton } from "./Header.styles";
6
6
  import UserMenu from "./UserMenu";
7
+ import AiIcon from "./AiIcon";
8
+
7
9
  /**
8
10
  * Header component for top of app.
9
11
  **/
@@ -20,7 +22,8 @@ const Header = _ref => {
20
22
  currentWorkspace,
21
23
  logo,
22
24
  userMenuContent,
23
- showLogoSeparator = false
25
+ showLogoSeparator = false,
26
+ onAIClick
24
27
  } = _ref;
25
28
  return /*#__PURE__*/_jsxs(Bar, {
26
29
  dataTestId: dataTestId,
@@ -45,6 +48,11 @@ const Header = _ref => {
45
48
  })]
46
49
  }), searchComponent && /*#__PURE__*/_jsx(SearchContainer, {
47
50
  children: searchComponent
51
+ }), onAIClick && /*#__PURE__*/_jsx(AiButton, {
52
+ type: "button",
53
+ "aria-label": "AI Assistant",
54
+ onClick: onAIClick,
55
+ children: /*#__PURE__*/_jsx(AiIcon, {})
48
56
  }), userName && /*#__PURE__*/_jsx(UserMenu, {
49
57
  userName: userName,
50
58
  avatarSource: avatarSource,
@@ -78,7 +86,9 @@ Header.propTypes = {
78
86
  /** Allows you to pass in child components to user dropdown menu */
79
87
  userMenuContent: PropTypes.node,
80
88
  /** use this to apply separator if logo and appName or currentWorkspace exists */
81
- showLogoSeparator: PropTypes.bool
89
+ showLogoSeparator: PropTypes.bool,
90
+ /** Function to run when the AI button is clicked */
91
+ onAIClick: PropTypes.func
82
92
  };
83
93
 
84
94
  /** @component */
@@ -174,6 +184,13 @@ Header.__docgenInfo = {
174
184
  "name": "node"
175
185
  },
176
186
  "required": false
187
+ },
188
+ "onAIClick": {
189
+ "description": "Function to run when the AI button is clicked",
190
+ "type": {
191
+ "name": "func"
192
+ },
193
+ "required": false
177
194
  }
178
195
  }
179
196
  };
@@ -341,4 +341,129 @@ 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
+ });
426
+ });
427
+ describe("Hover out debounce functionality", () => {
428
+ it("should not collapse navigation immediately on hover out", async () => {
429
+ const {
430
+ container
431
+ } = renderSideNav();
432
+
433
+ // First, manually collapse the navigation
434
+ const toggleButton = getToggleButton();
435
+ fireEvent.click(toggleButton);
436
+
437
+ // Wait for the navigation to collapse (check that toggle button shows "Keep Open")
438
+ await waitFor(() => {
439
+ expect(toggleButton).toHaveAttribute("data-testid", "toggle-handle");
440
+ });
441
+
442
+ // Hover in to expand
443
+ const sideNavItems = container.querySelector('[data-testid="side-nav-items"]');
444
+ fireEvent.mouseEnter(sideNavItems);
445
+
446
+ // Wait for expansion (check that toggle button shows "Collapse Navigation")
447
+ await waitFor(() => {
448
+ expect(toggleButton).toHaveAttribute("data-testid", "toggle-handle");
449
+ });
450
+
451
+ // Hover out
452
+ fireEvent.mouseLeave(sideNavItems);
453
+
454
+ // The navigation should still be expanded immediately after hover out
455
+ // We can verify this by checking that the SideNavItems still has the expanded styling
456
+ const sideNavItemsElement = container.querySelector('[data-testid="side-nav-items"]');
457
+ expect(sideNavItemsElement).toBeInTheDocument();
458
+
459
+ // Wait for the debounce delay to complete and check that the navigation collapses
460
+ // We'll verify this by checking if the toggle button behavior changes
461
+ await waitFor(() => {
462
+ // After the debounce delay, the navigation should collapse
463
+ // We can verify this by checking that the hover behavior is reset
464
+ expect(sideNavItemsElement).toBeInTheDocument();
465
+ }, {
466
+ timeout: 450
467
+ }); // 400ms debounce + buffer
468
+ });
344
469
  });
@@ -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": {
@@ -16,9 +16,10 @@ export const RESIZE_CONFIG = {
16
16
  };
17
17
 
18
18
  // New constants for better maintainability
19
- export const HOVER_TIMEOUT_MS = 300;
19
+ export const HOVER_TIMEOUT_MS = 400;
20
+ export const HOVER_OUT_DEBOUNCE_MS = 400; // Delay before triggering hover out actions
20
21
  export const DEFAULT_ANIMATION_DURATION = 200;
21
- export const RESIZE_CURSOR_TIMEOUT = 100;
22
+ export const RESIZE_CURSOR_TIMEOUT = 150;
22
23
  export const ITEM_TYPES = {
23
24
  COMPONENT: "component",
24
25
  LINK: "link",
@@ -4,7 +4,7 @@ import { getInitialSize as getInitialSizeUtil } from "../utils/resizeUtils";
4
4
  import useResponsive from "./useResponsive";
5
5
  import { useSideNavStateContext } from "../context/SideNavStateProvider";
6
6
  import { useState } from "react";
7
- import { HOVER_TIMEOUT_MS } from "../constants/sideNav";
7
+ import { HOVER_TIMEOUT_MS, HOVER_OUT_DEBOUNCE_MS } from "../constants/sideNav";
8
8
 
9
9
  /**
10
10
  * Custom hook to manage SideNavV2 state
@@ -35,6 +35,15 @@ 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);
43
+
44
+ // New refs for hover out debounce delays
45
+ const navCollapseTimeoutRef = useRef(null);
46
+ const panelHideTimeoutRef = useRef(null);
38
47
  const firstExpandedItemByDefault = findFirstExpandedByDefault(items);
39
48
 
40
49
  // Initialize expanded item by default
@@ -71,36 +80,89 @@ const useSideNavState = items => {
71
80
  if (unlockTimeoutRef.current) {
72
81
  clearTimeout(unlockTimeoutRef.current);
73
82
  }
83
+ if (navCollapseTimeoutRef.current) {
84
+ clearTimeout(navCollapseTimeoutRef.current);
85
+ }
86
+ if (panelHideTimeoutRef.current) {
87
+ clearTimeout(panelHideTimeoutRef.current);
88
+ }
74
89
  };
75
90
  }, []);
76
91
 
77
92
  // Extract common mouse event logic to reduce duplication
78
93
  const createMouseEventHandlers = useCallback(() => {
79
94
  const baseHandler = shouldExpand => {
80
- if (isLocked || isToggling) {
95
+ if (isToggling) {
81
96
  return;
82
97
  }
83
98
  isHoveringRef.current = true;
99
+ isHoveringInWrapperRef.current = true;
84
100
 
85
- // Only auto-expand if the user has manually collapsed it before
101
+ // Only auto-expand navigation if the user has manually collapsed it before
86
102
  // AND it wasn't collapsed while having an expanded panel
87
- if (shouldExpand && hasBeenManuallyCollapsedRef.current && !wasCollapsedWithExpandedPanelRef.current) {
103
+ // AND the navigation is not locked
104
+ if (shouldExpand && !isLocked && hasBeenManuallyCollapsedRef.current && !wasCollapsedWithExpandedPanelRef.current) {
88
105
  setIsExpanded(true);
89
106
  wasExpandedByHoverRef.current = true;
90
107
  }
108
+
109
+ // Auto-expand previously opened panel if hovering back in
110
+ // This should work regardless of navigation lock state
111
+ if (shouldExpand && previouslyOpenedPanelRef.current !== null && !wasPanelManuallyClosedRef.current && expandedItem === null) {
112
+ setExpandedItem(previouslyOpenedPanelRef.current);
113
+ // Mark that this panel was opened by hover (not manually)
114
+ wasPanelManuallyClosedRef.current = false;
115
+ }
91
116
  };
92
117
  return {
93
- handleEnter: () => baseHandler(true),
118
+ handleEnter: () => {
119
+ // Clear any existing timeouts when hovering back in
120
+ if (navCollapseTimeoutRef.current) {
121
+ clearTimeout(navCollapseTimeoutRef.current);
122
+ navCollapseTimeoutRef.current = null;
123
+ }
124
+ if (panelHideTimeoutRef.current) {
125
+ clearTimeout(panelHideTimeoutRef.current);
126
+ panelHideTimeoutRef.current = null;
127
+ }
128
+ baseHandler(true);
129
+ },
94
130
  handleLeave: () => {
95
- if (isLocked) return;
96
131
  isHoveringRef.current = false;
132
+ isHoveringInWrapperRef.current = false;
133
+
134
+ // Clear any existing timeouts when hovering back in
135
+ if (navCollapseTimeoutRef.current) {
136
+ clearTimeout(navCollapseTimeoutRef.current);
137
+ navCollapseTimeoutRef.current = null;
138
+ }
139
+ if (panelHideTimeoutRef.current) {
140
+ clearTimeout(panelHideTimeoutRef.current);
141
+ panelHideTimeoutRef.current = null;
142
+ }
97
143
 
98
- // Only auto-collapse if the user has manually collapsed it before
144
+ // Only auto-collapse navigation if the user has manually collapsed it before
99
145
  // AND it was expanded by hover (not locked)
100
146
  // AND it wasn't collapsed while having an expanded panel
101
- if (hasBeenManuallyCollapsedRef.current && wasExpandedByHoverRef.current && !wasCollapsedWithExpandedPanelRef.current) {
102
- setIsExpanded(false);
103
- wasExpandedByHoverRef.current = false;
147
+ if (!isLocked && hasBeenManuallyCollapsedRef.current && wasExpandedByHoverRef.current && !wasCollapsedWithExpandedPanelRef.current) {
148
+ // Add debounce delay before collapsing navigation
149
+ navCollapseTimeoutRef.current = setTimeout(() => {
150
+ setIsExpanded(false);
151
+ wasExpandedByHoverRef.current = false;
152
+ navCollapseTimeoutRef.current = null;
153
+ }, HOVER_OUT_DEBOUNCE_MS);
154
+ }
155
+
156
+ // Auto-hide ExpandedPanel when hovering out (if it was opened by hover)
157
+ // This should work regardless of navigation lock state
158
+ if (expandedItem !== null && previouslyOpenedPanelRef.current === expandedItem && !wasPanelManuallyClosedRef.current) {
159
+ // Add debounce delay before hiding the panel
160
+ panelHideTimeoutRef.current = setTimeout(() => {
161
+ // Store the panel that was open before hiding it
162
+ previouslyOpenedPanelRef.current = expandedItem;
163
+ setExpandedItem(null);
164
+ panelHideTimeoutRef.current = null;
165
+ }, HOVER_OUT_DEBOUNCE_MS);
104
166
  }
105
167
 
106
168
  // Reset the wasCollapsedWithExpandedPanelRef flag when hovering out of the entire wrapper
@@ -109,11 +171,11 @@ const useSideNavState = items => {
109
171
  // Don't reset hasBeenManuallyCollapsedRef here - it should persist until user manually expands again
110
172
  }
111
173
  };
112
- }, [isLocked, setIsExpanded, isToggling]);
174
+ }, [isLocked, setIsExpanded, isToggling, expandedItem, setExpandedItem]);
113
175
 
114
176
  // Mouse event handlers for hover state
115
177
  useEffect(() => {
116
- if (!wrapperRef.current || isLocked) return;
178
+ if (!wrapperRef.current) return;
117
179
  const wrapper = wrapperRef.current;
118
180
  const sideNavItems = wrapper.querySelector('[data-testid="side-nav-items"]');
119
181
  const toggleHandle = wrapper.querySelector(".toggle-popover");
@@ -126,12 +188,12 @@ const useSideNavState = items => {
126
188
  const handleSideNavItemsMouseEnter = handleEnter;
127
189
  const handleToggleMouseEnter = handleEnter;
128
190
  const handleSideNavItemsMouseLeave = () => {
129
- if (isLocked || isToggling) return;
191
+ if (isToggling) return;
130
192
  // Don't collapse immediately, let the wrapper handle it
131
193
  // This allows hovering over ExpandedPanel to keep it expanded
132
194
  };
133
195
  const handleToggleMouseLeave = () => {
134
- if (isLocked || isToggling) return;
196
+ if (isToggling) return;
135
197
  // Don't collapse immediately, let the wrapper handle it
136
198
  // This allows hovering over the toggle handle to keep it expanded
137
199
  };
@@ -159,8 +221,17 @@ const useSideNavState = items => {
159
221
  toggleHandle.removeEventListener("mouseleave", handleToggleMouseLeave);
160
222
  }
161
223
  };
162
- }, [isLocked, createMouseEventHandlers, isToggling]);
224
+ }, [createMouseEventHandlers, isToggling]);
163
225
  const handleItemClick = useCallback(item => {
226
+ // Clear any existing debounce timeouts when manually clicking items
227
+ if (navCollapseTimeoutRef.current) {
228
+ clearTimeout(navCollapseTimeoutRef.current);
229
+ navCollapseTimeoutRef.current = null;
230
+ }
231
+ if (panelHideTimeoutRef.current) {
232
+ clearTimeout(panelHideTimeoutRef.current);
233
+ panelHideTimeoutRef.current = null;
234
+ }
164
235
  const {
165
236
  index: itemIndex,
166
237
  actionType,
@@ -175,26 +246,51 @@ const useSideNavState = items => {
175
246
  // If we're already on this panel, toggle it closed
176
247
  if (expandedItem === relatedPanelIndex) {
177
248
  setExpandedItem(null);
249
+ // Mark that this panel was manually closed
250
+ if (previouslyOpenedPanelRef.current === relatedPanelIndex) {
251
+ wasPanelManuallyClosedRef.current = true;
252
+ }
178
253
  } else {
179
254
  // Otherwise, open the related panel
180
255
  setExpandedItem(relatedPanelIndex);
256
+ // Reset manual close flag when opening a panel
257
+ wasPanelManuallyClosedRef.current = false;
181
258
  }
182
259
  } else {
183
260
  // No related panel, close any open panel
184
261
  setExpandedItem(null);
262
+ // Mark that any open panel was manually closed
263
+ if (expandedItem !== null) {
264
+ wasPanelManuallyClosedRef.current = true;
265
+ }
185
266
  }
186
267
  } else {
187
268
  // For button items, just close any open panel
269
+ if (expandedItem !== null) {
270
+ wasPanelManuallyClosedRef.current = true;
271
+ }
188
272
  setExpandedItem(null);
189
273
  }
190
274
  onButtonClick && onButtonClick(item);
191
275
  } else {
192
276
  const wasExpanded = expandedItem !== null;
193
- setExpandedItem(itemIndex === expandedItem ? null : itemIndex);
277
+ const wasClosingPanel = itemIndex === expandedItem;
278
+ if (wasClosingPanel) {
279
+ // Closing a panel - mark it as manually closed
280
+ wasPanelManuallyClosedRef.current = true;
281
+ // Store reference to the panel that was closed
282
+ previouslyOpenedPanelRef.current = itemIndex;
283
+ } else {
284
+ // Opening a panel - reset manual close flag and track that it was opened manually
285
+ wasPanelManuallyClosedRef.current = false;
286
+ // Store reference to the panel that was opened
287
+ previouslyOpenedPanelRef.current = itemIndex;
288
+ }
289
+ setExpandedItem(wasClosingPanel ? null : itemIndex);
194
290
  onButtonClick && onButtonClick(item);
195
291
 
196
292
  // If we just closed an expanded panel, reset the flags to enable hover behavior
197
- if (wasExpanded && itemIndex === expandedItem) {
293
+ if (wasExpanded && wasClosingPanel) {
198
294
  // Reset the wasCollapsedWithExpandedPanelRef flag to enable hover behavior again
199
295
  wasCollapsedWithExpandedPanelRef.current = false;
200
296
  // Don't reset hasBeenManuallyCollapsedRef - the user has still manually collapsed the nav
@@ -214,6 +310,16 @@ const useSideNavState = items => {
214
310
  handleItemClick(item);
215
311
  }, [handleItemClick]);
216
312
  const handleExpandToggle = useCallback(() => {
313
+ // Clear any existing debounce timeouts when manually toggling
314
+ if (navCollapseTimeoutRef.current) {
315
+ clearTimeout(navCollapseTimeoutRef.current);
316
+ navCollapseTimeoutRef.current = null;
317
+ }
318
+ if (panelHideTimeoutRef.current) {
319
+ clearTimeout(panelHideTimeoutRef.current);
320
+ panelHideTimeoutRef.current = null;
321
+ }
322
+
217
323
  // Set toggling flag to prevent hover events
218
324
  setIsToggling(true);
219
325
 
@@ -278,7 +384,10 @@ const useSideNavState = items => {
278
384
  handleExpandToggle,
279
385
  isLocked,
280
386
  wasExpandedByHover: wasExpandedByHoverRef.current,
281
- hasBeenManuallyCollapsed: hasBeenManuallyCollapsedRef.current
387
+ hasBeenManuallyCollapsed: hasBeenManuallyCollapsedRef.current,
388
+ previouslyOpenedPanel: previouslyOpenedPanelRef.current,
389
+ wasPanelManuallyClosed: wasPanelManuallyClosedRef.current,
390
+ isHoveringInWrapper: isHoveringInWrapperRef.current
282
391
  };
283
392
  };
284
393
  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.55",
4
4
  "engines": {
5
5
  "node": "20.12.2"
6
6
  },