@itwin/itwinui-react 3.17.1 → 3.17.3

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 (35) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/DEV-cjs/core/Dialog/DialogMain.js +1 -1
  3. package/DEV-cjs/core/DropdownMenu/DropdownMenu.js +55 -27
  4. package/DEV-cjs/core/Menu/Menu.js +64 -45
  5. package/DEV-cjs/core/Menu/MenuItem.js +6 -0
  6. package/DEV-cjs/core/Tile/Tile.js +25 -27
  7. package/DEV-cjs/styles.js +1 -1
  8. package/DEV-cjs/utils/components/FocusTrap.js +23 -18
  9. package/DEV-esm/core/Dialog/DialogMain.js +1 -1
  10. package/DEV-esm/core/DropdownMenu/DropdownMenu.js +40 -24
  11. package/DEV-esm/core/Menu/Menu.js +57 -42
  12. package/DEV-esm/core/Menu/MenuItem.js +9 -0
  13. package/DEV-esm/core/Tile/Tile.js +29 -28
  14. package/DEV-esm/styles.js +1 -1
  15. package/DEV-esm/utils/components/FocusTrap.js +23 -18
  16. package/cjs/core/Dialog/DialogMain.js +1 -1
  17. package/cjs/core/DropdownMenu/DropdownMenu.d.ts +10 -0
  18. package/cjs/core/DropdownMenu/DropdownMenu.js +55 -27
  19. package/cjs/core/Menu/Menu.d.ts +9 -1
  20. package/cjs/core/Menu/Menu.js +64 -45
  21. package/cjs/core/Menu/MenuItem.js +6 -0
  22. package/cjs/core/Tile/Tile.js +25 -27
  23. package/cjs/styles.js +1 -1
  24. package/cjs/utils/components/FocusTrap.js +23 -18
  25. package/esm/core/Dialog/DialogMain.js +1 -1
  26. package/esm/core/DropdownMenu/DropdownMenu.d.ts +10 -0
  27. package/esm/core/DropdownMenu/DropdownMenu.js +40 -24
  28. package/esm/core/Menu/Menu.d.ts +9 -1
  29. package/esm/core/Menu/Menu.js +57 -42
  30. package/esm/core/Menu/MenuItem.js +9 -0
  31. package/esm/core/Tile/Tile.js +29 -28
  32. package/esm/styles.js +1 -1
  33. package/esm/utils/components/FocusTrap.js +23 -18
  34. package/package.json +1 -1
  35. package/styles.css +11 -11
@@ -30,34 +30,50 @@ let DropdownMenuContent = React.forwardRef((props, forwardedRef) => {
30
30
  visibleProp,
31
31
  onVisibleChange,
32
32
  );
33
+ let close = React.useCallback(() => {
34
+ setVisible(false);
35
+ }, [setVisible]);
33
36
  let menuContent = React.useMemo(() => {
34
- if ('function' == typeof menuItems)
35
- return menuItems(() => setVisible(false));
37
+ if ('function' == typeof menuItems) return menuItems(close);
36
38
  return menuItems;
37
- }, [menuItems, setVisible]);
39
+ }, [close, menuItems]);
40
+ let dropdownMenuContextValue = React.useMemo(
41
+ () => ({
42
+ close,
43
+ }),
44
+ [close],
45
+ );
38
46
  return React.createElement(
39
- Menu,
47
+ DropdownMenuContext.Provider,
40
48
  {
41
- trigger: children,
42
- onKeyDown: mergeEventHandlers(props.onKeyDown, (e) => {
43
- if (e.defaultPrevented) return;
44
- if ('Tab' === e.key) setVisible(false);
45
- }),
46
- role: role,
47
- ref: forwardedRef,
48
- portal: portal,
49
- popoverProps: React.useMemo(
50
- () => ({
51
- placement,
52
- matchWidth,
53
- visible,
54
- onVisibleChange: setVisible,
55
- middleware,
56
- }),
57
- [matchWidth, middleware, placement, setVisible, visible],
58
- ),
59
- ...rest,
49
+ value: dropdownMenuContextValue,
60
50
  },
61
- menuContent,
51
+ React.createElement(
52
+ Menu,
53
+ {
54
+ trigger: children,
55
+ onKeyDown: mergeEventHandlers(props.onKeyDown, (e) => {
56
+ if (e.defaultPrevented) return;
57
+ if ('Tab' === e.key) setVisible(false);
58
+ }),
59
+ role: role,
60
+ ref: forwardedRef,
61
+ portal: portal,
62
+ popoverProps: React.useMemo(
63
+ () => ({
64
+ placement,
65
+ matchWidth,
66
+ visible,
67
+ onVisibleChange: setVisible,
68
+ middleware,
69
+ }),
70
+ [matchWidth, middleware, placement, setVisible, visible],
71
+ ),
72
+ ...rest,
73
+ },
74
+ menuContent,
75
+ ),
62
76
  );
63
77
  });
78
+ export const DropdownMenuContext = React.createContext(void 0);
79
+ export const DropdownMenuCloseOnClickContext = React.createContext(void 0);
@@ -28,7 +28,12 @@ type MenuProps = {
28
28
  listNavigation?: Partial<UseListNavigationProps>;
29
29
  };
30
30
  };
31
- } & Pick<PortalProps, 'portal'>;
31
+ /**
32
+ * If not passed, uses the `portal` from its parent `Menu`.
33
+ * @see {@link PortalProps.portal} for docs on the prop.
34
+ */
35
+ portal?: PortalProps['portal'];
36
+ };
32
37
  /**
33
38
  * @private
34
39
  *
@@ -92,4 +97,7 @@ export declare const MenuContext: React.Context<{
92
97
  popoverGetItemProps: PopoverGetItemProps;
93
98
  focusableElements: HTMLElement[];
94
99
  } | undefined>;
100
+ export declare const MenuPortalContext: React.Context<boolean | {
101
+ to: HTMLElement | null | undefined | (() => HTMLElement | null | undefined);
102
+ } | undefined>;
95
103
  export {};
@@ -25,11 +25,13 @@ export const Menu = React.forwardRef((props, ref) => {
25
25
  className,
26
26
  trigger,
27
27
  positionReference,
28
- portal = true,
28
+ portal: portalProp,
29
29
  popoverProps: popoverPropsProp,
30
30
  children,
31
31
  ...rest
32
32
  } = props;
33
+ let menuPortalContext = React.useContext(MenuPortalContext);
34
+ let portal = portalProp ?? menuPortalContext;
33
35
  let tree = useFloatingTree();
34
36
  let nodeId = useFloatingNodeId();
35
37
  let parentId = useFloatingParentNodeId();
@@ -125,32 +127,35 @@ export const Menu = React.forwardRef((props, ref) => {
125
127
  () => void 0,
126
128
  () => void 0,
127
129
  );
128
- let popoverGetItemProps = ({ focusableItemIndex, userProps }) =>
129
- getItemProps({
130
- ...userProps,
131
- tabIndex:
132
- null != activeIndex &&
133
- activeIndex >= 0 &&
134
- null != focusableItemIndex &&
135
- focusableItemIndex >= 0 &&
136
- activeIndex === focusableItemIndex
137
- ? 0
138
- : -1,
139
- onFocus: mergeEventHandlers(userProps?.onFocus, () => {
140
- queueMicrotask(() => {
141
- setHasFocusedNodeInSubmenu(true);
142
- });
143
- tree?.events.emit('onNodeFocused', {
144
- nodeId: nodeId,
145
- parentId: parentId,
146
- });
130
+ let popoverGetItemProps = React.useCallback(
131
+ ({ focusableItemIndex, userProps }) =>
132
+ getItemProps({
133
+ ...userProps,
134
+ tabIndex:
135
+ null != activeIndex &&
136
+ activeIndex >= 0 &&
137
+ null != focusableItemIndex &&
138
+ focusableItemIndex >= 0 &&
139
+ activeIndex === focusableItemIndex
140
+ ? 0
141
+ : -1,
142
+ onFocus: mergeEventHandlers(userProps?.onFocus, () => {
143
+ queueMicrotask(() => {
144
+ setHasFocusedNodeInSubmenu(true);
145
+ });
146
+ tree?.events.emit('onNodeFocused', {
147
+ nodeId: nodeId,
148
+ parentId: parentId,
149
+ });
150
+ }),
151
+ onMouseEnter: mergeEventHandlers(userProps?.onMouseEnter, (event) => {
152
+ if (null != focusableItemIndex && focusableItemIndex >= 0)
153
+ setActiveIndex(focusableItemIndex);
154
+ if (event.target === event.currentTarget) event.currentTarget.focus();
155
+ }),
147
156
  }),
148
- onMouseEnter: mergeEventHandlers(userProps?.onMouseEnter, (event) => {
149
- if (null != focusableItemIndex && focusableItemIndex >= 0)
150
- setActiveIndex(focusableItemIndex);
151
- if (event.target === event.currentTarget) event.currentTarget.focus();
152
- }),
153
- });
157
+ [activeIndex, getItemProps, nodeId, parentId, tree?.events],
158
+ );
154
159
  let reference = cloneElementWithRef(trigger, (triggerChild) =>
155
160
  getReferenceProps(
156
161
  popover.getReferenceProps({
@@ -189,28 +194,38 @@ export const Menu = React.forwardRef((props, ref) => {
189
194
  React.createElement(
190
195
  MenuContext.Provider,
191
196
  {
192
- value: {
193
- popoverGetItemProps,
194
- focusableElements,
195
- },
197
+ value: React.useMemo(
198
+ () => ({
199
+ popoverGetItemProps,
200
+ focusableElements,
201
+ }),
202
+ [focusableElements, popoverGetItemProps],
203
+ ),
196
204
  },
197
205
  React.createElement(
198
- PopoverOpenContext.Provider,
206
+ MenuPortalContext.Provider,
199
207
  {
200
- value: popover.open,
208
+ value: portal,
201
209
  },
202
- reference,
210
+ React.createElement(
211
+ PopoverOpenContext.Provider,
212
+ {
213
+ value: popover.open,
214
+ },
215
+ reference,
216
+ ),
217
+ null != tree
218
+ ? React.createElement(
219
+ FloatingNode,
220
+ {
221
+ id: nodeId,
222
+ },
223
+ floating,
224
+ )
225
+ : floating,
203
226
  ),
204
- null != tree
205
- ? React.createElement(
206
- FloatingNode,
207
- {
208
- id: nodeId,
209
- },
210
- floating,
211
- )
212
- : floating,
213
227
  ),
214
228
  );
215
229
  });
216
230
  export const MenuContext = React.createContext(void 0);
231
+ export const MenuPortalContext = React.createContext(void 0);
@@ -8,6 +8,10 @@ import {
8
8
  import { Menu, MenuContext } from './Menu.js';
9
9
  import { ListItem } from '../List/ListItem.js';
10
10
  import cx from 'classnames';
11
+ import {
12
+ DropdownMenuCloseOnClickContext,
13
+ DropdownMenuContext,
14
+ } from '../DropdownMenu/DropdownMenu.js';
11
15
  export const MenuItem = React.forwardRef((props, forwardedRef) => {
12
16
  let {
13
17
  className,
@@ -28,6 +32,10 @@ export const MenuItem = React.forwardRef((props, forwardedRef) => {
28
32
  } = props;
29
33
  let logWarning = useWarningLogger();
30
34
  let parentMenu = React.useContext(MenuContext);
35
+ let dropdownMenu = React.useContext(DropdownMenuContext);
36
+ let shouldCloseMenuOnClick = React.useContext(
37
+ DropdownMenuCloseOnClickContext,
38
+ );
31
39
  let menuItemRef = React.useRef(null);
32
40
  let submenuId = useId();
33
41
  let popoverProps = React.useMemo(
@@ -46,6 +54,7 @@ export const MenuItem = React.forwardRef((props, forwardedRef) => {
46
54
  );
47
55
  let onClick = () => {
48
56
  if (disabled) return;
57
+ if (shouldCloseMenuOnClick) dropdownMenu?.close();
49
58
  onClickProp?.(value);
50
59
  };
51
60
  let handlers = {
@@ -10,7 +10,10 @@ import {
10
10
  polymorphic,
11
11
  Box,
12
12
  } from '../../utils/index.js';
13
- import { DropdownMenu } from '../DropdownMenu/DropdownMenu.js';
13
+ import {
14
+ DropdownMenu,
15
+ DropdownMenuCloseOnClickContext,
16
+ } from '../DropdownMenu/DropdownMenu.js';
14
17
  import { IconButton } from '../Buttons/IconButton.js';
15
18
  import { ProgressRadial } from '../ProgressIndicators/ProgressRadial.js';
16
19
  import { LinkAction } from '../LinkAction/LinkAction.js';
@@ -174,41 +177,39 @@ let TileMoreOptions = React.forwardRef((props, forwardedRef) => {
174
177
  let { className, children = [], buttonProps, ...rest } = props;
175
178
  let [isMenuVisible, setIsMenuVisible] = React.useState(false);
176
179
  return React.createElement(
177
- Box,
180
+ DropdownMenuCloseOnClickContext.Provider,
178
181
  {
179
- className: cx(
180
- 'iui-tile-more-options',
181
- {
182
- 'iui-visible': isMenuVisible,
183
- },
184
- className,
185
- ),
186
- ref: forwardedRef,
187
- ...rest,
182
+ value: true,
188
183
  },
189
184
  React.createElement(
190
- DropdownMenu,
185
+ Box,
191
186
  {
192
- onVisibleChange: setIsMenuVisible,
193
- menuItems: (close) =>
194
- children?.map((option) =>
195
- React.cloneElement(option, {
196
- onClick: (value) => {
197
- close();
198
- option.props.onClick?.(value);
199
- },
200
- }),
201
- ),
187
+ className: cx(
188
+ 'iui-tile-more-options',
189
+ {
190
+ 'iui-visible': isMenuVisible,
191
+ },
192
+ className,
193
+ ),
194
+ ref: forwardedRef,
195
+ ...rest,
202
196
  },
203
197
  React.createElement(
204
- IconButton,
198
+ DropdownMenu,
205
199
  {
206
- styleType: 'borderless',
207
- size: 'small',
208
- 'aria-label': 'More options',
209
- ...buttonProps,
200
+ onVisibleChange: setIsMenuVisible,
201
+ menuItems: children,
210
202
  },
211
- React.createElement(SvgMore, null),
203
+ React.createElement(
204
+ IconButton,
205
+ {
206
+ styleType: 'borderless',
207
+ size: 'small',
208
+ 'aria-label': 'More options',
209
+ ...buttonProps,
210
+ },
211
+ React.createElement(SvgMore, null),
212
+ ),
212
213
  ),
213
214
  ),
214
215
  );
package/esm/styles.js CHANGED
@@ -1,4 +1,4 @@
1
- const t = '3.17.1';
1
+ const t = '3.17.3';
2
2
  const u = new Proxy(
3
3
  {},
4
4
  {
@@ -1,36 +1,41 @@
1
1
  import * as React from 'react';
2
2
  import { getTabbableElements } from '../functions/focusable.js';
3
- import { cloneElementWithRef } from '../functions/react.js';
4
3
  export const FocusTrap = (props) => {
5
4
  let { children } = props;
6
- let childRef = React.useRef(void 0);
7
- let getFirstLastFocusables = () => {
8
- let elements = getTabbableElements(childRef.current);
5
+ let firstFocusTrapRef = React.useRef(null);
6
+ let getFirstLastFocusables = React.useCallback(() => {
7
+ let childrenElement = firstFocusTrapRef.current?.nextElementSibling;
8
+ let elements = getTabbableElements(childrenElement);
9
9
  let firstElement = elements[0];
10
10
  let lastElement = elements[(elements.length || 1) - 1];
11
11
  return [firstElement, lastElement];
12
- };
13
- let onFirstFocus = (event) => {
14
- let [firstElement, lastElement] = getFirstLastFocusables();
15
- if (event.relatedTarget === firstElement) lastElement?.focus();
16
- else firstElement?.focus();
17
- };
18
- let onLastFocus = (event) => {
19
- let [firstElement, lastElement] = getFirstLastFocusables();
20
- if (event.relatedTarget === lastElement) firstElement?.focus();
21
- else lastElement?.focus();
22
- };
12
+ }, []);
13
+ let onFirstFocus = React.useCallback(
14
+ (event) => {
15
+ let [firstElement, lastElement] = getFirstLastFocusables();
16
+ if (event.relatedTarget === firstElement) lastElement?.focus();
17
+ else firstElement?.focus();
18
+ },
19
+ [getFirstLastFocusables],
20
+ );
21
+ let onLastFocus = React.useCallback(
22
+ (event) => {
23
+ let [firstElement, lastElement] = getFirstLastFocusables();
24
+ if (event.relatedTarget === lastElement) firstElement?.focus();
25
+ else lastElement?.focus();
26
+ },
27
+ [getFirstLastFocusables],
28
+ );
23
29
  return React.createElement(
24
30
  React.Fragment,
25
31
  null,
26
32
  React.createElement('div', {
33
+ ref: firstFocusTrapRef,
27
34
  tabIndex: 0,
28
35
  onFocus: onFirstFocus,
29
36
  'aria-hidden': true,
30
37
  }),
31
- cloneElementWithRef(children, () => ({
32
- ref: childRef,
33
- })),
38
+ children,
34
39
  React.createElement('div', {
35
40
  tabIndex: 0,
36
41
  onFocus: onLastFocus,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itwin/itwinui-react",
3
- "version": "3.17.1",
3
+ "version": "3.17.3",
4
4
  "author": "Bentley Systems",
5
5
  "license": "MIT",
6
6
  "type": "module",