@carbon-labs/react-ui-shell 0.25.0 → 0.26.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.
Files changed (42) hide show
  1. package/es/components/SharkFinIcon.d.ts +24 -0
  2. package/es/components/SharkFinIcon.js +59 -0
  3. package/es/components/SideNavFlyoutMenu.d.ts +141 -0
  4. package/es/components/SideNavFlyoutMenu.js +365 -0
  5. package/es/components/SideNavMenu.d.ts +4 -0
  6. package/es/components/SideNavMenu.js +96 -75
  7. package/es/components/SideNavMenuItem.d.ts +4 -0
  8. package/es/components/SideNavMenuItem.js +17 -4
  9. package/es/index.d.ts +1 -0
  10. package/es/index.js +1 -0
  11. package/es/internal/keyboard/keys.js +13 -1
  12. package/es/internal/setupGetInstanceId.d.ts +11 -0
  13. package/es/internal/setupGetInstanceId.js +19 -0
  14. package/es/internal/useId.d.ts +21 -0
  15. package/es/internal/useId.js +100 -0
  16. package/es/internal/useIdPrefix.d.ts +12 -0
  17. package/es/internal/useIdPrefix.js +19 -0
  18. package/es/internal/usePrefix.d.ts +5 -0
  19. package/es/internal/usePrefix.js +6 -0
  20. package/lib/components/SharkFinIcon.d.ts +24 -0
  21. package/lib/components/SharkFinIcon.js +61 -0
  22. package/lib/components/SideNavFlyoutMenu.d.ts +141 -0
  23. package/lib/components/SideNavFlyoutMenu.js +367 -0
  24. package/lib/components/SideNavMenu.d.ts +4 -0
  25. package/lib/components/SideNavMenu.js +96 -75
  26. package/lib/components/SideNavMenuItem.d.ts +4 -0
  27. package/lib/components/SideNavMenuItem.js +17 -4
  28. package/lib/index.d.ts +1 -0
  29. package/lib/index.js +2 -0
  30. package/lib/internal/keyboard/keys.js +14 -0
  31. package/lib/internal/setupGetInstanceId.d.ts +11 -0
  32. package/lib/internal/setupGetInstanceId.js +23 -0
  33. package/lib/internal/useId.d.ts +21 -0
  34. package/lib/internal/useId.js +103 -0
  35. package/lib/internal/useIdPrefix.d.ts +12 -0
  36. package/lib/internal/useIdPrefix.js +22 -0
  37. package/lib/internal/usePrefix.d.ts +5 -0
  38. package/lib/internal/usePrefix.js +6 -0
  39. package/package.json +2 -2
  40. package/scss/styles/_shark-fin-icon.scss +14 -0
  41. package/scss/styles/_side-nav.scss +147 -0
  42. package/scss/ui-shell.scss +1 -0
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Copyright IBM Corp. 2016, 2023
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 PropTypes from 'prop-types';
8
+ import React from 'react';
9
+ import { PopoverAlignment } from '@carbon/react';
10
+ import { type PolymorphicProps } from '../types/common';
11
+ interface TooltipBaseProps {
12
+ /**
13
+ * Specify how the trigger should align with the tooltip
14
+ */
15
+ align?: PopoverAlignment;
16
+ /**
17
+ * Pass in the child to which the tooltip will be applied
18
+ */
19
+ children?: React.ReactElement;
20
+ /**
21
+ * Specify an optional className to be applied to the container node
22
+ */
23
+ className?: string;
24
+ /**
25
+ * Specify whether the tooltip should be open when it first renders
26
+ */
27
+ defaultOpen?: boolean;
28
+ /**
29
+ * Provide the description to be rendered inside of the Tooltip. The
30
+ * description will use `aria-describedby` and will describe the child node
31
+ * in addition to the text rendered inside of the child. This means that if you
32
+ * have text in the child node, that it will be announced alongside the
33
+ * description to the screen reader.
34
+ *
35
+ * Note: if label and description are both provided, label will be used and
36
+ * description will not be used
37
+ */
38
+ description?: React.ReactNode;
39
+ /**
40
+ * Specify whether a drop shadow should be rendered
41
+ */
42
+ dropShadow?: boolean;
43
+ /**
44
+ * Specify the duration in milliseconds to delay before displaying the tooltip
45
+ */
46
+ enterDelayMs?: number;
47
+ /**
48
+ * Provide the label to be rendered inside of the Tooltip. The label will use
49
+ * `aria-labelledby` and will fully describe the child node that is provided.
50
+ * This means that if you have text in the child node, that it will not be
51
+ * announced to the screen reader.
52
+ *
53
+ * Note: if label and description are both provided, description will not be
54
+ * used
55
+ */
56
+ label?: React.ReactNode;
57
+ /**
58
+ * Specify the duration in milliseconds to delay before hiding the tooltip
59
+ */
60
+ leaveDelayMs?: number;
61
+ /**
62
+ * The content to be rendered inside the popover menu.
63
+ */
64
+ menuContent?: React.ReactNode;
65
+ /**
66
+ * The boolean to show the flyout menu has been selected.
67
+ */
68
+ selected?: boolean;
69
+ /**
70
+ * The title for the overall menu name.
71
+ */
72
+ title?: string;
73
+ }
74
+ export type TooltipProps<T extends React.ElementType> = PolymorphicProps<T, TooltipBaseProps>;
75
+ declare function SideNavFlyoutMenu<T extends React.ElementType>({ align, className: customClassName, children, label, description, enterDelayMs, leaveDelayMs, defaultOpen, dropShadow, highContrast, menuContent, selected, title, ...rest }: TooltipProps<T>): import("react/jsx-runtime").JSX.Element;
76
+ declare namespace SideNavFlyoutMenu {
77
+ var propTypes: {
78
+ /**
79
+ * Specify how the trigger should align with the tooltip
80
+ */
81
+ align: PropTypes.Requireable<string>;
82
+ /**
83
+ * Pass in the child to which the tooltip will be applied
84
+ */
85
+ children: PropTypes.Requireable<PropTypes.ReactNodeLike>;
86
+ /**
87
+ * Specify an optional className to be applied to the container node
88
+ */
89
+ className: PropTypes.Requireable<string>;
90
+ /**
91
+ * Specify whether the tooltip should be open when it first renders
92
+ */
93
+ defaultOpen: PropTypes.Requireable<boolean>;
94
+ /**
95
+ * Provide the description to be rendered inside of the Tooltip. The
96
+ * description will use `aria-describedby` and will describe the child node
97
+ * in addition to the text rendered inside of the child. This means that if you
98
+ * have text in the child node, that it will be announced alongside the
99
+ * description to the screen reader.
100
+ *
101
+ * Note: if label and description are both provided, label will be used and
102
+ * description will not be used
103
+ */
104
+ description: PropTypes.Requireable<PropTypes.ReactNodeLike>;
105
+ /**
106
+ * Specify whether a drop shadow should be rendered
107
+ */
108
+ dropShadow: PropTypes.Requireable<boolean>;
109
+ /**
110
+ * Specify the duration in milliseconds to delay before displaying the tooltip
111
+ */
112
+ enterDelayMs: PropTypes.Requireable<number>;
113
+ /**
114
+ * Provide the label to be rendered inside of the Tooltip. The label will use
115
+ * `aria-labelledby` and will fully describe the child node that is provided.
116
+ * This means that if you have text in the child node, that it will not be
117
+ * announced to the screen reader.
118
+ *
119
+ * Note: if label and description are both provided, description will not be
120
+ * used
121
+ */
122
+ label: PropTypes.Requireable<PropTypes.ReactNodeLike>;
123
+ /**
124
+ * Specify the duration in milliseconds to delay before hiding the tooltip
125
+ */
126
+ leaveDelayMs: PropTypes.Requireable<number>;
127
+ /**
128
+ * The content to be rendered inside the popover menu.
129
+ */
130
+ menuContent: PropTypes.Requireable<PropTypes.ReactNodeLike>;
131
+ /**
132
+ * The boolean to show the flyout menu has been selected.
133
+ */
134
+ selected: PropTypes.Requireable<boolean>;
135
+ /**
136
+ * The title for the overall menu name.
137
+ */
138
+ title: PropTypes.Requireable<string>;
139
+ };
140
+ }
141
+ export { SideNavFlyoutMenu };
@@ -0,0 +1,367 @@
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 react = require('@carbon/react');
15
+ var keys = require('../internal/keyboard/keys.js');
16
+ var match = require('../internal/keyboard/match.js');
17
+ var useDelayedState = require('../internal/useDelayedState.js');
18
+ var useId = require('../internal/useId.js');
19
+ var usePrefix = require('../internal/usePrefix.js');
20
+
21
+ /**
22
+ * Event types that trigger a "drag" to stop.
23
+ */
24
+ const DRAG_STOP_EVENT_TYPES = new Set(['mouseup', 'touchend', 'touchcancel']);
25
+ function SideNavFlyoutMenu(_ref) {
26
+ let {
27
+ align = 'right-start',
28
+ className: customClassName,
29
+ children,
30
+ label,
31
+ description,
32
+ enterDelayMs = 100,
33
+ leaveDelayMs = 300,
34
+ defaultOpen = false,
35
+ dropShadow = true,
36
+ highContrast = false,
37
+ menuContent,
38
+ selected = false,
39
+ title,
40
+ ...rest
41
+ } = _ref;
42
+ const popoverRef = React.useRef(null);
43
+ const [open, setOpen] = useDelayedState.useDelayedState(defaultOpen);
44
+ const [isDragging, setIsDragging] = React.useState(false);
45
+ const [focusByMouse, setFocusByMouse] = React.useState(false);
46
+ const [isPointerIntersecting, setIsPointerIntersecting] = React.useState(false);
47
+ const id = useId.useId('tooltip');
48
+ const prefix = usePrefix.usePrefix();
49
+ const child = React.Children.only(children);
50
+ const menuButton = React.useRef();
51
+ const [clickMode, setClickMode] = React.useState(false);
52
+ const isFocusedInsideRef = React.useRef(false);
53
+ const popoverMenuLinks = React.useRef(null);
54
+ const [isButtonFocused, setIsButtonFocused] = React.useState(false);
55
+ const flyoutMenuItems = React.Children.map(menuContent, child => {
56
+ if (/*#__PURE__*/React.isValidElement(child)) {
57
+ return /*#__PURE__*/React.cloneElement(child, {
58
+ ...{
59
+ isFlyoutMenuItem: true
60
+ }
61
+ });
62
+ }
63
+ return child;
64
+ });
65
+ const triggerProps = {
66
+ onFocus: () => {
67
+ if (!focusByMouse) {
68
+ setOpen(true);
69
+ setIsButtonFocused(true);
70
+ setClickMode(false);
71
+ isFocusedInsideRef.current = false;
72
+ }
73
+ },
74
+ onBlur: () => {
75
+ if (!isFocusedInsideRef.current && !focusByMouse) {
76
+ closeMenu();
77
+ }
78
+ },
79
+ onClick: () => {
80
+ setIsButtonFocused(false);
81
+ setClickMode(!clickMode);
82
+ setOpen(!clickMode);
83
+ },
84
+ // This should be placed on the trigger in case the element is disabled
85
+ onMouseEnter,
86
+ onMouseLeave,
87
+ onMouseDown,
88
+ onMouseMove: onMouseMove,
89
+ onTouchStart: onDragStart
90
+ };
91
+ function getChildEventHandlers(childProps) {
92
+ const eventHandlerFunctions = Object.keys(triggerProps).filter(prop => prop.startsWith('on'));
93
+ const eventHandlers = {};
94
+ eventHandlerFunctions.forEach(functionName => {
95
+ eventHandlers[functionName] = evt => {
96
+ triggerProps[functionName](evt);
97
+ if (childProps?.[functionName]) {
98
+ childProps?.[functionName](evt);
99
+ }
100
+ };
101
+ });
102
+ return eventHandlers;
103
+ }
104
+ if (label) {
105
+ triggerProps['aria-labelledby'] = id;
106
+ } else {
107
+ triggerProps['aria-describedby'] = id;
108
+ }
109
+ function onKeyDown(event) {
110
+ if (open && match.match(event, keys.Escape)) {
111
+ event.stopPropagation();
112
+ if (!isButtonFocused) {
113
+ closeMenu({
114
+ revertFocus: true
115
+ });
116
+ }
117
+ setOpen(!isButtonFocused);
118
+ setIsButtonFocused(!isButtonFocused);
119
+ }
120
+ if (match.match(event, keys.Enter) || match.match(event, keys.Space)) {
121
+ setIsButtonFocused(false);
122
+ setOpen(true);
123
+ setFocusByMouse(false);
124
+
125
+ // focus on the first link
126
+ if (popoverMenuLinks?.current && !isFocusedInsideRef.current) {
127
+ const firstLink = popoverMenuLinks?.current?.[0];
128
+ setContentTabIndex('0');
129
+ if (firstLink) {
130
+ isFocusedInsideRef.current = true;
131
+ // avoid race condition
132
+ setTimeout(() => firstLink.focus(), 0);
133
+ }
134
+ }
135
+ }
136
+ }
137
+ function onMouseEnter() {
138
+ if (!clickMode) {
139
+ if (!rest?.onMouseEnter) {
140
+ setIsPointerIntersecting(true);
141
+ setOpen(true, enterDelayMs);
142
+ }
143
+ }
144
+ }
145
+ function onMouseDown() {
146
+ setFocusByMouse(true);
147
+ onDragStart();
148
+ }
149
+ function onMouseLeave() {
150
+ if (!clickMode && !isFocusedInsideRef.current) {
151
+ setIsPointerIntersecting(false);
152
+ if (isDragging) {
153
+ return;
154
+ }
155
+ setIsButtonFocused(false);
156
+ setOpen(false, leaveDelayMs);
157
+ }
158
+ }
159
+ function onMouseMove(evt) {
160
+ if (evt.buttons === 1) {
161
+ setIsDragging(true);
162
+ } else {
163
+ setIsDragging(false);
164
+ }
165
+ }
166
+ function onDragStart() {
167
+ setIsDragging(true);
168
+ }
169
+ const onDragStop = React.useCallback(() => {
170
+ setIsDragging(false);
171
+ // Close the tooltip, unless the mouse pointer is within the bounds of the
172
+ // trigger.
173
+ if (!isPointerIntersecting) {
174
+ setIsButtonFocused(false);
175
+ setOpen(false, leaveDelayMs);
176
+ }
177
+ }, [isPointerIntersecting, leaveDelayMs, setOpen]);
178
+ React.useEffect(() => {
179
+ if (isDragging) {
180
+ // Register drag stop handlers.
181
+ DRAG_STOP_EVENT_TYPES.forEach(eventType => {
182
+ document.addEventListener(eventType, onDragStop);
183
+ });
184
+ }
185
+ return () => {
186
+ DRAG_STOP_EVENT_TYPES.forEach(eventType => {
187
+ document.removeEventListener(eventType, onDragStop);
188
+ });
189
+ };
190
+ }, [isDragging, onDragStop]);
191
+ const setContentTabIndex = value => {
192
+ if (popoverMenuLinks.current) {
193
+ popoverMenuLinks.current.forEach(e => e.setAttribute('tabindex', value));
194
+ }
195
+ };
196
+ function handleBlur(event) {
197
+ const isFocusOutside = !popoverRef.current?.contains(event.relatedTarget);
198
+
199
+ // tab outside the menu
200
+ if (open && isFocusedInsideRef.current && !focusByMouse && isFocusOutside) {
201
+ closeMenu();
202
+ }
203
+
204
+ // if button has been clicked, and new click is outside the menu
205
+ if (clickMode && isFocusOutside) {
206
+ closeMenu();
207
+ }
208
+ }
209
+ function closeMenu() {
210
+ let {
211
+ revertFocus = false
212
+ } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
213
+ if (revertFocus) {
214
+ menuButton.current?.focus();
215
+ setOpen(true);
216
+ setIsButtonFocused(true);
217
+ } else {
218
+ setOpen(false);
219
+ setIsButtonFocused(false);
220
+ }
221
+ isFocusedInsideRef.current = false;
222
+ setFocusByMouse(false);
223
+ setIsPointerIntersecting(!clickMode);
224
+ setClickMode(false);
225
+ setContentTabIndex('-1');
226
+ }
227
+
228
+ // initiate menu content to be untabbable
229
+ React.useEffect(() => {
230
+ if (popoverRef.current) {
231
+ popoverMenuLinks.current = popoverRef.current.querySelectorAll(`.${prefix}--side-nav-menu--popover-content a`);
232
+ setContentTabIndex('-1');
233
+ menuButton.current = popoverRef.current.querySelector(`.${prefix}--side-nav__submenu`);
234
+ }
235
+ }, []);
236
+
237
+ // set menu content to be untabbable
238
+ React.useEffect(() => {
239
+ if (popoverRef.current && !popoverMenuLinks.current) {
240
+ popoverMenuLinks.current = popoverRef.current.querySelectorAll(`.${prefix}--side-nav-menu--popover-content a`);
241
+ setContentTabIndex('-1');
242
+ }
243
+ }, [open]);
244
+ return (
245
+ /*#__PURE__*/
246
+ // @ts-ignore-error Popover throws a TS error everytime is imported
247
+ React.createElement(react.Popover, _rollupPluginBabelHelpers.extends({
248
+ ref: popoverRef
249
+ }, rest, {
250
+ align: isButtonFocused ? 'right' : align,
251
+ className: index.default(customClassName, {
252
+ [`${prefix}--flyout-menu`]: true,
253
+ [`${prefix}--flyout-menu-clicked`]: clickMode,
254
+ [`${prefix}--flyout-menu-selected`]: selected,
255
+ [`${prefix}--flyout-menu-focused`]: isButtonFocused || open
256
+ }),
257
+ dropShadow: dropShadow,
258
+ highContrast: false,
259
+ onBlur: handleBlur,
260
+ onKeyDown: onKeyDown,
261
+ onMouseLeave: onMouseLeave,
262
+ open: open
263
+ }), child !== undefined ? /*#__PURE__*/React.cloneElement(child, {
264
+ ...triggerProps,
265
+ ...getChildEventHandlers(child.props)
266
+ }) : null, /*#__PURE__*/React.createElement(react.PopoverContent, {
267
+ "aria-hidden": open ? 'false' : 'true',
268
+ className: index.default({
269
+ [`${prefix}--side-nav-menu--popover-content`]: !isButtonFocused,
270
+ [`${prefix}--flyout-tooltip-content ${prefix}--tooltip-content`]: isButtonFocused
271
+ }),
272
+ id: id,
273
+ onMouseEnter: onMouseEnter,
274
+ role: "tooltip"
275
+ }, !isButtonFocused ? /*#__PURE__*/React.createElement(react.FormLabel, null, title) : title, /*#__PURE__*/React.createElement("div", {
276
+ style: {
277
+ display: isButtonFocused ? 'none' : 'block'
278
+ }
279
+ }, flyoutMenuItems)))
280
+ );
281
+ }
282
+ SideNavFlyoutMenu.propTypes = {
283
+ /**
284
+ * Specify how the trigger should align with the tooltip
285
+ */
286
+ align: PropTypes.oneOf(['top', 'top-left',
287
+ // deprecated use top-start instead
288
+ 'top-right',
289
+ // deprecated use top-end instead
290
+
291
+ 'bottom', 'bottom-left',
292
+ // deprecated use bottom-start instead
293
+ 'bottom-right',
294
+ // deprecated use bottom-end instead
295
+
296
+ 'left', 'left-bottom',
297
+ // deprecated use left-end instead
298
+ 'left-top',
299
+ // deprecated use left-start instead
300
+
301
+ 'right', 'right-bottom',
302
+ // deprecated use right-end instead
303
+ 'right-top',
304
+ // deprecated use right-start instead
305
+
306
+ // new values to match floating-ui
307
+ 'top-start', 'top-end', 'bottom-start', 'bottom-end', 'left-end', 'left-start', 'right-end', 'right-start']),
308
+ /**
309
+ * Pass in the child to which the tooltip will be applied
310
+ */
311
+ children: PropTypes.node,
312
+ /**
313
+ * Specify an optional className to be applied to the container node
314
+ */
315
+ className: PropTypes.string,
316
+ /**
317
+ * Specify whether the tooltip should be open when it first renders
318
+ */
319
+ defaultOpen: PropTypes.bool,
320
+ /**
321
+ * Provide the description to be rendered inside of the Tooltip. The
322
+ * description will use `aria-describedby` and will describe the child node
323
+ * in addition to the text rendered inside of the child. This means that if you
324
+ * have text in the child node, that it will be announced alongside the
325
+ * description to the screen reader.
326
+ *
327
+ * Note: if label and description are both provided, label will be used and
328
+ * description will not be used
329
+ */
330
+ description: PropTypes.node,
331
+ /**
332
+ * Specify whether a drop shadow should be rendered
333
+ */
334
+ dropShadow: PropTypes.bool,
335
+ /**
336
+ * Specify the duration in milliseconds to delay before displaying the tooltip
337
+ */
338
+ enterDelayMs: PropTypes.number,
339
+ /**
340
+ * Provide the label to be rendered inside of the Tooltip. The label will use
341
+ * `aria-labelledby` and will fully describe the child node that is provided.
342
+ * This means that if you have text in the child node, that it will not be
343
+ * announced to the screen reader.
344
+ *
345
+ * Note: if label and description are both provided, description will not be
346
+ * used
347
+ */
348
+ label: PropTypes.node,
349
+ /**
350
+ * Specify the duration in milliseconds to delay before hiding the tooltip
351
+ */
352
+ leaveDelayMs: PropTypes.number,
353
+ /**
354
+ * The content to be rendered inside the popover menu.
355
+ */
356
+ menuContent: PropTypes.node,
357
+ /**
358
+ * The boolean to show the flyout menu has been selected.
359
+ */
360
+ selected: PropTypes.bool,
361
+ /**
362
+ * The title for the overall menu name.
363
+ */
364
+ title: PropTypes.string
365
+ };
366
+
367
+ exports.SideNavFlyoutMenu = SideNavFlyoutMenu;
@@ -39,6 +39,10 @@ export interface SideNavMenuProps {
39
39
  * Indicates if the side navigation container is expanded or collapsed.
40
40
  */
41
41
  isSideNavExpanded?: boolean;
42
+ /**
43
+ * The boolean to show the flyout menu has been selected.
44
+ */
45
+ selected?: boolean;
42
46
  /**
43
47
  * The tabIndex for the button element.
44
48
  * If not specified, the default validation will be applied.