@itwin/itwinui-react 3.9.1 → 3.10.1

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 (65) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/cjs/core/Breadcrumbs/Breadcrumbs.js +2 -3
  3. package/cjs/core/Buttons/Button.js +1 -1
  4. package/cjs/core/Buttons/IconButton.js +1 -1
  5. package/cjs/core/Buttons/IdeasButton.js +6 -2
  6. package/cjs/core/ComboBox/ComboBox.js +1 -1
  7. package/cjs/core/DropdownMenu/DropdownMenu.js +36 -13
  8. package/cjs/core/Input/Input.js +1 -1
  9. package/cjs/core/LabeledSelect/LabeledSelect.d.ts +26 -4
  10. package/cjs/core/Menu/Menu.js +9 -0
  11. package/cjs/core/Menu/MenuItem.d.ts +12 -0
  12. package/cjs/core/Menu/MenuItem.js +114 -66
  13. package/cjs/core/NotificationMarker/NotificationMarker.d.ts +7 -6
  14. package/cjs/core/Popover/Popover.d.ts +32 -9
  15. package/cjs/core/Popover/Popover.js +68 -17
  16. package/cjs/core/Select/Select.js +2 -3
  17. package/cjs/core/SideNavigation/SideNavigation.d.ts +1 -0
  18. package/cjs/core/SideNavigation/SideNavigation.js +20 -21
  19. package/cjs/core/SideNavigation/SidenavButton.js +5 -1
  20. package/cjs/core/Table/TablePaginator.js +1 -3
  21. package/cjs/core/Table/columns/selectionColumn.js +10 -1
  22. package/cjs/core/Table/hooks/useSubRowSelection.js +1 -1
  23. package/cjs/core/ThemeProvider/ThemeProvider.js +53 -17
  24. package/cjs/core/TimePicker/TimePicker.js +12 -12
  25. package/cjs/core/ToggleSwitch/ToggleSwitch.d.ts +4 -0
  26. package/cjs/core/ToggleSwitch/ToggleSwitch.js +2 -2
  27. package/cjs/utils/components/Portal.d.ts +6 -2
  28. package/cjs/utils/components/Portal.js +11 -14
  29. package/cjs/utils/providers/ScopeProvider.d.ts +26 -0
  30. package/cjs/utils/providers/ScopeProvider.js +77 -0
  31. package/cjs/utils/providers/index.d.ts +1 -0
  32. package/cjs/utils/providers/index.js +1 -0
  33. package/esm/core/Breadcrumbs/Breadcrumbs.js +2 -3
  34. package/esm/core/Buttons/Button.js +1 -1
  35. package/esm/core/Buttons/IconButton.js +1 -1
  36. package/esm/core/Buttons/IdeasButton.js +3 -2
  37. package/esm/core/ComboBox/ComboBox.js +1 -1
  38. package/esm/core/DropdownMenu/DropdownMenu.js +36 -13
  39. package/esm/core/Input/Input.js +1 -1
  40. package/esm/core/LabeledSelect/LabeledSelect.d.ts +26 -4
  41. package/esm/core/Menu/Menu.js +9 -0
  42. package/esm/core/Menu/MenuItem.d.ts +12 -0
  43. package/esm/core/Menu/MenuItem.js +114 -66
  44. package/esm/core/NotificationMarker/NotificationMarker.d.ts +7 -6
  45. package/esm/core/Popover/Popover.d.ts +32 -9
  46. package/esm/core/Popover/Popover.js +71 -20
  47. package/esm/core/Select/Select.js +2 -3
  48. package/esm/core/SideNavigation/SideNavigation.d.ts +1 -0
  49. package/esm/core/SideNavigation/SideNavigation.js +20 -21
  50. package/esm/core/SideNavigation/SidenavButton.js +5 -1
  51. package/esm/core/Table/TablePaginator.js +2 -4
  52. package/esm/core/Table/columns/selectionColumn.js +10 -1
  53. package/esm/core/Table/hooks/useSubRowSelection.js +1 -1
  54. package/esm/core/ThemeProvider/ThemeProvider.js +54 -18
  55. package/esm/core/TimePicker/TimePicker.js +12 -12
  56. package/esm/core/ToggleSwitch/ToggleSwitch.d.ts +4 -0
  57. package/esm/core/ToggleSwitch/ToggleSwitch.js +2 -2
  58. package/esm/utils/components/Portal.d.ts +6 -2
  59. package/esm/utils/components/Portal.js +9 -8
  60. package/esm/utils/providers/ScopeProvider.d.ts +26 -0
  61. package/esm/utils/providers/ScopeProvider.js +48 -0
  62. package/esm/utils/providers/index.d.ts +1 -0
  63. package/esm/utils/providers/index.js +1 -0
  64. package/package.json +2 -1
  65. package/styles.css +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.10.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#2031](https://github.com/iTwin/iTwinUI/pull/2031): Fixed an issue where popovers and dropdown menus used with `SidenavButton` were showing up inside a tooltip.
8
+ - [#2030](https://github.com/iTwin/iTwinUI/pull/2030): Fixed a visual bug where items in a `ButtonGroup` were displaying a double border when an associated `Popover` was opened.
9
+ - [#2026](https://github.com/iTwin/iTwinUI/pull/2026): Submenus within a `DropdownMenu` will now consistently require less precision when moving the mouse between the parent item and the submenu.
10
+
11
+ ## 3.10.0
12
+
13
+ ### Minor Changes
14
+
15
+ - [#1942](https://github.com/iTwin/iTwinUI/pull/1942): Clicking a `MenuItem` with `submenuItems` now toggles the submenu visibility.
16
+ - If both `submenuItems` and `onClick` props are passed, then clicking the `MenuItem` will toggle the submenu visibility but also _still_ call the `onClick`. However, this behavior can lead to a confusing UX and is not recommended, so a warning will be shown.
17
+ - [#1919](https://github.com/iTwin/iTwinUI/pull/1919): Aggregated a subset of CSS styles across some field components: `Button`, `Input`, `Textarea`, and `Select`, in order to reduce the CSS size and improve visual consistency. Some resulting changes:
18
+ - `Input`, `Textarea`, and `Select` have a similar hover state as `Button`.
19
+ - `Input`, `Textarea`, and `Select` now show their value as greyed out when disabled.
20
+ - [#1942](https://github.com/iTwin/iTwinUI/pull/1942): `DropdownMenu`'s keyboard navigation, hover triggers, and overall behavior has been improved.
21
+ - [#2010](https://github.com/iTwin/iTwinUI/pull/2010): Added new `labelProps` to `ToggleSwitch` to allow for customization of the label element.
22
+ - [#2011](https://github.com/iTwin/iTwinUI/pull/2011): Added dependency on `jotai`.
23
+
24
+ ### Patch Changes
25
+
26
+ - [#1942](https://github.com/iTwin/iTwinUI/pull/1942): Disabled `MenuItem`s no longer show their submenu.
27
+ - [#1942](https://github.com/iTwin/iTwinUI/pull/1942): Fixed an issue in `DropdownMenu` where the submenus would not close in some circumstances, despite calling `close()` in `onClick`.
28
+ - [#2013](https://github.com/iTwin/iTwinUI/pull/2013): Fixed an issue with `Table` row selection not correctly deselecting all sub rows when the row has disabled sub rows or when some rows are filtered out.
29
+ - [#2009](https://github.com/iTwin/iTwinUI/pull/2009): Fixed an issue where `Popover` wasn't respecting the `ThemeProvider`'s `portalContainer`.
30
+ - [#2011](https://github.com/iTwin/iTwinUI/pull/2011): When `ThemeProvider` is portaled into popup windows, it will now automatically create a portal container in the correct document, avoiding the need to manually specify `portalContainer`.
31
+ - [#1919](https://github.com/iTwin/iTwinUI/pull/1919): The small sized `TablePaginator`'s buttons are now squares instead of rectangles. This makes it consistent with the shape of the regular sized `TablePaginator` buttons.
32
+ - [#1919](https://github.com/iTwin/iTwinUI/pull/1919): The hover styling of `Breadcrumbs.Item` has been made more consistent across buttons and anchors.
33
+
3
34
  ## 3.9.1
4
35
 
5
36
  ### Patch Changes
@@ -102,10 +102,9 @@ const ListItem = ({ item, isActive, }) => {
102
102
  const Separator = ({ separator }) => (React.createElement(index_js_1.Box, { as: 'li', className: 'iui-breadcrumbs-separator', "aria-hidden": true }, separator ?? React.createElement(index_js_1.SvgChevronRight, null)));
103
103
  // ----------------------------------------------------------------------------
104
104
  const BreadcrumbsItem = React.forwardRef((props, forwardedRef) => {
105
- const { children: childrenProp, className, ...rest } = props;
105
+ const { children, className, ...rest } = props;
106
106
  const defaultAs = !!props.href ? Anchor_js_1.Anchor : !!props.onClick ? 'button' : 'span';
107
- const children = defaultAs === 'button' ? React.createElement("span", null, childrenProp) : childrenProp;
108
- return (React.createElement(index_js_1.Box, { as: defaultAs, className: (0, classnames_1.default)('iui-breadcrumbs-content', className), ref: forwardedRef, ...rest }, children));
107
+ return (React.createElement(Button_js_1.Button, { as: defaultAs, className: (0, classnames_1.default)('iui-breadcrumbs-content', className), styleType: 'borderless', ref: forwardedRef, ...rest }, children));
109
108
  });
110
109
  BreadcrumbsItem.displayName = 'Breadcrumbs.Item';
111
110
  // ----------------------------------------------------------------------------
@@ -45,7 +45,7 @@ const index_js_1 = require("../../utils/index.js");
45
45
  */
46
46
  exports.Button = React.forwardRef((props, ref) => {
47
47
  const { children, className, size, styleType = 'default', startIcon, endIcon, labelProps, startIconProps, endIconProps, stretched, ...rest } = props;
48
- return (React.createElement(index_js_1.ButtonBase, { ref: ref, className: (0, classnames_1.default)('iui-button', className), "data-iui-variant": styleType !== 'default' ? styleType : undefined, "data-iui-size": size, ...rest, style: {
48
+ return (React.createElement(index_js_1.ButtonBase, { ref: ref, className: (0, classnames_1.default)('iui-button', 'iui-field', className), "data-iui-variant": styleType !== 'default' ? styleType : undefined, "data-iui-size": size, ...rest, style: {
49
49
  '--_iui-width': stretched ? '100%' : undefined,
50
50
  ...props.style,
51
51
  } },
@@ -47,7 +47,7 @@ const ButtonGroup_js_1 = require("../ButtonGroup/ButtonGroup.js");
47
47
  exports.IconButton = React.forwardRef((props, ref) => {
48
48
  const { isActive, children, styleType = 'default', size, className, label, iconProps, labelProps, ...rest } = props;
49
49
  const buttonGroupOrientation = React.useContext(ButtonGroup_js_1.ButtonGroupContext);
50
- const button = (React.createElement(index_js_1.ButtonBase, { ref: ref, className: (0, classnames_1.default)('iui-button', className), "data-iui-variant": styleType !== 'default' ? styleType : undefined, "data-iui-size": size, "data-iui-active": isActive, "aria-pressed": isActive, ...rest },
50
+ const button = (React.createElement(index_js_1.ButtonBase, { ref: ref, className: (0, classnames_1.default)('iui-button', 'iui-field', className), "data-iui-variant": styleType !== 'default' ? styleType : undefined, "data-iui-size": size, "data-iui-active": isActive, "aria-pressed": isActive, ...rest },
51
51
  React.createElement(index_js_1.Box, { as: 'span', "aria-hidden": true, ...iconProps, className: (0, classnames_1.default)('iui-button-icon', iconProps?.className) }, children),
52
52
  label ? React.createElement(VisuallyHidden_js_1.VisuallyHidden, null, label) : null));
53
53
  return label ? (React.createElement(Tooltip_js_1.Tooltip, { placement: buttonGroupOrientation === 'vertical' ? 'right' : 'top', ...labelProps, content: label, ariaStrategy: 'none' }, button)) : (button);
@@ -22,12 +22,16 @@ var __importStar = (this && this.__importStar) || function (mod) {
22
22
  __setModuleDefault(result, mod);
23
23
  return result;
24
24
  };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
25
28
  Object.defineProperty(exports, "__esModule", { value: true });
26
29
  exports.IdeasButton = void 0;
27
30
  /*---------------------------------------------------------------------------------------------
28
31
  * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
29
32
  * See LICENSE.md in the project root for license terms and full copyright notice.
30
33
  *--------------------------------------------------------------------------------------------*/
34
+ const classnames_1 = __importDefault(require("classnames"));
31
35
  const React = __importStar(require("react"));
32
36
  const index_js_1 = require("../../utils/index.js");
33
37
  const Button_js_1 = require("./Button.js");
@@ -37,6 +41,6 @@ const Button_js_1 = require("./Button.js");
37
41
  * <IdeasButton />
38
42
  */
39
43
  exports.IdeasButton = React.forwardRef((props, ref) => {
40
- const { feedbackLabel = 'Feedback', onClick, ...rest } = props;
41
- return (React.createElement(Button_js_1.Button, { ref: ref, "data-iui-variant": 'idea', onClick: onClick, startIcon: React.createElement(index_js_1.SvgSmileyHappy, { "aria-hidden": true }), ...rest }, feedbackLabel));
44
+ const { feedbackLabel = 'Feedback', className, onClick, ...rest } = props;
45
+ return (React.createElement(Button_js_1.Button, { ref: ref, className: (0, classnames_1.default)('iui-button-idea', className), "data-iui-variant": 'high-visibility', onClick: onClick, startIcon: React.createElement(index_js_1.SvgSmileyHappy, { "aria-hidden": true }), ...rest }, feedbackLabel));
42
46
  });
@@ -308,7 +308,7 @@ exports.ComboBox = React.forwardRef((props, forwardedRef) => {
308
308
  onVisibleChange: (open) => (open ? show() : hide()),
309
309
  matchWidth: true,
310
310
  closeOnOutsideClick: true,
311
- trigger: { focus: true },
311
+ interactions: { click: false, focus: true },
312
312
  });
313
313
  return (React.createElement(helpers_js_1.ComboBoxRefsContext.Provider, { value: { inputRef, menuRef, optionsExtraInfoRef } },
314
314
  React.createElement(helpers_js_1.ComboBoxActionContext.Provider, { value: dispatch },
@@ -32,6 +32,8 @@ const React = __importStar(require("react"));
32
32
  const index_js_1 = require("../../utils/index.js");
33
33
  const Menu_js_1 = require("../Menu/Menu.js");
34
34
  const Popover_js_1 = require("../Popover/Popover.js");
35
+ const react_1 = require("@floating-ui/react");
36
+ const MenuItem_js_1 = require("../Menu/MenuItem.js");
35
37
  /**
36
38
  * Dropdown menu component.
37
39
  * Built on top of the {@link Popover} component.
@@ -52,6 +54,11 @@ const Popover_js_1 = require("../Popover/Popover.js");
52
54
  * </DropdownMenu>
53
55
  */
54
56
  exports.DropdownMenu = React.forwardRef((props, forwardedRef) => {
57
+ return (React.createElement(react_1.FloatingTree, null,
58
+ React.createElement(DropdownMenuContent, { ref: forwardedRef, ...props })));
59
+ });
60
+ // ----------------------------------------------------------------------------
61
+ const DropdownMenuContent = React.forwardRef((props, forwardedRef) => {
55
62
  const { menuItems, children, role = 'menu', visible: visibleProp, placement = 'bottom-start', matchWidth = false, onVisibleChange, portal = true, ...rest } = props;
56
63
  const [visible, setVisible] = (0, index_js_1.useControlledState)(false, visibleProp, onVisibleChange);
57
64
  const triggerRef = React.useRef(null);
@@ -65,11 +72,23 @@ exports.DropdownMenu = React.forwardRef((props, forwardedRef) => {
65
72
  }
66
73
  return menuItems;
67
74
  }, [menuItems, close]);
75
+ const [currentFocusedNodeIndex, setCurrentFocusedNodeIndex] = React.useState(null);
76
+ const focusableNodes = React.useRef([]);
77
+ const nodeId = (0, react_1.useFloatingNodeId)();
68
78
  const popover = (0, Popover_js_1.usePopover)({
79
+ nodeId,
69
80
  visible,
70
81
  onVisibleChange: (open) => (open ? setVisible(true) : close()),
71
82
  placement,
72
83
  matchWidth,
84
+ interactions: {
85
+ listNavigation: {
86
+ activeIndex: currentFocusedNodeIndex,
87
+ onNavigate: setCurrentFocusedNodeIndex,
88
+ listRef: focusableNodes,
89
+ focusItemOnOpen: true,
90
+ },
91
+ },
73
92
  });
74
93
  const popoverRef = (0, index_js_1.useMergedRefs)(forwardedRef, popover.refs.setFloating);
75
94
  return (React.createElement(React.Fragment, null,
@@ -78,17 +97,21 @@ exports.DropdownMenu = React.forwardRef((props, forwardedRef) => {
78
97
  'aria-expanded': popover.open,
79
98
  ref: (0, index_js_1.mergeRefs)(triggerRef, popover.refs.setReference),
80
99
  })),
81
- popover.open && (React.createElement(index_js_1.Portal, { portal: portal },
82
- React.createElement(Menu_js_1.Menu, { ...popover.getFloatingProps({
83
- role,
84
- ...rest,
85
- onKeyDown: (0, index_js_1.mergeEventHandlers)(props.onKeyDown, (e) => {
86
- if (e.defaultPrevented) {
87
- return;
88
- }
89
- if (e.key === 'Tab') {
90
- close();
91
- }
92
- }),
93
- }), ref: popoverRef }, menuContent)))));
100
+ React.createElement(react_1.FloatingNode, { id: nodeId }, popover.open && (React.createElement(index_js_1.Portal, { portal: portal },
101
+ React.createElement(MenuItem_js_1.MenuItemContext.Provider, { value: {
102
+ setCurrentFocusedNodeIndex,
103
+ focusableNodes,
104
+ } },
105
+ React.createElement(Menu_js_1.Menu, { setFocus: false, ...popover.getFloatingProps({
106
+ role,
107
+ ...rest,
108
+ onKeyDown: (0, index_js_1.mergeEventHandlers)(props.onKeyDown, (e) => {
109
+ if (e.defaultPrevented) {
110
+ return;
111
+ }
112
+ if (e.key === 'Tab') {
113
+ close();
114
+ }
115
+ }),
116
+ }), ref: popoverRef }, menuContent)))))));
94
117
  });
@@ -44,5 +44,5 @@ exports.Input = React.forwardRef((props, ref) => {
44
44
  const { size, htmlSize, status, className, ...rest } = props;
45
45
  const inputRef = React.useRef(null);
46
46
  const refs = (0, index_js_1.useMergedRefs)(inputRef, ref);
47
- return (React.createElement(index_js_1.Box, { as: 'input', className: (0, classnames_1.default)('iui-input', className), "data-iui-size": size, "data-iui-status": status, size: htmlSize, ref: refs, ...rest }));
47
+ return (React.createElement(index_js_1.Box, { as: 'input', className: (0, classnames_1.default)('iui-input', 'iui-field', className), "data-iui-size": size, "data-iui-status": status, size: htmlSize, ref: refs, ...rest }));
48
48
  });
@@ -298,6 +298,11 @@ export declare const LabeledSelect: <T>(props: ({
298
298
  menuStyle?: React.CSSProperties | undefined;
299
299
  popoverProps?: Pick<{
300
300
  placement?: import("@floating-ui/utils").Placement | undefined;
301
+ /**
302
+ * @deprecated Pass a `<StatusMessage startIcon={svgIcon} />` to the `message` prop instead.
303
+ *
304
+ * Custom svg icon. Will override status icon if specified.
305
+ */
301
306
  visible?: boolean | undefined;
302
307
  onVisibleChange?: ((visible: boolean) => void) | undefined;
303
308
  closeOnOutsideClick?: boolean | undefined;
@@ -317,10 +322,16 @@ export declare const LabeledSelect: <T>(props: ({
317
322
  animationFrame?: boolean | undefined;
318
323
  layoutShift?: boolean | undefined;
319
324
  } | undefined;
320
- trigger?: Partial<Record<"click" | "hover" | "focus", boolean>> | undefined;
325
+ interactions?: {
326
+ click?: boolean | import("@floating-ui/react").UseClickProps | undefined;
327
+ dismiss?: boolean | import("@floating-ui/react").UseDismissProps | undefined;
328
+ hover?: boolean | import("@floating-ui/react").UseHoverProps<import("@floating-ui/react").ReferenceType> | undefined;
329
+ focus?: boolean | import("@floating-ui/react").UseFocusProps | undefined;
330
+ listNavigation?: import("@floating-ui/react").UseListNavigationProps | undefined;
331
+ } | undefined;
321
332
  role?: "dialog" | "menu" | "listbox" | undefined;
322
333
  matchWidth?: boolean | undefined;
323
- }, "placement" | "visible" | "onVisibleChange" | "closeOnOutsideClick" | "matchWidth"> | undefined;
334
+ } & Omit<import("@floating-ui/react").UseFloatingOptions<import("@floating-ui/react").ReferenceType>, "placement" | "middleware">, "placement" | "visible" | "onVisibleChange" | "closeOnOutsideClick" | "matchWidth"> | undefined;
324
335
  triggerProps?: (Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & {
325
336
  ref?: ((instance: HTMLDivElement | null) => void) | React.RefObject<HTMLDivElement> | null | undefined;
326
337
  }) | undefined;
@@ -428,6 +439,11 @@ export declare const LabeledSelect: <T>(props: ({
428
439
  menuStyle?: React.CSSProperties | undefined;
429
440
  popoverProps?: Pick<{
430
441
  placement?: import("@floating-ui/utils").Placement | undefined;
442
+ /**
443
+ * @deprecated Pass a `<StatusMessage startIcon={svgIcon} />` to the `message` prop instead.
444
+ *
445
+ * Custom svg icon. Will override status icon if specified.
446
+ */
431
447
  visible?: boolean | undefined;
432
448
  onVisibleChange?: ((visible: boolean) => void) | undefined;
433
449
  closeOnOutsideClick?: boolean | undefined;
@@ -447,10 +463,16 @@ export declare const LabeledSelect: <T>(props: ({
447
463
  animationFrame?: boolean | undefined;
448
464
  layoutShift?: boolean | undefined;
449
465
  } | undefined;
450
- trigger?: Partial<Record<"click" | "hover" | "focus", boolean>> | undefined;
466
+ interactions?: {
467
+ click?: boolean | import("@floating-ui/react").UseClickProps | undefined;
468
+ dismiss?: boolean | import("@floating-ui/react").UseDismissProps | undefined;
469
+ hover?: boolean | import("@floating-ui/react").UseHoverProps<import("@floating-ui/react").ReferenceType> | undefined;
470
+ focus?: boolean | import("@floating-ui/react").UseFocusProps | undefined;
471
+ listNavigation?: import("@floating-ui/react").UseListNavigationProps | undefined;
472
+ } | undefined;
451
473
  role?: "dialog" | "menu" | "listbox" | undefined;
452
474
  matchWidth?: boolean | undefined;
453
- }, "placement" | "visible" | "onVisibleChange" | "closeOnOutsideClick" | "matchWidth"> | undefined;
475
+ } & Omit<import("@floating-ui/react").UseFloatingOptions<import("@floating-ui/react").ReferenceType>, "placement" | "middleware">, "placement" | "visible" | "onVisibleChange" | "closeOnOutsideClick" | "matchWidth"> | undefined;
454
476
  triggerProps?: (Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & {
455
477
  ref?: ((instance: HTMLDivElement | null) => void) | React.RefObject<HTMLDivElement> | null | undefined;
456
478
  }) | undefined;
@@ -34,6 +34,7 @@ exports.Menu = void 0;
34
34
  const React = __importStar(require("react"));
35
35
  const classnames_1 = __importDefault(require("classnames"));
36
36
  const index_js_1 = require("../../utils/index.js");
37
+ const MenuItem_js_1 = require("./MenuItem.js");
37
38
  /**
38
39
  * Basic menu component. Can be used for select or dropdown components.
39
40
  */
@@ -42,11 +43,19 @@ exports.Menu = React.forwardRef((props, ref) => {
42
43
  const [focusedIndex, setFocusedIndex] = React.useState();
43
44
  const menuRef = React.useRef(null);
44
45
  const refs = (0, index_js_1.useMergedRefs)(menuRef, ref);
46
+ const menuItemContext = React.useContext(MenuItem_js_1.MenuItemContext);
45
47
  const getFocusableNodes = React.useCallback(() => {
46
48
  const focusableItems = (0, index_js_1.getFocusableElements)(menuRef.current);
47
49
  // Filter out focusable elements that are inside each menu item, e.g. checkbox, anchor
48
50
  return focusableItems.filter((i) => !focusableItems.some((p) => p.contains(i.parentElement)));
49
51
  }, []);
52
+ React.useEffect(() => {
53
+ const focusableNodes = getFocusableNodes();
54
+ if (menuItemContext != null &&
55
+ menuItemContext.focusableNodes.current !== focusableNodes) {
56
+ menuItemContext.focusableNodes.current = focusableNodes;
57
+ }
58
+ }, [getFocusableNodes, menuItemContext]);
50
59
  React.useEffect(() => {
51
60
  const items = getFocusableNodes();
52
61
  if (focusedIndex != null) {
@@ -1,6 +1,14 @@
1
1
  import * as React from 'react';
2
2
  import type { PolymorphicForwardRefComponent } from '../../utils/index.js';
3
3
  import type { ListItemOwnProps } from '../List/ListItem.js';
4
+ /**
5
+ * Should be wrapped around the `Menu` containing the `MenuItem`s.
6
+ */
7
+ export declare const MenuItemContext: React.Context<{
8
+ setCurrentFocusedNodeIndex: React.Dispatch<React.SetStateAction<number | null>>;
9
+ focusableNodes: React.MutableRefObject<(HTMLElement | null)[]>;
10
+ setHasFocusedNodeInSubmenu?: React.Dispatch<React.SetStateAction<boolean>> | undefined;
11
+ }>;
4
12
  export type MenuItemProps = {
5
13
  /**
6
14
  * Item is selected.
@@ -65,3 +73,7 @@ export type MenuItemProps = {
65
73
  * Basic menu item component. Should be used inside `Menu` component for each item.
66
74
  */
67
75
  export declare const MenuItem: PolymorphicForwardRefComponent<"div", MenuItemProps>;
76
+ export type TreeEvent = {
77
+ nodeId: string;
78
+ parentId: string | null;
79
+ };
@@ -23,7 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
23
23
  return result;
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.MenuItem = void 0;
26
+ exports.MenuItem = exports.MenuItemContext = void 0;
27
27
  /*---------------------------------------------------------------------------------------------
28
28
  * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
29
29
  * See LICENSE.md in the project root for license terms and full copyright notice.
@@ -32,34 +32,78 @@ const React = __importStar(require("react"));
32
32
  const index_js_1 = require("../../utils/index.js");
33
33
  const Menu_js_1 = require("./Menu.js");
34
34
  const ListItem_js_1 = require("../List/ListItem.js");
35
- const react_dom_1 = require("react-dom");
36
35
  const Popover_js_1 = require("../Popover/Popover.js");
36
+ const react_1 = require("@floating-ui/react");
37
+ const logWarningInDev = (0, index_js_1.createWarningLogger)();
37
38
  /**
38
- * Context used to provide menu item ref to sub-menu items.
39
+ * Should be wrapped around the `Menu` containing the `MenuItem`s.
39
40
  */
40
- const MenuItemContext = React.createContext({ ref: undefined, setIsNestedSubmenuVisible: () => { } });
41
+ exports.MenuItemContext = React.createContext({
42
+ setCurrentFocusedNodeIndex: () => { },
43
+ focusableNodes: { current: [] },
44
+ setHasFocusedNodeInSubmenu: undefined,
45
+ });
41
46
  /**
42
47
  * Basic menu item component. Should be used inside `Menu` component for each item.
43
48
  */
44
49
  exports.MenuItem = React.forwardRef((props, forwardedRef) => {
45
- const { children, isSelected, disabled, value, onClick, sublabel, size = !!sublabel ? 'large' : 'default', icon, startIcon = icon, badge, endIcon = badge, role = 'menuitem', subMenuItems = [], ...rest } = props;
50
+ const { children, isSelected, disabled, value, onClick: onClickProp, sublabel, size = !!sublabel ? 'large' : 'default', icon, startIcon = icon, badge, endIcon = badge, role = 'menuitem', subMenuItems = [], ...rest } = props;
51
+ if (onClickProp != null && subMenuItems.length > 0) {
52
+ logWarningInDev('Passing a non-empty submenuItems array and onClick to MenuItem at the same time is not supported. This is because when a non empty submenuItems array is passed, clicking the MenuItem toggles the submenu visibility.');
53
+ }
54
+ const parent = React.useContext(exports.MenuItemContext);
46
55
  const menuItemRef = React.useRef(null);
47
- const [focusOnSubmenu, setFocusOnSubmenu] = React.useState(false);
48
56
  const submenuId = (0, index_js_1.useId)();
49
57
  const [isSubmenuVisible, setIsSubmenuVisible] = React.useState(false);
50
- const [isNestedSubmenuVisible, setIsNestedSubmenuVisible] = React.useState(false);
51
- const parent = React.useContext(MenuItemContext);
52
- const onVisibleChange = (open) => {
53
- setIsSubmenuVisible(open);
54
- // we don't want parent to close when mouse goes into a nested submenu,
55
- // so we need to let the parent know whether the submenu is still open.
56
- parent.setIsNestedSubmenuVisible(open);
57
- };
58
+ const [currentFocusedNodeIndex, setCurrentFocusedNodeIndex] = React.useState(null);
59
+ const [hasFocusedNodeInSubmenu, setHasFocusedNodeInSubmenu] = React.useState(false);
60
+ const nodeId = (0, react_1.useFloatingNodeId)();
61
+ const tree = (0, react_1.useFloatingTree)();
62
+ const parentId = (0, react_1.useFloatingParentNodeId)();
63
+ const focusableNodeIndexInParentTree = parent.focusableNodes.current.findIndex((el) => el === menuItemRef.current);
64
+ (0, index_js_1.useSyncExternalStore)(React.useCallback(() => {
65
+ const closeUnrelatedMenus = (event) => {
66
+ if (
67
+ // When a node "X" is focused, close "X"'s siblings' submenus
68
+ // i.e. only one submenu in each menu can be open at a time
69
+ (parentId === event.parentId && nodeId !== event.nodeId) ||
70
+ // Consider a node "X" with its submenu "Y".
71
+ // Focusing "X" should close all submenus of "Y".
72
+ parentId === event.nodeId) {
73
+ setIsSubmenuVisible(false);
74
+ setHasFocusedNodeInSubmenu(false);
75
+ }
76
+ };
77
+ tree?.events.on('onNodeFocused', closeUnrelatedMenus);
78
+ return () => {
79
+ tree?.events.off('onNodeFocused', closeUnrelatedMenus);
80
+ };
81
+ }, [nodeId, parentId, tree?.events]), () => undefined, () => undefined);
82
+ const focusableNodes = React.useRef([]);
58
83
  const popover = (0, Popover_js_1.usePopover)({
59
- visible: isSubmenuVisible || isNestedSubmenuVisible,
60
- onVisibleChange,
84
+ nodeId,
85
+ visible: isSubmenuVisible,
86
+ onVisibleChange: React.useCallback((visible) => {
87
+ if (!visible) {
88
+ setHasFocusedNodeInSubmenu(false);
89
+ }
90
+ setIsSubmenuVisible(visible);
91
+ }, []),
61
92
  placement: 'right-start',
62
- trigger: { hover: true, focus: true },
93
+ interactions: !disabled
94
+ ? {
95
+ click: false,
96
+ hover: {
97
+ enabled: !hasFocusedNodeInSubmenu, // If focus is still inside submenu, don't close the submenu upon hovering out.
98
+ },
99
+ listNavigation: {
100
+ listRef: focusableNodes,
101
+ activeIndex: currentFocusedNodeIndex,
102
+ nested: subMenuItems.length > 0,
103
+ onNavigate: setCurrentFocusedNodeIndex,
104
+ },
105
+ }
106
+ : {},
63
107
  });
64
108
  const onKeyDown = (event) => {
65
109
  if (event.altKey) {
@@ -69,61 +113,65 @@ exports.MenuItem = React.forwardRef((props, forwardedRef) => {
69
113
  case 'Enter':
70
114
  case ' ':
71
115
  case 'Spacebar': {
72
- !disabled && onClick?.(value);
73
- event.preventDefault();
74
- break;
75
- }
76
- case 'ArrowRight': {
77
- if (subMenuItems.length > 0) {
78
- setIsSubmenuVisible(true);
79
- // flush and reset state so we are ready to focus again next time
80
- (0, react_dom_1.flushSync)(() => setFocusOnSubmenu(true));
81
- setFocusOnSubmenu(false);
82
- event.preventDefault();
83
- event.stopPropagation();
84
- }
85
- break;
86
- }
87
- case 'ArrowLeft': {
88
- if (parent.ref) {
89
- parent.ref.current?.focus();
90
- parent.setIsNestedSubmenuVisible(false);
91
- }
92
- event.stopPropagation();
116
+ onClick();
93
117
  event.preventDefault();
94
118
  break;
95
119
  }
96
- case 'Escape': {
97
- // focus might get lost if submenu closes so move it back to parent
98
- parent.ref?.current?.focus();
99
- break;
100
- }
101
120
  default:
102
121
  break;
103
122
  }
104
123
  };
105
- const handlers = {
106
- onClick: () => !disabled && onClick?.(value),
107
- onKeyDown,
124
+ const onMouseEnter = (e) => {
125
+ // Focus the item when hovered.
126
+ if (e.target === e.currentTarget) {
127
+ menuItemRef.current?.focus();
128
+ // Since we manually focus the MenuItem on hover, we need to manually update the active index for
129
+ // Floating UI's keyboard navigation to work correctly.
130
+ if (parent != null && focusableNodeIndexInParentTree != null) {
131
+ parent.setCurrentFocusedNodeIndex(focusableNodeIndexInParentTree);
132
+ }
133
+ }
134
+ };
135
+ const onFocus = () => {
136
+ // Set hasFocusedNodeInSubmenu in a microtask to ensure the submenu stays open reliably.
137
+ // E.g. Even when hovering into it rapidly.
138
+ queueMicrotask(() => {
139
+ parent.setHasFocusedNodeInSubmenu?.(true);
140
+ });
141
+ tree?.events.emit('onNodeFocused', {
142
+ nodeId,
143
+ parentId,
144
+ });
108
145
  };
109
- return (React.createElement(ListItem_js_1.ListItem, { as: 'div', actionable: true, size: size, active: isSelected, disabled: disabled, ref: (0, index_js_1.useMergedRefs)(menuItemRef, forwardedRef, subMenuItems.length > 0 ? popover.refs.setReference : null), role: role, tabIndex: disabled || role === 'presentation' ? undefined : -1, "aria-selected": isSelected, "aria-haspopup": subMenuItems.length > 0 ? 'true' : undefined, "aria-controls": subMenuItems.length > 0 ? submenuId : undefined, "aria-expanded": subMenuItems.length > 0 ? popover.open : undefined, "aria-disabled": disabled, ...(subMenuItems.length === 0
110
- ? { ...handlers, ...rest }
111
- : popover.getReferenceProps({ ...handlers, ...rest })) },
112
- startIcon && (React.createElement(ListItem_js_1.ListItem.Icon, { as: 'span', "aria-hidden": true }, startIcon)),
113
- React.createElement(ListItem_js_1.ListItem.Content, null,
114
- React.createElement("div", null, children),
115
- sublabel && React.createElement(ListItem_js_1.ListItem.Description, null, sublabel)),
116
- !endIcon && subMenuItems.length > 0 && (React.createElement(ListItem_js_1.ListItem.Icon, { as: 'span', "aria-hidden": true },
117
- React.createElement(index_js_1.SvgCaretRightSmall, null))),
118
- endIcon && (React.createElement(ListItem_js_1.ListItem.Icon, { as: 'span', "aria-hidden": true }, endIcon)),
119
- subMenuItems.length > 0 && popover.open && (React.createElement(index_js_1.Portal, null,
120
- React.createElement(MenuItemContext.Provider, { value: { ref: menuItemRef, setIsNestedSubmenuVisible } },
121
- React.createElement(Menu_js_1.Menu, { setFocus: focusOnSubmenu, ref: popover.refs.setFloating, ...popover.getFloatingProps({
122
- id: submenuId,
123
- onPointerMove: () => {
124
- // pointer might move into a nested submenu and set isSubmenuVisible to false,
125
- // so we need to flip it back to true when pointer re-enters this submenu.
126
- setIsSubmenuVisible(true);
127
- },
128
- }) }, subMenuItems))))));
146
+ const onClick = () => {
147
+ onClickProp?.(value);
148
+ setIsSubmenuVisible((prev) => !prev);
149
+ };
150
+ const handlers = !disabled
151
+ ? {
152
+ onClick,
153
+ onKeyDown,
154
+ onMouseEnter,
155
+ onFocus,
156
+ }
157
+ : {};
158
+ return (React.createElement(React.Fragment, null,
159
+ React.createElement(ListItem_js_1.ListItem, { as: 'div', actionable: true, size: size, active: isSelected, disabled: disabled, ref: (0, index_js_1.useMergedRefs)(menuItemRef, forwardedRef, subMenuItems.length > 0 ? popover.refs.setReference : null), role: role, tabIndex: disabled || role === 'presentation' ? undefined : -1, "aria-selected": isSelected, "aria-haspopup": subMenuItems.length > 0 ? 'true' : undefined, "aria-controls": subMenuItems.length > 0 ? submenuId : undefined, "aria-expanded": subMenuItems.length > 0 ? popover.open : undefined, "aria-disabled": disabled, ...(subMenuItems.length === 0
160
+ ? { ...handlers, ...rest }
161
+ : popover.getReferenceProps({ ...handlers, ...rest })) },
162
+ startIcon && (React.createElement(ListItem_js_1.ListItem.Icon, { as: 'span', "aria-hidden": true }, startIcon)),
163
+ React.createElement(ListItem_js_1.ListItem.Content, null,
164
+ React.createElement("div", null, children),
165
+ sublabel && React.createElement(ListItem_js_1.ListItem.Description, null, sublabel)),
166
+ !endIcon && subMenuItems.length > 0 && (React.createElement(ListItem_js_1.ListItem.Icon, { as: 'span', "aria-hidden": true },
167
+ React.createElement(index_js_1.SvgCaretRightSmall, null))),
168
+ endIcon && (React.createElement(ListItem_js_1.ListItem.Icon, { as: 'span', "aria-hidden": true }, endIcon))),
169
+ subMenuItems.length > 0 && !disabled && popover.open && (React.createElement(react_1.FloatingNode, { id: nodeId },
170
+ React.createElement(index_js_1.Portal, null,
171
+ React.createElement(exports.MenuItemContext.Provider, { value: {
172
+ setCurrentFocusedNodeIndex,
173
+ focusableNodes,
174
+ setHasFocusedNodeInSubmenu,
175
+ } },
176
+ React.createElement(Menu_js_1.Menu, { setFocus: false, ref: popover.refs.setFloating, ...popover.getFloatingProps({ id: submenuId }) }, subMenuItems)))))));
129
177
  });
@@ -24,14 +24,15 @@ type NotificationMarkerProps = {
24
24
  */
25
25
  pulsing?: boolean;
26
26
  /**
27
- * Set this programmatically to false when you just want to render the passed children without the notification
28
- * @default true
27
+ * Instead of conditionally rendering the `NotificationMarker`, the `enabled` prop can be used.
28
+ *
29
+ * When `enabled` is set to `false`, the DOM element will still be present, but the notification marker will not be displayed visually.
30
+ *
29
31
  * @example
30
- * let [newMessagesCount, ...] = useState(0);
31
- * ...
32
- * <NotificationMarker enabled={newMessagesCount > 0}>
33
- * <SvgNotification />
32
+ * <NotificationMarker enabled={notifications.length > 0}>
33
+ *
34
34
  * </NotificationMarker>
35
+ * @default true
35
36
  */
36
37
  enabled?: boolean;
37
38
  };