@carbon-labs/react-ui-shell 0.16.0 → 0.17.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.
@@ -34,10 +34,14 @@ export interface SideNavProps extends ComponentProps<'nav'>, TranslateWithId<Tra
34
34
  isCollapsible?: boolean;
35
35
  hideOverlay?: boolean;
36
36
  navType: SIDE_NAV_TYPE;
37
+ isTreeview: boolean;
37
38
  }
38
39
  interface SideNavContextData {
40
+ expanded?: boolean;
39
41
  isRail?: boolean;
40
42
  navType?: SIDE_NAV_TYPE;
43
+ isTreeview?: boolean;
44
+ setIsTreeview?: (value: boolean) => void;
41
45
  }
42
46
  export declare const SideNavContext: React.Context<SideNavContextData>;
43
47
  export declare const SideNav: React.ForwardRefExoticComponent<Omit<SideNavProps, "ref"> & React.RefAttributes<HTMLElement>>;
@@ -6,13 +6,13 @@
6
6
  */
7
7
 
8
8
  import { extends as _extends } from '../_virtual/_rollupPluginBabelHelpers.js';
9
- import React, { createContext, useRef, isValidElement, useEffect } from 'react';
9
+ import React, { createContext, useState, useRef, isValidElement, useEffect } from 'react';
10
10
  import cx from '../_virtual/index.js';
11
11
  import PropTypes from 'prop-types';
12
12
  import { AriaLabelPropType } from '../prop-types/AriaPropTypes.js';
13
13
  import { CARBON_SIDENAV_ITEMS } from './_utils.js';
14
14
  import { usePrefix } from '../internal/usePrefix.js';
15
- import { ArrowUp, ArrowDown, Home, End, Escape, Tab } from '../internal/keyboard/keys.js';
15
+ import { Escape, ArrowUp, ArrowDown, Home, End, Tab } from '../internal/keyboard/keys.js';
16
16
  import { match, matches } from '../internal/keyboard/match.js';
17
17
  import { useMergedRefs } from '../internal/useMergedRefs.js';
18
18
  import { useWindowEvent } from '../internal/useEvent.js';
@@ -52,6 +52,7 @@ function SideNavRenderFunction(_ref, ref) {
52
52
  isFixedNav = false,
53
53
  isRail,
54
54
  isPersistent = true,
55
+ isTreeview: isTreeviewProp,
55
56
  navType = SIDE_NAV_TYPE.DEFAULT,
56
57
  addFocusListeners = true,
57
58
  addMouseListeners = true,
@@ -63,6 +64,7 @@ function SideNavRenderFunction(_ref, ref) {
63
64
  translateWithId: t = defaultTranslateWithId,
64
65
  ...other
65
66
  } = _ref;
67
+ const [internalIsTreeview, setInternalIsTreeview] = useState(isTreeviewProp ?? false);
66
68
  const prefix = usePrefix();
67
69
  const {
68
70
  current: controlled
@@ -114,7 +116,10 @@ function SideNavRenderFunction(_ref, ref) {
114
116
  // avoid spreading `isSideNavExpanded` to non-Carbon UI Shell children
115
117
  return /*#__PURE__*/React.cloneElement(childJsxElement, {
116
118
  ...(CARBON_SIDENAV_ITEMS.includes(childJsxElement.type?.displayName ?? childJsxElement.type?.name) ? {
117
- isSideNavExpanded: currentExpansionState
119
+ isSideNavExpanded: currentExpansionState,
120
+ ...(childJsxElement.type?.displayName === 'SideNavItems' && {
121
+ accessibilityLabel: accessibilityLabel
122
+ })
118
123
  } : {})
119
124
  });
120
125
  }
@@ -123,26 +128,28 @@ function SideNavRenderFunction(_ref, ref) {
123
128
  const eventHandlers = {};
124
129
  const treeWalkerRef = useRef(null);
125
130
  useEffect(() => {
126
- treeWalkerRef.current = treeWalkerRef.current ?? document.createTreeWalker(sideNavRef?.current, NodeFilter.SHOW_ELEMENT, {
127
- acceptNode: function (node) {
128
- if (!(node instanceof Element)) {
131
+ if (internalIsTreeview) {
132
+ treeWalkerRef.current = treeWalkerRef.current ?? document.createTreeWalker(sideNavRef?.current, NodeFilter.SHOW_ELEMENT, {
133
+ acceptNode: function (node) {
134
+ if (!(node instanceof Element)) {
135
+ return NodeFilter.FILTER_SKIP;
136
+ }
137
+ if (node.classList.contains(`${prefix}--side-nav__divider`)) {
138
+ return NodeFilter.FILTER_REJECT;
139
+ }
140
+ if (node.matches(`li.${prefix}--side-nav__item`) || node.matches(`li.${prefix}--side-nav__menu-item`)) {
141
+ return NodeFilter.FILTER_ACCEPT;
142
+ }
129
143
  return NodeFilter.FILTER_SKIP;
130
144
  }
131
- if (node.classList.contains(`${prefix}--side-nav__divider`)) {
132
- return NodeFilter.FILTER_REJECT;
133
- }
134
- if (node.matches(`li.${prefix}--side-nav__item`) || node.matches(`li.${prefix}--side-nav__menu-item`)) {
135
- return NodeFilter.FILTER_ACCEPT;
136
- }
137
- return NodeFilter.FILTER_SKIP;
145
+ });
146
+ resetNodeTabIndices();
147
+ const firstElement = sideNavRef?.current?.querySelector('a, button');
148
+ if (firstElement) {
149
+ firstElement.tabIndex = 0;
138
150
  }
139
- });
140
- resetNodeTabIndices();
141
- const firstElement = sideNavRef?.current?.querySelector('a, button');
142
- if (firstElement) {
143
- firstElement.tabIndex = 0;
144
151
  }
145
- }, [prefix]);
152
+ }, [prefix, internalIsTreeview]);
146
153
 
147
154
  /**
148
155
  * Returns the parent SideNavMenu, if node is actually inside one.
@@ -174,98 +181,111 @@ function SideNavRenderFunction(_ref, ref) {
174
181
  }
175
182
  };
176
183
  eventHandlers.onKeyDown = event => {
177
- if (!treeWalkerRef.current) return;
178
- const treeWalker = treeWalkerRef.current;
179
- event.stopPropagation();
180
-
181
- // stops page from scrolling
182
- if (matches(event, [ArrowUp, ArrowDown, Home, End,
183
- // @ts-ignore - `matches` doesn't like the object syntax without missing properties
184
- {
185
- code: 'KeyA'
186
- }])) {
187
- event.preventDefault();
188
- }
189
- treeWalker.currentNode = event.target.closest(`li`) ?? treeWalker?.currentNode;
190
- let nextFocusNode = null;
191
- if (match(event, ArrowUp)) {
192
- const parentNode = parentSideNavMenu(treeWalker.currentNode);
193
- let previousSideNavMenu = parentNode?.previousElementSibling;
194
-
195
- // skip the divider
196
- if (previousSideNavMenu?.classList.contains(`${prefix}--side-nav__divider`)) {
197
- previousSideNavMenu = previousSideNavMenu?.previousElementSibling;
198
- }
199
-
200
- // when previous sibling is open, go to its last item
201
- if (previousSideNavMenu?.getAttribute('aria-expanded') == 'true') {
202
- nextFocusNode = treeWalker.previousNode();
203
- } else {
204
- nextFocusNode = treeWalker.previousSibling();
205
-
206
- // first item in the menu, go back up to SideNavMenu button
207
- if (nextFocusNode == null) {
208
- nextFocusNode = parentNode;
184
+ // close menu
185
+ if (match(event, Escape)) {
186
+ if (expanded && !isFixedNav) {
187
+ resetNodeTabIndices();
188
+ if (onSideNavBlur) {
189
+ onSideNavBlur();
209
190
  }
210
191
  }
211
- }
212
- if (match(event, ArrowDown)) {
213
- if (treeWalker.currentNode.getAttribute('aria-expanded') == 'false') {
214
- nextFocusNode = treeWalker.nextSibling();
215
- } else {
216
- nextFocusNode = treeWalker.nextNode();
192
+ handleToggle(event, false);
193
+ if (href) {
194
+ window.location.href = href;
217
195
  }
218
196
  }
219
197
 
220
- // Home/End functionality
221
- if (matches(event, [Home, End])) {
222
- if (!sideNavRef?.current) {
223
- return;
198
+ // Treeview keyboard navigation
199
+ if (treeWalkerRef?.current && internalIsTreeview) {
200
+ const treeWalker = treeWalkerRef.current;
201
+ event.stopPropagation();
202
+
203
+ // stops page from scrolling
204
+ if (matches(event, [ArrowUp, ArrowDown, Home, End,
205
+ // @ts-ignore - `matches` doesn't like the object syntax without missing properties
206
+ {
207
+ code: 'KeyA'
208
+ }])) {
209
+ event.preventDefault();
224
210
  }
225
- const allItems = Array.from(sideNavRef.current.querySelectorAll('a, button'));
226
- if (match(event, Home)) {
227
- const firstElement = allItems[0];
228
- if (firstElement) {
229
- firstElement.tabIndex = 0;
230
- firstElement?.focus();
211
+ treeWalker.currentNode = event.target.closest(`li`) ?? treeWalker?.currentNode;
212
+ let nextFocusNode = null;
213
+ if (match(event, ArrowUp)) {
214
+ const parentNode = parentSideNavMenu(treeWalker.currentNode);
215
+ let previousSideNavMenu = parentNode?.previousElementSibling;
216
+
217
+ // skip the divider
218
+ if (previousSideNavMenu?.classList.contains(`${prefix}--side-nav__divider`)) {
219
+ previousSideNavMenu = previousSideNavMenu?.previousElementSibling;
220
+ }
221
+
222
+ // when previous sibling is open, go to its last item
223
+ if (previousSideNavMenu?.getAttribute('aria-expanded') == 'true') {
224
+ const allItems = previousSideNavMenu.querySelectorAll(`.${prefix}--side-nav__item`);
225
+ const lastMenu = allItems[allItems.length - 1];
226
+ if (lastMenu && lastMenu.getAttribute('aria-expanded') == 'false') {
227
+ nextFocusNode = lastMenu;
228
+ } else {
229
+ nextFocusNode = treeWalker.previousNode();
230
+ }
231
+ } else {
232
+ nextFocusNode = treeWalker.previousSibling();
233
+
234
+ // first item in the menu, go back up to SideNavMenu button
235
+ if (nextFocusNode == null) {
236
+ nextFocusNode = parentNode;
237
+ }
231
238
  }
232
239
  }
233
- if (match(event, End)) {
234
- const allItems = Array.from(sideNavRef.current.querySelectorAll('li'));
235
- const lastVisibleItem = allItems.reverse().find(item => getComputedStyle(item).visibility !== 'hidden');
236
- if (lastVisibleItem) {
237
- const node = lastVisibleItem.querySelector('button') ?? lastVisibleItem.querySelector('a');
238
- if (node) {
239
- node.tabIndex = 0;
240
- node?.focus();
240
+ if (match(event, ArrowDown)) {
241
+ if (treeWalker.currentNode.getAttribute('aria-expanded') == 'false') {
242
+ nextFocusNode = treeWalker.nextSibling();
243
+ if (!nextFocusNode) {
244
+ const parent = parentSideNavMenu(treeWalker.currentNode);
245
+ nextFocusNode = parent?.nextElementSibling;
241
246
  }
247
+ } else {
248
+ nextFocusNode = treeWalker.nextNode();
242
249
  }
243
250
  }
244
- }
245
251
 
246
- // focus on the focusable element within the node
247
- if (nextFocusNode && nextFocusNode !== event.target) {
248
- resetNodeTabIndices();
249
- if (nextFocusNode instanceof HTMLElement) {
250
- const node = nextFocusNode.querySelector('button') ?? nextFocusNode.querySelector('a');
251
- if (node) {
252
- node.tabIndex = 0;
253
- node?.focus();
252
+ // Home/End functionality
253
+ if (matches(event, [Home, End])) {
254
+ if (!sideNavRef?.current) {
255
+ return;
256
+ }
257
+ const allItems = Array.from(sideNavRef.current.querySelectorAll('a, button'));
258
+ if (match(event, Home)) {
259
+ const firstElement = allItems[0];
260
+ if (firstElement) {
261
+ firstElement.tabIndex = 0;
262
+ firstElement?.focus();
263
+ }
264
+ }
265
+ if (match(event, End)) {
266
+ const allItems = Array.from(sideNavRef.current.querySelectorAll('li'));
267
+ const lastVisibleItem = allItems.reverse().find(item => getComputedStyle(item).visibility !== 'hidden');
268
+ if (lastVisibleItem) {
269
+ const node = lastVisibleItem.querySelector('button') ?? lastVisibleItem.querySelector('a');
270
+ if (node) {
271
+ node.tabIndex = 0;
272
+ node?.focus();
273
+ }
274
+ }
254
275
  }
255
276
  }
256
- }
257
277
 
258
- // close menu
259
- if (match(event, Escape)) {
260
- if (expanded && !isFixedNav) {
261
- if (onSideNavBlur) {
262
- onSideNavBlur();
278
+ // focus on the focusable element within the node
279
+ if (nextFocusNode && nextFocusNode !== event.target) {
280
+ resetNodeTabIndices();
281
+ if (nextFocusNode instanceof HTMLElement) {
282
+ const node = nextFocusNode.querySelector('button') ?? nextFocusNode.querySelector('a');
283
+ if (node) {
284
+ node.tabIndex = 0;
285
+ node?.focus();
286
+ }
263
287
  }
264
288
  }
265
- handleToggle(event, false);
266
- if (href) {
267
- window.location.href = href;
268
- }
269
289
  }
270
290
  };
271
291
  }
@@ -304,9 +324,27 @@ function SideNavRenderFunction(_ref, ref) {
304
324
  item.tabIndex = -1;
305
325
  });
306
326
  }
327
+
328
+ // ensure that changes are in sync with internal treeview prop
329
+ useEffect(() => {
330
+ if (isTreeviewProp !== undefined) {
331
+ setInternalIsTreeview(isTreeviewProp);
332
+ }
333
+ }, [isTreeviewProp]);
334
+
335
+ // prevent changes if prop is passed in
336
+ const setIsTreeview = value => {
337
+ if (isTreeviewProp === undefined) {
338
+ setInternalIsTreeview(value);
339
+ }
340
+ };
307
341
  return /*#__PURE__*/React.createElement(SideNavContext.Provider, {
308
342
  value: {
309
- isRail
343
+ isRail,
344
+ navType,
345
+ expanded: expanded,
346
+ isTreeview: internalIsTreeview,
347
+ setIsTreeview
310
348
  }
311
349
  }, isFixedNav || hideOverlay ? null :
312
350
  /*#__PURE__*/
@@ -315,7 +353,7 @@ function SideNavRenderFunction(_ref, ref) {
315
353
  className: overlayClassName,
316
354
  onClick: onOverlayClick
317
355
  }), /*#__PURE__*/React.createElement("nav", _extends({
318
- role: "tree",
356
+ role: 'navigation',
319
357
  tabIndex: -1,
320
358
  ref: navRef,
321
359
  className: `${prefix}--side-nav__navigation ${className}`,
@@ -6,6 +6,11 @@
6
6
  */
7
7
  import React from 'react';
8
8
  export interface SideNavItemsProps {
9
+ /**
10
+ * Object to provide an aria-label to the component when used in treeview,
11
+ * to ensure it meets a11y requirements.
12
+ */
13
+ accessibilityLabel: object;
9
14
  /**
10
15
  * Provide a single icon as the child to `SideNavIcon` to render in the
11
16
  * container
@@ -5,18 +5,25 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
+ import { extends as _extends } from '../_virtual/_rollupPluginBabelHelpers.js';
8
9
  import cx from '../_virtual/index.js';
9
10
  import PropTypes from 'prop-types';
10
- import React from 'react';
11
+ import React, { useContext, useRef, useEffect } from 'react';
11
12
  import { CARBON_SIDENAV_ITEMS } from './_utils.js';
12
13
  import { usePrefix } from '../internal/usePrefix.js';
14
+ import { SideNavContext } from './SideNav.js';
13
15
 
14
16
  const SideNavItems = _ref => {
15
17
  let {
16
18
  className: customClassName,
17
19
  children,
18
- isSideNavExpanded
20
+ isSideNavExpanded,
21
+ accessibilityLabel: accessibilityLabel
19
22
  } = _ref;
23
+ const {
24
+ isTreeview
25
+ } = useContext(SideNavContext);
26
+ const listRef = useRef(null); // Adjust type if necessary
20
27
  const prefix = usePrefix();
21
28
  const className = cx([`${prefix}--side-nav__items`], customClassName);
22
29
  const childrenWithExpandedState = React.Children.map(children, child => {
@@ -36,9 +43,22 @@ const SideNavItems = _ref => {
36
43
  });
37
44
  }
38
45
  });
39
- return /*#__PURE__*/React.createElement("ul", {
40
- className: className
41
- }, childrenWithExpandedState);
46
+ useEffect(() => {
47
+ // set SideNavLink's role without needing to extend original component
48
+ if (isTreeview && listRef.current) {
49
+ const sideNavItem = listRef.current.querySelectorAll(`.${prefix}--side-nav__item a`);
50
+ sideNavItem.forEach(e => {
51
+ if (!e.hasAttribute('role')) {
52
+ e.setAttribute('role', 'treeitem');
53
+ }
54
+ });
55
+ }
56
+ }, [isTreeview]);
57
+ return /*#__PURE__*/React.createElement("ul", _extends({}, isTreeview && accessibilityLabel, {
58
+ ref: listRef,
59
+ className: className,
60
+ role: isTreeview ? 'tree' : ''
61
+ }), childrenWithExpandedState);
42
62
  };
43
63
  SideNavItems.displayName = 'SideNavItems';
44
64
  SideNavItems.propTypes = {
@@ -13,7 +13,7 @@ import { SideNavIcon } from '@carbon/react';
13
13
  import { Escape, ArrowLeft, ArrowRight } from '../internal/keyboard/keys.js';
14
14
  import { match } from '../internal/keyboard/match.js';
15
15
  import { usePrefix } from '../internal/usePrefix.js';
16
- import { SideNavContext } from './SideNav.js';
16
+ import { SideNavContext, SIDE_NAV_TYPE } from './SideNav.js';
17
17
  import { useMergedRefs } from '../internal/useMergedRefs.js';
18
18
  import { ChevronDown } from '../node_modules/@carbon/icons-react/es/generated/bucket-3.js';
19
19
 
@@ -28,16 +28,21 @@ const SideNavMenu = /*#__PURE__*/React.forwardRef(function SideNavMenu(_ref, ref
28
28
  large = false,
29
29
  renderIcon: IconElement,
30
30
  isSideNavExpanded,
31
- tabIndex,
32
31
  title
33
32
  } = _ref;
34
33
  const depth = propDepth;
35
34
  const {
36
- isRail
35
+ isTreeview,
36
+ expanded,
37
+ navType,
38
+ isRail,
39
+ setIsTreeview
37
40
  } = useContext(SideNavContext);
41
+ const sideNavExpanded = expanded;
38
42
  const prefix = usePrefix();
39
43
  const [isExpanded, setIsExpanded] = useState(defaultExpanded);
40
44
  const [active, setActive] = useState(isActive);
45
+ const firstLink = useRef(null);
41
46
  const [prevExpanded, setPrevExpanded] = useState(defaultExpanded);
42
47
  const className = cx({
43
48
  [`${prefix}--side-nav__item`]: true,
@@ -80,23 +85,35 @@ const SideNavMenu = /*#__PURE__*/React.forwardRef(function SideNavMenu(_ref, ref
80
85
  return child;
81
86
  });
82
87
  useEffect(() => {
83
- if (depth === 0) return;
84
- const calcButtonOffset = () => {
85
- // menu with icon
86
- if (children && IconElement) {
87
- return depth + 3;
88
+ if (navType == SIDE_NAV_TYPE.PANEL) {
89
+ // grab first link to redirect if clicked when not expanded
90
+ if (!firstLink?.current && listRef?.current) {
91
+ const firstLinkElement = listRef.current.querySelector(`.${prefix}--side-nav__menu-item a`);
92
+ firstLink.current = firstLinkElement?.getAttribute('href') ?? '';
88
93
  }
94
+ }
95
+ if (depth === 0) return;
96
+
97
+ // if depth is more than 0, that means its nested, thus we set treeview mode
98
+ setIsTreeview?.(true);
99
+ if (isTreeview) {
100
+ const calcButtonOffset = () => {
101
+ // menu with icon
102
+ if (children && IconElement) {
103
+ return depth + 3;
104
+ }
89
105
 
90
- // menu without icon
91
- if (children) {
92
- return depth * 4;
106
+ // menu without icon
107
+ if (children) {
108
+ return depth * 4;
109
+ }
110
+ return depth;
111
+ };
112
+ if (buttonRef.current) {
113
+ buttonRef.current.style.paddingLeft = `${calcButtonOffset()}rem`;
93
114
  }
94
- return depth;
95
- };
96
- if (buttonRef.current) {
97
- buttonRef.current.style.paddingLeft = `${calcButtonOffset()}rem`;
98
115
  }
99
- }, []);
116
+ }, [isTreeview]);
100
117
 
101
118
  /**
102
119
  * Returns the parent SideNavMenu, if node is actually inside one.
@@ -112,56 +129,71 @@ const SideNavMenu = /*#__PURE__*/React.forwardRef(function SideNavMenu(_ref, ref
112
129
  if (match(event, Escape)) {
113
130
  setIsExpanded(false);
114
131
  }
115
- const node = event.target;
116
- const isMenu = node.hasAttribute('aria-expanded');
117
- const isExpanded = node.getAttribute('aria-expanded');
118
- const parent = parentSideNavMenu(node);
119
- if (match(event, ArrowLeft)) {
120
- event.stopPropagation();
121
- if (isMenu) {
122
- // collapse menu
123
- if (isExpanded == 'true') {
124
- setIsExpanded(false);
132
+ if (isTreeview) {
133
+ const node = event.target;
134
+ const isMenu = node.hasAttribute('aria-expanded');
135
+ const isExpanded = node.getAttribute('aria-expanded');
136
+ const parent = parentSideNavMenu(node);
137
+ if (match(event, ArrowLeft)) {
138
+ event.stopPropagation();
139
+ if (isMenu) {
140
+ // collapse menu
141
+ if (isExpanded == 'true') {
142
+ setIsExpanded(false);
125
143
 
126
- // go to previous level's side nav menu button
127
- } else {
128
- // since we're in a menu, it finds its own <li>, we go up one more
129
- const previousMenu = parentSideNavMenu(parent);
130
- const button = previousMenu.querySelector('button');
144
+ // go to previous level's side nav menu button
145
+ } else {
146
+ // since we're in a menu, it finds its own <li>, we go up one more
147
+ const previousMenu = parentSideNavMenu(parent);
148
+ const button = previousMenu.querySelector('button');
149
+ button.tabIndex = 0;
150
+ button?.focus();
151
+ }
152
+
153
+ // go to side nav menu button
154
+ } else if (parent) {
155
+ const button = parent.querySelector('button');
131
156
  button.tabIndex = 0;
132
157
  button?.focus();
133
158
  }
134
-
135
- // go to side nav menu button
136
- } else if (parent) {
137
- const button = parent.querySelector('button');
138
- button.tabIndex = 0;
139
- button?.focus();
140
159
  }
141
- }
142
- if (match(event, ArrowRight)) {
143
- event.stopPropagation();
160
+ if (match(event, ArrowRight)) {
161
+ event.stopPropagation();
144
162
 
145
- // expand menu
146
- if (isMenu) {
147
- setIsExpanded(true);
163
+ // expand menu
164
+ if (isMenu) {
165
+ setIsExpanded(true);
148
166
 
149
- // if already expanded, focus on first element
150
- if (isExpanded == 'true') {
151
- let nextNode = node.nextElementSibling?.querySelector('a, button');
152
- if (nextNode) {
153
- nextNode.tabIndex = 0;
154
- nextNode.focus();
167
+ // if already expanded, focus on first element
168
+ if (isExpanded == 'true') {
169
+ let nextNode = node.nextElementSibling?.querySelector('a, button');
170
+ if (nextNode) {
171
+ nextNode.tabIndex = 0;
172
+ nextNode.focus();
173
+ }
155
174
  }
156
175
  }
157
176
  }
158
177
  }
159
178
  }
179
+
180
+ // save expanded state before SideNav collapse
181
+ const [lastExpandedState, setLastExpandedState] = useState(isExpanded);
182
+
183
+ // reset when SideNav is panel
184
+ useEffect(() => {
185
+ if (navType == SIDE_NAV_TYPE.PANEL && !sideNavExpanded) {
186
+ setLastExpandedState(isExpanded);
187
+ setIsExpanded(false);
188
+ } else {
189
+ setIsExpanded(lastExpandedState);
190
+ }
191
+ }, [sideNavExpanded]);
160
192
  return (
161
193
  /*#__PURE__*/
162
194
  // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
163
195
  React.createElement("li", {
164
- role: "treeitem",
196
+ role: isTreeview ? 'treeitem' : undefined,
165
197
  "aria-expanded": isExpanded,
166
198
  className: className,
167
199
  ref: listRef,
@@ -170,11 +202,17 @@ const SideNavMenu = /*#__PURE__*/React.forwardRef(function SideNavMenu(_ref, ref
170
202
  "aria-expanded": isExpanded,
171
203
  className: buttonClassName,
172
204
  onClick: () => {
173
- setIsExpanded(!isExpanded);
205
+ // only when sidenav is panel view
206
+ if (navType == SIDE_NAV_TYPE.PANEL && !isExpanded && firstLink.current && !sideNavExpanded) {
207
+ window.location.href = firstLink.current;
208
+ } else {
209
+ setIsExpanded(!isExpanded);
210
+ setLastExpandedState(!isExpanded);
211
+ }
174
212
  },
175
213
  ref: menuRef,
176
214
  type: "button",
177
- tabIndex: -1
215
+ tabIndex: isTreeview ? -1 : 0
178
216
  }, IconElement && /*#__PURE__*/React.createElement(SideNavIcon, null, /*#__PURE__*/React.createElement(IconElement, null)), /*#__PURE__*/React.createElement("span", {
179
217
  className: `${prefix}--side-nav__submenu-title`
180
218
  }, title), /*#__PURE__*/React.createElement(SideNavIcon, {
@@ -8,11 +8,12 @@
8
8
  import { extends as _extends } from '../_virtual/_rollupPluginBabelHelpers.js';
9
9
  import cx from '../_virtual/index.js';
10
10
  import PropTypes from 'prop-types';
11
- import React, { useRef, useEffect } from 'react';
11
+ import React, { useContext, useRef, useEffect } from 'react';
12
12
  import { SideNavLinkText } from '@carbon/react';
13
13
  import Link from './Link.js';
14
14
  import { usePrefix } from '../internal/usePrefix.js';
15
15
  import { useMergedRefs } from '../internal/useMergedRefs.js';
16
+ import { SideNavContext } from './SideNav.js';
16
17
 
17
18
  const SideNavMenuItem = /*#__PURE__*/React.forwardRef(function SideNavMenuItem(props, ref) {
18
19
  const prefix = usePrefix();
@@ -24,6 +25,9 @@ const SideNavMenuItem = /*#__PURE__*/React.forwardRef(function SideNavMenuItem(p
24
25
  isActive,
25
26
  ...rest
26
27
  } = props;
28
+ const {
29
+ isTreeview
30
+ } = useContext(SideNavContext);
27
31
  const className = cx(`${prefix}--side-nav__menu-item`, customClassName);
28
32
  const depth = propDepth;
29
33
  const linkClassName = cx({
@@ -39,14 +43,14 @@ const SideNavMenuItem = /*#__PURE__*/React.forwardRef(function SideNavMenuItem(p
39
43
  if (linkRef.current) {
40
44
  linkRef.current.style.paddingLeft = `${calcLinkOffset()}rem`;
41
45
  }
42
- }, []);
46
+ }, [isTreeview]);
43
47
  return /*#__PURE__*/React.createElement("li", {
44
- role: "treeitem",
45
- "aria-selected": isActive ? 'true' : 'false',
46
48
  className: className
47
49
  }, /*#__PURE__*/React.createElement(Component, _extends({}, rest, {
50
+ "aria-selected": isActive ? 'true' : 'false',
51
+ role: isTreeview ? 'treeitem' : undefined,
48
52
  className: linkClassName,
49
- tabIndex: -1,
53
+ tabIndex: isTreeview ? -1 : 0,
50
54
  ref: itemRef
51
55
  }), /*#__PURE__*/React.createElement(SideNavLinkText, null, children)));
52
56
  });
@@ -34,10 +34,14 @@ export interface SideNavProps extends ComponentProps<'nav'>, TranslateWithId<Tra
34
34
  isCollapsible?: boolean;
35
35
  hideOverlay?: boolean;
36
36
  navType: SIDE_NAV_TYPE;
37
+ isTreeview: boolean;
37
38
  }
38
39
  interface SideNavContextData {
40
+ expanded?: boolean;
39
41
  isRail?: boolean;
40
42
  navType?: SIDE_NAV_TYPE;
43
+ isTreeview?: boolean;
44
+ setIsTreeview?: (value: boolean) => void;
41
45
  }
42
46
  export declare const SideNavContext: React.Context<SideNavContextData>;
43
47
  export declare const SideNav: React.ForwardRefExoticComponent<Omit<SideNavProps, "ref"> & React.RefAttributes<HTMLElement>>;
@@ -54,6 +54,7 @@ function SideNavRenderFunction(_ref, ref) {
54
54
  isFixedNav = false,
55
55
  isRail,
56
56
  isPersistent = true,
57
+ isTreeview: isTreeviewProp,
57
58
  navType = SIDE_NAV_TYPE.DEFAULT,
58
59
  addFocusListeners = true,
59
60
  addMouseListeners = true,
@@ -65,6 +66,7 @@ function SideNavRenderFunction(_ref, ref) {
65
66
  translateWithId: t = defaultTranslateWithId,
66
67
  ...other
67
68
  } = _ref;
69
+ const [internalIsTreeview, setInternalIsTreeview] = React.useState(isTreeviewProp ?? false);
68
70
  const prefix = usePrefix.usePrefix();
69
71
  const {
70
72
  current: controlled
@@ -116,7 +118,10 @@ function SideNavRenderFunction(_ref, ref) {
116
118
  // avoid spreading `isSideNavExpanded` to non-Carbon UI Shell children
117
119
  return /*#__PURE__*/React.cloneElement(childJsxElement, {
118
120
  ...(_utils.CARBON_SIDENAV_ITEMS.includes(childJsxElement.type?.displayName ?? childJsxElement.type?.name) ? {
119
- isSideNavExpanded: currentExpansionState
121
+ isSideNavExpanded: currentExpansionState,
122
+ ...(childJsxElement.type?.displayName === 'SideNavItems' && {
123
+ accessibilityLabel: accessibilityLabel
124
+ })
120
125
  } : {})
121
126
  });
122
127
  }
@@ -125,26 +130,28 @@ function SideNavRenderFunction(_ref, ref) {
125
130
  const eventHandlers = {};
126
131
  const treeWalkerRef = React.useRef(null);
127
132
  React.useEffect(() => {
128
- treeWalkerRef.current = treeWalkerRef.current ?? document.createTreeWalker(sideNavRef?.current, NodeFilter.SHOW_ELEMENT, {
129
- acceptNode: function (node) {
130
- if (!(node instanceof Element)) {
133
+ if (internalIsTreeview) {
134
+ treeWalkerRef.current = treeWalkerRef.current ?? document.createTreeWalker(sideNavRef?.current, NodeFilter.SHOW_ELEMENT, {
135
+ acceptNode: function (node) {
136
+ if (!(node instanceof Element)) {
137
+ return NodeFilter.FILTER_SKIP;
138
+ }
139
+ if (node.classList.contains(`${prefix}--side-nav__divider`)) {
140
+ return NodeFilter.FILTER_REJECT;
141
+ }
142
+ if (node.matches(`li.${prefix}--side-nav__item`) || node.matches(`li.${prefix}--side-nav__menu-item`)) {
143
+ return NodeFilter.FILTER_ACCEPT;
144
+ }
131
145
  return NodeFilter.FILTER_SKIP;
132
146
  }
133
- if (node.classList.contains(`${prefix}--side-nav__divider`)) {
134
- return NodeFilter.FILTER_REJECT;
135
- }
136
- if (node.matches(`li.${prefix}--side-nav__item`) || node.matches(`li.${prefix}--side-nav__menu-item`)) {
137
- return NodeFilter.FILTER_ACCEPT;
138
- }
139
- return NodeFilter.FILTER_SKIP;
147
+ });
148
+ resetNodeTabIndices();
149
+ const firstElement = sideNavRef?.current?.querySelector('a, button');
150
+ if (firstElement) {
151
+ firstElement.tabIndex = 0;
140
152
  }
141
- });
142
- resetNodeTabIndices();
143
- const firstElement = sideNavRef?.current?.querySelector('a, button');
144
- if (firstElement) {
145
- firstElement.tabIndex = 0;
146
153
  }
147
- }, [prefix]);
154
+ }, [prefix, internalIsTreeview]);
148
155
 
149
156
  /**
150
157
  * Returns the parent SideNavMenu, if node is actually inside one.
@@ -176,98 +183,111 @@ function SideNavRenderFunction(_ref, ref) {
176
183
  }
177
184
  };
178
185
  eventHandlers.onKeyDown = event => {
179
- if (!treeWalkerRef.current) return;
180
- const treeWalker = treeWalkerRef.current;
181
- event.stopPropagation();
182
-
183
- // stops page from scrolling
184
- if (match.matches(event, [keys.ArrowUp, keys.ArrowDown, keys.Home, keys.End,
185
- // @ts-ignore - `matches` doesn't like the object syntax without missing properties
186
- {
187
- code: 'KeyA'
188
- }])) {
189
- event.preventDefault();
190
- }
191
- treeWalker.currentNode = event.target.closest(`li`) ?? treeWalker?.currentNode;
192
- let nextFocusNode = null;
193
- if (match.match(event, keys.ArrowUp)) {
194
- const parentNode = parentSideNavMenu(treeWalker.currentNode);
195
- let previousSideNavMenu = parentNode?.previousElementSibling;
196
-
197
- // skip the divider
198
- if (previousSideNavMenu?.classList.contains(`${prefix}--side-nav__divider`)) {
199
- previousSideNavMenu = previousSideNavMenu?.previousElementSibling;
200
- }
201
-
202
- // when previous sibling is open, go to its last item
203
- if (previousSideNavMenu?.getAttribute('aria-expanded') == 'true') {
204
- nextFocusNode = treeWalker.previousNode();
205
- } else {
206
- nextFocusNode = treeWalker.previousSibling();
207
-
208
- // first item in the menu, go back up to SideNavMenu button
209
- if (nextFocusNode == null) {
210
- nextFocusNode = parentNode;
186
+ // close menu
187
+ if (match.match(event, keys.Escape)) {
188
+ if (expanded && !isFixedNav) {
189
+ resetNodeTabIndices();
190
+ if (onSideNavBlur) {
191
+ onSideNavBlur();
211
192
  }
212
193
  }
213
- }
214
- if (match.match(event, keys.ArrowDown)) {
215
- if (treeWalker.currentNode.getAttribute('aria-expanded') == 'false') {
216
- nextFocusNode = treeWalker.nextSibling();
217
- } else {
218
- nextFocusNode = treeWalker.nextNode();
194
+ handleToggle(event, false);
195
+ if (href) {
196
+ window.location.href = href;
219
197
  }
220
198
  }
221
199
 
222
- // Home/End functionality
223
- if (match.matches(event, [keys.Home, keys.End])) {
224
- if (!sideNavRef?.current) {
225
- return;
200
+ // Treeview keyboard navigation
201
+ if (treeWalkerRef?.current && internalIsTreeview) {
202
+ const treeWalker = treeWalkerRef.current;
203
+ event.stopPropagation();
204
+
205
+ // stops page from scrolling
206
+ if (match.matches(event, [keys.ArrowUp, keys.ArrowDown, keys.Home, keys.End,
207
+ // @ts-ignore - `matches` doesn't like the object syntax without missing properties
208
+ {
209
+ code: 'KeyA'
210
+ }])) {
211
+ event.preventDefault();
226
212
  }
227
- const allItems = Array.from(sideNavRef.current.querySelectorAll('a, button'));
228
- if (match.match(event, keys.Home)) {
229
- const firstElement = allItems[0];
230
- if (firstElement) {
231
- firstElement.tabIndex = 0;
232
- firstElement?.focus();
213
+ treeWalker.currentNode = event.target.closest(`li`) ?? treeWalker?.currentNode;
214
+ let nextFocusNode = null;
215
+ if (match.match(event, keys.ArrowUp)) {
216
+ const parentNode = parentSideNavMenu(treeWalker.currentNode);
217
+ let previousSideNavMenu = parentNode?.previousElementSibling;
218
+
219
+ // skip the divider
220
+ if (previousSideNavMenu?.classList.contains(`${prefix}--side-nav__divider`)) {
221
+ previousSideNavMenu = previousSideNavMenu?.previousElementSibling;
222
+ }
223
+
224
+ // when previous sibling is open, go to its last item
225
+ if (previousSideNavMenu?.getAttribute('aria-expanded') == 'true') {
226
+ const allItems = previousSideNavMenu.querySelectorAll(`.${prefix}--side-nav__item`);
227
+ const lastMenu = allItems[allItems.length - 1];
228
+ if (lastMenu && lastMenu.getAttribute('aria-expanded') == 'false') {
229
+ nextFocusNode = lastMenu;
230
+ } else {
231
+ nextFocusNode = treeWalker.previousNode();
232
+ }
233
+ } else {
234
+ nextFocusNode = treeWalker.previousSibling();
235
+
236
+ // first item in the menu, go back up to SideNavMenu button
237
+ if (nextFocusNode == null) {
238
+ nextFocusNode = parentNode;
239
+ }
233
240
  }
234
241
  }
235
- if (match.match(event, keys.End)) {
236
- const allItems = Array.from(sideNavRef.current.querySelectorAll('li'));
237
- const lastVisibleItem = allItems.reverse().find(item => getComputedStyle(item).visibility !== 'hidden');
238
- if (lastVisibleItem) {
239
- const node = lastVisibleItem.querySelector('button') ?? lastVisibleItem.querySelector('a');
240
- if (node) {
241
- node.tabIndex = 0;
242
- node?.focus();
242
+ if (match.match(event, keys.ArrowDown)) {
243
+ if (treeWalker.currentNode.getAttribute('aria-expanded') == 'false') {
244
+ nextFocusNode = treeWalker.nextSibling();
245
+ if (!nextFocusNode) {
246
+ const parent = parentSideNavMenu(treeWalker.currentNode);
247
+ nextFocusNode = parent?.nextElementSibling;
243
248
  }
249
+ } else {
250
+ nextFocusNode = treeWalker.nextNode();
244
251
  }
245
252
  }
246
- }
247
253
 
248
- // focus on the focusable element within the node
249
- if (nextFocusNode && nextFocusNode !== event.target) {
250
- resetNodeTabIndices();
251
- if (nextFocusNode instanceof HTMLElement) {
252
- const node = nextFocusNode.querySelector('button') ?? nextFocusNode.querySelector('a');
253
- if (node) {
254
- node.tabIndex = 0;
255
- node?.focus();
254
+ // Home/End functionality
255
+ if (match.matches(event, [keys.Home, keys.End])) {
256
+ if (!sideNavRef?.current) {
257
+ return;
258
+ }
259
+ const allItems = Array.from(sideNavRef.current.querySelectorAll('a, button'));
260
+ if (match.match(event, keys.Home)) {
261
+ const firstElement = allItems[0];
262
+ if (firstElement) {
263
+ firstElement.tabIndex = 0;
264
+ firstElement?.focus();
265
+ }
266
+ }
267
+ if (match.match(event, keys.End)) {
268
+ const allItems = Array.from(sideNavRef.current.querySelectorAll('li'));
269
+ const lastVisibleItem = allItems.reverse().find(item => getComputedStyle(item).visibility !== 'hidden');
270
+ if (lastVisibleItem) {
271
+ const node = lastVisibleItem.querySelector('button') ?? lastVisibleItem.querySelector('a');
272
+ if (node) {
273
+ node.tabIndex = 0;
274
+ node?.focus();
275
+ }
276
+ }
256
277
  }
257
278
  }
258
- }
259
279
 
260
- // close menu
261
- if (match.match(event, keys.Escape)) {
262
- if (expanded && !isFixedNav) {
263
- if (onSideNavBlur) {
264
- onSideNavBlur();
280
+ // focus on the focusable element within the node
281
+ if (nextFocusNode && nextFocusNode !== event.target) {
282
+ resetNodeTabIndices();
283
+ if (nextFocusNode instanceof HTMLElement) {
284
+ const node = nextFocusNode.querySelector('button') ?? nextFocusNode.querySelector('a');
285
+ if (node) {
286
+ node.tabIndex = 0;
287
+ node?.focus();
288
+ }
265
289
  }
266
290
  }
267
- handleToggle(event, false);
268
- if (href) {
269
- window.location.href = href;
270
- }
271
291
  }
272
292
  };
273
293
  }
@@ -306,9 +326,27 @@ function SideNavRenderFunction(_ref, ref) {
306
326
  item.tabIndex = -1;
307
327
  });
308
328
  }
329
+
330
+ // ensure that changes are in sync with internal treeview prop
331
+ React.useEffect(() => {
332
+ if (isTreeviewProp !== undefined) {
333
+ setInternalIsTreeview(isTreeviewProp);
334
+ }
335
+ }, [isTreeviewProp]);
336
+
337
+ // prevent changes if prop is passed in
338
+ const setIsTreeview = value => {
339
+ if (isTreeviewProp === undefined) {
340
+ setInternalIsTreeview(value);
341
+ }
342
+ };
309
343
  return /*#__PURE__*/React.createElement(SideNavContext.Provider, {
310
344
  value: {
311
- isRail
345
+ isRail,
346
+ navType,
347
+ expanded: expanded,
348
+ isTreeview: internalIsTreeview,
349
+ setIsTreeview
312
350
  }
313
351
  }, isFixedNav || hideOverlay ? null :
314
352
  /*#__PURE__*/
@@ -317,7 +355,7 @@ function SideNavRenderFunction(_ref, ref) {
317
355
  className: overlayClassName,
318
356
  onClick: onOverlayClick
319
357
  }), /*#__PURE__*/React.createElement("nav", _rollupPluginBabelHelpers.extends({
320
- role: "tree",
358
+ role: 'navigation',
321
359
  tabIndex: -1,
322
360
  ref: navRef,
323
361
  className: `${prefix}--side-nav__navigation ${className}`,
@@ -6,6 +6,11 @@
6
6
  */
7
7
  import React from 'react';
8
8
  export interface SideNavItemsProps {
9
+ /**
10
+ * Object to provide an aria-label to the component when used in treeview,
11
+ * to ensure it meets a11y requirements.
12
+ */
13
+ accessibilityLabel: object;
9
14
  /**
10
15
  * Provide a single icon as the child to `SideNavIcon` to render in the
11
16
  * container
@@ -7,18 +7,25 @@
7
7
 
8
8
  'use strict';
9
9
 
10
+ var _rollupPluginBabelHelpers = require('../_virtual/_rollupPluginBabelHelpers.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');
14
15
  var usePrefix = require('../internal/usePrefix.js');
16
+ var SideNav = require('./SideNav.js');
15
17
 
16
18
  const SideNavItems = _ref => {
17
19
  let {
18
20
  className: customClassName,
19
21
  children,
20
- isSideNavExpanded
22
+ isSideNavExpanded,
23
+ accessibilityLabel: accessibilityLabel
21
24
  } = _ref;
25
+ const {
26
+ isTreeview
27
+ } = React.useContext(SideNav.SideNavContext);
28
+ const listRef = React.useRef(null); // Adjust type if necessary
22
29
  const prefix = usePrefix.usePrefix();
23
30
  const className = index.default([`${prefix}--side-nav__items`], customClassName);
24
31
  const childrenWithExpandedState = React.Children.map(children, child => {
@@ -38,9 +45,22 @@ const SideNavItems = _ref => {
38
45
  });
39
46
  }
40
47
  });
41
- return /*#__PURE__*/React.createElement("ul", {
42
- className: className
43
- }, childrenWithExpandedState);
48
+ React.useEffect(() => {
49
+ // set SideNavLink's role without needing to extend original component
50
+ if (isTreeview && listRef.current) {
51
+ const sideNavItem = listRef.current.querySelectorAll(`.${prefix}--side-nav__item a`);
52
+ sideNavItem.forEach(e => {
53
+ if (!e.hasAttribute('role')) {
54
+ e.setAttribute('role', 'treeitem');
55
+ }
56
+ });
57
+ }
58
+ }, [isTreeview]);
59
+ return /*#__PURE__*/React.createElement("ul", _rollupPluginBabelHelpers.extends({}, isTreeview && accessibilityLabel, {
60
+ ref: listRef,
61
+ className: className,
62
+ role: isTreeview ? 'tree' : ''
63
+ }), childrenWithExpandedState);
44
64
  };
45
65
  SideNavItems.displayName = 'SideNavItems';
46
66
  SideNavItems.propTypes = {
@@ -30,16 +30,21 @@ const SideNavMenu = /*#__PURE__*/React.forwardRef(function SideNavMenu(_ref, ref
30
30
  large = false,
31
31
  renderIcon: IconElement,
32
32
  isSideNavExpanded,
33
- tabIndex,
34
33
  title
35
34
  } = _ref;
36
35
  const depth = propDepth;
37
36
  const {
38
- isRail
37
+ isTreeview,
38
+ expanded,
39
+ navType,
40
+ isRail,
41
+ setIsTreeview
39
42
  } = React.useContext(SideNav.SideNavContext);
43
+ const sideNavExpanded = expanded;
40
44
  const prefix = usePrefix.usePrefix();
41
45
  const [isExpanded, setIsExpanded] = React.useState(defaultExpanded);
42
46
  const [active, setActive] = React.useState(isActive);
47
+ const firstLink = React.useRef(null);
43
48
  const [prevExpanded, setPrevExpanded] = React.useState(defaultExpanded);
44
49
  const className = index.default({
45
50
  [`${prefix}--side-nav__item`]: true,
@@ -82,23 +87,35 @@ const SideNavMenu = /*#__PURE__*/React.forwardRef(function SideNavMenu(_ref, ref
82
87
  return child;
83
88
  });
84
89
  React.useEffect(() => {
85
- if (depth === 0) return;
86
- const calcButtonOffset = () => {
87
- // menu with icon
88
- if (children && IconElement) {
89
- return depth + 3;
90
+ if (navType == SideNav.SIDE_NAV_TYPE.PANEL) {
91
+ // grab first link to redirect if clicked when not expanded
92
+ if (!firstLink?.current && listRef?.current) {
93
+ const firstLinkElement = listRef.current.querySelector(`.${prefix}--side-nav__menu-item a`);
94
+ firstLink.current = firstLinkElement?.getAttribute('href') ?? '';
90
95
  }
96
+ }
97
+ if (depth === 0) return;
98
+
99
+ // if depth is more than 0, that means its nested, thus we set treeview mode
100
+ setIsTreeview?.(true);
101
+ if (isTreeview) {
102
+ const calcButtonOffset = () => {
103
+ // menu with icon
104
+ if (children && IconElement) {
105
+ return depth + 3;
106
+ }
91
107
 
92
- // menu without icon
93
- if (children) {
94
- return depth * 4;
108
+ // menu without icon
109
+ if (children) {
110
+ return depth * 4;
111
+ }
112
+ return depth;
113
+ };
114
+ if (buttonRef.current) {
115
+ buttonRef.current.style.paddingLeft = `${calcButtonOffset()}rem`;
95
116
  }
96
- return depth;
97
- };
98
- if (buttonRef.current) {
99
- buttonRef.current.style.paddingLeft = `${calcButtonOffset()}rem`;
100
117
  }
101
- }, []);
118
+ }, [isTreeview]);
102
119
 
103
120
  /**
104
121
  * Returns the parent SideNavMenu, if node is actually inside one.
@@ -114,56 +131,71 @@ const SideNavMenu = /*#__PURE__*/React.forwardRef(function SideNavMenu(_ref, ref
114
131
  if (match.match(event, keys.Escape)) {
115
132
  setIsExpanded(false);
116
133
  }
117
- const node = event.target;
118
- const isMenu = node.hasAttribute('aria-expanded');
119
- const isExpanded = node.getAttribute('aria-expanded');
120
- const parent = parentSideNavMenu(node);
121
- if (match.match(event, keys.ArrowLeft)) {
122
- event.stopPropagation();
123
- if (isMenu) {
124
- // collapse menu
125
- if (isExpanded == 'true') {
126
- setIsExpanded(false);
134
+ if (isTreeview) {
135
+ const node = event.target;
136
+ const isMenu = node.hasAttribute('aria-expanded');
137
+ const isExpanded = node.getAttribute('aria-expanded');
138
+ const parent = parentSideNavMenu(node);
139
+ if (match.match(event, keys.ArrowLeft)) {
140
+ event.stopPropagation();
141
+ if (isMenu) {
142
+ // collapse menu
143
+ if (isExpanded == 'true') {
144
+ setIsExpanded(false);
127
145
 
128
- // go to previous level's side nav menu button
129
- } else {
130
- // since we're in a menu, it finds its own <li>, we go up one more
131
- const previousMenu = parentSideNavMenu(parent);
132
- const button = previousMenu.querySelector('button');
146
+ // go to previous level's side nav menu button
147
+ } else {
148
+ // since we're in a menu, it finds its own <li>, we go up one more
149
+ const previousMenu = parentSideNavMenu(parent);
150
+ const button = previousMenu.querySelector('button');
151
+ button.tabIndex = 0;
152
+ button?.focus();
153
+ }
154
+
155
+ // go to side nav menu button
156
+ } else if (parent) {
157
+ const button = parent.querySelector('button');
133
158
  button.tabIndex = 0;
134
159
  button?.focus();
135
160
  }
136
-
137
- // go to side nav menu button
138
- } else if (parent) {
139
- const button = parent.querySelector('button');
140
- button.tabIndex = 0;
141
- button?.focus();
142
161
  }
143
- }
144
- if (match.match(event, keys.ArrowRight)) {
145
- event.stopPropagation();
162
+ if (match.match(event, keys.ArrowRight)) {
163
+ event.stopPropagation();
146
164
 
147
- // expand menu
148
- if (isMenu) {
149
- setIsExpanded(true);
165
+ // expand menu
166
+ if (isMenu) {
167
+ setIsExpanded(true);
150
168
 
151
- // if already expanded, focus on first element
152
- if (isExpanded == 'true') {
153
- let nextNode = node.nextElementSibling?.querySelector('a, button');
154
- if (nextNode) {
155
- nextNode.tabIndex = 0;
156
- nextNode.focus();
169
+ // if already expanded, focus on first element
170
+ if (isExpanded == 'true') {
171
+ let nextNode = node.nextElementSibling?.querySelector('a, button');
172
+ if (nextNode) {
173
+ nextNode.tabIndex = 0;
174
+ nextNode.focus();
175
+ }
157
176
  }
158
177
  }
159
178
  }
160
179
  }
161
180
  }
181
+
182
+ // save expanded state before SideNav collapse
183
+ const [lastExpandedState, setLastExpandedState] = React.useState(isExpanded);
184
+
185
+ // reset when SideNav is panel
186
+ React.useEffect(() => {
187
+ if (navType == SideNav.SIDE_NAV_TYPE.PANEL && !sideNavExpanded) {
188
+ setLastExpandedState(isExpanded);
189
+ setIsExpanded(false);
190
+ } else {
191
+ setIsExpanded(lastExpandedState);
192
+ }
193
+ }, [sideNavExpanded]);
162
194
  return (
163
195
  /*#__PURE__*/
164
196
  // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
165
197
  React.createElement("li", {
166
- role: "treeitem",
198
+ role: isTreeview ? 'treeitem' : undefined,
167
199
  "aria-expanded": isExpanded,
168
200
  className: className,
169
201
  ref: listRef,
@@ -172,11 +204,17 @@ const SideNavMenu = /*#__PURE__*/React.forwardRef(function SideNavMenu(_ref, ref
172
204
  "aria-expanded": isExpanded,
173
205
  className: buttonClassName,
174
206
  onClick: () => {
175
- setIsExpanded(!isExpanded);
207
+ // only when sidenav is panel view
208
+ if (navType == SideNav.SIDE_NAV_TYPE.PANEL && !isExpanded && firstLink.current && !sideNavExpanded) {
209
+ window.location.href = firstLink.current;
210
+ } else {
211
+ setIsExpanded(!isExpanded);
212
+ setLastExpandedState(!isExpanded);
213
+ }
176
214
  },
177
215
  ref: menuRef,
178
216
  type: "button",
179
- tabIndex: -1
217
+ tabIndex: isTreeview ? -1 : 0
180
218
  }, IconElement && /*#__PURE__*/React.createElement(react.SideNavIcon, null, /*#__PURE__*/React.createElement(IconElement, null)), /*#__PURE__*/React.createElement("span", {
181
219
  className: `${prefix}--side-nav__submenu-title`
182
220
  }, title), /*#__PURE__*/React.createElement(react.SideNavIcon, {
@@ -15,6 +15,7 @@ var react = require('@carbon/react');
15
15
  var Link = require('./Link.js');
16
16
  var usePrefix = require('../internal/usePrefix.js');
17
17
  var useMergedRefs = require('../internal/useMergedRefs.js');
18
+ var SideNav = require('./SideNav.js');
18
19
 
19
20
  const SideNavMenuItem = /*#__PURE__*/React.forwardRef(function SideNavMenuItem(props, ref) {
20
21
  const prefix = usePrefix.usePrefix();
@@ -26,6 +27,9 @@ const SideNavMenuItem = /*#__PURE__*/React.forwardRef(function SideNavMenuItem(p
26
27
  isActive,
27
28
  ...rest
28
29
  } = props;
30
+ const {
31
+ isTreeview
32
+ } = React.useContext(SideNav.SideNavContext);
29
33
  const className = index.default(`${prefix}--side-nav__menu-item`, customClassName);
30
34
  const depth = propDepth;
31
35
  const linkClassName = index.default({
@@ -41,14 +45,14 @@ const SideNavMenuItem = /*#__PURE__*/React.forwardRef(function SideNavMenuItem(p
41
45
  if (linkRef.current) {
42
46
  linkRef.current.style.paddingLeft = `${calcLinkOffset()}rem`;
43
47
  }
44
- }, []);
48
+ }, [isTreeview]);
45
49
  return /*#__PURE__*/React.createElement("li", {
46
- role: "treeitem",
47
- "aria-selected": isActive ? 'true' : 'false',
48
50
  className: className
49
51
  }, /*#__PURE__*/React.createElement(Component, _rollupPluginBabelHelpers.extends({}, rest, {
52
+ "aria-selected": isActive ? 'true' : 'false',
53
+ role: isTreeview ? 'treeitem' : undefined,
50
54
  className: linkClassName,
51
- tabIndex: -1,
55
+ tabIndex: isTreeview ? -1 : 0,
52
56
  ref: itemRef
53
57
  }), /*#__PURE__*/React.createElement(react.SideNavLinkText, null, children)));
54
58
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carbon-labs/react-ui-shell",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": true
@@ -33,5 +33,5 @@
33
33
  "dependencies": {
34
34
  "@ibm/telemetry-js": "^1.9.1"
35
35
  },
36
- "gitHead": "7643e53c72a3bd556f5ac4c721e0de5156a19906"
36
+ "gitHead": "b9ed739e909accebbb96349c611213832b779c84"
37
37
  }
@@ -48,12 +48,20 @@ div:has(.#{$prefix}--header)
48
48
  font-weight: 600;
49
49
  }
50
50
 
51
+ .#{$prefix}--side-nav__icon > svg,
52
+ .#{$prefix}--side-nav__submenu-chevron > svg {
53
+ fill: $icon-primary;
54
+ }
55
+ }
56
+
57
+ .#{$prefix}--side-nav__link:hover,
58
+ .#{$prefix}--side-nav__submenu:hover {
59
+ .#{$prefix}--side-nav__icon > svg,
51
60
  .#{$prefix}--side-nav__submenu-chevron > svg {
52
61
  fill: $icon-primary;
53
62
  }
54
63
  }
55
64
 
56
- //----------------------------------------------------------------------------
57
65
  // Side-nav Panel
58
66
  //----------------------------------------------------------------------------
59
67
  .#{$prefix}--side-nav--panel {
@@ -63,6 +71,12 @@ div:has(.#{$prefix}--header)
63
71
  margin-inline-end: $spacing-05;
64
72
  }
65
73
 
74
+ .#{$prefix}--side-nav__item.#{$prefix}--side-nav__link:hover {
75
+ .#{$prefix}--side-nav__icon > svg {
76
+ fill: $icon-primary;
77
+ }
78
+ }
79
+
66
80
  .#{$prefix}--side-nav__item.#{$prefix}--side-nav__item--icon
67
81
  a.#{$prefix}--side-nav__link {
68
82
  padding-inline-start: $spacing-10;