@carbon-labs/react-ui-shell 0.6.0 → 0.8.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.
- package/es/components/SideNav.js +131 -3
- package/es/components/SideNavItems.d.ts +24 -0
- package/es/components/SideNavMenu.d.ts +49 -0
- package/es/components/SideNavMenuItem.d.ts +38 -0
- package/lib/components/SideNav.js +129 -1
- package/lib/components/SideNavItems.d.ts +24 -0
- package/lib/components/SideNavMenu.d.ts +49 -0
- package/lib/components/SideNavMenuItem.d.ts +38 -0
- package/package.json +2 -2
- package/scss/styles/_side-nav.scss +20 -0
package/es/components/SideNav.js
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { extends as _extends } from '../_virtual/_rollupPluginBabelHelpers.js';
|
|
9
|
-
import React, { useRef, isValidElement, createContext } from 'react';
|
|
9
|
+
import React, { useRef, isValidElement, useEffect, createContext } from 'react';
|
|
10
10
|
import cx from '../_virtual/index.js';
|
|
11
11
|
import PropTypes from 'prop-types';
|
|
12
12
|
import { AriaLabelPropType } from '@carbon/react/lib/prop-types/AriaPropTypes';
|
|
13
13
|
import { CARBON_SIDENAV_ITEMS } from './_utils.js';
|
|
14
14
|
import { usePrefix } from '@carbon/react/lib/internal/usePrefix';
|
|
15
15
|
import * as keys from '@carbon/react/lib/internal/keyboard/keys';
|
|
16
|
-
import { match } from '@carbon/react/lib/internal/keyboard/match';
|
|
16
|
+
import { match, matches } from '@carbon/react/lib/internal/keyboard/match';
|
|
17
17
|
import { useMergedRefs } from '@carbon/react/lib/internal/useMergedRefs';
|
|
18
18
|
import { useWindowEvent } from '@carbon/react/lib/internal/useEvent';
|
|
19
19
|
import { useDelayedState } from '@carbon/react/lib/internal/useDelayedState';
|
|
@@ -121,6 +121,39 @@ function SideNavRenderFunction(_ref, ref) {
|
|
|
121
121
|
return child;
|
|
122
122
|
});
|
|
123
123
|
const eventHandlers = {};
|
|
124
|
+
const treeWalkerRef = useRef(null);
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
treeWalkerRef.current = treeWalkerRef.current ?? document.createTreeWalker(sideNavRef?.current, NodeFilter.SHOW_ELEMENT, {
|
|
127
|
+
acceptNode: function (node) {
|
|
128
|
+
if (!(node instanceof Element)) {
|
|
129
|
+
return NodeFilter.FILTER_SKIP;
|
|
130
|
+
}
|
|
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;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
resetNodeTabIndices();
|
|
141
|
+
const firstElement = sideNavRef?.current?.querySelector('a, button');
|
|
142
|
+
if (firstElement) {
|
|
143
|
+
firstElement.tabIndex = 0;
|
|
144
|
+
}
|
|
145
|
+
}, [prefix]);
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Returns the parent SideNavMenu, if node is actually inside one.
|
|
149
|
+
* @param node
|
|
150
|
+
* @returns parent side nav menu node
|
|
151
|
+
*/
|
|
152
|
+
function parentSideNavMenu(node) {
|
|
153
|
+
const parentNode = node.parentElement?.closest(`.${prefix}--side-nav__item`);
|
|
154
|
+
if (parentNode) return parentNode;
|
|
155
|
+
return node;
|
|
156
|
+
}
|
|
124
157
|
if (addFocusListeners) {
|
|
125
158
|
eventHandlers.onFocus = event => {
|
|
126
159
|
if (!event.currentTarget.contains(event.relatedTarget) && isRail) {
|
|
@@ -141,7 +174,94 @@ function SideNavRenderFunction(_ref, ref) {
|
|
|
141
174
|
}
|
|
142
175
|
};
|
|
143
176
|
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, [keys.ArrowUp, keys.ArrowDown, keys.Home, keys.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, keys.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;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (match(event, keys.ArrowDown)) {
|
|
213
|
+
if (treeWalker.currentNode.getAttribute('aria-expanded') == 'false') {
|
|
214
|
+
nextFocusNode = treeWalker.nextSibling();
|
|
215
|
+
} else {
|
|
216
|
+
nextFocusNode = treeWalker.nextNode();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Home/End functionality
|
|
221
|
+
if (matches(event, [keys.Home, keys.End])) {
|
|
222
|
+
if (!sideNavRef?.current) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const allItems = Array.from(sideNavRef.current.querySelectorAll('a, button'));
|
|
226
|
+
if (match(event, keys.Home)) {
|
|
227
|
+
const firstElement = allItems[0];
|
|
228
|
+
if (firstElement) {
|
|
229
|
+
firstElement.tabIndex = 0;
|
|
230
|
+
firstElement?.focus();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (match(event, keys.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();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
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();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// close menu
|
|
144
259
|
if (match(event, keys.Escape)) {
|
|
260
|
+
if (expanded && !isFixedNav) {
|
|
261
|
+
if (onSideNavBlur) {
|
|
262
|
+
onSideNavBlur();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
145
265
|
handleToggle(event, false);
|
|
146
266
|
if (href) {
|
|
147
267
|
window.location.href = href;
|
|
@@ -167,12 +287,19 @@ function SideNavRenderFunction(_ref, ref) {
|
|
|
167
287
|
}
|
|
168
288
|
useWindowEvent('keydown', event => {
|
|
169
289
|
const focusedElement = document.activeElement;
|
|
170
|
-
|
|
290
|
+
|
|
291
|
+
// going from header menu to sideNav
|
|
292
|
+
if (match(event, keys.Tab) && expanded && !isFixedNav && sideNavRef?.current && focusedElement?.classList.contains(`${prefix}--header__menu-toggle`) && !focusedElement.closest('nav')) {
|
|
171
293
|
sideNavRef.current.focus();
|
|
172
294
|
}
|
|
173
295
|
});
|
|
174
296
|
const lgMediaQuery = `(min-width: ${breakpoints.lg.width})`;
|
|
175
297
|
const isLg = useMatchMedia(lgMediaQuery);
|
|
298
|
+
function resetNodeTabIndices() {
|
|
299
|
+
Array.prototype.forEach.call(sideNavRef?.current?.querySelectorAll('[tabIndex="0"]') ?? [], item => {
|
|
300
|
+
item.tabIndex = -1;
|
|
301
|
+
});
|
|
302
|
+
}
|
|
176
303
|
return /*#__PURE__*/React.createElement(SideNavContext.Provider, {
|
|
177
304
|
value: {
|
|
178
305
|
isRail
|
|
@@ -184,6 +311,7 @@ function SideNavRenderFunction(_ref, ref) {
|
|
|
184
311
|
className: overlayClassName,
|
|
185
312
|
onClick: onOverlayClick
|
|
186
313
|
}), /*#__PURE__*/React.createElement("nav", _extends({
|
|
314
|
+
role: "tree",
|
|
187
315
|
tabIndex: -1,
|
|
188
316
|
ref: navRef,
|
|
189
317
|
className: `${prefix}--side-nav__navigation ${className}`,
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
import React from 'react';
|
|
8
|
+
export interface SideNavItemsProps {
|
|
9
|
+
/**
|
|
10
|
+
* Provide a single icon as the child to `SideNavIcon` to render in the
|
|
11
|
+
* container
|
|
12
|
+
*/
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
/**
|
|
15
|
+
* Provide an optional class to be applied to the containing node
|
|
16
|
+
*/
|
|
17
|
+
className?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Property to indicate if the side nav container is open (or not). Use to
|
|
20
|
+
* keep local state and styling in step with the SideNav expansion state.
|
|
21
|
+
*/
|
|
22
|
+
isSideNavExpanded?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare const SideNavItems: React.FC<SideNavItemsProps>;
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
import React from 'react';
|
|
8
|
+
export interface SideNavMenuProps {
|
|
9
|
+
/**
|
|
10
|
+
* An optional CSS class to apply to the component.
|
|
11
|
+
*/
|
|
12
|
+
className?: string;
|
|
13
|
+
/**
|
|
14
|
+
* The content to render within the SideNavMenu component.
|
|
15
|
+
*/
|
|
16
|
+
children?: React.ReactNode;
|
|
17
|
+
/**
|
|
18
|
+
* Specifies whether the menu should be expanded by default.
|
|
19
|
+
*/
|
|
20
|
+
defaultExpanded?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* **Note:** this is controlled by the parent SideNav component, do not set manually.
|
|
23
|
+
* SideNavMenu depth to determine spacing
|
|
24
|
+
*/
|
|
25
|
+
depth?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Indicates whether the SideNavMenu is active.
|
|
28
|
+
*/
|
|
29
|
+
isActive?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Specifies whether the SideNavMenu is in a large variation.
|
|
32
|
+
*/
|
|
33
|
+
large?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* A custom icon to render next to the SideNavMenu title. This can be a function returning JSX or JSX itself.
|
|
36
|
+
*/
|
|
37
|
+
renderIcon?: React.ComponentType;
|
|
38
|
+
/**
|
|
39
|
+
* Indicates if the side navigation container is expanded or collapsed.
|
|
40
|
+
*/
|
|
41
|
+
isSideNavExpanded?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* The tabIndex for the button element.
|
|
44
|
+
* If not specified, the default validation will be applied.
|
|
45
|
+
*/
|
|
46
|
+
tabIndex?: number;
|
|
47
|
+
title: string;
|
|
48
|
+
}
|
|
49
|
+
export declare const SideNavMenu: React.ForwardRefExoticComponent<SideNavMenuProps & React.RefAttributes<HTMLElement>>;
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
import React, { ElementType, ComponentProps } from 'react';
|
|
8
|
+
import Link from '@carbon/react/lib/components/UIShell/Link';
|
|
9
|
+
export interface SideNavMenuItemProps extends ComponentProps<typeof Link> {
|
|
10
|
+
/**
|
|
11
|
+
* Specify the children to be rendered inside of the `SideNavMenuItem`
|
|
12
|
+
*/
|
|
13
|
+
children?: React.ReactNode;
|
|
14
|
+
/**
|
|
15
|
+
* Provide an optional class to be applied to the containing node
|
|
16
|
+
*/
|
|
17
|
+
className?: string;
|
|
18
|
+
/**
|
|
19
|
+
* **Note:** this is controlled by the parent SideNavMenu component, do not set manually.
|
|
20
|
+
* SideNavMenu depth to determine spacing
|
|
21
|
+
*/
|
|
22
|
+
depth?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Optionally specify whether the link is "active". An active link is one that
|
|
25
|
+
* has an href that is the same as the current page. Can also pass in
|
|
26
|
+
* `aria-current="page"`, as well.
|
|
27
|
+
*/
|
|
28
|
+
isActive?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Optionally provide an href for the underlying li`
|
|
31
|
+
*/
|
|
32
|
+
href?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Optional component to render instead of default Link
|
|
35
|
+
*/
|
|
36
|
+
as?: ElementType;
|
|
37
|
+
}
|
|
38
|
+
export declare const SideNavMenuItem: React.ForwardRefExoticComponent<Omit<SideNavMenuItemProps, "ref"> & React.RefAttributes<HTMLElement>>;
|
|
@@ -142,6 +142,39 @@ function SideNavRenderFunction(_ref, ref) {
|
|
|
142
142
|
return child;
|
|
143
143
|
});
|
|
144
144
|
const eventHandlers = {};
|
|
145
|
+
const treeWalkerRef = React.useRef(null);
|
|
146
|
+
React.useEffect(() => {
|
|
147
|
+
treeWalkerRef.current = treeWalkerRef.current ?? document.createTreeWalker(sideNavRef?.current, NodeFilter.SHOW_ELEMENT, {
|
|
148
|
+
acceptNode: function (node) {
|
|
149
|
+
if (!(node instanceof Element)) {
|
|
150
|
+
return NodeFilter.FILTER_SKIP;
|
|
151
|
+
}
|
|
152
|
+
if (node.classList.contains(`${prefix}--side-nav__divider`)) {
|
|
153
|
+
return NodeFilter.FILTER_REJECT;
|
|
154
|
+
}
|
|
155
|
+
if (node.matches(`li.${prefix}--side-nav__item`) || node.matches(`li.${prefix}--side-nav__menu-item`)) {
|
|
156
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
157
|
+
}
|
|
158
|
+
return NodeFilter.FILTER_SKIP;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
resetNodeTabIndices();
|
|
162
|
+
const firstElement = sideNavRef?.current?.querySelector('a, button');
|
|
163
|
+
if (firstElement) {
|
|
164
|
+
firstElement.tabIndex = 0;
|
|
165
|
+
}
|
|
166
|
+
}, [prefix]);
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Returns the parent SideNavMenu, if node is actually inside one.
|
|
170
|
+
* @param node
|
|
171
|
+
* @returns parent side nav menu node
|
|
172
|
+
*/
|
|
173
|
+
function parentSideNavMenu(node) {
|
|
174
|
+
const parentNode = node.parentElement?.closest(`.${prefix}--side-nav__item`);
|
|
175
|
+
if (parentNode) return parentNode;
|
|
176
|
+
return node;
|
|
177
|
+
}
|
|
145
178
|
if (addFocusListeners) {
|
|
146
179
|
eventHandlers.onFocus = event => {
|
|
147
180
|
if (!event.currentTarget.contains(event.relatedTarget) && isRail) {
|
|
@@ -162,7 +195,94 @@ function SideNavRenderFunction(_ref, ref) {
|
|
|
162
195
|
}
|
|
163
196
|
};
|
|
164
197
|
eventHandlers.onKeyDown = event => {
|
|
198
|
+
if (!treeWalkerRef.current) return;
|
|
199
|
+
const treeWalker = treeWalkerRef.current;
|
|
200
|
+
event.stopPropagation();
|
|
201
|
+
|
|
202
|
+
// stops page from scrolling
|
|
203
|
+
if (match.matches(event, [keys__namespace.ArrowUp, keys__namespace.ArrowDown, keys__namespace.Home, keys__namespace.End,
|
|
204
|
+
// @ts-ignore - `matches` doesn't like the object syntax without missing properties
|
|
205
|
+
{
|
|
206
|
+
code: 'KeyA'
|
|
207
|
+
}])) {
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
}
|
|
210
|
+
treeWalker.currentNode = event.target.closest(`li`) ?? treeWalker?.currentNode;
|
|
211
|
+
let nextFocusNode = null;
|
|
212
|
+
if (match.match(event, keys__namespace.ArrowUp)) {
|
|
213
|
+
const parentNode = parentSideNavMenu(treeWalker.currentNode);
|
|
214
|
+
let previousSideNavMenu = parentNode?.previousElementSibling;
|
|
215
|
+
|
|
216
|
+
// skip the divider
|
|
217
|
+
if (previousSideNavMenu?.classList.contains(`${prefix}--side-nav__divider`)) {
|
|
218
|
+
previousSideNavMenu = previousSideNavMenu?.previousElementSibling;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// when previous sibling is open, go to its last item
|
|
222
|
+
if (previousSideNavMenu?.getAttribute('aria-expanded') == 'true') {
|
|
223
|
+
nextFocusNode = treeWalker.previousNode();
|
|
224
|
+
} else {
|
|
225
|
+
nextFocusNode = treeWalker.previousSibling();
|
|
226
|
+
|
|
227
|
+
// first item in the menu, go back up to SideNavMenu button
|
|
228
|
+
if (nextFocusNode == null) {
|
|
229
|
+
nextFocusNode = parentNode;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (match.match(event, keys__namespace.ArrowDown)) {
|
|
234
|
+
if (treeWalker.currentNode.getAttribute('aria-expanded') == 'false') {
|
|
235
|
+
nextFocusNode = treeWalker.nextSibling();
|
|
236
|
+
} else {
|
|
237
|
+
nextFocusNode = treeWalker.nextNode();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Home/End functionality
|
|
242
|
+
if (match.matches(event, [keys__namespace.Home, keys__namespace.End])) {
|
|
243
|
+
if (!sideNavRef?.current) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const allItems = Array.from(sideNavRef.current.querySelectorAll('a, button'));
|
|
247
|
+
if (match.match(event, keys__namespace.Home)) {
|
|
248
|
+
const firstElement = allItems[0];
|
|
249
|
+
if (firstElement) {
|
|
250
|
+
firstElement.tabIndex = 0;
|
|
251
|
+
firstElement?.focus();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (match.match(event, keys__namespace.End)) {
|
|
255
|
+
const allItems = Array.from(sideNavRef.current.querySelectorAll('li'));
|
|
256
|
+
const lastVisibleItem = allItems.reverse().find(item => getComputedStyle(item).visibility !== 'hidden');
|
|
257
|
+
if (lastVisibleItem) {
|
|
258
|
+
const node = lastVisibleItem.querySelector('button') ?? lastVisibleItem.querySelector('a');
|
|
259
|
+
if (node) {
|
|
260
|
+
node.tabIndex = 0;
|
|
261
|
+
node?.focus();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// focus on the focusable element within the node
|
|
268
|
+
if (nextFocusNode && nextFocusNode !== event.target) {
|
|
269
|
+
resetNodeTabIndices();
|
|
270
|
+
if (nextFocusNode instanceof HTMLElement) {
|
|
271
|
+
const node = nextFocusNode.querySelector('button') ?? nextFocusNode.querySelector('a');
|
|
272
|
+
if (node) {
|
|
273
|
+
node.tabIndex = 0;
|
|
274
|
+
node?.focus();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// close menu
|
|
165
280
|
if (match.match(event, keys__namespace.Escape)) {
|
|
281
|
+
if (expanded && !isFixedNav) {
|
|
282
|
+
if (onSideNavBlur) {
|
|
283
|
+
onSideNavBlur();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
166
286
|
handleToggle(event, false);
|
|
167
287
|
if (href) {
|
|
168
288
|
window.location.href = href;
|
|
@@ -188,12 +308,19 @@ function SideNavRenderFunction(_ref, ref) {
|
|
|
188
308
|
}
|
|
189
309
|
useEvent.useWindowEvent('keydown', event => {
|
|
190
310
|
const focusedElement = document.activeElement;
|
|
191
|
-
|
|
311
|
+
|
|
312
|
+
// going from header menu to sideNav
|
|
313
|
+
if (match.match(event, keys__namespace.Tab) && expanded && !isFixedNav && sideNavRef?.current && focusedElement?.classList.contains(`${prefix}--header__menu-toggle`) && !focusedElement.closest('nav')) {
|
|
192
314
|
sideNavRef.current.focus();
|
|
193
315
|
}
|
|
194
316
|
});
|
|
195
317
|
const lgMediaQuery = `(min-width: ${index$1.breakpoints.lg.width})`;
|
|
196
318
|
const isLg = useMatchMedia.useMatchMedia(lgMediaQuery);
|
|
319
|
+
function resetNodeTabIndices() {
|
|
320
|
+
Array.prototype.forEach.call(sideNavRef?.current?.querySelectorAll('[tabIndex="0"]') ?? [], item => {
|
|
321
|
+
item.tabIndex = -1;
|
|
322
|
+
});
|
|
323
|
+
}
|
|
197
324
|
return /*#__PURE__*/React.createElement(SideNavContext.Provider, {
|
|
198
325
|
value: {
|
|
199
326
|
isRail
|
|
@@ -205,6 +332,7 @@ function SideNavRenderFunction(_ref, ref) {
|
|
|
205
332
|
className: overlayClassName,
|
|
206
333
|
onClick: onOverlayClick
|
|
207
334
|
}), /*#__PURE__*/React.createElement("nav", _rollupPluginBabelHelpers.extends({
|
|
335
|
+
role: "tree",
|
|
208
336
|
tabIndex: -1,
|
|
209
337
|
ref: navRef,
|
|
210
338
|
className: `${prefix}--side-nav__navigation ${className}`,
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
import React from 'react';
|
|
8
|
+
export interface SideNavItemsProps {
|
|
9
|
+
/**
|
|
10
|
+
* Provide a single icon as the child to `SideNavIcon` to render in the
|
|
11
|
+
* container
|
|
12
|
+
*/
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
/**
|
|
15
|
+
* Provide an optional class to be applied to the containing node
|
|
16
|
+
*/
|
|
17
|
+
className?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Property to indicate if the side nav container is open (or not). Use to
|
|
20
|
+
* keep local state and styling in step with the SideNav expansion state.
|
|
21
|
+
*/
|
|
22
|
+
isSideNavExpanded?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare const SideNavItems: React.FC<SideNavItemsProps>;
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
import React from 'react';
|
|
8
|
+
export interface SideNavMenuProps {
|
|
9
|
+
/**
|
|
10
|
+
* An optional CSS class to apply to the component.
|
|
11
|
+
*/
|
|
12
|
+
className?: string;
|
|
13
|
+
/**
|
|
14
|
+
* The content to render within the SideNavMenu component.
|
|
15
|
+
*/
|
|
16
|
+
children?: React.ReactNode;
|
|
17
|
+
/**
|
|
18
|
+
* Specifies whether the menu should be expanded by default.
|
|
19
|
+
*/
|
|
20
|
+
defaultExpanded?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* **Note:** this is controlled by the parent SideNav component, do not set manually.
|
|
23
|
+
* SideNavMenu depth to determine spacing
|
|
24
|
+
*/
|
|
25
|
+
depth?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Indicates whether the SideNavMenu is active.
|
|
28
|
+
*/
|
|
29
|
+
isActive?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Specifies whether the SideNavMenu is in a large variation.
|
|
32
|
+
*/
|
|
33
|
+
large?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* A custom icon to render next to the SideNavMenu title. This can be a function returning JSX or JSX itself.
|
|
36
|
+
*/
|
|
37
|
+
renderIcon?: React.ComponentType;
|
|
38
|
+
/**
|
|
39
|
+
* Indicates if the side navigation container is expanded or collapsed.
|
|
40
|
+
*/
|
|
41
|
+
isSideNavExpanded?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* The tabIndex for the button element.
|
|
44
|
+
* If not specified, the default validation will be applied.
|
|
45
|
+
*/
|
|
46
|
+
tabIndex?: number;
|
|
47
|
+
title: string;
|
|
48
|
+
}
|
|
49
|
+
export declare const SideNavMenu: React.ForwardRefExoticComponent<SideNavMenuProps & React.RefAttributes<HTMLElement>>;
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
import React, { ElementType, ComponentProps } from 'react';
|
|
8
|
+
import Link from '@carbon/react/lib/components/UIShell/Link';
|
|
9
|
+
export interface SideNavMenuItemProps extends ComponentProps<typeof Link> {
|
|
10
|
+
/**
|
|
11
|
+
* Specify the children to be rendered inside of the `SideNavMenuItem`
|
|
12
|
+
*/
|
|
13
|
+
children?: React.ReactNode;
|
|
14
|
+
/**
|
|
15
|
+
* Provide an optional class to be applied to the containing node
|
|
16
|
+
*/
|
|
17
|
+
className?: string;
|
|
18
|
+
/**
|
|
19
|
+
* **Note:** this is controlled by the parent SideNavMenu component, do not set manually.
|
|
20
|
+
* SideNavMenu depth to determine spacing
|
|
21
|
+
*/
|
|
22
|
+
depth?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Optionally specify whether the link is "active". An active link is one that
|
|
25
|
+
* has an href that is the same as the current page. Can also pass in
|
|
26
|
+
* `aria-current="page"`, as well.
|
|
27
|
+
*/
|
|
28
|
+
isActive?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Optionally provide an href for the underlying li`
|
|
31
|
+
*/
|
|
32
|
+
href?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Optional component to render instead of default Link
|
|
35
|
+
*/
|
|
36
|
+
as?: ElementType;
|
|
37
|
+
}
|
|
38
|
+
export declare const SideNavMenuItem: React.ForwardRefExoticComponent<Omit<SideNavMenuItemProps, "ref"> & React.RefAttributes<HTMLElement>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carbon-labs/react-ui-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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": "
|
|
36
|
+
"gitHead": "5b474b82f13a6c1fec121c8729d208082140b9f5"
|
|
37
37
|
}
|
|
@@ -28,10 +28,30 @@ $prefix: 'cds' !default;
|
|
|
28
28
|
inline-size: convert.to-rem(256px);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
//----------------------------------------------------------------------------
|
|
32
|
+
// Treeview Side-nav
|
|
33
|
+
//----------------------------------------------------------------------------
|
|
34
|
+
.#{$prefix}--side-nav__icon:not(.#{$prefix}--side-nav__submenu-chevron) {
|
|
35
|
+
margin-inline-end: $spacing-05;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.#{$prefix}--side-nav__submenu.#{$prefix}--side-nav__submenu--active {
|
|
39
|
+
> span {
|
|
40
|
+
color: $text-primary;
|
|
41
|
+
font-weight: 600;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.#{$prefix}--side-nav__submenu-chevron > svg {
|
|
45
|
+
fill: $icon-primary;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
//----------------------------------------------------------------------------
|
|
32
50
|
// Side-nav Panel
|
|
33
51
|
//----------------------------------------------------------------------------
|
|
34
52
|
.#{$prefix}--side-nav--panel {
|
|
53
|
+
z-index: 7999; /* needs to be below header */
|
|
54
|
+
|
|
35
55
|
.#{$prefix}--side-nav__icon:not(.#{$prefix}--side-nav__submenu-chevron) {
|
|
36
56
|
margin-inline-end: $spacing-05;
|
|
37
57
|
}
|