@carbon-labs/react-ui-shell 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Copyright IBM Corp. 2024
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ var index = require('../_virtual/index.js');
11
+ var PropTypes = require('prop-types');
12
+ var React = require('react');
13
+ var _utils = require('./_utils.js');
14
+ var usePrefix = require('@carbon/react/lib/internal/usePrefix');
15
+
16
+ const SideNavItems = _ref => {
17
+ let {
18
+ className: customClassName,
19
+ children,
20
+ isSideNavExpanded
21
+ } = _ref;
22
+ const prefix = usePrefix.usePrefix();
23
+ const className = index.default([`${prefix}--side-nav__items`], customClassName);
24
+ const childrenWithExpandedState = React.Children.map(children, child => {
25
+ if (/*#__PURE__*/React.isValidElement(child)) {
26
+ // avoid spreading `isSideNavExpanded` to non-Carbon UI Shell children
27
+ const childJsxElement = child;
28
+ const childDisplayName = childJsxElement.type?.displayName ?? childJsxElement.type?.name;
29
+ const isCarbonSideNavItem = _utils.CARBON_SIDENAV_ITEMS.includes(childDisplayName);
30
+ const isSideNavMenu = childDisplayName === 'SideNavMenu';
31
+ return /*#__PURE__*/React.cloneElement(child, {
32
+ ...(isCarbonSideNavItem && {
33
+ isSideNavExpanded: isSideNavExpanded
34
+ }),
35
+ ...(isSideNavMenu && {
36
+ depth: 0
37
+ })
38
+ });
39
+ }
40
+ });
41
+ return /*#__PURE__*/React.createElement("ul", {
42
+ className: className
43
+ }, childrenWithExpandedState);
44
+ };
45
+ SideNavItems.displayName = 'SideNavItems';
46
+ SideNavItems.propTypes = {
47
+ /**
48
+ * Provide a single icon as the child to `SideNavIcon` to render in the
49
+ * container
50
+ */
51
+ children: PropTypes.node,
52
+ /**
53
+ * Provide an optional class to be applied to the containing node
54
+ */
55
+ className: PropTypes.string,
56
+ /**
57
+ * Property to indicate if the side nav container is open (or not). Use to
58
+ * keep local state and styling in step with the SideNav expansion state.
59
+ */
60
+ isSideNavExpanded: PropTypes.bool
61
+ };
62
+
63
+ exports.SideNavItems = SideNavItems;
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Copyright IBM Corp. 2024
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ var index = require('../_virtual/index.js');
11
+ var PropTypes = require('prop-types');
12
+ var React = require('react');
13
+ var _utils = require('./_utils.js');
14
+ var SideNavIcon = require('@carbon/react/lib/components/UIShell/SideNavIcon');
15
+ var match = require('@carbon/react/lib/internal/keyboard/match');
16
+ var usePrefix = require('@carbon/react/lib/internal/usePrefix');
17
+ var SideNav = require('./SideNav.js');
18
+ var useMergedRefs = require('@carbon/react/lib/internal/useMergedRefs');
19
+ var keys = require('@carbon/react/lib/internal/keyboard/keys');
20
+ var bucket3 = require('../node_modules/@carbon/icons-react/es/generated/bucket-3.js');
21
+
22
+ function _interopNamespaceDefault(e) {
23
+ var n = Object.create(null);
24
+ if (e) {
25
+ Object.keys(e).forEach(function (k) {
26
+ if (k !== 'default') {
27
+ var d = Object.getOwnPropertyDescriptor(e, k);
28
+ Object.defineProperty(n, k, d.get ? d : {
29
+ enumerable: true,
30
+ get: function () { return e[k]; }
31
+ });
32
+ }
33
+ });
34
+ }
35
+ n.default = e;
36
+ return Object.freeze(n);
37
+ }
38
+
39
+ var keys__namespace = /*#__PURE__*/_interopNamespaceDefault(keys);
40
+
41
+ var _ChevronDown;
42
+ const SideNavMenu = /*#__PURE__*/React.forwardRef(function SideNavMenu(_ref, ref) {
43
+ let {
44
+ className: customClassName,
45
+ children,
46
+ defaultExpanded = false,
47
+ depth: propDepth,
48
+ isActive = false,
49
+ large = false,
50
+ renderIcon: IconElement,
51
+ isSideNavExpanded,
52
+ tabIndex,
53
+ title
54
+ } = _ref;
55
+ const depth = propDepth;
56
+ const {
57
+ isRail
58
+ } = React.useContext(SideNav.SideNavContext);
59
+ const prefix = usePrefix.usePrefix();
60
+ const [isExpanded, setIsExpanded] = React.useState(defaultExpanded);
61
+ const [active, setActive] = React.useState(isActive);
62
+ const [prevExpanded, setPrevExpanded] = React.useState(defaultExpanded);
63
+ const className = index.default({
64
+ [`${prefix}--side-nav__item`]: true,
65
+ [`${prefix}--side-nav__item--active`]: active || hasActiveDescendant(children) && !isExpanded,
66
+ [`${prefix}--side-nav__item--icon`]: IconElement,
67
+ [`${prefix}--side-nav__item--large`]: large,
68
+ [customClassName]: !!customClassName
69
+ });
70
+ const buttonClassName = index.default({
71
+ [`${prefix}--side-nav__submenu`]: true,
72
+ [`${prefix}--side-nav__submenu--active`]: active || hasActiveDescendant(children) && isExpanded
73
+ });
74
+ const buttonRef = React.useRef(null);
75
+ const listRef = React.useRef(null);
76
+ const menuRef = useMergedRefs.useMergedRefs([buttonRef, ref]);
77
+ if (!isSideNavExpanded && isExpanded && isRail) {
78
+ setIsExpanded(false);
79
+ setPrevExpanded(true);
80
+ } else if (isSideNavExpanded && prevExpanded && isRail) {
81
+ setIsExpanded(true);
82
+ setPrevExpanded(false);
83
+ }
84
+ let childrenToRender = children;
85
+
86
+ // modify nested SideNavMenus
87
+ childrenToRender = React.Children.map(children, child => {
88
+ if (/*#__PURE__*/React.isValidElement(child)) {
89
+ const childJsxElement = child;
90
+ const childDisplayName = childJsxElement.type?.displayName ?? childJsxElement.type?.name;
91
+ const isCarbonSideNavItem = _utils.CARBON_SIDENAV_ITEMS.includes(childDisplayName);
92
+ return /*#__PURE__*/React.cloneElement(child, {
93
+ ...(isCarbonSideNavItem && {
94
+ isSideNavExpanded: isSideNavExpanded
95
+ }),
96
+ ...{
97
+ depth: depth + 1
98
+ }
99
+ });
100
+ }
101
+ return child;
102
+ });
103
+ React.useEffect(() => {
104
+ if (depth === 0) return;
105
+ const calcButtonOffset = () => {
106
+ // menu with icon
107
+ if (children && IconElement) {
108
+ return depth + 3;
109
+ }
110
+
111
+ // menu without icon
112
+ if (children) {
113
+ return depth * 4;
114
+ }
115
+ return depth;
116
+ };
117
+ if (buttonRef.current) {
118
+ buttonRef.current.style.paddingLeft = `${calcButtonOffset()}rem`;
119
+ }
120
+ }, []);
121
+
122
+ /**
123
+ * Returns the parent SideNavMenu, if node is actually inside one.
124
+ * @param node
125
+ * @returns parent side nav menu node
126
+ */
127
+ function parentSideNavMenu(node) {
128
+ const parentNode = node.parentElement?.closest(`.${prefix}--side-nav__item`);
129
+ if (parentNode) return parentNode;
130
+ return node;
131
+ }
132
+ function handleKeyDown(event) {
133
+ if (match.match(event, keys__namespace.Escape)) {
134
+ setIsExpanded(false);
135
+ }
136
+ const node = event.target;
137
+ const isMenu = node.hasAttribute('aria-expanded');
138
+ const isExpanded = node.getAttribute('aria-expanded');
139
+ const parent = parentSideNavMenu(node);
140
+ if (match.match(event, keys__namespace.ArrowLeft)) {
141
+ event.stopPropagation();
142
+ if (isMenu) {
143
+ // collapse menu
144
+ if (isExpanded == 'true') {
145
+ setIsExpanded(false);
146
+
147
+ // go to previous level's side nav menu button
148
+ } else {
149
+ // since we're in a menu, it finds its own <li>, we go up one more
150
+ const previousMenu = parentSideNavMenu(parent);
151
+ const button = previousMenu.querySelector('button');
152
+ button.tabIndex = 0;
153
+ button?.focus();
154
+ }
155
+
156
+ // go to side nav menu button
157
+ } else if (parent) {
158
+ const button = parent.querySelector('button');
159
+ button.tabIndex = 0;
160
+ button?.focus();
161
+ }
162
+ }
163
+ if (match.match(event, keys__namespace.ArrowRight)) {
164
+ event.stopPropagation();
165
+
166
+ // expand menu
167
+ if (isMenu) {
168
+ setIsExpanded(true);
169
+
170
+ // if already expanded, focus on first element
171
+ if (isExpanded == 'true') {
172
+ let nextNode = node.nextElementSibling?.querySelector('a, button');
173
+ if (nextNode) {
174
+ nextNode.tabIndex = 0;
175
+ nextNode.focus();
176
+ }
177
+ }
178
+ }
179
+ }
180
+ }
181
+ return (
182
+ /*#__PURE__*/
183
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
184
+ React.createElement("li", {
185
+ role: "treeitem",
186
+ "aria-expanded": isExpanded,
187
+ className: className,
188
+ ref: listRef,
189
+ onKeyDown: handleKeyDown
190
+ }, /*#__PURE__*/React.createElement("button", {
191
+ "aria-expanded": isExpanded,
192
+ className: buttonClassName,
193
+ onClick: () => {
194
+ setIsExpanded(!isExpanded);
195
+ },
196
+ ref: menuRef,
197
+ type: "button",
198
+ tabIndex: -1
199
+ }, IconElement && /*#__PURE__*/React.createElement(SideNavIcon, null, /*#__PURE__*/React.createElement(IconElement, null)), /*#__PURE__*/React.createElement("span", {
200
+ className: `${prefix}--side-nav__submenu-title`
201
+ }, title), /*#__PURE__*/React.createElement(SideNavIcon, {
202
+ className: `${prefix}--side-nav__submenu-chevron`,
203
+ small: true
204
+ }, _ChevronDown || (_ChevronDown = /*#__PURE__*/React.createElement(bucket3.ChevronDown, {
205
+ size: 20
206
+ })))), /*#__PURE__*/React.createElement("ul", {
207
+ className: `${prefix}--side-nav__menu`,
208
+ role: "group"
209
+ }, childrenToRender))
210
+ );
211
+ });
212
+ SideNavMenu.displayName = 'SideNavMenu';
213
+ SideNavMenu.propTypes = {
214
+ /**
215
+ * Provide <SideNavMenuItem>'s inside of the `SideNavMenu`
216
+ */
217
+ children: PropTypes.node,
218
+ /**
219
+ * Provide an optional class to be applied to the containing node
220
+ */
221
+ className: PropTypes.string,
222
+ /**
223
+ * Specify whether the menu should default to expanded. By default, it will
224
+ * be closed.
225
+ */
226
+ defaultExpanded: PropTypes.bool,
227
+ /**
228
+ * **Note:** this is controlled by the parent SideNav component, do not set manually.
229
+ * SideNavMenu depth to determine spacing
230
+ */
231
+ depth: PropTypes.number,
232
+ /**
233
+ * Specify whether the `SideNavMenu` is "active". `SideNavMenu` should be
234
+ * considered active if one of its menu items are a link for the current
235
+ * page.
236
+ */
237
+ isActive: PropTypes.bool,
238
+ /**
239
+ * Property to indicate if the side nav container is open (or not). Use to
240
+ * keep local state and styling in step with the SideNav expansion state.
241
+ */
242
+ isSideNavExpanded: PropTypes.bool,
243
+ /**
244
+ * Specify if this is a large variation of the SideNavMenu
245
+ */
246
+ large: PropTypes.bool,
247
+ /**
248
+ * Pass in a custom icon to render next to the `SideNavMenu` title
249
+ */
250
+ // @ts-expect-error - PropTypes are unable to cover this case.
251
+ renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
252
+ /**
253
+ * Optional prop to specify the tabIndex of the button. If undefined, it will be applied default validation
254
+ */
255
+ tabIndex: PropTypes.number,
256
+ /**
257
+ * Provide the text for the overall menu name
258
+ */
259
+ title: PropTypes.string.isRequired
260
+ };
261
+
262
+ /**
263
+ Defining the children parameter with the type ReactNode | ReactNode[]. This allows for various possibilities:
264
+ a single element, an array of elements, or null or undefined.
265
+ **/
266
+ function hasActiveDescendant(children) {
267
+ if (Array.isArray(children)) {
268
+ return children.some(child => {
269
+ if (! /*#__PURE__*/React.isValidElement(child)) {
270
+ return false;
271
+ }
272
+
273
+ /** Explicitly defining the expected prop types (isActive and 'aria-current) for the children to ensure type
274
+ safety when accessing their props.
275
+ **/
276
+ const props = child.props;
277
+ if (props.isActive === true || props['aria-current'] || props.children instanceof Array && hasActiveDescendant(props.children)) {
278
+ return true;
279
+ }
280
+ return false;
281
+ });
282
+ }
283
+
284
+ // We use React.isValidElement(child) to check if the child is a valid React element before accessing its props
285
+
286
+ if (/*#__PURE__*/React.isValidElement(children)) {
287
+ const props = children.props;
288
+ if (props.isActive === true || props['aria-current']) {
289
+ return true;
290
+ }
291
+ }
292
+ return false;
293
+ }
294
+
295
+ exports.SideNavMenu = SideNavMenu;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Copyright IBM Corp. 2024
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ var _rollupPluginBabelHelpers = require('../_virtual/_rollupPluginBabelHelpers.js');
11
+ var index = require('../_virtual/index.js');
12
+ var PropTypes = require('prop-types');
13
+ var React = require('react');
14
+ var SideNavLinkText = require('@carbon/react/lib/components/UIShell/SideNavLinkText');
15
+ var Link = require('@carbon/react/lib/components/UIShell/Link');
16
+ var usePrefix = require('@carbon/react/lib/internal/usePrefix');
17
+ var useMergedRefs = require('@carbon/react/lib/internal/useMergedRefs');
18
+
19
+ const SideNavMenuItem = /*#__PURE__*/React.forwardRef(function SideNavMenuItem(props, ref) {
20
+ const prefix = usePrefix.usePrefix();
21
+ const {
22
+ children,
23
+ className: customClassName,
24
+ depth: propDepth,
25
+ as: Component = Link,
26
+ isActive,
27
+ ...rest
28
+ } = props;
29
+ const className = index.default(`${prefix}--side-nav__menu-item`, customClassName);
30
+ const depth = propDepth;
31
+ const linkClassName = index.default({
32
+ [`${prefix}--side-nav__link`]: true,
33
+ [`${prefix}--side-nav__link--current`]: isActive
34
+ });
35
+ const linkRef = React.useRef(null);
36
+ const itemRef = useMergedRefs.useMergedRefs([linkRef, ref]);
37
+ React.useEffect(() => {
38
+ const calcLinkOffset = () => {
39
+ return 4 + Math.max(0, depth - 1) * 1;
40
+ };
41
+ if (linkRef.current) {
42
+ linkRef.current.style.paddingLeft = `${calcLinkOffset()}rem`;
43
+ }
44
+ }, []);
45
+ return /*#__PURE__*/React.createElement("li", {
46
+ role: "treeitem",
47
+ "aria-selected": isActive ? 'true' : 'false',
48
+ className: className
49
+ }, /*#__PURE__*/React.createElement(Component, _rollupPluginBabelHelpers.extends({}, rest, {
50
+ className: linkClassName,
51
+ tabIndex: -1,
52
+ ref: itemRef
53
+ }), /*#__PURE__*/React.createElement(SideNavLinkText, null, children)));
54
+ });
55
+ SideNavMenuItem.displayName = 'SideNavMenuItem';
56
+ SideNavMenuItem.propTypes = {
57
+ /**
58
+ * Specify the children to be rendered inside of the `SideNavMenuItem`
59
+ */
60
+ children: PropTypes.node,
61
+ /**
62
+ * Provide an optional class to be applied to the containing node
63
+ */
64
+ className: PropTypes.string,
65
+ /**
66
+ * **Note:** this is controlled by the parent SideNavMenu component, do not set manually.
67
+ * SideNavMenu depth to determine spacing
68
+ */
69
+ depth: PropTypes.number,
70
+ /**
71
+ * Optionally provide an href for the underlying li`
72
+ */
73
+ href: PropTypes.string,
74
+ /**
75
+ * Optionally specify whether the link is "active". An active link is one that
76
+ * has an href that is the same as the current page. Can also pass in
77
+ * `aria-current="page"`, as well.
78
+ */
79
+ isActive: PropTypes.bool,
80
+ /**
81
+ * Optional component to render instead of default Link
82
+ */
83
+ as: PropTypes.elementType
84
+ };
85
+
86
+ exports.SideNavMenuItem = SideNavMenuItem;
package/lib/index.d.ts CHANGED
@@ -6,5 +6,8 @@
6
6
  * This source code is licensed under the Apache-2.0 license found in the
7
7
  * LICENSE file in the root directory of this source tree.
8
8
  */
9
- export { SideNav } from './components/SideNav.js';
9
+ export { SideNav, SIDE_NAV_TYPE } from './components/SideNav.js';
10
+ export { SideNavItems } from './components/SideNavItems.js';
11
+ export { SideNavMenu } from './components/SideNavMenu.js';
12
+ export { SideNavMenuItem } from './components/SideNavMenuItem.js';
10
13
  export { HeaderPanel } from './components/HeaderPanel';
package/lib/index.js CHANGED
@@ -8,9 +8,16 @@
8
8
  'use strict';
9
9
 
10
10
  var SideNav = require('./components/SideNav.js');
11
+ var SideNavItems = require('./components/SideNavItems.js');
12
+ var SideNavMenu = require('./components/SideNavMenu.js');
13
+ var SideNavMenuItem = require('./components/SideNavMenuItem.js');
11
14
  var HeaderPanel = require('./components/HeaderPanel.js');
12
15
 
13
16
 
14
17
 
18
+ exports.SIDE_NAV_TYPE = SideNav.SIDE_NAV_TYPE;
15
19
  exports.SideNav = SideNav.SideNav;
20
+ exports.SideNavItems = SideNavItems.SideNavItems;
21
+ exports.SideNavMenu = SideNavMenu.SideNavMenu;
22
+ exports.SideNavMenuItem = SideNavMenuItem.SideNavMenuItem;
16
23
  exports.HeaderPanel = HeaderPanel.HeaderPanel;