@carbon-labs/react-ui-shell 0.41.0 → 0.43.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.
@@ -21,6 +21,7 @@ export interface SideNavProps extends ComponentProps<'nav'>, TranslateWithId<Tra
21
21
  defaultExpanded?: boolean;
22
22
  isChildOfHeader?: boolean;
23
23
  onToggle?: (event: FocusEvent<HTMLElement> | KeyboardEvent<HTMLElement> | boolean, value: boolean) => void;
24
+ hideRailBreakpointDown?: 'sm' | 'md' | 'lg' | 'xlg' | 'max';
24
25
  href?: string;
25
26
  isFixedNav?: boolean;
26
27
  isRail?: boolean;
@@ -42,6 +43,8 @@ interface SideNavContextData {
42
43
  navType?: SIDE_NAV_TYPE;
43
44
  isTreeview?: boolean;
44
45
  setIsTreeview?: (value: boolean) => void;
46
+ currentPrimaryMenu?: string;
47
+ setCurrentPrimaryMenu?: (value: string) => void;
45
48
  }
46
49
  export declare const SideNavContext: React.Context<SideNavContextData>;
47
50
  export declare const SideNav: React.ForwardRefExoticComponent<Omit<SideNavProps, "ref"> & React.RefAttributes<HTMLElement>>;
@@ -50,6 +50,7 @@ function SideNavRenderFunction(_ref, ref) {
50
50
  children,
51
51
  onToggle,
52
52
  className: customClassName,
53
+ hideRailBreakpointDown,
53
54
  href,
54
55
  isFixedNav = false,
55
56
  isRail,
@@ -76,6 +77,7 @@ function SideNavRenderFunction(_ref, ref) {
76
77
  const expanded = controlled ? expandedProp : expandedState;
77
78
  const sideNavRef = React.useRef(null);
78
79
  const navRef = useMergedRefs.useMergedRefs([sideNavRef, ref]);
80
+ const [currentPrimaryMenu, setCurrentPrimaryMenu] = React.useState();
79
81
  const sideNavToggleText = expandedState ? t('collapse.sidenav') : t('expand.sidenav');
80
82
  const handleToggle = function (event) {
81
83
  let value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : !expanded;
@@ -97,6 +99,7 @@ function SideNavRenderFunction(_ref, ref) {
97
99
  [`${prefix}--side-nav`]: true,
98
100
  [`${prefix}--side-nav--expanded`]: expanded || expandedViaHoverState,
99
101
  [`${prefix}--side-nav--collapsed`]: !expanded && isFixedNav,
102
+ [`${prefix}--side-nav--hide-rail-breakpoint-down-${hideRailBreakpointDown}`]: hideRailBreakpointDown,
100
103
  [`${prefix}--side-nav--rail`]: isRail,
101
104
  [`${prefix}--side-nav--panel`]: navType === SIDE_NAV_TYPE.PANEL,
102
105
  [`${prefix}--side-nav--ux`]: isChildOfHeader,
@@ -149,11 +152,22 @@ function SideNavRenderFunction(_ref, ref) {
149
152
  resetNodeTabIndices();
150
153
  }
151
154
  }, [prefix, internalIsTreeview]);
155
+ const smMediaQuery = `(min-width: ${index$1.breakpoints.sm.width})`;
156
+ const isSm = useMatchMedia.useMatchMedia(smMediaQuery);
152
157
  React.useEffect(() => {
153
158
  if (sideNavRef.current) {
159
+ const backButton = sideNavRef?.current.querySelector(`.${prefix}--side-nav__back-button`);
154
160
  const firstElement = sideNavRef?.current?.querySelector('a, button');
155
- if (firstElement && (navType == SIDE_NAV_TYPE.PANEL || expanded)) {
156
- firstElement.tabIndex = 0;
161
+ if (navType == SIDE_NAV_TYPE.PANEL || expanded) {
162
+ if (isSm && backButton) {
163
+ backButton.tabIndex = 0;
164
+ const firstElementAfterBack = backButton.nextElementSibling?.querySelector('a, button');
165
+ if (firstElementAfterBack) {
166
+ firstElementAfterBack.tabIndex = 0;
167
+ }
168
+ } else if (firstElement) {
169
+ firstElement.tabIndex = 0;
170
+ }
157
171
  }
158
172
  }
159
173
  }, [expanded]);
@@ -221,15 +235,17 @@ function SideNavRenderFunction(_ref, ref) {
221
235
  let nextFocusNode = null;
222
236
  if (match.match(event, keys.ArrowUp)) {
223
237
  const parentNode = parentSideNavMenu(treeWalker.currentNode);
224
- let previousSideNavMenu = parentNode?.previousElementSibling;
225
-
238
+ let previousSideNavMenu = treeWalker.currentNode?.previousSibling;
226
239
  // skip the divider
227
240
  if (previousSideNavMenu?.classList.contains(`${prefix}--side-nav__divider`)) {
228
241
  previousSideNavMenu = previousSideNavMenu?.previousElementSibling;
229
242
  }
230
-
231
- // when previous sibling is open, go to its last item
232
- if (previousSideNavMenu?.getAttribute('aria-expanded') == 'true') {
243
+ if (previousSideNavMenu?.classList.contains(`${prefix}--side-nav__item--primary`)) {
244
+ nextFocusNode = previousSideNavMenu;
245
+ } else if (treeWalker.currentNode.classList.contains(`${prefix}--side-nav__item--primary`)) {
246
+ nextFocusNode = treeWalker.currentNode.previousSibling;
247
+ } // when previous sibling is open, go to its last item
248
+ else if (previousSideNavMenu?.getAttribute('aria-expanded') == 'true') {
233
249
  const allItems = previousSideNavMenu.querySelectorAll(`.${prefix}--side-nav__item`);
234
250
  const lastMenu = allItems[allItems.length - 1];
235
251
  if (lastMenu && lastMenu.getAttribute('aria-expanded') == 'false') {
@@ -241,7 +257,7 @@ function SideNavRenderFunction(_ref, ref) {
241
257
  nextFocusNode = treeWalker.previousSibling();
242
258
 
243
259
  // first item in the menu, go back up to SideNavMenu button
244
- if (nextFocusNode == null) {
260
+ if (nextFocusNode == null && !parentNode.classList.contains(`${prefix}--side-nav__item--primary`)) {
245
261
  nextFocusNode = parentNode;
246
262
  }
247
263
  }
@@ -253,6 +269,11 @@ function SideNavRenderFunction(_ref, ref) {
253
269
  const parent = parentSideNavMenu(treeWalker.currentNode);
254
270
  nextFocusNode = parent?.nextElementSibling;
255
271
  }
272
+ } else if (treeWalker.currentNode.classList.contains(`${prefix}--side-nav__item--primary`)) {
273
+ nextFocusNode = treeWalker.currentNode.nextSibling;
274
+ if (nextFocusNode?.classList.contains(`${prefix}--side-nav__divider`)) {
275
+ nextFocusNode = nextFocusNode.nextSibling;
276
+ }
256
277
  } else {
257
278
  nextFocusNode = treeWalker.nextNode();
258
279
  }
@@ -327,7 +348,7 @@ function SideNavRenderFunction(_ref, ref) {
327
348
  function resetNodeTabIndices() {
328
349
  const items = sideNavRef?.current?.querySelectorAll('[tabIndex="0"]') ?? [];
329
350
  items.forEach(item => {
330
- if (item.classList.contains(`${prefix}--side-nav__toggle`)) {
351
+ if (item.classList.contains(`${prefix}--side-nav__toggle`) || item.classList.contains(`${prefix}--side-nav__back-button`)) {
331
352
  return;
332
353
  }
333
354
  item.tabIndex = -1;
@@ -358,7 +379,9 @@ function SideNavRenderFunction(_ref, ref) {
358
379
  isRail,
359
380
  navType,
360
381
  isTreeview: internalIsTreeview,
361
- setIsTreeview
382
+ setIsTreeview,
383
+ currentPrimaryMenu,
384
+ setCurrentPrimaryMenu
362
385
  }
363
386
  }, isFixedNav || hideOverlay ? null :
364
387
  /*#__PURE__*/
@@ -412,6 +435,12 @@ SideNav.propTypes = {
412
435
  * If `true`, the overlay will be hidden. Defaults to `false`.
413
436
  */
414
437
  hideOverlay: PropTypes.bool,
438
+ /**
439
+ * Specify the breakpoint at which the SideNav will be hidden.
440
+ * Can be one of `sm`, `md`, `lg`, `xlg`, or `max`.
441
+ * Only applies when `isRail` is `true`.
442
+ */
443
+ hideRailBreakpointDown: PropTypes.oneOf(['sm', 'md', 'lg', 'xlg', 'max']),
415
444
  /**
416
445
  * Provide the `href` to the id of the element on your package that is the
417
446
  * main content.
@@ -6,6 +6,15 @@
6
6
  */
7
7
  import React from 'react';
8
8
  export interface SideNavMenuProps {
9
+ /**
10
+ * Title for back button in sm screen
11
+ */
12
+ backButtonTitle?: string;
13
+ /**
14
+ * A custom icon to render on the back button in sm screen
15
+ * default is ArrowLeft
16
+ */
17
+ backButtonRenderIcon?: React.ComponentType;
9
18
  /**
10
19
  * An optional CSS class to apply to the component.
11
20
  */
@@ -23,6 +32,10 @@ export interface SideNavMenuProps {
23
32
  * SideNavMenu depth to determine spacing
24
33
  */
25
34
  depth?: number;
35
+ /**
36
+ * Provide a unique id
37
+ */
38
+ id?: string;
26
39
  /**
27
40
  * Indicates whether the SideNavMenu is active.
28
41
  */
@@ -44,6 +57,12 @@ export interface SideNavMenuProps {
44
57
  * Indicates if the side navigation container is expanded or collapsed.
45
58
  */
46
59
  isSideNavExpanded?: boolean;
60
+ /**
61
+ * Specifies if this is the primary SideNav.
62
+ * If true, child components will open to the right,
63
+ * creating the double-wide navigation layout
64
+ */
65
+ primary?: boolean;
47
66
  /**
48
67
  * The boolean to show the flyout menu has been selected.
49
68
  */
@@ -7,10 +7,12 @@
7
7
 
8
8
  'use strict';
9
9
 
10
+ var index$1 = require('../node_modules/@carbon/layout/es/index.js');
10
11
  var index = require('../_virtual/index.js');
11
12
  var PropTypes = require('prop-types');
12
13
  var React = require('react');
13
14
  var _utils = require('./_utils.js');
15
+ var useId = require('../internal/useId.js');
14
16
  var react = require('@carbon/react');
15
17
  var keys = require('../internal/keyboard/keys.js');
16
18
  var match = require('../internal/keyboard/match.js');
@@ -19,21 +21,31 @@ var SideNav = require('./SideNav.js');
19
21
  var useMergedRefs = require('../internal/useMergedRefs.js');
20
22
  var SharkFinIcon = require('./SharkFinIcon.js');
21
23
  var SideNavFlyoutMenu = require('./SideNavFlyoutMenu.js');
24
+ var SideNavItems = require('./SideNavItems.js');
25
+ var useMatchMedia = require('../internal/useMatchMedia.js');
26
+ var bucket0 = require('../node_modules/@carbon/icons-react/es/generated/bucket-0.js');
22
27
  var bucket3 = require('../node_modules/@carbon/icons-react/es/generated/bucket-3.js');
23
28
 
24
- var _SharkFinIcon, _ChevronDown;
29
+ var _ArrowLeft, _SharkFinIcon, _ChevronRight, _ChevronDown;
30
+ const smMediaQuery = `(max-width: ${index$1.breakpoints.md.width})`;
25
31
  const SideNavMenu = /*#__PURE__*/React.forwardRef(function SideNavMenu(_ref, ref) {
26
32
  let {
33
+ backButtonRenderIcon = () => _ArrowLeft || (_ArrowLeft = /*#__PURE__*/React.createElement(bucket0.ArrowLeft, {
34
+ size: 16
35
+ })),
36
+ backButtonTitle = 'My products',
27
37
  className: customClassName,
28
38
  children,
29
39
  defaultExpanded = false,
30
40
  depth: propDepth,
41
+ id,
31
42
  isActive = false,
32
43
  large = false,
33
44
  renderIcon: IconElement,
34
45
  isSideNavExpanded,
35
46
  title,
36
- onMenuToggle
47
+ onMenuToggle,
48
+ primary
37
49
  } = _ref;
38
50
  const depth = propDepth;
39
51
  const {
@@ -48,10 +60,19 @@ const SideNavMenu = /*#__PURE__*/React.forwardRef(function SideNavMenu(_ref, ref
48
60
  const [isExpanded, setIsExpanded] = React.useState(defaultExpanded);
49
61
  const [active, setActive] = React.useState(isActive);
50
62
  const firstLink = React.useRef(null);
63
+ const backButtonRef = React.useRef(null);
64
+ const uid = useId.useId('side-nav-menu');
65
+ const uniqueId = id || uid;
51
66
  const [prevExpanded, setPrevExpanded] = React.useState(defaultExpanded);
67
+ const [isSecondaryOpen, setSecondaryOpen] = React.useState(defaultExpanded);
68
+ const {
69
+ currentPrimaryMenu,
70
+ setCurrentPrimaryMenu
71
+ } = React.useContext(SideNav.SideNavContext);
52
72
  const className = index.default({
53
73
  [`${prefix}--side-nav__item`]: true,
54
- [`${prefix}--side-nav__item--active`]: active || hasActiveDescendant(children) && !isExpanded,
74
+ [`${prefix}--side-nav__item--primary`]: primary,
75
+ [`${prefix}--side-nav__item--active`]: !primary && (active || hasActiveDescendant(children) && !isExpanded),
55
76
  [`${prefix}--side-nav__item--icon`]: IconElement,
56
77
  [`${prefix}--side-nav__item--large`]: large,
57
78
  [customClassName]: !!customClassName
@@ -60,6 +81,10 @@ const SideNavMenu = /*#__PURE__*/React.forwardRef(function SideNavMenu(_ref, ref
60
81
  [`${prefix}--side-nav__submenu`]: true,
61
82
  [`${prefix}--side-nav__submenu--active`]: active || hasActiveDescendant(children) && isExpanded
62
83
  });
84
+ const primaryClassNames = index.default({
85
+ [`${prefix}--side-nav__menu-secondary-wrapper`]: true,
86
+ [`${prefix}--side-nav__menu-secondary-wrapper-expanded`]: isSideNavExpanded && isSecondaryOpen && currentPrimaryMenu === uniqueId
87
+ });
63
88
  const buttonRef = React.useRef(null);
64
89
  const listRef = React.useRef(null);
65
90
  const menuRef = useMergedRefs.useMergedRefs([buttonRef, ref]);
@@ -170,10 +195,11 @@ const SideNavMenu = /*#__PURE__*/React.forwardRef(function SideNavMenu(_ref, ref
170
195
  if (onMenuToggle) {
171
196
  onMenuToggle();
172
197
  }
173
- setIsExpanded(false);
174
-
198
+ if (!primary && isExpanded) {
199
+ setIsExpanded(false);
200
+ }
175
201
  // go to previous level's side nav menu button
176
- } else {
202
+ } else if (!isSm) {
177
203
  // since we're in a menu, it finds its own <li>, we go up one more
178
204
  const previousMenu = parentSideNavMenu(parent);
179
205
  const button = previousMenu.querySelector('button');
@@ -183,33 +209,74 @@ const SideNavMenu = /*#__PURE__*/React.forwardRef(function SideNavMenu(_ref, ref
183
209
 
184
210
  // go to side nav menu button
185
211
  } else if (parent) {
186
- const button = parent.querySelector('button');
187
- button.tabIndex = 0;
188
- button?.focus();
212
+ if (parent.hasAttribute('aria-expanded')) {
213
+ const button = parent.querySelector('button');
214
+ if (button) {
215
+ button.tabIndex = 0;
216
+ button.focus();
217
+ }
218
+ } else if (!isSm) {
219
+ const previousMenu = parentSideNavMenu(parent);
220
+ const button = previousMenu.querySelector('button');
221
+ button.tabIndex = 0;
222
+ button?.focus();
223
+ }
189
224
  }
190
225
  }
191
226
  if (match.match(event, keys.ArrowRight)) {
227
+ setIsExpanded(true);
228
+ if (primary && node.hasAttribute('aria-expanded')) {
229
+ event.preventDefault();
230
+ }
192
231
  event.stopPropagation();
193
232
 
194
233
  // expand menu when sidenav is expanded
195
234
  if (isMenu && sideNavExpanded) {
196
- setIsExpanded(true);
197
235
  if (onMenuToggle) {
198
236
  onMenuToggle();
199
237
  }
200
238
 
201
239
  // if already expanded, focus on first element
202
- if (isExpanded == 'true') {
203
- let nextNode = node.nextElementSibling?.querySelector('a, button');
240
+ if (isExpanded == 'true' || isSm) {
241
+ const nextNode = node.nextElementSibling?.querySelector('a, button');
204
242
  if (nextNode) {
205
243
  nextNode.tabIndex = 0;
206
244
  nextNode.focus();
207
245
  }
246
+ if (isSm) {
247
+ const nextNodeAfterBackButton = nextNode.nextElementSibling?.querySelector('a, button');
248
+ if (nextNodeAfterBackButton) {
249
+ nextNodeAfterBackButton.tabIndex = 0;
250
+ }
251
+ }
208
252
  }
209
253
  }
210
254
  }
211
255
  }
212
256
  }
257
+ function handleOnBackButtonClick(event) {
258
+ const node = event.target;
259
+ const parent = parentSideNavMenu(node);
260
+ const button = parent.querySelector('button');
261
+ if (button) {
262
+ button.tabIndex = 0;
263
+ button.focus();
264
+ }
265
+ setIsExpanded(false);
266
+ }
267
+ React.useEffect(() => {
268
+ if (isExpanded && primary && setCurrentPrimaryMenu) {
269
+ setCurrentPrimaryMenu(uniqueId);
270
+ }
271
+ setSecondaryOpen(isExpanded);
272
+ }, [isExpanded]);
273
+ React.useEffect(() => {
274
+ if (currentPrimaryMenu !== uniqueId) {
275
+ setIsExpanded(false);
276
+ } else {
277
+ setIsExpanded(true);
278
+ }
279
+ }, [currentPrimaryMenu]);
213
280
 
214
281
  // save expanded state before SideNav collapse
215
282
  const [lastExpandedState, setLastExpandedState] = React.useState(isExpanded);
@@ -224,6 +291,14 @@ const SideNavMenu = /*#__PURE__*/React.forwardRef(function SideNavMenu(_ref, ref
224
291
  }
225
292
  }, [sideNavExpanded]);
226
293
  const [openPopover, setOpenPopover] = React.useState(false);
294
+ const isSm = useMatchMedia.useMatchMedia(smMediaQuery);
295
+
296
+ // keeps the secondary open when moving from small to large breakpoints
297
+ React.useEffect(() => {
298
+ if (!isSm && uniqueId === currentPrimaryMenu) {
299
+ setIsExpanded(true);
300
+ }
301
+ }, [isSm]);
227
302
  const content =
228
303
  /*#__PURE__*/
229
304
  // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
@@ -232,7 +307,8 @@ const SideNavMenu = /*#__PURE__*/React.forwardRef(function SideNavMenu(_ref, ref
232
307
  "aria-expanded": isExpanded,
233
308
  className: className,
234
309
  ref: listRef,
235
- onKeyDown: handleKeyDown
310
+ onKeyDown: handleKeyDown,
311
+ id: uniqueId
236
312
  }, /*#__PURE__*/React.createElement("button", {
237
313
  "aria-expanded": isExpanded,
238
314
  className: buttonClassName,
@@ -245,10 +321,13 @@ const SideNavMenu = /*#__PURE__*/React.forwardRef(function SideNavMenu(_ref, ref
245
321
  if (navType == SideNav.SIDE_NAV_TYPE.PANEL && !isExpanded && firstLink.current && !sideNavExpanded) {
246
322
  setOpenPopover(!openPopover);
247
323
  // window.location.href = firstLink.current;
248
- } else {
324
+ } else if (isSm || !primary || currentPrimaryMenu !== uniqueId) {
249
325
  setIsExpanded(!isExpanded);
250
326
  setLastExpandedState(!isExpanded);
251
327
  }
328
+ if (isSm && backButtonRef.current) {
329
+ backButtonRef.current.focus();
330
+ }
252
331
  },
253
332
  ref: menuRef,
254
333
  type: "button",
@@ -262,9 +341,24 @@ const SideNavMenu = /*#__PURE__*/React.forwardRef(function SideNavMenu(_ref, ref
262
341
  }, title), /*#__PURE__*/React.createElement(react.SideNavIcon, {
263
342
  className: `${prefix}--side-nav__submenu-chevron`,
264
343
  small: true
265
- }, _ChevronDown || (_ChevronDown = /*#__PURE__*/React.createElement(bucket3.ChevronDown, {
344
+ }, primary ? _ChevronRight || (_ChevronRight = /*#__PURE__*/React.createElement(bucket3.ChevronRight, {
266
345
  size: 20
267
- })))), /*#__PURE__*/React.createElement("ul", {
346
+ })) : _ChevronDown || (_ChevronDown = /*#__PURE__*/React.createElement(bucket3.ChevronDown, {
347
+ size: 20
348
+ })))), primary && /*#__PURE__*/React.createElement("div", {
349
+ className: primaryClassNames
350
+ }, /*#__PURE__*/React.createElement(SideNavItems.SideNavItems, {
351
+ accessibilityLabel: {
352
+ 'aria-label': `${title} submenu`
353
+ }
354
+ }, isSm && /*#__PURE__*/React.createElement(react.Button, {
355
+ ref: backButtonRef,
356
+ kind: "ghost",
357
+ size: "md",
358
+ onClick: handleOnBackButtonClick,
359
+ className: `${prefix}--side-nav__back-button`,
360
+ renderIcon: backButtonRenderIcon
361
+ }, backButtonTitle), childrenToRender)), /*#__PURE__*/React.createElement("ul", {
268
362
  className: `${prefix}--side-nav__menu`,
269
363
  role: "group"
270
364
  }, childrenToRender));
@@ -277,6 +371,15 @@ const SideNavMenu = /*#__PURE__*/React.forwardRef(function SideNavMenu(_ref, ref
277
371
  });
278
372
  SideNavMenu.displayName = 'SideNavMenu';
279
373
  SideNavMenu.propTypes = {
374
+ /**
375
+ * A custom icon to render on the back button in sm screen
376
+ */
377
+ // @ts-expect-error - PropTypes are unable to cover this case.
378
+ backButtonRenderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
379
+ /**
380
+ * Title for back button in sm screen
381
+ */
382
+ backButtonTitle: PropTypes.string,
280
383
  /**
281
384
  * Provide <SideNavMenuItem>'s inside of the `SideNavMenu`
282
385
  */
@@ -295,6 +398,10 @@ SideNavMenu.propTypes = {
295
398
  * SideNavMenu depth to determine spacing
296
399
  */
297
400
  depth: PropTypes.number,
401
+ /**
402
+ * Provide a unique id
403
+ */
404
+ id: PropTypes.string,
298
405
  /**
299
406
  * Specify whether the `SideNavMenu` is "active". `SideNavMenu` should be
300
407
  * considered active if one of its menu items are a link for the current