@carbon-labs/react-ui-shell 0.82.0 → 0.84.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.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @license
3
+ *
4
+ * Copyright IBM Corp. 2025
5
+ *
6
+ * This source code is licensed under the Apache-2.0 license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+ import React from 'react';
10
+ export interface HeaderOverflowPanelProps {
11
+ /**
12
+ * Provide an optional class to be applied to the containing node
13
+ */
14
+ className?: string;
15
+ /**
16
+ * Custom children to be rendered within the popover of the Overflow panel menu
17
+ */
18
+ children?: React.ReactNode;
19
+ /**
20
+ * Provide the Overflow panel's label
21
+ */
22
+ label?: string;
23
+ }
24
+ export declare const HeaderOverflowPanel: React.ForwardRefExoticComponent<HeaderOverflowPanelProps & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Copyright IBM Corp. 2024
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import { extends as _extends } from '../_virtual/_rollupPluginBabelHelpers.js';
9
+ import cx from 'classnames';
10
+ import PropTypes from 'prop-types';
11
+ import React__default from 'react';
12
+ import { usePrefix } from '../internal/usePrefix.js';
13
+ import { HeaderPopover, HeaderPopoverButton, HeaderPopoverContent } from './HeaderPopover.js';
14
+ import { OverflowMenuVertical } from '@carbon/icons-react';
15
+ import { useMatchMedia } from '../internal/useMatchMedia.js';
16
+ import { breakpoints } from '../node_modules/@carbon/layout/es/index.js';
17
+
18
+ var _OverflowMenuVertical;
19
+ const mdMediaQuery = `(max-width: ${breakpoints.md.width})`;
20
+ const HeaderOverflowPanel = /*#__PURE__*/React__default.forwardRef(function HeaderOverflowPanel({
21
+ className: customClassName,
22
+ children,
23
+ label,
24
+ ...rest
25
+ }, ref) {
26
+ const prefix = usePrefix();
27
+ const className = cx({
28
+ [`${prefix}--header-overflow-panel`]: true,
29
+ [customClassName]: !!customClassName
30
+ });
31
+ const isMd = useMatchMedia(mdMediaQuery);
32
+ return /*#__PURE__*/React__default.createElement(HeaderPopover, _extends({
33
+ ref: ref,
34
+ align: "bottom-end",
35
+ className: className
36
+ }, rest), /*#__PURE__*/React__default.createElement(HeaderPopoverButton, {
37
+ align: isMd ? 'bottom-end' : 'bottom',
38
+ label: label
39
+ }, _OverflowMenuVertical || (_OverflowMenuVertical = /*#__PURE__*/React__default.createElement(OverflowMenuVertical, null))), /*#__PURE__*/React__default.createElement(HeaderPopoverContent, null, /*#__PURE__*/React__default.createElement("ul", null, children)));
40
+ });
41
+ HeaderOverflowPanel.propTypes = {
42
+ /**
43
+ * Custom children to be rendered within the popover of the Profile menu
44
+ */
45
+ children: PropTypes.any,
46
+ /**
47
+ * Provide an optional class to be applied to the containing node
48
+ */
49
+ className: PropTypes.string,
50
+ /**
51
+ * Provide the Overflow panel's label
52
+ */
53
+ label: PropTypes.string
54
+ };
55
+
56
+ export { HeaderOverflowPanel };
@@ -30,7 +30,7 @@ const Profile = /*#__PURE__*/React__default.forwardRef(function Profile({
30
30
  align: "bottom-end",
31
31
  className: className
32
32
  }, rest), /*#__PURE__*/React__default.createElement(HeaderPopoverButton, {
33
- align: "bottom",
33
+ align: "bottom-end",
34
34
  label: label
35
35
  }, IconElement), /*#__PURE__*/React__default.createElement(HeaderPopoverContent, null, children));
36
36
  });
@@ -38,6 +38,7 @@ export interface SideNavProps extends ComponentProps<'nav'>, TranslateWithId<Tra
38
38
  hideOverlay?: boolean;
39
39
  navType?: SIDE_NAV_TYPE;
40
40
  isTreeview?: boolean;
41
+ headerOverflowPanel?: boolean;
41
42
  }
42
43
  interface SideNavContextData {
43
44
  autoExpand?: boolean;
@@ -48,6 +49,7 @@ interface SideNavContextData {
48
49
  setIsTreeview?: (value: boolean) => void;
49
50
  currentPrimaryMenu?: string;
50
51
  setCurrentPrimaryMenu?: (value: string) => void;
52
+ headerOverflowPanel?: boolean;
51
53
  }
52
54
  export declare const SideNavContext: React.Context<SideNavContextData>;
53
55
  export declare const SideNav: React.ForwardRefExoticComponent<Omit<SideNavProps, "ref"> & React.RefAttributes<HTMLElement>>;
@@ -68,6 +68,7 @@ function SideNavRenderFunction({
68
68
  isCollapsible = false,
69
69
  hideOverlay = false,
70
70
  translateWithId: t = defaultTranslateWithId,
71
+ headerOverflowPanel,
71
72
  ...other
72
73
  }, ref) {
73
74
  const [internalIsTreeview, setInternalIsTreeview] = useState(isTreeviewProp ?? false);
@@ -141,7 +142,7 @@ function SideNavRenderFunction({
141
142
  const resetNodeTabIndices = useCallback(() => {
142
143
  const items = sideNavRef?.current?.querySelectorAll('[tabIndex="0"]') ?? [];
143
144
  items.forEach(item => {
144
- if (item.classList.contains(`${prefix}--side-nav__toggle`) || item.classList.contains(`${prefix}--side-nav__back-button`) || item.closest(`.${prefix}--side-nav__slot-item`) || item.classList.contains(`${prefix}--side-nav__link`) && item.closest('ul')?.getAttribute('aria-label') === ariaLabel) {
145
+ if (item.classList.contains(`${prefix}--side-nav__toggle`) || item.classList.contains(`${prefix}--side-nav__back-button`) || item.closest(`.${prefix}--side-nav__slot-item`) || item.classList.contains(`${prefix}--side-nav__link`) && item.closest('ul')?.getAttribute('aria-label') === ariaLabel || item.closest(`.${prefix}--header-overflow-panel-secondary-container`)) {
145
146
  return;
146
147
  }
147
148
  item.tabIndex = -1;
@@ -168,7 +169,9 @@ function SideNavRenderFunction({
168
169
  }
169
170
  }, [prefix, internalIsTreeview, resetNodeTabIndices]);
170
171
  const smMediaQuery = `(min-width: ${breakpoints.sm.width})`;
171
- const isSm = useMatchMedia(smMediaQuery);
172
+ const lgMediaQuery = `(min-width: ${breakpoints.lg.width})`;
173
+ const query = !headerOverflowPanel ? smMediaQuery : lgMediaQuery;
174
+ const isSm = useMatchMedia(query);
172
175
  useEffect(() => {
173
176
  if (sideNavRef.current) {
174
177
  const backButton = sideNavRef?.current.querySelector(`.${prefix}--side-nav__back-button`);
@@ -379,7 +382,6 @@ function SideNavRenderFunction({
379
382
  sideNavRef.current.focus();
380
383
  }
381
384
  });
382
- const lgMediaQuery = `(min-width: ${breakpoints.lg.width})`;
383
385
  const isLg = useMatchMedia(lgMediaQuery);
384
386
 
385
387
  // ensure that changes are in sync with internal treeview prop
@@ -417,7 +419,8 @@ function SideNavRenderFunction({
417
419
  isTreeview: internalIsTreeview,
418
420
  setIsTreeview,
419
421
  currentPrimaryMenu,
420
- setCurrentPrimaryMenu
422
+ setCurrentPrimaryMenu,
423
+ headerOverflowPanel
421
424
  }
422
425
  }, isFixedNav || hideOverlay || navType === SIDE_NAV_TYPE.RAIL_PANEL ? null :
423
426
  /*#__PURE__*/
@@ -458,84 +461,74 @@ SideNav.propTypes = {
458
461
  */
459
462
  addMouseListeners: PropTypes.bool,
460
463
  /**
461
- * Optionally provide a custom class to apply to the underlying `<li>` node
464
+ * Optionally provide a custom class to apply to the `<nav>` element
462
465
  */
463
466
  className: PropTypes.string,
464
467
  /**
465
- * If `true`, the SideNav will be open on initial render.
468
+ * Specify whether the `SideNav` starts expanded when initially rendered. Only applies when using the `SideNav` as an uncontrolled component.
466
469
  */
467
470
  defaultExpanded: PropTypes.bool,
468
471
  /**
469
- * Specify the duration in milliseconds to delay before displaying the sidenavigation
472
+ * Specify the duration in milliseconds to delay before displaying the `SideNav`.
470
473
  */
471
474
  enterDelayMs: PropTypes.number,
472
475
  /**
473
- * If `true`, the SideNav will be expanded, otherwise it will be collapsed.
474
- * Using this prop causes SideNav to become a controled component.
476
+ * Control the expanded state of the `SideNav` externally. When provided, the `SideNav` becomes a controlled component and you must handle toggle events.
475
477
  */
476
478
  expanded: PropTypes.bool,
477
479
  /**
478
- * If `true`, the overlay will be hidden. Defaults to `false`.
480
+ * Specify whether the `SideNav` is rendered inside a `HeaderOverflowPanel`. When `true`, adjusts the responsive behavior to work correctly within the overflow menu at mobile/tablet breakpoints.
481
+ */
482
+ headerOverflowPanel: PropTypes.bool,
483
+ /**
484
+ * If `true`, the backdrop overlay will be hidden at all breakpoints. By default, the overlay appears behind the `SideNav` on mobile and tablet (below `lg` breakpoint).
479
485
  */
480
486
  hideOverlay: PropTypes.bool,
481
487
  /**
482
- * Specify the breakpoint at which the SideNav will be hidden.
483
- * Can be one of `sm`, `md`, `lg`, `xlg`, or `max`.
484
- * Only applies when `isRail` is `true`.
488
+ * Specify the breakpoint at which the `SideNav` will be hidden. Can be one of `sm`, `md`, `lg`, `xlg`, or `max`. Only applies when `isRail` is `true` or `navType` is `RAIL_PANEL`.
485
489
  */
486
490
  hideRailBreakpointDown: PropTypes.oneOf(['sm', 'md', 'lg', 'xlg', 'max']),
487
491
  /**
488
- * Provide the `href` to the id of the element on your package that is the
489
- * main content.
492
+ * Provide an `href` (typically an anchor like `#main-content`) to move focus to when closing the `SideNav` with the Escape key.
490
493
  */
491
494
  href: PropTypes.string,
492
495
  /**
493
- * Optionally provide a custom class to apply to the underlying `<li>` node
496
+ * Specify whether the `SideNav` is the primary navigation controlled by the header. When `true`, the `SideNav` is part of the UI Shell header layout (full-width on desktop, collapses on mobile). Set to `false` for secondary navigation / rails, overflow panels, or standalone navigation that is independent of the header.
494
497
  */
495
498
  isChildOfHeader: PropTypes.bool,
496
499
  /**
497
- * Specify whether the SideNav is collapsible at desktop
500
+ * Specify whether the `SideNav` can be toggled open/closed on desktop. When `true`, the `SideNav` starts collapsed and users can expand it. Requires `isChildOfHeader` to be `true` (default).
498
501
  */
499
502
  isCollapsible: PropTypes.bool,
500
503
  /**
501
- * Specify if sideNav is standalone
504
+ * Specify if `SideNav` is standalone.
502
505
  */
503
506
  isFixedNav: PropTypes.bool,
504
507
  /**
505
- * Specify if the sideNav will be persistent above the lg breakpoint
508
+ * Specify whether the `SideNav` is visible by default. When `false`, applies the hidden class which sets width to 0.
506
509
  */
507
510
  isPersistent: PropTypes.bool,
508
511
  /**
509
- * Optional prop to display the side nav rail.
512
+ * Specify whether to display the `SideNav` rail variant. When `true`, the `SideNav` displays as a narrow rail (48px) that expands to full-width on hover.
510
513
  */
511
514
  isRail: PropTypes.bool,
512
515
  /**
513
- * An optional listener that is called when the SideNav overlay is clicked
516
+ * An optional listener that is called when the `SideNav` overlay is clicked.
514
517
  *
515
518
  * @param {object} event
516
519
  */
517
520
  onOverlayClick: PropTypes.func,
518
521
  /**
519
- * An optional listener that is called a callback to collapse the SideNav
522
+ * An optional listener that is called as a callback to collapse the `SideNav`.
520
523
  */
521
-
522
524
  onSideNavBlur: PropTypes.func,
523
525
  /**
524
- * An optional listener that is called when an event that would cause
525
- * toggling the SideNav occurs.
526
+ * An optional listener that is called when an event that would cause toggling the `SideNav` occurs.
526
527
  *
527
528
  * @param {object} event
528
529
  * @param {boolean} value
529
530
  */
530
531
  onToggle: PropTypes.func
531
-
532
- /**
533
- * Provide a custom function for translating all message ids within this
534
- * component. This function will take in two arguments: the mesasge Id and the
535
- * state of the component. From this, you should return a string representing
536
- * the label you want displayed or read by screen readers.
537
- */
538
- // translateById: PropTypes.func,
539
532
  };
540
533
 
541
534
  export { SIDE_NAV_TYPE, SideNav, SideNavContext, translationIds };
@@ -25,6 +25,7 @@ import { useMatchMedia } from '../internal/useMatchMedia.js';
25
25
 
26
26
  var _ArrowLeft, _SharkFinIcon, _ChevronRight, _ChevronDown;
27
27
  const smMediaQuery = `(max-width: ${breakpoints.md.width})`;
28
+ const mdMediaQuery = `(max-width: ${breakpoints.lg.width})`;
28
29
  const SideNavMenu = /*#__PURE__*/React__default.forwardRef(function SideNavMenu({
29
30
  backButtonRenderIcon = () => _ArrowLeft || (_ArrowLeft = /*#__PURE__*/React__default.createElement(ArrowLeft, {
30
31
  size: 16
@@ -50,7 +51,8 @@ const SideNavMenu = /*#__PURE__*/React__default.forwardRef(function SideNavMenu(
50
51
  expanded,
51
52
  navType,
52
53
  isRail,
53
- setIsTreeview
54
+ setIsTreeview,
55
+ headerOverflowPanel
54
56
  } = useContext(SideNavContext);
55
57
  const sideNavExpanded = expanded;
56
58
  const prefix = usePrefix();
@@ -97,8 +99,6 @@ const SideNavMenu = /*#__PURE__*/React__default.forwardRef(function SideNavMenu(
97
99
  if (/*#__PURE__*/React__default.isValidElement(children)) {
98
100
  const props = children.props;
99
101
  if (props.isActive === true || props['aria-current']) {
100
- // setActive(true);
101
-
102
102
  return true;
103
103
  }
104
104
  }
@@ -138,7 +138,7 @@ const SideNavMenu = /*#__PURE__*/React__default.forwardRef(function SideNavMenu(
138
138
  [`${prefix}--side-nav__submenu`]: true,
139
139
  [`${prefix}--side-nav__submenu--active`]: active || hasActiveDescendant && isExpanded
140
140
  });
141
- const primaryClassNames = cx({
141
+ const secondaryClassNames = cx({
142
142
  [`${prefix}--side-nav__menu-secondary-wrapper`]: true,
143
143
  [`${prefix}--side-nav__menu-secondary-wrapper-expanded`]: isSideNavExpanded && isSecondaryOpen && currentPrimaryMenu === uniqueId
144
144
  });
@@ -301,7 +301,8 @@ const SideNavMenu = /*#__PURE__*/React__default.forwardRef(function SideNavMenu(
301
301
  }
302
302
  }, [sideNavExpanded]);
303
303
  const [openPopover, setOpenPopover] = React__default.useState(false);
304
- const isSm = useMatchMedia(smMediaQuery);
304
+ const query = !headerOverflowPanel ? smMediaQuery : mdMediaQuery;
305
+ const isSm = useMatchMedia(query);
305
306
 
306
307
  // keeps the secondary open when moving from small to large breakpoints
307
308
  useEffect(() => {
@@ -355,8 +356,8 @@ const SideNavMenu = /*#__PURE__*/React__default.forwardRef(function SideNavMenu(
355
356
  })) : _ChevronDown || (_ChevronDown = /*#__PURE__*/React__default.createElement(ChevronDown, {
356
357
  size: 20
357
358
  })))), primary ? /*#__PURE__*/React__default.createElement(Layer, null, /*#__PURE__*/React__default.createElement("div", {
358
- className: primaryClassNames
359
- }, /*#__PURE__*/React__default.createElement(SideNavItems, {
359
+ className: secondaryClassNames
360
+ }, !headerOverflowPanel ? /*#__PURE__*/React__default.createElement(SideNavItems, {
360
361
  accessibilityLabel: {
361
362
  'aria-label': `${title} submenu`
362
363
  }
@@ -367,6 +368,15 @@ const SideNavMenu = /*#__PURE__*/React__default.forwardRef(function SideNavMenu(
367
368
  onClick: handleOnBackButtonClick,
368
369
  className: `${prefix}--side-nav__back-button`,
369
370
  renderIcon: backButtonRenderIcon
371
+ }, backButtonTitle), childrenToRender) : /*#__PURE__*/React__default.createElement("div", {
372
+ className: `${prefix}--header-overflow-panel-secondary-container`
373
+ }, /*#__PURE__*/React__default.createElement(Button, {
374
+ ref: backButtonRef,
375
+ kind: "ghost",
376
+ size: "md",
377
+ onClick: handleOnBackButtonClick,
378
+ className: `${prefix}--side-nav__back-button`,
379
+ renderIcon: backButtonRenderIcon
370
380
  }, backButtonTitle), childrenToRender))) : /*#__PURE__*/React__default.createElement("ul", {
371
381
  className: `${prefix}--side-nav__menu`,
372
382
  role: "group"
package/es/index.d.ts CHANGED
@@ -18,5 +18,6 @@ export { SharkFinIcon } from './components/SharkFinIcon';
18
18
  export { HeaderDivider } from './components/HeaderDivider';
19
19
  export { TrialCountdown } from './components/TrialCountdown';
20
20
  export * as Profile from './components/Profile';
21
+ export { HeaderOverflowPanel } from './components/HeaderOverflowPanel';
21
22
  export { SideNavSlot } from './components/SideNavSlot';
22
23
  export { SideNavTitle } from './components/SideNavTitle';
package/es/index.js CHANGED
@@ -18,5 +18,6 @@ export { HeaderDivider } from './components/HeaderDivider.js';
18
18
  export { TrialCountdown } from './components/TrialCountdown.js';
19
19
  import * as Profile from './components/Profile.js';
20
20
  export { Profile };
21
+ export { HeaderOverflowPanel } from './components/HeaderOverflowPanel.js';
21
22
  export { SideNavSlot } from './components/SideNavSlot.js';
22
23
  export { SideNavTitle } from './components/SideNavTitle.js';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @license
3
+ *
4
+ * Copyright IBM Corp. 2025
5
+ *
6
+ * This source code is licensed under the Apache-2.0 license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+ import React from 'react';
10
+ export interface HeaderOverflowPanelProps {
11
+ /**
12
+ * Provide an optional class to be applied to the containing node
13
+ */
14
+ className?: string;
15
+ /**
16
+ * Custom children to be rendered within the popover of the Overflow panel menu
17
+ */
18
+ children?: React.ReactNode;
19
+ /**
20
+ * Provide the Overflow panel's label
21
+ */
22
+ label?: string;
23
+ }
24
+ export declare const HeaderOverflowPanel: React.ForwardRefExoticComponent<HeaderOverflowPanelProps & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Copyright IBM Corp. 2024
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ var _rollupPluginBabelHelpers = require('../_virtual/_rollupPluginBabelHelpers.js');
11
+ var cx = require('classnames');
12
+ var PropTypes = require('prop-types');
13
+ var React__default = require('react');
14
+ var usePrefix = require('../internal/usePrefix.js');
15
+ var HeaderPopover = require('./HeaderPopover.js');
16
+ var iconsReact = require('@carbon/icons-react');
17
+ var useMatchMedia = require('../internal/useMatchMedia.js');
18
+ var index = require('../node_modules/@carbon/layout/es/index.js');
19
+
20
+ var _OverflowMenuVertical;
21
+ const mdMediaQuery = `(max-width: ${index.breakpoints.md.width})`;
22
+ const HeaderOverflowPanel = /*#__PURE__*/React__default.forwardRef(function HeaderOverflowPanel({
23
+ className: customClassName,
24
+ children,
25
+ label,
26
+ ...rest
27
+ }, ref) {
28
+ const prefix = usePrefix.usePrefix();
29
+ const className = cx({
30
+ [`${prefix}--header-overflow-panel`]: true,
31
+ [customClassName]: !!customClassName
32
+ });
33
+ const isMd = useMatchMedia.useMatchMedia(mdMediaQuery);
34
+ return /*#__PURE__*/React__default.createElement(HeaderPopover.HeaderPopover, _rollupPluginBabelHelpers.extends({
35
+ ref: ref,
36
+ align: "bottom-end",
37
+ className: className
38
+ }, rest), /*#__PURE__*/React__default.createElement(HeaderPopover.HeaderPopoverButton, {
39
+ align: isMd ? 'bottom-end' : 'bottom',
40
+ label: label
41
+ }, _OverflowMenuVertical || (_OverflowMenuVertical = /*#__PURE__*/React__default.createElement(iconsReact.OverflowMenuVertical, null))), /*#__PURE__*/React__default.createElement(HeaderPopover.HeaderPopoverContent, null, /*#__PURE__*/React__default.createElement("ul", null, children)));
42
+ });
43
+ HeaderOverflowPanel.propTypes = {
44
+ /**
45
+ * Custom children to be rendered within the popover of the Profile menu
46
+ */
47
+ children: PropTypes.any,
48
+ /**
49
+ * Provide an optional class to be applied to the containing node
50
+ */
51
+ className: PropTypes.string,
52
+ /**
53
+ * Provide the Overflow panel's label
54
+ */
55
+ label: PropTypes.string
56
+ };
57
+
58
+ exports.HeaderOverflowPanel = HeaderOverflowPanel;
@@ -32,7 +32,7 @@ const Profile = /*#__PURE__*/React__default.forwardRef(function Profile({
32
32
  align: "bottom-end",
33
33
  className: className
34
34
  }, rest), /*#__PURE__*/React__default.createElement(HeaderPopover.HeaderPopoverButton, {
35
- align: "bottom",
35
+ align: "bottom-end",
36
36
  label: label
37
37
  }, IconElement), /*#__PURE__*/React__default.createElement(HeaderPopover.HeaderPopoverContent, null, children));
38
38
  });
@@ -38,6 +38,7 @@ export interface SideNavProps extends ComponentProps<'nav'>, TranslateWithId<Tra
38
38
  hideOverlay?: boolean;
39
39
  navType?: SIDE_NAV_TYPE;
40
40
  isTreeview?: boolean;
41
+ headerOverflowPanel?: boolean;
41
42
  }
42
43
  interface SideNavContextData {
43
44
  autoExpand?: boolean;
@@ -48,6 +49,7 @@ interface SideNavContextData {
48
49
  setIsTreeview?: (value: boolean) => void;
49
50
  currentPrimaryMenu?: string;
50
51
  setCurrentPrimaryMenu?: (value: string) => void;
52
+ headerOverflowPanel?: boolean;
51
53
  }
52
54
  export declare const SideNavContext: React.Context<SideNavContextData>;
53
55
  export declare const SideNav: React.ForwardRefExoticComponent<Omit<SideNavProps, "ref"> & React.RefAttributes<HTMLElement>>;
@@ -70,6 +70,7 @@ function SideNavRenderFunction({
70
70
  isCollapsible = false,
71
71
  hideOverlay = false,
72
72
  translateWithId: t = defaultTranslateWithId,
73
+ headerOverflowPanel,
73
74
  ...other
74
75
  }, ref) {
75
76
  const [internalIsTreeview, setInternalIsTreeview] = React__default.useState(isTreeviewProp ?? false);
@@ -143,7 +144,7 @@ function SideNavRenderFunction({
143
144
  const resetNodeTabIndices = React__default.useCallback(() => {
144
145
  const items = sideNavRef?.current?.querySelectorAll('[tabIndex="0"]') ?? [];
145
146
  items.forEach(item => {
146
- if (item.classList.contains(`${prefix}--side-nav__toggle`) || item.classList.contains(`${prefix}--side-nav__back-button`) || item.closest(`.${prefix}--side-nav__slot-item`) || item.classList.contains(`${prefix}--side-nav__link`) && item.closest('ul')?.getAttribute('aria-label') === ariaLabel) {
147
+ if (item.classList.contains(`${prefix}--side-nav__toggle`) || item.classList.contains(`${prefix}--side-nav__back-button`) || item.closest(`.${prefix}--side-nav__slot-item`) || item.classList.contains(`${prefix}--side-nav__link`) && item.closest('ul')?.getAttribute('aria-label') === ariaLabel || item.closest(`.${prefix}--header-overflow-panel-secondary-container`)) {
147
148
  return;
148
149
  }
149
150
  item.tabIndex = -1;
@@ -170,7 +171,9 @@ function SideNavRenderFunction({
170
171
  }
171
172
  }, [prefix, internalIsTreeview, resetNodeTabIndices]);
172
173
  const smMediaQuery = `(min-width: ${index.breakpoints.sm.width})`;
173
- const isSm = useMatchMedia.useMatchMedia(smMediaQuery);
174
+ const lgMediaQuery = `(min-width: ${index.breakpoints.lg.width})`;
175
+ const query = !headerOverflowPanel ? smMediaQuery : lgMediaQuery;
176
+ const isSm = useMatchMedia.useMatchMedia(query);
174
177
  React__default.useEffect(() => {
175
178
  if (sideNavRef.current) {
176
179
  const backButton = sideNavRef?.current.querySelector(`.${prefix}--side-nav__back-button`);
@@ -381,7 +384,6 @@ function SideNavRenderFunction({
381
384
  sideNavRef.current.focus();
382
385
  }
383
386
  });
384
- const lgMediaQuery = `(min-width: ${index.breakpoints.lg.width})`;
385
387
  const isLg = useMatchMedia.useMatchMedia(lgMediaQuery);
386
388
 
387
389
  // ensure that changes are in sync with internal treeview prop
@@ -419,7 +421,8 @@ function SideNavRenderFunction({
419
421
  isTreeview: internalIsTreeview,
420
422
  setIsTreeview,
421
423
  currentPrimaryMenu,
422
- setCurrentPrimaryMenu
424
+ setCurrentPrimaryMenu,
425
+ headerOverflowPanel
423
426
  }
424
427
  }, isFixedNav || hideOverlay || navType === SIDE_NAV_TYPE.RAIL_PANEL ? null :
425
428
  /*#__PURE__*/
@@ -460,84 +463,74 @@ SideNav.propTypes = {
460
463
  */
461
464
  addMouseListeners: PropTypes.bool,
462
465
  /**
463
- * Optionally provide a custom class to apply to the underlying `<li>` node
466
+ * Optionally provide a custom class to apply to the `<nav>` element
464
467
  */
465
468
  className: PropTypes.string,
466
469
  /**
467
- * If `true`, the SideNav will be open on initial render.
470
+ * Specify whether the `SideNav` starts expanded when initially rendered. Only applies when using the `SideNav` as an uncontrolled component.
468
471
  */
469
472
  defaultExpanded: PropTypes.bool,
470
473
  /**
471
- * Specify the duration in milliseconds to delay before displaying the sidenavigation
474
+ * Specify the duration in milliseconds to delay before displaying the `SideNav`.
472
475
  */
473
476
  enterDelayMs: PropTypes.number,
474
477
  /**
475
- * If `true`, the SideNav will be expanded, otherwise it will be collapsed.
476
- * Using this prop causes SideNav to become a controled component.
478
+ * Control the expanded state of the `SideNav` externally. When provided, the `SideNav` becomes a controlled component and you must handle toggle events.
477
479
  */
478
480
  expanded: PropTypes.bool,
479
481
  /**
480
- * If `true`, the overlay will be hidden. Defaults to `false`.
482
+ * Specify whether the `SideNav` is rendered inside a `HeaderOverflowPanel`. When `true`, adjusts the responsive behavior to work correctly within the overflow menu at mobile/tablet breakpoints.
483
+ */
484
+ headerOverflowPanel: PropTypes.bool,
485
+ /**
486
+ * If `true`, the backdrop overlay will be hidden at all breakpoints. By default, the overlay appears behind the `SideNav` on mobile and tablet (below `lg` breakpoint).
481
487
  */
482
488
  hideOverlay: PropTypes.bool,
483
489
  /**
484
- * Specify the breakpoint at which the SideNav will be hidden.
485
- * Can be one of `sm`, `md`, `lg`, `xlg`, or `max`.
486
- * Only applies when `isRail` is `true`.
490
+ * Specify the breakpoint at which the `SideNav` will be hidden. Can be one of `sm`, `md`, `lg`, `xlg`, or `max`. Only applies when `isRail` is `true` or `navType` is `RAIL_PANEL`.
487
491
  */
488
492
  hideRailBreakpointDown: PropTypes.oneOf(['sm', 'md', 'lg', 'xlg', 'max']),
489
493
  /**
490
- * Provide the `href` to the id of the element on your package that is the
491
- * main content.
494
+ * Provide an `href` (typically an anchor like `#main-content`) to move focus to when closing the `SideNav` with the Escape key.
492
495
  */
493
496
  href: PropTypes.string,
494
497
  /**
495
- * Optionally provide a custom class to apply to the underlying `<li>` node
498
+ * Specify whether the `SideNav` is the primary navigation controlled by the header. When `true`, the `SideNav` is part of the UI Shell header layout (full-width on desktop, collapses on mobile). Set to `false` for secondary navigation / rails, overflow panels, or standalone navigation that is independent of the header.
496
499
  */
497
500
  isChildOfHeader: PropTypes.bool,
498
501
  /**
499
- * Specify whether the SideNav is collapsible at desktop
502
+ * Specify whether the `SideNav` can be toggled open/closed on desktop. When `true`, the `SideNav` starts collapsed and users can expand it. Requires `isChildOfHeader` to be `true` (default).
500
503
  */
501
504
  isCollapsible: PropTypes.bool,
502
505
  /**
503
- * Specify if sideNav is standalone
506
+ * Specify if `SideNav` is standalone.
504
507
  */
505
508
  isFixedNav: PropTypes.bool,
506
509
  /**
507
- * Specify if the sideNav will be persistent above the lg breakpoint
510
+ * Specify whether the `SideNav` is visible by default. When `false`, applies the hidden class which sets width to 0.
508
511
  */
509
512
  isPersistent: PropTypes.bool,
510
513
  /**
511
- * Optional prop to display the side nav rail.
514
+ * Specify whether to display the `SideNav` rail variant. When `true`, the `SideNav` displays as a narrow rail (48px) that expands to full-width on hover.
512
515
  */
513
516
  isRail: PropTypes.bool,
514
517
  /**
515
- * An optional listener that is called when the SideNav overlay is clicked
518
+ * An optional listener that is called when the `SideNav` overlay is clicked.
516
519
  *
517
520
  * @param {object} event
518
521
  */
519
522
  onOverlayClick: PropTypes.func,
520
523
  /**
521
- * An optional listener that is called a callback to collapse the SideNav
524
+ * An optional listener that is called as a callback to collapse the `SideNav`.
522
525
  */
523
-
524
526
  onSideNavBlur: PropTypes.func,
525
527
  /**
526
- * An optional listener that is called when an event that would cause
527
- * toggling the SideNav occurs.
528
+ * An optional listener that is called when an event that would cause toggling the `SideNav` occurs.
528
529
  *
529
530
  * @param {object} event
530
531
  * @param {boolean} value
531
532
  */
532
533
  onToggle: PropTypes.func
533
-
534
- /**
535
- * Provide a custom function for translating all message ids within this
536
- * component. This function will take in two arguments: the mesasge Id and the
537
- * state of the component. From this, you should return a string representing
538
- * the label you want displayed or read by screen readers.
539
- */
540
- // translateById: PropTypes.func,
541
534
  };
542
535
 
543
536
  exports.SIDE_NAV_TYPE = SIDE_NAV_TYPE;
@@ -27,6 +27,7 @@ var useMatchMedia = require('../internal/useMatchMedia.js');
27
27
 
28
28
  var _ArrowLeft, _SharkFinIcon, _ChevronRight, _ChevronDown;
29
29
  const smMediaQuery = `(max-width: ${index.breakpoints.md.width})`;
30
+ const mdMediaQuery = `(max-width: ${index.breakpoints.lg.width})`;
30
31
  const SideNavMenu = /*#__PURE__*/React__default.forwardRef(function SideNavMenu({
31
32
  backButtonRenderIcon = () => _ArrowLeft || (_ArrowLeft = /*#__PURE__*/React__default.createElement(iconsReact.ArrowLeft, {
32
33
  size: 16
@@ -52,7 +53,8 @@ const SideNavMenu = /*#__PURE__*/React__default.forwardRef(function SideNavMenu(
52
53
  expanded,
53
54
  navType,
54
55
  isRail,
55
- setIsTreeview
56
+ setIsTreeview,
57
+ headerOverflowPanel
56
58
  } = React__default.useContext(SideNav.SideNavContext);
57
59
  const sideNavExpanded = expanded;
58
60
  const prefix = usePrefix.usePrefix();
@@ -99,8 +101,6 @@ const SideNavMenu = /*#__PURE__*/React__default.forwardRef(function SideNavMenu(
99
101
  if (/*#__PURE__*/React__default.isValidElement(children)) {
100
102
  const props = children.props;
101
103
  if (props.isActive === true || props['aria-current']) {
102
- // setActive(true);
103
-
104
104
  return true;
105
105
  }
106
106
  }
@@ -140,7 +140,7 @@ const SideNavMenu = /*#__PURE__*/React__default.forwardRef(function SideNavMenu(
140
140
  [`${prefix}--side-nav__submenu`]: true,
141
141
  [`${prefix}--side-nav__submenu--active`]: active || hasActiveDescendant && isExpanded
142
142
  });
143
- const primaryClassNames = cx({
143
+ const secondaryClassNames = cx({
144
144
  [`${prefix}--side-nav__menu-secondary-wrapper`]: true,
145
145
  [`${prefix}--side-nav__menu-secondary-wrapper-expanded`]: isSideNavExpanded && isSecondaryOpen && currentPrimaryMenu === uniqueId
146
146
  });
@@ -303,7 +303,8 @@ const SideNavMenu = /*#__PURE__*/React__default.forwardRef(function SideNavMenu(
303
303
  }
304
304
  }, [sideNavExpanded]);
305
305
  const [openPopover, setOpenPopover] = React__default.useState(false);
306
- const isSm = useMatchMedia.useMatchMedia(smMediaQuery);
306
+ const query = !headerOverflowPanel ? smMediaQuery : mdMediaQuery;
307
+ const isSm = useMatchMedia.useMatchMedia(query);
307
308
 
308
309
  // keeps the secondary open when moving from small to large breakpoints
309
310
  React__default.useEffect(() => {
@@ -357,8 +358,8 @@ const SideNavMenu = /*#__PURE__*/React__default.forwardRef(function SideNavMenu(
357
358
  })) : _ChevronDown || (_ChevronDown = /*#__PURE__*/React__default.createElement(iconsReact.ChevronDown, {
358
359
  size: 20
359
360
  })))), primary ? /*#__PURE__*/React__default.createElement(react.Layer, null, /*#__PURE__*/React__default.createElement("div", {
360
- className: primaryClassNames
361
- }, /*#__PURE__*/React__default.createElement(SideNavItems.SideNavItems, {
361
+ className: secondaryClassNames
362
+ }, !headerOverflowPanel ? /*#__PURE__*/React__default.createElement(SideNavItems.SideNavItems, {
362
363
  accessibilityLabel: {
363
364
  'aria-label': `${title} submenu`
364
365
  }
@@ -369,6 +370,15 @@ const SideNavMenu = /*#__PURE__*/React__default.forwardRef(function SideNavMenu(
369
370
  onClick: handleOnBackButtonClick,
370
371
  className: `${prefix}--side-nav__back-button`,
371
372
  renderIcon: backButtonRenderIcon
373
+ }, backButtonTitle), childrenToRender) : /*#__PURE__*/React__default.createElement("div", {
374
+ className: `${prefix}--header-overflow-panel-secondary-container`
375
+ }, /*#__PURE__*/React__default.createElement(react.Button, {
376
+ ref: backButtonRef,
377
+ kind: "ghost",
378
+ size: "md",
379
+ onClick: handleOnBackButtonClick,
380
+ className: `${prefix}--side-nav__back-button`,
381
+ renderIcon: backButtonRenderIcon
372
382
  }, backButtonTitle), childrenToRender))) : /*#__PURE__*/React__default.createElement("ul", {
373
383
  className: `${prefix}--side-nav__menu`,
374
384
  role: "group"
package/lib/index.d.ts CHANGED
@@ -18,5 +18,6 @@ export { SharkFinIcon } from './components/SharkFinIcon';
18
18
  export { HeaderDivider } from './components/HeaderDivider';
19
19
  export { TrialCountdown } from './components/TrialCountdown';
20
20
  export * as Profile from './components/Profile';
21
+ export { HeaderOverflowPanel } from './components/HeaderOverflowPanel';
21
22
  export { SideNavSlot } from './components/SideNavSlot';
22
23
  export { SideNavTitle } from './components/SideNavTitle';
package/lib/index.js CHANGED
@@ -19,6 +19,7 @@ var SharkFinIcon = require('./components/SharkFinIcon.js');
19
19
  var HeaderDivider = require('./components/HeaderDivider.js');
20
20
  var TrialCountdown = require('./components/TrialCountdown.js');
21
21
  var Profile = require('./components/Profile.js');
22
+ var HeaderOverflowPanel = require('./components/HeaderOverflowPanel.js');
22
23
  var SideNavSlot = require('./components/SideNavSlot.js');
23
24
  var SideNavTitle = require('./components/SideNavTitle.js');
24
25
 
@@ -40,5 +41,6 @@ exports.SharkFinIcon = SharkFinIcon.SharkFinIcon;
40
41
  exports.HeaderDivider = HeaderDivider.HeaderDivider;
41
42
  exports.TrialCountdown = TrialCountdown.TrialCountdown;
42
43
  exports.Profile = Profile;
44
+ exports.HeaderOverflowPanel = HeaderOverflowPanel.HeaderOverflowPanel;
43
45
  exports.SideNavSlot = SideNavSlot.SideNavSlot;
44
46
  exports.SideNavTitle = SideNavTitle.SideNavTitle;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carbon-labs/react-ui-shell",
3
- "version": "0.82.0",
3
+ "version": "0.84.0",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": true
@@ -42,5 +42,5 @@
42
42
  "dependencies": {
43
43
  "@ibm/telemetry-js": "^1.10.2"
44
44
  },
45
- "gitHead": "5520cf06630215524af572ddea13596eaf8a0811"
45
+ "gitHead": "a0eaa1502bff6b7ee998a43d67e260a4ede0e2eb"
46
46
  }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Copyright IBM Corp. 2025
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ @use '@carbon/styles/scss/spacing' as *;
9
+ @use '@carbon/styles/scss/theme' as *;
10
+ @use '@carbon/react/scss/utilities/convert' as convert;
11
+ @use '@carbon/styles/scss/breakpoint' as *;
12
+ @use '@carbon/styles/scss/type' as *;
13
+
14
+ $prefix: 'cds' !default;
15
+
16
+ @include breakpoint-up(lg) {
17
+ .#{$prefix}--header-overflow-panel {
18
+ display: none;
19
+ }
20
+ }
21
+
22
+ // COPIED FROM SIDE NAV BUT NOW AT LG BREAKPOINT
23
+ @include breakpoint-down(lg) {
24
+ .#{$prefix}--header-overflow-panel
25
+ .#{$prefix}--side-nav--collapsible.#{$prefix}--side-nav--expanded {
26
+ inline-size: 100%;
27
+ max-inline-size: 100%;
28
+ }
29
+
30
+ .#{$prefix}--header-overflow-panel
31
+ .#{$prefix}--header-overflow-panel-secondary-container {
32
+ padding-block-start: $spacing-05;
33
+ }
34
+
35
+ .#{$prefix}--header-overflow-panel
36
+ .#{$prefix}--side-nav__menu-secondary-wrapper-expanded {
37
+ z-index: 1;
38
+ background-color: $background;
39
+ inline-size: 100%;
40
+ inset-inline-start: 0;
41
+ }
42
+
43
+ .#{$prefix}--header-overflow-panel
44
+ .#{$prefix}--side-nav__back-button.#{$prefix}--btn--ghost {
45
+ flex-direction: row-reverse;
46
+ justify-content: flex-end;
47
+ gap: 1rem;
48
+ inline-size: 100%;
49
+ max-inline-size: 100%;
50
+
51
+ &:focus {
52
+ box-shadow: inset 0 0 0 1px $focus;
53
+ }
54
+ }
55
+
56
+ .#{$prefix}--header-overflow-panel
57
+ .#{$prefix}--side-nav__back-button.#{$prefix}--btn--ghost:not([disabled]) {
58
+ svg {
59
+ align-self: center;
60
+ fill: $icon-interactive;
61
+ }
62
+
63
+ &:hover svg {
64
+ fill: $link-primary-hover;
65
+ }
66
+ }
67
+
68
+ .#{$prefix}--header-overflow-panel .#{$prefix}--side-nav__item {
69
+ .#{$prefix}--side-nav__link,
70
+ .#{$prefix}--side-nav__submenu {
71
+ block-size: $spacing-08;
72
+ }
73
+ }
74
+ }
75
+
76
+ @include breakpoint(md) {
77
+ .#{$prefix}--header-overflow-panel.#{$prefix}--popover--tab-tip.#{$prefix}--popover--bottom-end
78
+ .#{$prefix}--popover
79
+ > .#{$prefix}--header-action__content.#{$prefix}--popover-content {
80
+ inset-inline-end: -$spacing-09;
81
+ }
82
+ }
83
+
84
+ .#{$prefix}--header-overflow-panel .#{$prefix}--toggletip-content {
85
+ padding: 0;
86
+ background-color: $background;
87
+ block-size: 100vh;
88
+ border-block-end: 1px solid $border-subtle;
89
+ border-inline-start: 1px solid $border-subtle;
90
+ gap: 0;
91
+ inline-size: convert.to-rem(256px);
92
+ overflow-y: auto;
93
+
94
+ > * {
95
+ padding: $spacing-05;
96
+ outline: convert.to-rem(0.5px) solid $border-subtle;
97
+ }
98
+ }
99
+
100
+ .#{$prefix}--header-overflow-panel .#{$prefix}--side-nav__link-text {
101
+ display: flex;
102
+ align-items: center;
103
+ justify-content: space-between;
104
+ inline-size: 100%;
105
+ }
@@ -15,10 +15,12 @@
15
15
  $prefix: 'cds' !default;
16
16
 
17
17
  // profile
18
+
18
19
  .#{$prefix}--profile.#{$prefix}--popover--open {
19
20
  background-color: $background;
20
21
  }
21
22
 
23
+ .#{$prefix}--header-overflow-panel .#{$prefix}--toggletip-content,
22
24
  .#{$prefix}--profile .#{$prefix}--toggletip-content {
23
25
  padding: 0;
24
26
  background-color: $background;
@@ -28,15 +30,21 @@ $prefix: 'cds' !default;
28
30
  inline-size: convert.to-rem(256px);
29
31
  max-block-size: 100vh;
30
32
  overflow-y: auto;
33
+ }
31
34
 
32
- > * {
35
+ .#{$prefix}--header-overflow-panel,
36
+ .#{$prefix}--profile {
37
+ .#{$prefix}--profile-user-info,
38
+ .#{$prefix}--profile-read-only,
39
+ .clabs--theme-settings {
33
40
  padding: $spacing-05;
34
- outline: convert.to-rem(0.5px) solid $border-subtle;
41
+ border-block-end: convert.to-rem(0.5px) solid $border-subtle;
42
+ border-block-start: convert.to-rem(0.5px) solid $border-subtle;
35
43
  }
36
44
  }
37
45
 
38
46
  // user-info
39
- .#{$prefix}--profile .#{$prefix}--profile-user-info {
47
+ .#{$prefix}--profile-user-info {
40
48
  display: flex;
41
49
  }
42
50
 
@@ -133,7 +133,7 @@ div:has(.#{$prefix}--header)
133
133
  block-size: $spacing-08;
134
134
  }
135
135
 
136
- .#{$prefix}--side-nav__icon {
136
+ .#{$prefix}--side-nav__menu-secondary-wrapper .#{$prefix}--side-nav__icon {
137
137
  margin-inline-end: convert.to-rem(14px);
138
138
  }
139
139
 
@@ -10,5 +10,6 @@
10
10
  @use 'styles/content';
11
11
  @use 'styles/shark-fin-icon.scss';
12
12
  @use 'styles/header-divider.scss';
13
+ @use 'styles/header-overflow-panel.scss';
13
14
  @use 'styles/trial-countdown.scss';
14
15
  @use 'styles/profile.scss';