@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
@@ -12,6 +12,7 @@ import {
12
12
  useLatestRef,
13
13
  useIsomorphicLayoutEffect,
14
14
  AutoclearingHiddenLiveRegion,
15
+ usePopover,
15
16
  } from '../utils/index.js';
16
17
  import {
17
18
  ComboBoxActionContext,
@@ -19,7 +20,6 @@ import {
19
20
  ComboBoxRefsContext,
20
21
  ComboBoxStateContext,
21
22
  } from './helpers.js';
22
- import { ComboBoxDropdown } from './ComboBoxDropdown.js';
23
23
  import { ComboBoxEndIcon } from './ComboBoxEndIcon.js';
24
24
  import { ComboBoxInput } from './ComboBoxInput.js';
25
25
  import { ComboBoxInputContainer } from './ComboBoxInputContainer.js';
@@ -62,8 +62,8 @@ export const ComboBox = (props) => {
62
62
  itemRenderer,
63
63
  enableVirtualization = false,
64
64
  multiple = false,
65
- onShow,
66
- onHide,
65
+ onShow: onShowProp,
66
+ onHide: onHideProp,
67
67
  ...rest
68
68
  } = props;
69
69
  // Generate a stateful random id if not specified
@@ -121,6 +121,16 @@ export const ComboBox = (props) => {
121
121
  focusedIndex: -1,
122
122
  },
123
123
  );
124
+ const onShowRef = useLatestRef(onShowProp);
125
+ const onHideRef = useLatestRef(onHideProp);
126
+ const show = React.useCallback(() => {
127
+ dispatch({ type: 'open' });
128
+ onShowRef.current?.();
129
+ }, [onShowRef]);
130
+ const hide = React.useCallback(() => {
131
+ dispatch({ type: 'close' });
132
+ onHideRef.current?.();
133
+ }, [onHideRef]);
124
134
  useIsomorphicLayoutEffect(() => {
125
135
  // When the dropdown opens
126
136
  if (isOpen) {
@@ -145,13 +155,6 @@ export const ComboBox = (props) => {
145
155
  }
146
156
  }
147
157
  }, [isOpen, multiple, optionsRef, selected]);
148
- // Set min-width of menu to be same as input
149
- const [minWidth, setMinWidth] = React.useState(0);
150
- React.useEffect(() => {
151
- if (inputRef.current) {
152
- setMinWidth(inputRef.current.offsetWidth);
153
- }
154
- }, [isOpen]);
155
158
  // Update filtered options to the latest value options according to input value
156
159
  const [filteredOptions, setFilteredOptions] = React.useState(options);
157
160
  React.useEffect(() => {
@@ -178,7 +181,7 @@ export const ComboBox = (props) => {
178
181
  (event) => {
179
182
  const { value } = event.currentTarget;
180
183
  setInputValue(value);
181
- dispatch({ type: 'open' }); // reopen when typing
184
+ show(); // reopen when typing
182
185
  setFilteredOptions(
183
186
  filterFunction?.(optionsRef.current, value) ??
184
187
  optionsRef.current.filter((option) =>
@@ -190,7 +193,7 @@ export const ComboBox = (props) => {
190
193
  }
191
194
  inputProps?.onChange?.(event);
192
195
  },
193
- [filterFunction, focusedIndex, inputProps, optionsRef],
196
+ [filterFunction, focusedIndex, inputProps, optionsRef, show],
194
197
  );
195
198
  // When the value prop changes, update the selected index/indices
196
199
  React.useEffect(() => {
@@ -280,7 +283,7 @@ export const ComboBox = (props) => {
280
283
  );
281
284
  } else {
282
285
  dispatch({ type: 'select', value: __originalIndex });
283
- dispatch({ type: 'close' });
286
+ hide();
284
287
  onChangeHandler(__originalIndex);
285
288
  }
286
289
  },
@@ -291,6 +294,7 @@ export const ComboBox = (props) => {
291
294
  onChangeHandler,
292
295
  selected,
293
296
  optionsRef,
297
+ hide,
294
298
  ],
295
299
  );
296
300
  const getMenuItem = React.useCallback(
@@ -366,6 +370,13 @@ export const ComboBox = (props) => {
366
370
  ),
367
371
  [emptyStateMessage],
368
372
  );
373
+ const popover = usePopover({
374
+ visible: isOpen,
375
+ onVisibleChange: (open) => (open ? show() : hide()),
376
+ matchWidth: true,
377
+ closeOnOutsideClick: true,
378
+ trigger: { focus: true },
379
+ });
369
380
  return React.createElement(
370
381
  ComboBoxRefsContext.Provider,
371
382
  { value: { inputRef, menuRef, optionsExtraInfoRef } },
@@ -377,7 +388,6 @@ export const ComboBox = (props) => {
377
388
  {
378
389
  value: {
379
390
  id,
380
- minWidth,
381
391
  isOpen,
382
392
  focusedIndex,
383
393
  onClickHandler,
@@ -385,11 +395,14 @@ export const ComboBox = (props) => {
385
395
  filteredOptions,
386
396
  getMenuItem,
387
397
  multiple,
398
+ popover,
399
+ show,
400
+ hide,
388
401
  },
389
402
  },
390
403
  React.createElement(
391
404
  ComboBoxInputContainer,
392
- { ...rest },
405
+ { disabled: inputProps?.disabled, ...rest },
393
406
  React.createElement(
394
407
  React.Fragment,
395
408
  null,
@@ -420,15 +433,11 @@ export const ComboBox = (props) => {
420
433
  : null,
421
434
  ),
422
435
  React.createElement(
423
- ComboBoxDropdown,
424
- { ...dropdownMenuProps, onShow: onShow, onHide: onHide },
425
- React.createElement(
426
- ComboBoxMenu,
427
- null,
428
- filteredOptions.length > 0 && !enableVirtualization
429
- ? filteredOptions.map(getMenuItem)
430
- : emptyContent,
431
- ),
436
+ ComboBoxMenu,
437
+ { as: 'div', ...dropdownMenuProps },
438
+ filteredOptions.length > 0 && !enableVirtualization
439
+ ? filteredOptions.map(getMenuItem)
440
+ : emptyContent,
432
441
  ),
433
442
  ),
434
443
  ),
@@ -19,9 +19,9 @@ import {
19
19
  export const ComboBoxInput = React.forwardRef((props, forwardedRef) => {
20
20
  const {
21
21
  onKeyDown: onKeyDownProp,
22
- onFocus: onFocusProp,
23
22
  onClick: onClickProp,
24
23
  selectTags,
24
+ size,
25
25
  ...rest
26
26
  } = props;
27
27
  const {
@@ -31,11 +31,14 @@ export const ComboBoxInput = React.forwardRef((props, forwardedRef) => {
31
31
  enableVirtualization,
32
32
  multiple,
33
33
  onClickHandler,
34
+ popover,
35
+ show,
36
+ hide,
34
37
  } = useSafeContext(ComboBoxStateContext);
35
38
  const dispatch = useSafeContext(ComboBoxActionContext);
36
39
  const { inputRef, menuRef, optionsExtraInfoRef } =
37
40
  useSafeContext(ComboBoxRefsContext);
38
- const refs = useMergedRefs(inputRef, forwardedRef);
41
+ const refs = useMergedRefs(inputRef, popover.refs.setReference, forwardedRef);
39
42
  const focusedIndexRef = React.useRef(focusedIndex ?? -1);
40
43
  React.useEffect(() => {
41
44
  focusedIndexRef.current = focusedIndex ?? -1;
@@ -47,7 +50,6 @@ export const ComboBoxInput = React.forwardRef((props, forwardedRef) => {
47
50
  };
48
51
  const handleKeyDown = React.useCallback(
49
52
  (event) => {
50
- onKeyDownProp?.(event);
51
53
  const length = Object.keys(optionsExtraInfoRef.current).length ?? 0;
52
54
  if (event.altKey) {
53
55
  return;
@@ -56,7 +58,7 @@ export const ComboBoxInput = React.forwardRef((props, forwardedRef) => {
56
58
  case 'ArrowDown': {
57
59
  event.preventDefault();
58
60
  if (!isOpen) {
59
- return dispatch({ type: 'open' });
61
+ return show();
60
62
  }
61
63
  if (length === 0) {
62
64
  return;
@@ -98,7 +100,7 @@ export const ComboBoxInput = React.forwardRef((props, forwardedRef) => {
98
100
  case 'ArrowUp': {
99
101
  event.preventDefault();
100
102
  if (!isOpen) {
101
- return dispatch({ type: 'open' });
103
+ return show();
102
104
  }
103
105
  if (length === 0) {
104
106
  return;
@@ -142,17 +144,17 @@ export const ComboBoxInput = React.forwardRef((props, forwardedRef) => {
142
144
  onClickHandler?.(focusedIndexRef.current);
143
145
  }
144
146
  } else {
145
- dispatch({ type: 'open' });
147
+ show();
146
148
  }
147
149
  break;
148
150
  }
149
151
  case 'Escape': {
150
152
  event.preventDefault();
151
- dispatch({ type: 'close' });
153
+ hide();
152
154
  break;
153
155
  }
154
156
  case 'Tab':
155
- dispatch({ type: 'close' });
157
+ hide();
156
158
  break;
157
159
  }
158
160
  },
@@ -162,31 +164,26 @@ export const ComboBoxInput = React.forwardRef((props, forwardedRef) => {
162
164
  isOpen,
163
165
  menuRef,
164
166
  onClickHandler,
165
- onKeyDownProp,
166
167
  optionsExtraInfoRef,
168
+ show,
169
+ hide,
167
170
  ],
168
171
  );
169
- const handleFocus = React.useCallback(
170
- (event) => {
171
- dispatch({ type: 'open' });
172
- onFocusProp?.(event);
173
- },
174
- [dispatch, onFocusProp],
175
- );
176
172
  const handleClick = React.useCallback(() => {
177
173
  if (!isOpen) {
178
- dispatch({ type: 'open' });
174
+ show();
175
+ } else {
176
+ hide();
179
177
  }
180
- }, [dispatch, isOpen]);
178
+ }, [hide, isOpen, show]);
181
179
  const [tagContainerWidthRef, tagContainerWidth] = useContainerWidth();
182
180
  return React.createElement(
183
181
  React.Fragment,
184
182
  null,
185
183
  React.createElement(Input, {
186
184
  ref: refs,
187
- onKeyDown: handleKeyDown,
188
185
  onClick: mergeEventHandlers(onClickProp, handleClick),
189
- onFocus: handleFocus,
186
+ 'aria-expanded': isOpen,
190
187
  'aria-activedescendant':
191
188
  isOpen && focusedIndex != undefined && focusedIndex > -1
192
189
  ? getIdFromIndex(focusedIndex)
@@ -199,7 +196,11 @@ export const ComboBoxInput = React.forwardRef((props, forwardedRef) => {
199
196
  autoCorrect: 'off',
200
197
  style: multiple ? { paddingInlineStart: tagContainerWidth + 18 } : {},
201
198
  'aria-describedby': multiple ? `${id}-selected-live` : undefined,
202
- ...rest,
199
+ size: size,
200
+ ...popover.getReferenceProps({
201
+ onKeyDown: mergeEventHandlers(onKeyDownProp, handleKeyDown),
202
+ ...rest,
203
+ }),
203
204
  }),
204
205
  multiple && selectTags
205
206
  ? React.createElement(ComboBoxMultipleContainer, {
@@ -5,109 +5,89 @@
5
5
  import cx from 'classnames';
6
6
  import * as React from 'react';
7
7
  import { Menu } from '../Menu/index.js';
8
- import { Surface } from '../Surface/index.js';
9
8
  import {
10
9
  useSafeContext,
11
10
  useMergedRefs,
12
11
  useVirtualization,
13
- mergeRefs,
14
- getWindow,
12
+ Portal,
13
+ Box,
15
14
  } from '../utils/index.js';
16
15
  import { ComboBoxStateContext, ComboBoxRefsContext } from './helpers.js';
17
- const isOverflowOverlaySupported = () =>
18
- getWindow()?.CSS?.supports?.('overflow: overlay');
19
- const VirtualizedComboBoxMenu = React.forwardRef(
20
- ({ children, className, style, ...rest }, forwardedRef) => {
21
- const { minWidth, id, filteredOptions, getMenuItem, focusedIndex } =
22
- useSafeContext(ComboBoxStateContext);
23
- const { menuRef } = useSafeContext(ComboBoxRefsContext);
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],
16
+ const VirtualizedComboBoxMenu = (props) => {
17
+ const { children, ...rest } = props;
18
+ const { filteredOptions, getMenuItem, focusedIndex } =
19
+ useSafeContext(ComboBoxStateContext);
20
+ const { menuRef } = useSafeContext(ComboBoxRefsContext);
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 } = useVirtualization({
43
- // 'Fool' VirtualScroll by passing length 1
44
- // whenever there is no elements, to show empty state message
45
- itemsLength: filteredOptions.length || 1,
46
- itemRenderer: virtualItemRenderer,
47
- scrollToIndex: focusedVisibleIndex,
48
- });
49
- const surfaceStyles = {
50
- minInlineSize: minWidth,
51
- // set as constant because we don't want it shifting when items are unmounted
52
- maxInlineSize: minWidth,
53
- // max-height must be on the outermost element for virtual scroll
54
- maxBlockSize: 'calc((var(--iui-component-height) - 1px) * 8.5)',
55
- overflowY: isOverflowOverlaySupported() ? 'overlay' : 'auto',
56
- ...style,
57
- };
58
- return React.createElement(
59
- Surface,
60
- { style: surfaceStyles },
61
- React.createElement(
62
- 'div',
63
- { ...outerProps },
64
- React.createElement(
65
- Menu,
66
- {
67
- id: `${id}-list`,
68
- setFocus: false,
69
- role: 'listbox',
70
- ref: mergeRefs(menuRef, innerProps.ref, forwardedRef),
71
- className: className,
72
- style: innerProps.style,
73
- ...rest,
74
- },
75
- visibleChildren,
76
- ),
77
- ),
32
+ if (!currentElement) {
33
+ return focusedIndex;
34
+ }
35
+ return Number(
36
+ currentElement.getAttribute('data-iui-filtered-index') ?? focusedIndex,
78
37
  );
79
- },
80
- );
38
+ }, [focusedIndex, menuRef]);
39
+ const { outerProps, innerProps, visibleChildren } = useVirtualization({
40
+ // 'Fool' VirtualScroll by passing length 1
41
+ // whenever there is no elements, to show empty state message
42
+ itemsLength: filteredOptions.length || 1,
43
+ itemRenderer: virtualItemRenderer,
44
+ scrollToIndex: focusedVisibleIndex,
45
+ });
46
+ return React.createElement(
47
+ Box,
48
+ { as: 'div', ...outerProps, ...rest },
49
+ React.createElement(
50
+ 'div',
51
+ { ...innerProps, ref: innerProps.ref },
52
+ visibleChildren,
53
+ ),
54
+ );
55
+ };
81
56
  export const ComboBoxMenu = React.forwardRef((props, forwardedRef) => {
82
- const { className, style, ...rest } = props;
83
- const { minWidth, id, enableVirtualization } =
57
+ const { className, children, style, ...rest } = props;
58
+ const { id, enableVirtualization, popover } =
84
59
  useSafeContext(ComboBoxStateContext);
85
60
  const { menuRef } = useSafeContext(ComboBoxRefsContext);
86
- const refs = useMergedRefs(menuRef, forwardedRef);
87
- const styles = React.useMemo(
88
- () => ({
89
- minInlineSize: minWidth,
90
- maxInlineSize: `min(${minWidth * 2}px, 90vw)`,
91
- }),
92
- [minWidth],
93
- );
94
- return React.createElement(
95
- React.Fragment,
96
- null,
97
- !enableVirtualization
98
- ? React.createElement(Menu, {
61
+ const refs = useMergedRefs(popover.refs.setFloating, forwardedRef, menuRef);
62
+ return (
63
+ popover.open &&
64
+ React.createElement(
65
+ Portal,
66
+ { portal: true },
67
+ React.createElement(
68
+ Menu,
69
+ {
99
70
  id: `${id}-list`,
100
- style: { ...styles, ...style },
101
71
  setFocus: false,
102
72
  role: 'listbox',
103
73
  ref: refs,
104
74
  className: cx('iui-scroll', className),
105
- ...rest,
106
- })
107
- : React.createElement(VirtualizedComboBoxMenu, {
108
- ref: forwardedRef,
109
- ...props,
110
- }),
75
+ ...popover.getFloatingProps({
76
+ style: !enableVirtualization
77
+ ? style
78
+ : {
79
+ // set as constant because we don't want it shifting when items are unmounted
80
+ maxInlineSize: 0,
81
+ ...style,
82
+ },
83
+ ...rest,
84
+ }),
85
+ },
86
+ !enableVirtualization
87
+ ? children
88
+ : React.createElement(VirtualizedComboBoxMenu, null, children),
89
+ ),
90
+ )
111
91
  );
112
92
  });
113
93
  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;
@@ -3,11 +3,20 @@
3
3
  * See LICENSE.md in the project root for license terms and full copyright notice.
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  import * as React from 'react';
6
- import { Popover, mergeRefs } from '../utils/index.js';
6
+ import {
7
+ Popover,
8
+ useMergedRefs,
9
+ usePopover,
10
+ Portal,
11
+ cloneElementWithRef,
12
+ useControlledState,
13
+ mergeRefs,
14
+ mergeEventHandlers,
15
+ } from '../utils/index.js';
7
16
  import { Menu } from '../Menu/index.js';
8
17
  /**
9
18
  * Dropdown menu component.
10
- * Uses the {@link Popover} component, which is a wrapper around [tippy.js](https://atomiks.github.io/tippyjs).
19
+ * Built on top of the {@link Popover} component.
11
20
  * @example
12
21
  * const menuItems = (close: () => void) => [
13
22
  * <MenuItem key={1} onClick={onClick(1, close)}>
@@ -24,74 +33,73 @@ import { Menu } from '../Menu/index.js';
24
33
  * <Button>Menu</Button>
25
34
  * </DropdownMenu>
26
35
  */
27
- export const DropdownMenu = (props) => {
36
+ export const DropdownMenu = React.forwardRef((props, forwardedRef) => {
28
37
  const {
29
38
  menuItems,
30
39
  children,
31
- className,
32
- style,
33
40
  role = 'menu',
34
- visible,
41
+ visible: visibleProp,
35
42
  placement = 'bottom-start',
36
- onShow,
37
- onHide,
38
- trigger,
39
- id,
43
+ matchWidth = false,
44
+ onVisibleChange,
45
+ portal = true,
40
46
  ...rest
41
47
  } = props;
42
- const [isVisible, setIsVisible] = React.useState(visible ?? false);
43
- React.useEffect(() => {
44
- setIsVisible(visible ?? false);
45
- }, [visible]);
46
- const open = React.useCallback(() => setIsVisible(true), []);
47
- const close = React.useCallback(() => setIsVisible(false), []);
48
+ const [visible, setVisible] = useControlledState(
49
+ false,
50
+ visibleProp,
51
+ onVisibleChange,
52
+ );
53
+ const triggerRef = React.useRef(null);
54
+ const close = React.useCallback(() => {
55
+ setVisible(false);
56
+ triggerRef.current?.focus({ preventScroll: true });
57
+ }, [setVisible]);
48
58
  const menuContent = React.useMemo(() => {
49
59
  if (typeof menuItems === 'function') {
50
60
  return menuItems(close);
51
61
  }
52
62
  return menuItems;
53
63
  }, [menuItems, close]);
54
- const targetRef = React.useRef(null);
55
- const onShowHandler = React.useCallback(
56
- (instance) => {
57
- setIsVisible(true);
58
- onShow?.(instance);
59
- },
60
- [onShow],
61
- );
62
- const onHideHandler = React.useCallback(
63
- (instance) => {
64
- setIsVisible(false);
65
- targetRef.current?.focus();
66
- onHide?.(instance);
67
- },
68
- [onHide],
69
- );
64
+ const popover = usePopover({
65
+ visible,
66
+ onVisibleChange: (open) => (open ? setVisible(true) : close()),
67
+ placement,
68
+ matchWidth,
69
+ });
70
+ const popoverRef = useMergedRefs(forwardedRef, popover.refs.setFloating);
70
71
  return React.createElement(
71
- Popover,
72
- {
73
- content: React.createElement(
74
- Menu,
75
- { className: className, style: style, role: role, id: id },
76
- menuContent,
77
- ),
78
- visible: trigger === undefined ? isVisible : undefined,
79
- onClickOutside: close,
80
- placement: placement,
81
- onShow: onShowHandler,
82
- onHide: onHideHandler,
83
- trigger: visible === undefined ? trigger : undefined,
84
- ...rest,
85
- },
86
- React.isValidElement(children)
87
- ? React.cloneElement(children, {
88
- ref: mergeRefs(targetRef, props.children.ref),
89
- onClick: (args) => {
90
- trigger === undefined && (isVisible ? close() : open());
91
- children.props.onClick?.(args);
72
+ React.Fragment,
73
+ null,
74
+ cloneElementWithRef(children, (children) => ({
75
+ ...popover.getReferenceProps(children.props),
76
+ 'aria-expanded': popover.open,
77
+ ref: mergeRefs(triggerRef, popover.refs.setReference),
78
+ })),
79
+ popover.open &&
80
+ React.createElement(
81
+ Portal,
82
+ { portal: portal },
83
+ React.createElement(
84
+ Menu,
85
+ {
86
+ ...popover.getFloatingProps({
87
+ role,
88
+ ...rest,
89
+ onKeyDown: mergeEventHandlers(props.onKeyDown, (e) => {
90
+ if (e.defaultPrevented) {
91
+ return;
92
+ }
93
+ if (e.key === 'Tab') {
94
+ close();
95
+ }
96
+ }),
97
+ }),
98
+ ref: popoverRef,
92
99
  },
93
- })
94
- : React.createElement(React.Fragment, null),
100
+ menuContent,
101
+ ),
102
+ ),
95
103
  );
96
- };
104
+ });
97
105
  export default DropdownMenu;
@@ -27,8 +27,7 @@ export const 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,
@@ -46,8 +46,7 @@ export const HeaderSplitButton = React.forwardRef((props, forwardedRef) => {
46
46
  placement: menuPlacement,
47
47
  menuItems: menuItems,
48
48
  style: { minInlineSize: menuWidth },
49
- onShow: React.useCallback(() => setIsMenuOpen(true), []),
50
- onHide: React.useCallback(() => setIsMenuOpen(false), []),
49
+ onVisibleChange: (open) => setIsMenuOpen(open),
51
50
  },
52
51
  React.createElement(
53
52
  ButtonBase,