@itwin/itwinui-react 3.0.0-dev.8 → 3.0.0-dev.9

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 (61) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/cjs/core/Buttons/DropdownButton/DropdownButton.js +7 -19
  3. package/cjs/core/Buttons/SplitButton/SplitButton.d.ts +4 -4
  4. package/cjs/core/Buttons/SplitButton/SplitButton.js +53 -31
  5. package/cjs/core/ComboBox/ComboBox.d.ts +2 -2
  6. package/cjs/core/ComboBox/ComboBox.js +32 -24
  7. package/cjs/core/ComboBox/ComboBoxInput.js +29 -21
  8. package/cjs/core/ComboBox/ComboBoxMenu.js +73 -93
  9. package/cjs/core/ComboBox/helpers.d.ts +4 -1
  10. package/cjs/core/DropdownMenu/DropdownMenu.d.ts +6 -5
  11. package/cjs/core/DropdownMenu/DropdownMenu.js +59 -55
  12. package/cjs/core/Header/HeaderDropdownButton.js +1 -2
  13. package/cjs/core/Header/HeaderSplitButton.js +1 -2
  14. package/cjs/core/Menu/Menu.js +1 -1
  15. package/cjs/core/Menu/MenuItem.js +77 -55
  16. package/cjs/core/Select/Select.d.ts +5 -5
  17. package/cjs/core/Select/Select.js +74 -93
  18. package/cjs/core/Table/columns/actionColumn.js +3 -7
  19. package/cjs/core/Table/filters/DateRangeFilter/DatePickerInput.js +36 -41
  20. package/cjs/core/Table/filters/FilterToggle.js +3 -2
  21. package/cjs/core/Tile/Tile.js +21 -22
  22. package/cjs/core/index.d.ts +1 -1
  23. package/cjs/core/index.js +8 -1
  24. package/cjs/core/utils/components/InputContainer.d.ts +4 -4
  25. package/cjs/core/utils/components/InputContainer.js +7 -3
  26. package/cjs/core/utils/components/Popover.d.ts +113 -27
  27. package/cjs/core/utils/components/Popover.js +156 -118
  28. package/cjs/styles.js +2 -5
  29. package/esm/core/Buttons/DropdownButton/DropdownButton.js +8 -24
  30. package/esm/core/Buttons/SplitButton/SplitButton.d.ts +4 -4
  31. package/esm/core/Buttons/SplitButton/SplitButton.js +53 -28
  32. package/esm/core/ComboBox/ComboBox.d.ts +2 -2
  33. package/esm/core/ComboBox/ComboBox.js +33 -24
  34. package/esm/core/ComboBox/ComboBoxInput.js +22 -21
  35. package/esm/core/ComboBox/ComboBoxMenu.js +67 -87
  36. package/esm/core/ComboBox/helpers.d.ts +4 -1
  37. package/esm/core/DropdownMenu/DropdownMenu.d.ts +6 -5
  38. package/esm/core/DropdownMenu/DropdownMenu.js +64 -56
  39. package/esm/core/Header/HeaderDropdownButton.js +1 -2
  40. package/esm/core/Header/HeaderSplitButton.js +1 -2
  41. package/esm/core/Menu/Menu.js +7 -2
  42. package/esm/core/Menu/MenuItem.js +84 -52
  43. package/esm/core/Select/Select.d.ts +5 -5
  44. package/esm/core/Select/Select.js +74 -90
  45. package/esm/core/Table/columns/actionColumn.js +3 -7
  46. package/esm/core/Table/filters/DateRangeFilter/DatePickerInput.js +36 -41
  47. package/esm/core/Table/filters/FilterToggle.js +3 -2
  48. package/esm/core/Tile/Tile.js +21 -22
  49. package/esm/core/index.d.ts +1 -1
  50. package/esm/core/index.js +1 -0
  51. package/esm/core/utils/components/InputContainer.d.ts +4 -4
  52. package/esm/core/utils/components/InputContainer.js +7 -2
  53. package/esm/core/utils/components/Popover.d.ts +113 -27
  54. package/esm/core/utils/components/Popover.js +175 -118
  55. package/esm/styles.js +2 -5
  56. package/package.json +2 -4
  57. package/styles.css +3 -3
  58. package/cjs/core/ComboBox/ComboBoxDropdown.d.ts +0 -7
  59. package/cjs/core/ComboBox/ComboBoxDropdown.js +0 -43
  60. package/esm/core/ComboBox/ComboBoxDropdown.d.ts +0 -7
  61. package/esm/core/ComboBox/ComboBoxDropdown.js +0 -37
@@ -9,113 +9,93 @@ const tslib_1 = require('tslib');
9
9
  const classnames_1 = tslib_1.__importDefault(require('classnames'));
10
10
  const React = tslib_1.__importStar(require('react'));
11
11
  const index_js_1 = require('../Menu/index.js');
12
- const index_js_2 = require('../Surface/index.js');
13
- const index_js_3 = require('../utils/index.js');
12
+ const index_js_2 = require('../utils/index.js');
14
13
  const helpers_js_1 = require('./helpers.js');
15
- const isOverflowOverlaySupported = () =>
16
- (0, index_js_3.getWindow)()?.CSS?.supports?.('overflow: overlay');
17
- const VirtualizedComboBoxMenu = React.forwardRef(
18
- ({ children, className, style, ...rest }, forwardedRef) => {
19
- const { minWidth, id, filteredOptions, getMenuItem, focusedIndex } = (0,
20
- index_js_3.useSafeContext)(helpers_js_1.ComboBoxStateContext);
21
- const { menuRef } = (0, index_js_3.useSafeContext)(
22
- helpers_js_1.ComboBoxRefsContext,
23
- );
24
- const virtualItemRenderer = React.useCallback(
25
- (index) =>
26
- filteredOptions.length > 0
27
- ? getMenuItem(filteredOptions[index], index)
28
- : children, // Here is expected empty state content
29
- [filteredOptions, getMenuItem, children],
14
+ const VirtualizedComboBoxMenu = (props) => {
15
+ const { children, ...rest } = props;
16
+ const { filteredOptions, getMenuItem, focusedIndex } = (0,
17
+ index_js_2.useSafeContext)(helpers_js_1.ComboBoxStateContext);
18
+ const { menuRef } = (0, index_js_2.useSafeContext)(
19
+ helpers_js_1.ComboBoxRefsContext,
20
+ );
21
+ const virtualItemRenderer = React.useCallback(
22
+ (index) =>
23
+ filteredOptions.length > 0
24
+ ? getMenuItem(filteredOptions[index], index)
25
+ : children, // Here is expected empty state content
26
+ [filteredOptions, getMenuItem, children],
27
+ );
28
+ const focusedVisibleIndex = React.useMemo(() => {
29
+ const currentElement = menuRef.current?.querySelector(
30
+ `[data-iui-index="${focusedIndex}"]`,
30
31
  );
31
- const focusedVisibleIndex = React.useMemo(() => {
32
- const currentElement = menuRef.current?.querySelector(
33
- `[data-iui-index="${focusedIndex}"]`,
34
- );
35
- if (!currentElement) {
36
- return focusedIndex;
37
- }
38
- return Number(
39
- currentElement.getAttribute('data-iui-filtered-index') ?? focusedIndex,
40
- );
41
- }, [focusedIndex, menuRef]);
42
- const { outerProps, innerProps, visibleChildren } = (0,
43
- index_js_3.useVirtualization)({
44
- // 'Fool' VirtualScroll by passing length 1
45
- // whenever there is no elements, to show empty state message
46
- itemsLength: filteredOptions.length || 1,
47
- itemRenderer: virtualItemRenderer,
48
- scrollToIndex: focusedVisibleIndex,
49
- });
50
- const surfaceStyles = {
51
- minInlineSize: minWidth,
52
- // set as constant because we don't want it shifting when items are unmounted
53
- maxInlineSize: minWidth,
54
- // max-height must be on the outermost element for virtual scroll
55
- maxBlockSize: 'calc((var(--iui-component-height) - 1px) * 8.5)',
56
- overflowY: isOverflowOverlaySupported() ? 'overlay' : 'auto',
57
- ...style,
58
- };
59
- return React.createElement(
60
- index_js_2.Surface,
61
- { style: surfaceStyles },
62
- React.createElement(
63
- 'div',
64
- { ...outerProps },
65
- React.createElement(
66
- index_js_1.Menu,
67
- {
68
- id: `${id}-list`,
69
- setFocus: false,
70
- role: 'listbox',
71
- ref: (0, index_js_3.mergeRefs)(
72
- menuRef,
73
- innerProps.ref,
74
- forwardedRef,
75
- ),
76
- className: className,
77
- style: innerProps.style,
78
- ...rest,
79
- },
80
- visibleChildren,
81
- ),
82
- ),
32
+ if (!currentElement) {
33
+ return focusedIndex;
34
+ }
35
+ return Number(
36
+ currentElement.getAttribute('data-iui-filtered-index') ?? focusedIndex,
83
37
  );
84
- },
85
- );
38
+ }, [focusedIndex, menuRef]);
39
+ const { outerProps, innerProps, visibleChildren } = (0,
40
+ index_js_2.useVirtualization)({
41
+ // 'Fool' VirtualScroll by passing length 1
42
+ // whenever there is no elements, to show empty state message
43
+ itemsLength: filteredOptions.length || 1,
44
+ itemRenderer: virtualItemRenderer,
45
+ scrollToIndex: focusedVisibleIndex,
46
+ });
47
+ return React.createElement(
48
+ index_js_2.Box,
49
+ { as: 'div', ...outerProps, ...rest },
50
+ React.createElement(
51
+ 'div',
52
+ { ...innerProps, ref: innerProps.ref },
53
+ visibleChildren,
54
+ ),
55
+ );
56
+ };
86
57
  exports.ComboBoxMenu = React.forwardRef((props, forwardedRef) => {
87
- const { className, style, ...rest } = props;
88
- const { minWidth, id, enableVirtualization } = (0, index_js_3.useSafeContext)(
58
+ const { className, children, style, ...rest } = props;
59
+ const { id, enableVirtualization, popover } = (0, index_js_2.useSafeContext)(
89
60
  helpers_js_1.ComboBoxStateContext,
90
61
  );
91
- const { menuRef } = (0, index_js_3.useSafeContext)(
62
+ const { menuRef } = (0, index_js_2.useSafeContext)(
92
63
  helpers_js_1.ComboBoxRefsContext,
93
64
  );
94
- const refs = (0, index_js_3.useMergedRefs)(menuRef, forwardedRef);
95
- const styles = React.useMemo(
96
- () => ({
97
- minInlineSize: minWidth,
98
- maxInlineSize: `min(${minWidth * 2}px, 90vw)`,
99
- }),
100
- [minWidth],
65
+ const refs = (0, index_js_2.useMergedRefs)(
66
+ popover.refs.setFloating,
67
+ forwardedRef,
68
+ menuRef,
101
69
  );
102
- return React.createElement(
103
- React.Fragment,
104
- null,
105
- !enableVirtualization
106
- ? React.createElement(index_js_1.Menu, {
70
+ return (
71
+ popover.open &&
72
+ React.createElement(
73
+ index_js_2.Portal,
74
+ { portal: true },
75
+ React.createElement(
76
+ index_js_1.Menu,
77
+ {
107
78
  id: `${id}-list`,
108
- style: { ...styles, ...style },
109
79
  setFocus: false,
110
80
  role: 'listbox',
111
81
  ref: refs,
112
82
  className: (0, classnames_1.default)('iui-scroll', className),
113
- ...rest,
114
- })
115
- : React.createElement(VirtualizedComboBoxMenu, {
116
- ref: forwardedRef,
117
- ...props,
118
- }),
83
+ ...popover.getFloatingProps({
84
+ style: !enableVirtualization
85
+ ? style
86
+ : {
87
+ // set as constant because we don't want it shifting when items are unmounted
88
+ maxInlineSize: 0,
89
+ ...style,
90
+ },
91
+ ...rest,
92
+ }),
93
+ },
94
+ !enableVirtualization
95
+ ? children
96
+ : React.createElement(VirtualizedComboBoxMenu, null, children),
97
+ ),
98
+ )
119
99
  );
120
100
  });
121
101
  exports.ComboBoxMenu.displayName = 'ComboBoxMenu';
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import type { SelectOption } from '../Select/Select.js';
3
+ import type { usePopover } from '../utils/index.js';
3
4
  type ComboBoxAction = {
4
5
  type: 'multiselect';
5
6
  value: number[];
@@ -33,13 +34,15 @@ export declare const ComboBoxRefsContext: React.Context<{
33
34
  type ComboBoxStateContextProps<T = unknown> = {
34
35
  isOpen: boolean;
35
36
  id: string;
36
- minWidth: number;
37
37
  enableVirtualization: boolean;
38
38
  filteredOptions: SelectOption<T>[];
39
39
  onClickHandler?: (prop: number) => void;
40
40
  getMenuItem: (option: SelectOption<T>, filteredIndex?: number) => JSX.Element;
41
41
  focusedIndex?: number;
42
42
  multiple?: boolean;
43
+ popover: ReturnType<typeof usePopover>;
44
+ show: () => void;
45
+ hide: () => void;
43
46
  };
44
47
  export declare const ComboBoxStateContext: React.Context<ComboBoxStateContextProps<unknown> | undefined>;
45
48
  export declare const ComboBoxActionContext: React.Context<((x: ComboBoxAction) => void) | undefined>;
@@ -1,11 +1,12 @@
1
1
  import * as React from 'react';
2
- import type { CommonProps, PopoverProps } from '../utils/index.js';
2
+ import { Popover } from '../utils/index.js';
3
+ import type { PolymorphicForwardRefComponent, PortalProps } from '../utils/index.js';
3
4
  export type DropdownMenuProps = {
4
5
  /**
5
6
  * List of menu items. Recommended to use MenuItem component.
6
7
  * You can pass function that takes argument `close` that closes the dropdown menu, or a list of MenuItems.
7
8
  */
8
- menuItems: (close: () => void) => JSX.Element[] | JSX.Element[] | JSX.Element;
9
+ menuItems: ((close: () => void) => JSX.Element[]) | JSX.Element[] | JSX.Element;
9
10
  /**
10
11
  * ARIA role. Role of menu. For menu use 'menu', for select use 'listbox'.
11
12
  * @default 'menu'
@@ -15,10 +16,10 @@ export type DropdownMenuProps = {
15
16
  * Child element to wrap dropdown with.
16
17
  */
17
18
  children: React.ReactNode;
18
- } & Omit<PopoverProps, 'content'> & Omit<CommonProps, 'title'>;
19
+ } & Pick<React.ComponentProps<typeof Popover>, 'visible' | 'onVisibleChange' | 'placement' | 'matchWidth'> & React.ComponentPropsWithoutRef<'ul'> & Pick<PortalProps, 'portal'>;
19
20
  /**
20
21
  * Dropdown menu component.
21
- * Uses the {@link Popover} component, which is a wrapper around [tippy.js](https://atomiks.github.io/tippyjs).
22
+ * Built on top of the {@link Popover} component.
22
23
  * @example
23
24
  * const menuItems = (close: () => void) => [
24
25
  * <MenuItem key={1} onClick={onClick(1, close)}>
@@ -35,5 +36,5 @@ export type DropdownMenuProps = {
35
36
  * <Button>Menu</Button>
36
37
  * </DropdownMenu>
37
38
  */
38
- export declare const DropdownMenu: (props: DropdownMenuProps) => React.JSX.Element;
39
+ export declare const DropdownMenu: PolymorphicForwardRefComponent<"div", DropdownMenuProps>;
39
40
  export default DropdownMenu;
@@ -11,7 +11,7 @@ const index_js_1 = require('../utils/index.js');
11
11
  const index_js_2 = require('../Menu/index.js');
12
12
  /**
13
13
  * Dropdown menu component.
14
- * Uses the {@link Popover} component, which is a wrapper around [tippy.js](https://atomiks.github.io/tippyjs).
14
+ * Built on top of the {@link Popover} component.
15
15
  * @example
16
16
  * const menuItems = (close: () => void) => [
17
17
  * <MenuItem key={1} onClick={onClick(1, close)}>
@@ -28,75 +28,79 @@ const index_js_2 = require('../Menu/index.js');
28
28
  * <Button>Menu</Button>
29
29
  * </DropdownMenu>
30
30
  */
31
- const DropdownMenu = (props) => {
31
+ exports.DropdownMenu = React.forwardRef((props, forwardedRef) => {
32
32
  const {
33
33
  menuItems,
34
34
  children,
35
- className,
36
- style,
37
35
  role = 'menu',
38
- visible,
36
+ visible: visibleProp,
39
37
  placement = 'bottom-start',
40
- onShow,
41
- onHide,
42
- trigger,
43
- id,
38
+ matchWidth = false,
39
+ onVisibleChange,
40
+ portal = true,
44
41
  ...rest
45
42
  } = props;
46
- const [isVisible, setIsVisible] = React.useState(visible ?? false);
47
- React.useEffect(() => {
48
- setIsVisible(visible ?? false);
49
- }, [visible]);
50
- const open = React.useCallback(() => setIsVisible(true), []);
51
- const close = React.useCallback(() => setIsVisible(false), []);
43
+ const [visible, setVisible] = (0, index_js_1.useControlledState)(
44
+ false,
45
+ visibleProp,
46
+ onVisibleChange,
47
+ );
48
+ const triggerRef = React.useRef(null);
49
+ const close = React.useCallback(() => {
50
+ setVisible(false);
51
+ triggerRef.current?.focus({ preventScroll: true });
52
+ }, [setVisible]);
52
53
  const menuContent = React.useMemo(() => {
53
54
  if (typeof menuItems === 'function') {
54
55
  return menuItems(close);
55
56
  }
56
57
  return menuItems;
57
58
  }, [menuItems, close]);
58
- const targetRef = React.useRef(null);
59
- const onShowHandler = React.useCallback(
60
- (instance) => {
61
- setIsVisible(true);
62
- onShow?.(instance);
63
- },
64
- [onShow],
65
- );
66
- const onHideHandler = React.useCallback(
67
- (instance) => {
68
- setIsVisible(false);
69
- targetRef.current?.focus();
70
- onHide?.(instance);
71
- },
72
- [onHide],
59
+ const popover = (0, index_js_1.usePopover)({
60
+ visible,
61
+ onVisibleChange: (open) => (open ? setVisible(true) : close()),
62
+ placement,
63
+ matchWidth,
64
+ });
65
+ const popoverRef = (0, index_js_1.useMergedRefs)(
66
+ forwardedRef,
67
+ popover.refs.setFloating,
73
68
  );
74
69
  return React.createElement(
75
- index_js_1.Popover,
76
- {
77
- content: React.createElement(
78
- index_js_2.Menu,
79
- { className: className, style: style, role: role, id: id },
80
- menuContent,
81
- ),
82
- visible: trigger === undefined ? isVisible : undefined,
83
- onClickOutside: close,
84
- placement: placement,
85
- onShow: onShowHandler,
86
- onHide: onHideHandler,
87
- trigger: visible === undefined ? trigger : undefined,
88
- ...rest,
89
- },
90
- React.isValidElement(children)
91
- ? React.cloneElement(children, {
92
- ref: (0, index_js_1.mergeRefs)(targetRef, props.children.ref),
93
- onClick: (args) => {
94
- trigger === undefined && (isVisible ? close() : open());
95
- children.props.onClick?.(args);
70
+ React.Fragment,
71
+ null,
72
+ (0, index_js_1.cloneElementWithRef)(children, (children) => ({
73
+ ...popover.getReferenceProps(children.props),
74
+ 'aria-expanded': popover.open,
75
+ ref: (0, index_js_1.mergeRefs)(triggerRef, popover.refs.setReference),
76
+ })),
77
+ popover.open &&
78
+ React.createElement(
79
+ index_js_1.Portal,
80
+ { portal: portal },
81
+ React.createElement(
82
+ index_js_2.Menu,
83
+ {
84
+ ...popover.getFloatingProps({
85
+ role,
86
+ ...rest,
87
+ onKeyDown: (0, index_js_1.mergeEventHandlers)(
88
+ props.onKeyDown,
89
+ (e) => {
90
+ if (e.defaultPrevented) {
91
+ return;
92
+ }
93
+ if (e.key === 'Tab') {
94
+ close();
95
+ }
96
+ },
97
+ ),
98
+ }),
99
+ ref: popoverRef,
96
100
  },
97
- })
98
- : React.createElement(React.Fragment, null),
101
+ menuContent,
102
+ ),
103
+ ),
99
104
  );
100
- };
101
- exports.DropdownMenu = DropdownMenu;
105
+ });
102
106
  exports.default = exports.DropdownMenu;
@@ -27,8 +27,7 @@ exports.HeaderDropdownButton = React.forwardRef((props, ref) => {
27
27
  {
28
28
  menuItems: menuItems,
29
29
  style: { minInlineSize: menuWidth },
30
- onShow: () => setIsMenuOpen(true),
31
- onHide: () => setIsMenuOpen(false),
30
+ onVisibleChange: (open) => setIsMenuOpen(open),
32
31
  },
33
32
  React.createElement(
34
33
  HeaderBasicButton_js_1.HeaderBasicButton,
@@ -48,8 +48,7 @@ exports.HeaderSplitButton = React.forwardRef((props, forwardedRef) => {
48
48
  placement: menuPlacement,
49
49
  menuItems: menuItems,
50
50
  style: { minInlineSize: menuWidth },
51
- onShow: React.useCallback(() => setIsMenuOpen(true), []),
52
- onHide: React.useCallback(() => setIsMenuOpen(false), []),
51
+ onVisibleChange: (open) => setIsMenuOpen(open),
53
52
  },
54
53
  React.createElement(
55
54
  index_js_2.ButtonBase,
@@ -69,9 +69,9 @@ exports.Menu = React.forwardRef((props, ref) => {
69
69
  as: 'div',
70
70
  className: (0, classnames_1.default)('iui-menu', className),
71
71
  role: 'menu',
72
- onKeyDown: onKeyDown,
73
72
  ref: refs,
74
73
  ...rest,
74
+ onKeyDown: (0, index_js_1.mergeEventHandlers)(props.onKeyDown, onKeyDown),
75
75
  });
76
76
  });
77
77
  exports.default = exports.Menu;
@@ -10,14 +10,18 @@ const React = tslib_1.__importStar(require('react'));
10
10
  const index_js_1 = require('../utils/index.js');
11
11
  const Menu_js_1 = require('./Menu.js');
12
12
  const ListItem_js_1 = require('../List/ListItem.js');
13
+ const react_dom_1 = require('react-dom');
13
14
  /**
14
15
  * Context used to provide menu item ref to sub-menu items.
15
16
  */
16
- const MenuItemContext = React.createContext({ ref: undefined });
17
+ const MenuItemContext = React.createContext({
18
+ ref: undefined,
19
+ setIsNestedSubmenuVisible: () => {},
20
+ });
17
21
  /**
18
22
  * Basic menu item component. Should be used inside `Menu` component for each item.
19
23
  */
20
- exports.MenuItem = React.forwardRef((props, ref) => {
24
+ exports.MenuItem = React.forwardRef((props, forwardedRef) => {
21
25
  const {
22
26
  children,
23
27
  isSelected,
@@ -26,21 +30,33 @@ exports.MenuItem = React.forwardRef((props, ref) => {
26
30
  onClick,
27
31
  sublabel,
28
32
  size = !!sublabel ? 'large' : 'default',
29
- startIcon: startIconProp,
30
33
  icon,
31
- endIcon: endIconProp,
34
+ startIcon = icon,
32
35
  badge,
36
+ endIcon = badge,
33
37
  role = 'menuitem',
34
38
  subMenuItems = [],
35
39
  ...rest
36
40
  } = props;
37
41
  const menuItemRef = React.useRef(null);
38
- const refs = (0, index_js_1.useMergedRefs)(menuItemRef, ref);
39
- const { ref: parentMenuItemRef } = React.useContext(MenuItemContext);
40
- const subMenuRef = React.useRef(null);
42
+ const [focusOnSubmenu, setFocusOnSubmenu] = React.useState(false);
43
+ const submenuId = (0, index_js_1.useId)();
41
44
  const [isSubmenuVisible, setIsSubmenuVisible] = React.useState(false);
42
- const startIcon = startIconProp ?? icon;
43
- const endIcon = endIconProp ?? badge;
45
+ const [isNestedSubmenuVisible, setIsNestedSubmenuVisible] =
46
+ React.useState(false);
47
+ const parent = React.useContext(MenuItemContext);
48
+ const onVisibleChange = (open) => {
49
+ setIsSubmenuVisible(open);
50
+ // we don't want parent to close when mouse goes into a nested submenu,
51
+ // so we need to let the parent know whether the submenu is still open.
52
+ parent.setIsNestedSubmenuVisible(open);
53
+ };
54
+ const popover = (0, index_js_1.usePopover)({
55
+ visible: isSubmenuVisible || isNestedSubmenuVisible,
56
+ onVisibleChange,
57
+ placement: 'right-start',
58
+ trigger: { hover: true, focus: true },
59
+ });
44
60
  const onKeyDown = (event) => {
45
61
  if (event.altKey) {
46
62
  return;
@@ -56,22 +72,37 @@ exports.MenuItem = React.forwardRef((props, ref) => {
56
72
  case 'ArrowRight': {
57
73
  if (subMenuItems.length > 0) {
58
74
  setIsSubmenuVisible(true);
75
+ // flush and reset state so we are ready to focus again next time
76
+ (0, react_dom_1.flushSync)(() => setFocusOnSubmenu(true));
77
+ setFocusOnSubmenu(false);
59
78
  event.preventDefault();
60
79
  event.stopPropagation();
61
80
  }
62
81
  break;
63
82
  }
64
83
  case 'ArrowLeft': {
65
- parentMenuItemRef?.current?.focus();
84
+ if (parent.ref) {
85
+ parent.ref.current?.focus();
86
+ parent.setIsNestedSubmenuVisible(false);
87
+ }
66
88
  event.stopPropagation();
67
89
  event.preventDefault();
68
90
  break;
69
91
  }
92
+ case 'Escape': {
93
+ // focus might get lost if submenu closes so move it back to parent
94
+ parent.ref?.current?.focus();
95
+ break;
96
+ }
70
97
  default:
71
98
  break;
72
99
  }
73
100
  };
74
- const listItem = React.createElement(
101
+ const handlers = {
102
+ onClick: () => !disabled && onClick?.(value),
103
+ onKeyDown,
104
+ };
105
+ return React.createElement(
75
106
  ListItem_js_1.ListItem,
76
107
  {
77
108
  as: 'div',
@@ -79,24 +110,21 @@ exports.MenuItem = React.forwardRef((props, ref) => {
79
110
  size: size,
80
111
  active: isSelected,
81
112
  disabled: disabled,
82
- onClick: () => !disabled && onClick?.(value),
83
- ref: refs,
113
+ ref: (0, index_js_1.useMergedRefs)(
114
+ menuItemRef,
115
+ forwardedRef,
116
+ subMenuItems.length > 0 ? popover.refs.setReference : null,
117
+ ),
84
118
  role: role,
85
119
  tabIndex: disabled || role === 'presentation' ? undefined : -1,
86
120
  'aria-selected': isSelected,
87
- 'aria-haspopup': subMenuItems.length > 0,
121
+ 'aria-haspopup': subMenuItems.length > 0 ? 'true' : undefined,
122
+ 'aria-controls': subMenuItems.length > 0 ? submenuId : undefined,
123
+ 'aria-expanded': subMenuItems.length > 0 ? popover.open : undefined,
88
124
  'aria-disabled': disabled,
89
- onKeyDown: onKeyDown,
90
- onMouseEnter: () => setIsSubmenuVisible(true),
91
- onMouseLeave: (e) => {
92
- if (
93
- !(e.relatedTarget instanceof Node) ||
94
- !subMenuRef.current?.contains(e.relatedTarget)
95
- ) {
96
- setIsSubmenuVisible(false);
97
- }
98
- },
99
- ...rest,
125
+ ...(subMenuItems.length === 0
126
+ ? { ...handlers, ...rest }
127
+ : popover.getReferenceProps({ ...handlers, ...rest })),
100
128
  },
101
129
  startIcon &&
102
130
  React.createElement(
@@ -124,38 +152,32 @@ exports.MenuItem = React.forwardRef((props, ref) => {
124
152
  { as: 'span', 'aria-hidden': true },
125
153
  endIcon,
126
154
  ),
127
- );
128
- return subMenuItems.length === 0
129
- ? listItem
130
- : React.createElement(
131
- MenuItemContext.Provider,
132
- { value: { ref: menuItemRef } },
155
+ subMenuItems.length > 0 &&
156
+ popover.open &&
157
+ React.createElement(
158
+ index_js_1.Portal,
159
+ null,
133
160
  React.createElement(
134
- index_js_1.Popover,
135
- {
136
- placement: 'right-start',
137
- visible: isSubmenuVisible,
138
- appendTo: 'parent',
139
- content: React.createElement(
140
- 'div',
141
- {
142
- onMouseLeave: () => setIsSubmenuVisible(false),
143
- onBlur: (e) => {
144
- !!(e.relatedTarget instanceof Node) &&
145
- !subMenuRef.current?.contains(e.relatedTarget) &&
146
- !subMenuRef.current?.isEqualNode(e.relatedTarget) &&
147
- setIsSubmenuVisible(false);
161
+ MenuItemContext.Provider,
162
+ { value: { ref: menuItemRef, setIsNestedSubmenuVisible } },
163
+ React.createElement(
164
+ Menu_js_1.Menu,
165
+ {
166
+ setFocus: focusOnSubmenu,
167
+ ref: popover.refs.setFloating,
168
+ ...popover.getFloatingProps({
169
+ id: submenuId,
170
+ onPointerMove: () => {
171
+ // pointer might move into a nested submenu and set isSubmenuVisible to false,
172
+ // so we need to flip it back to true when pointer re-enters this submenu.
173
+ setIsSubmenuVisible(true);
148
174
  },
149
- },
150
- React.createElement(
151
- Menu_js_1.Menu,
152
- { ref: subMenuRef },
153
- subMenuItems,
154
- ),
155
- ),
156
- },
157
- listItem,
175
+ }),
176
+ },
177
+ subMenuItems,
178
+ ),
158
179
  ),
159
- );
180
+ ),
181
+ );
160
182
  });
161
183
  exports.default = exports.MenuItem;
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
- import type { PopoverProps, CommonProps } from '../utils/index.js';
2
+ import { usePopover } from '../utils/index.js';
3
+ import type { CommonProps } from '../utils/index.js';
3
4
  export type ItemRendererProps = {
4
5
  /**
5
6
  * Close handler that closes the dropdown.
@@ -110,15 +111,14 @@ export type SelectProps<T> = {
110
111
  */
111
112
  menuStyle?: React.CSSProperties;
112
113
  /**
113
- * Props to customize {@link Popover} behavior.
114
- * @see [tippy.js props](https://atomiks.github.io/tippyjs/v6/all-props/)
114
+ * Props to customize Popover behavior.
115
115
  */
116
- popoverProps?: Omit<PopoverProps, 'onShow' | 'onHide' | 'disabled'>;
116
+ popoverProps?: Pick<Parameters<typeof usePopover>[0], 'visible' | 'onVisibleChange' | 'placement' | 'matchWidth' | 'closeOnOutsideClick'>;
117
117
  /**
118
118
  * Props to pass to the select button (trigger) element.
119
119
  */
120
120
  triggerProps?: React.ComponentPropsWithoutRef<'div'>;
121
- } & SelectMultipleTypeProps<T> & Pick<PopoverProps, 'onShow' | 'onHide'> & Omit<React.ComponentPropsWithoutRef<'div'>, 'size' | 'disabled' | 'placeholder' | 'onChange'>;
121
+ } & SelectMultipleTypeProps<T> & Omit<React.ComponentPropsWithoutRef<'div'>, 'size' | 'disabled' | 'placeholder' | 'onChange'>;
122
122
  /**
123
123
  * Select component to select value from options.
124
124
  * Generic type is used for value. It prevents you from mistakenly using other types in `options`, `value` and `onChange`.