@itwin/itwinui-react 3.9.1 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/cjs/core/Breadcrumbs/Breadcrumbs.js +2 -3
- package/cjs/core/Buttons/Button.js +1 -1
- package/cjs/core/Buttons/IconButton.js +1 -1
- package/cjs/core/Buttons/IdeasButton.js +6 -2
- package/cjs/core/ComboBox/ComboBox.js +1 -1
- package/cjs/core/DropdownMenu/DropdownMenu.js +36 -13
- package/cjs/core/Input/Input.js +1 -1
- package/cjs/core/LabeledSelect/LabeledSelect.d.ts +26 -4
- package/cjs/core/Menu/Menu.js +9 -0
- package/cjs/core/Menu/MenuItem.d.ts +12 -0
- package/cjs/core/Menu/MenuItem.js +105 -66
- package/cjs/core/NotificationMarker/NotificationMarker.d.ts +7 -6
- package/cjs/core/Popover/Popover.d.ts +32 -9
- package/cjs/core/Popover/Popover.js +65 -17
- package/cjs/core/Select/Select.js +2 -3
- package/cjs/core/SideNavigation/SideNavigation.js +1 -1
- package/cjs/core/Table/TablePaginator.js +1 -3
- package/cjs/core/Table/columns/selectionColumn.js +10 -1
- package/cjs/core/Table/hooks/useSubRowSelection.js +1 -1
- package/cjs/core/ThemeProvider/ThemeProvider.js +53 -17
- package/cjs/core/TimePicker/TimePicker.js +12 -12
- package/cjs/core/ToggleSwitch/ToggleSwitch.d.ts +4 -0
- package/cjs/core/ToggleSwitch/ToggleSwitch.js +2 -2
- package/cjs/utils/components/Portal.d.ts +6 -2
- package/cjs/utils/components/Portal.js +11 -14
- package/cjs/utils/providers/ScopeProvider.d.ts +26 -0
- package/cjs/utils/providers/ScopeProvider.js +77 -0
- package/cjs/utils/providers/index.d.ts +1 -0
- package/cjs/utils/providers/index.js +1 -0
- package/esm/core/Breadcrumbs/Breadcrumbs.js +2 -3
- package/esm/core/Buttons/Button.js +1 -1
- package/esm/core/Buttons/IconButton.js +1 -1
- package/esm/core/Buttons/IdeasButton.js +3 -2
- package/esm/core/ComboBox/ComboBox.js +1 -1
- package/esm/core/DropdownMenu/DropdownMenu.js +36 -13
- package/esm/core/Input/Input.js +1 -1
- package/esm/core/LabeledSelect/LabeledSelect.d.ts +26 -4
- package/esm/core/Menu/Menu.js +9 -0
- package/esm/core/Menu/MenuItem.d.ts +12 -0
- package/esm/core/Menu/MenuItem.js +105 -66
- package/esm/core/NotificationMarker/NotificationMarker.d.ts +7 -6
- package/esm/core/Popover/Popover.d.ts +32 -9
- package/esm/core/Popover/Popover.js +68 -20
- package/esm/core/Select/Select.js +2 -3
- package/esm/core/SideNavigation/SideNavigation.js +1 -1
- package/esm/core/Table/TablePaginator.js +2 -4
- package/esm/core/Table/columns/selectionColumn.js +10 -1
- package/esm/core/Table/hooks/useSubRowSelection.js +1 -1
- package/esm/core/ThemeProvider/ThemeProvider.js +54 -18
- package/esm/core/TimePicker/TimePicker.js +12 -12
- package/esm/core/ToggleSwitch/ToggleSwitch.d.ts +4 -0
- package/esm/core/ToggleSwitch/ToggleSwitch.js +2 -2
- package/esm/utils/components/Portal.d.ts +6 -2
- package/esm/utils/components/Portal.js +9 -8
- package/esm/utils/providers/ScopeProvider.d.ts +26 -0
- package/esm/utils/providers/ScopeProvider.js +48 -0
- package/esm/utils/providers/index.d.ts +1 -0
- package/esm/utils/providers/index.js +1 -0
- package/package.json +2 -1
- package/styles.css +1 -1
|
@@ -6,6 +6,8 @@ import * as React from 'react';
|
|
|
6
6
|
import { useMergedRefs, Portal, cloneElementWithRef, useControlledState, mergeRefs, mergeEventHandlers, } from '../../utils/index.js';
|
|
7
7
|
import { Menu } from '../Menu/Menu.js';
|
|
8
8
|
import { usePopover } from '../Popover/Popover.js';
|
|
9
|
+
import { FloatingNode, FloatingTree, useFloatingNodeId, } from '@floating-ui/react';
|
|
10
|
+
import { MenuItemContext } from '../Menu/MenuItem.js';
|
|
9
11
|
/**
|
|
10
12
|
* Dropdown menu component.
|
|
11
13
|
* Built on top of the {@link Popover} component.
|
|
@@ -26,6 +28,11 @@ import { usePopover } from '../Popover/Popover.js';
|
|
|
26
28
|
* </DropdownMenu>
|
|
27
29
|
*/
|
|
28
30
|
export const DropdownMenu = React.forwardRef((props, forwardedRef) => {
|
|
31
|
+
return (React.createElement(FloatingTree, null,
|
|
32
|
+
React.createElement(DropdownMenuContent, { ref: forwardedRef, ...props })));
|
|
33
|
+
});
|
|
34
|
+
// ----------------------------------------------------------------------------
|
|
35
|
+
const DropdownMenuContent = React.forwardRef((props, forwardedRef) => {
|
|
29
36
|
const { menuItems, children, role = 'menu', visible: visibleProp, placement = 'bottom-start', matchWidth = false, onVisibleChange, portal = true, ...rest } = props;
|
|
30
37
|
const [visible, setVisible] = useControlledState(false, visibleProp, onVisibleChange);
|
|
31
38
|
const triggerRef = React.useRef(null);
|
|
@@ -39,11 +46,23 @@ export const DropdownMenu = React.forwardRef((props, forwardedRef) => {
|
|
|
39
46
|
}
|
|
40
47
|
return menuItems;
|
|
41
48
|
}, [menuItems, close]);
|
|
49
|
+
const [currentFocusedNodeIndex, setCurrentFocusedNodeIndex] = React.useState(null);
|
|
50
|
+
const focusableNodes = React.useRef([]);
|
|
51
|
+
const nodeId = useFloatingNodeId();
|
|
42
52
|
const popover = usePopover({
|
|
53
|
+
nodeId,
|
|
43
54
|
visible,
|
|
44
55
|
onVisibleChange: (open) => (open ? setVisible(true) : close()),
|
|
45
56
|
placement,
|
|
46
57
|
matchWidth,
|
|
58
|
+
interactions: {
|
|
59
|
+
listNavigation: {
|
|
60
|
+
activeIndex: currentFocusedNodeIndex,
|
|
61
|
+
onNavigate: setCurrentFocusedNodeIndex,
|
|
62
|
+
listRef: focusableNodes,
|
|
63
|
+
focusItemOnOpen: true,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
47
66
|
});
|
|
48
67
|
const popoverRef = useMergedRefs(forwardedRef, popover.refs.setFloating);
|
|
49
68
|
return (React.createElement(React.Fragment, null,
|
|
@@ -52,17 +71,21 @@ export const DropdownMenu = React.forwardRef((props, forwardedRef) => {
|
|
|
52
71
|
'aria-expanded': popover.open,
|
|
53
72
|
ref: mergeRefs(triggerRef, popover.refs.setReference),
|
|
54
73
|
})),
|
|
55
|
-
popover.open && (React.createElement(Portal, { portal: portal },
|
|
56
|
-
React.createElement(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
React.createElement(FloatingNode, { id: nodeId }, popover.open && (React.createElement(Portal, { portal: portal },
|
|
75
|
+
React.createElement(MenuItemContext.Provider, { value: {
|
|
76
|
+
setCurrentFocusedNodeIndex,
|
|
77
|
+
focusableNodes,
|
|
78
|
+
} },
|
|
79
|
+
React.createElement(Menu, { setFocus: false, ...popover.getFloatingProps({
|
|
80
|
+
role,
|
|
81
|
+
...rest,
|
|
82
|
+
onKeyDown: mergeEventHandlers(props.onKeyDown, (e) => {
|
|
83
|
+
if (e.defaultPrevented) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (e.key === 'Tab') {
|
|
87
|
+
close();
|
|
88
|
+
}
|
|
89
|
+
}),
|
|
90
|
+
}), ref: popoverRef }, menuContent)))))));
|
|
68
91
|
});
|
package/esm/core/Input/Input.js
CHANGED
|
@@ -15,5 +15,5 @@ export const Input = React.forwardRef((props, ref) => {
|
|
|
15
15
|
const { size, htmlSize, status, className, ...rest } = props;
|
|
16
16
|
const inputRef = React.useRef(null);
|
|
17
17
|
const refs = useMergedRefs(inputRef, ref);
|
|
18
|
-
return (React.createElement(Box, { as: 'input', className: cx('iui-input', className), "data-iui-size": size, "data-iui-status": status, size: htmlSize, ref: refs, ...rest }));
|
|
18
|
+
return (React.createElement(Box, { as: 'input', className: cx('iui-input', 'iui-field', className), "data-iui-size": size, "data-iui-status": status, size: htmlSize, ref: refs, ...rest }));
|
|
19
19
|
});
|
|
@@ -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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
}
|
|
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;
|
package/esm/core/Menu/Menu.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import * as React from 'react';
|
|
6
6
|
import cx from 'classnames';
|
|
7
7
|
import { useMergedRefs, getFocusableElements, Box, mergeEventHandlers, } from '../../utils/index.js';
|
|
8
|
+
import { MenuItemContext } from './MenuItem.js';
|
|
8
9
|
/**
|
|
9
10
|
* Basic menu component. Can be used for select or dropdown components.
|
|
10
11
|
*/
|
|
@@ -13,11 +14,19 @@ export const Menu = React.forwardRef((props, ref) => {
|
|
|
13
14
|
const [focusedIndex, setFocusedIndex] = React.useState();
|
|
14
15
|
const menuRef = React.useRef(null);
|
|
15
16
|
const refs = useMergedRefs(menuRef, ref);
|
|
17
|
+
const menuItemContext = React.useContext(MenuItemContext);
|
|
16
18
|
const getFocusableNodes = React.useCallback(() => {
|
|
17
19
|
const focusableItems = getFocusableElements(menuRef.current);
|
|
18
20
|
// Filter out focusable elements that are inside each menu item, e.g. checkbox, anchor
|
|
19
21
|
return focusableItems.filter((i) => !focusableItems.some((p) => p.contains(i.parentElement)));
|
|
20
22
|
}, []);
|
|
23
|
+
React.useEffect(() => {
|
|
24
|
+
const focusableNodes = getFocusableNodes();
|
|
25
|
+
if (menuItemContext != null &&
|
|
26
|
+
menuItemContext.focusableNodes.current !== focusableNodes) {
|
|
27
|
+
menuItemContext.focusableNodes.current = focusableNodes;
|
|
28
|
+
}
|
|
29
|
+
}, [getFocusableNodes, menuItemContext]);
|
|
21
30
|
React.useEffect(() => {
|
|
22
31
|
const items = getFocusableNodes();
|
|
23
32
|
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
|
+
};
|
|
@@ -3,37 +3,76 @@
|
|
|
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 { SvgCaretRightSmall, Portal, useMergedRefs, useId, } from '../../utils/index.js';
|
|
6
|
+
import { SvgCaretRightSmall, Portal, useMergedRefs, useId, useSyncExternalStore, createWarningLogger, } from '../../utils/index.js';
|
|
7
7
|
import { Menu } from './Menu.js';
|
|
8
8
|
import { ListItem } from '../List/ListItem.js';
|
|
9
|
-
import { flushSync } from 'react-dom';
|
|
10
9
|
import { usePopover } from '../Popover/Popover.js';
|
|
10
|
+
import { FloatingNode, useFloatingNodeId, useFloatingParentNodeId, useFloatingTree, } from '@floating-ui/react';
|
|
11
|
+
const logWarningInDev = createWarningLogger();
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
+
* Should be wrapped around the `Menu` containing the `MenuItem`s.
|
|
13
14
|
*/
|
|
14
|
-
const MenuItemContext = React.createContext({
|
|
15
|
+
export const MenuItemContext = React.createContext({
|
|
16
|
+
setCurrentFocusedNodeIndex: () => { },
|
|
17
|
+
focusableNodes: { current: [] },
|
|
18
|
+
setHasFocusedNodeInSubmenu: undefined,
|
|
19
|
+
});
|
|
15
20
|
/**
|
|
16
21
|
* Basic menu item component. Should be used inside `Menu` component for each item.
|
|
17
22
|
*/
|
|
18
23
|
export const MenuItem = React.forwardRef((props, forwardedRef) => {
|
|
19
|
-
const { children, isSelected, disabled, value, onClick, sublabel, size = !!sublabel ? 'large' : 'default', icon, startIcon = icon, badge, endIcon = badge, role = 'menuitem', subMenuItems = [], ...rest } = props;
|
|
24
|
+
const { children, isSelected, disabled, value, onClick: onClickProp, sublabel, size = !!sublabel ? 'large' : 'default', icon, startIcon = icon, badge, endIcon = badge, role = 'menuitem', subMenuItems = [], ...rest } = props;
|
|
25
|
+
if (onClickProp != null && subMenuItems.length > 0) {
|
|
26
|
+
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.');
|
|
27
|
+
}
|
|
28
|
+
const parent = React.useContext(MenuItemContext);
|
|
20
29
|
const menuItemRef = React.useRef(null);
|
|
21
|
-
const [focusOnSubmenu, setFocusOnSubmenu] = React.useState(false);
|
|
22
30
|
const submenuId = useId();
|
|
23
31
|
const [isSubmenuVisible, setIsSubmenuVisible] = React.useState(false);
|
|
24
|
-
const [
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
const [currentFocusedNodeIndex, setCurrentFocusedNodeIndex] = React.useState(null);
|
|
33
|
+
const [hasFocusedNodeInSubmenu, setHasFocusedNodeInSubmenu] = React.useState(false);
|
|
34
|
+
const nodeId = useFloatingNodeId();
|
|
35
|
+
const tree = useFloatingTree();
|
|
36
|
+
const parentId = useFloatingParentNodeId();
|
|
37
|
+
const focusableNodeIndexInParentTree = parent.focusableNodes.current.findIndex((el) => el === menuItemRef.current);
|
|
38
|
+
useSyncExternalStore(React.useCallback(() => {
|
|
39
|
+
const closeUnrelatedMenus = (event) => {
|
|
40
|
+
if (
|
|
41
|
+
// When a node "X" is focused, close "X"'s siblings' submenus
|
|
42
|
+
// i.e. only one submenu in each menu can be open at a time
|
|
43
|
+
(parentId === event.parentId && nodeId !== event.nodeId) ||
|
|
44
|
+
// Consider a node "X" with its submenu "Y".
|
|
45
|
+
// Focusing "X" should close all submenus of "Y".
|
|
46
|
+
parentId === event.nodeId) {
|
|
47
|
+
setIsSubmenuVisible(false);
|
|
48
|
+
setHasFocusedNodeInSubmenu(false);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
tree?.events.on('onNodeFocused', closeUnrelatedMenus);
|
|
52
|
+
return () => {
|
|
53
|
+
tree?.events.off('onNodeFocused', closeUnrelatedMenus);
|
|
54
|
+
};
|
|
55
|
+
}, [nodeId, parentId, tree?.events]), () => undefined, () => undefined);
|
|
56
|
+
const focusableNodes = React.useRef([]);
|
|
32
57
|
const popover = usePopover({
|
|
33
|
-
|
|
34
|
-
|
|
58
|
+
nodeId,
|
|
59
|
+
visible: isSubmenuVisible,
|
|
60
|
+
onVisibleChange: setIsSubmenuVisible,
|
|
35
61
|
placement: 'right-start',
|
|
36
|
-
|
|
62
|
+
interactions: !disabled
|
|
63
|
+
? {
|
|
64
|
+
click: false,
|
|
65
|
+
hover: {
|
|
66
|
+
enabled: !hasFocusedNodeInSubmenu, // If focus is still inside submenu, don't close the submenu upon hovering out.
|
|
67
|
+
},
|
|
68
|
+
listNavigation: {
|
|
69
|
+
listRef: focusableNodes,
|
|
70
|
+
activeIndex: currentFocusedNodeIndex,
|
|
71
|
+
nested: subMenuItems.length > 0,
|
|
72
|
+
onNavigate: setCurrentFocusedNodeIndex,
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
: {},
|
|
37
76
|
});
|
|
38
77
|
const onKeyDown = (event) => {
|
|
39
78
|
if (event.altKey) {
|
|
@@ -43,61 +82,61 @@ export const MenuItem = React.forwardRef((props, forwardedRef) => {
|
|
|
43
82
|
case 'Enter':
|
|
44
83
|
case ' ':
|
|
45
84
|
case 'Spacebar': {
|
|
46
|
-
|
|
47
|
-
event.preventDefault();
|
|
48
|
-
break;
|
|
49
|
-
}
|
|
50
|
-
case 'ArrowRight': {
|
|
51
|
-
if (subMenuItems.length > 0) {
|
|
52
|
-
setIsSubmenuVisible(true);
|
|
53
|
-
// flush and reset state so we are ready to focus again next time
|
|
54
|
-
flushSync(() => setFocusOnSubmenu(true));
|
|
55
|
-
setFocusOnSubmenu(false);
|
|
56
|
-
event.preventDefault();
|
|
57
|
-
event.stopPropagation();
|
|
58
|
-
}
|
|
59
|
-
break;
|
|
60
|
-
}
|
|
61
|
-
case 'ArrowLeft': {
|
|
62
|
-
if (parent.ref) {
|
|
63
|
-
parent.ref.current?.focus();
|
|
64
|
-
parent.setIsNestedSubmenuVisible(false);
|
|
65
|
-
}
|
|
66
|
-
event.stopPropagation();
|
|
85
|
+
onClick();
|
|
67
86
|
event.preventDefault();
|
|
68
87
|
break;
|
|
69
88
|
}
|
|
70
|
-
case 'Escape': {
|
|
71
|
-
// focus might get lost if submenu closes so move it back to parent
|
|
72
|
-
parent.ref?.current?.focus();
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
89
|
default:
|
|
76
90
|
break;
|
|
77
91
|
}
|
|
78
92
|
};
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
const onMouseEnter = (e) => {
|
|
94
|
+
// Focus the item when hovered.
|
|
95
|
+
if (e.target === e.currentTarget) {
|
|
96
|
+
menuItemRef.current?.focus();
|
|
97
|
+
// Since we manually focus the MenuItem on hover, we need to manually update the active index for
|
|
98
|
+
// Floating UI's keyboard navigation to work correctly.
|
|
99
|
+
if (parent != null && focusableNodeIndexInParentTree != null) {
|
|
100
|
+
parent.setCurrentFocusedNodeIndex(focusableNodeIndexInParentTree);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const onFocus = () => {
|
|
105
|
+
parent.setHasFocusedNodeInSubmenu?.(true);
|
|
106
|
+
tree?.events.emit('onNodeFocused', {
|
|
107
|
+
nodeId,
|
|
108
|
+
parentId,
|
|
109
|
+
});
|
|
82
110
|
};
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
111
|
+
const onClick = () => {
|
|
112
|
+
onClickProp?.(value);
|
|
113
|
+
setIsSubmenuVisible((prev) => !prev);
|
|
114
|
+
};
|
|
115
|
+
const handlers = !disabled
|
|
116
|
+
? {
|
|
117
|
+
onClick,
|
|
118
|
+
onKeyDown,
|
|
119
|
+
onMouseEnter,
|
|
120
|
+
onFocus,
|
|
121
|
+
}
|
|
122
|
+
: {};
|
|
123
|
+
return (React.createElement(React.Fragment, null,
|
|
124
|
+
React.createElement(ListItem, { as: 'div', actionable: true, size: size, active: isSelected, disabled: disabled, ref: 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
|
|
125
|
+
? { ...handlers, ...rest }
|
|
126
|
+
: popover.getReferenceProps({ ...handlers, ...rest })) },
|
|
127
|
+
startIcon && (React.createElement(ListItem.Icon, { as: 'span', "aria-hidden": true }, startIcon)),
|
|
128
|
+
React.createElement(ListItem.Content, null,
|
|
129
|
+
React.createElement("div", null, children),
|
|
130
|
+
sublabel && React.createElement(ListItem.Description, null, sublabel)),
|
|
131
|
+
!endIcon && subMenuItems.length > 0 && (React.createElement(ListItem.Icon, { as: 'span', "aria-hidden": true },
|
|
132
|
+
React.createElement(SvgCaretRightSmall, null))),
|
|
133
|
+
endIcon && (React.createElement(ListItem.Icon, { as: 'span', "aria-hidden": true }, endIcon))),
|
|
134
|
+
subMenuItems.length > 0 && !disabled && popover.open && (React.createElement(FloatingNode, { id: nodeId },
|
|
135
|
+
React.createElement(Portal, null,
|
|
136
|
+
React.createElement(MenuItemContext.Provider, { value: {
|
|
137
|
+
setCurrentFocusedNodeIndex,
|
|
138
|
+
focusableNodes,
|
|
139
|
+
setHasFocusedNodeInSubmenu,
|
|
140
|
+
} },
|
|
141
|
+
React.createElement(Menu, { setFocus: false, ref: popover.refs.setFloating, ...popover.getFloatingProps({ id: submenuId }) }, subMenuItems)))))));
|
|
103
142
|
});
|
|
@@ -24,14 +24,15 @@ type NotificationMarkerProps = {
|
|
|
24
24
|
*/
|
|
25
25
|
pulsing?: boolean;
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
-
*
|
|
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
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import type { Placement } from '@floating-ui/react';
|
|
2
|
+
import type { Placement, UseListNavigationProps, ReferenceType, UseFloatingOptions, UseHoverProps, UseClickProps, UseFocusProps, UseDismissProps } from '@floating-ui/react';
|
|
3
3
|
import type { PolymorphicForwardRefComponent } from '../../utils/index.js';
|
|
4
4
|
import type { PortalProps } from '../../utils/components/Portal.js';
|
|
5
5
|
type PopoverOptions = {
|
|
@@ -59,16 +59,39 @@ type PopoverInternalProps = {
|
|
|
59
59
|
layoutShift?: boolean;
|
|
60
60
|
};
|
|
61
61
|
/**
|
|
62
|
-
* By default, the
|
|
63
|
-
*
|
|
62
|
+
* By default, only the click and dismiss interactions/triggers are enabled.
|
|
63
|
+
* Explicitly pass `false` to disable the defaults.
|
|
64
|
+
*
|
|
65
|
+
* Pass a boolean to enable/disable any of the supported interactions.
|
|
66
|
+
* Alternatively, pass an object to override the default props that the Popover sets for an interaction/trigger.
|
|
67
|
+
*
|
|
68
|
+
* When additional parameters are _required_ for an interaction/trigger, an object must be passed to enable it.
|
|
69
|
+
* Booleans will not be allowed in this case.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* <Popover
|
|
73
|
+
* interactions={{
|
|
74
|
+
* click: false,
|
|
75
|
+
* focus: true,
|
|
76
|
+
* hover: { move: false },
|
|
77
|
+
* listNavigation: { … },
|
|
78
|
+
* }}
|
|
79
|
+
* // …
|
|
80
|
+
* >…</Popover>
|
|
64
81
|
*/
|
|
65
|
-
|
|
82
|
+
interactions?: {
|
|
83
|
+
click?: boolean | UseClickProps;
|
|
84
|
+
dismiss?: boolean | UseDismissProps;
|
|
85
|
+
hover?: boolean | UseHoverProps<ReferenceType>;
|
|
86
|
+
focus?: boolean | UseFocusProps;
|
|
87
|
+
listNavigation?: UseListNavigationProps;
|
|
88
|
+
};
|
|
66
89
|
role?: 'dialog' | 'menu' | 'listbox';
|
|
67
90
|
/**
|
|
68
91
|
* Whether the popover should match the width of the trigger.
|
|
69
92
|
*/
|
|
70
93
|
matchWidth?: boolean;
|
|
71
|
-
}
|
|
94
|
+
} & Omit<UseFloatingOptions, 'middleware' | 'placement'>;
|
|
72
95
|
export declare const usePopover: (options: PopoverOptions & PopoverInternalProps) => {
|
|
73
96
|
placement: Placement;
|
|
74
97
|
strategy: import("@floating-ui/utils").Strategy;
|
|
@@ -83,11 +106,11 @@ export declare const usePopover: (options: PopoverOptions & PopoverInternalProps
|
|
|
83
106
|
floating: React.MutableRefObject<HTMLElement | null>;
|
|
84
107
|
setReference: (node: import("@floating-ui/react-dom").ReferenceType | null) => void;
|
|
85
108
|
setFloating: (node: HTMLElement | null) => void;
|
|
86
|
-
} & import("@floating-ui/react").ExtendedRefs<
|
|
109
|
+
} & import("@floating-ui/react").ExtendedRefs<ReferenceType>;
|
|
87
110
|
elements: {
|
|
88
111
|
reference: import("@floating-ui/react-dom").ReferenceType | null;
|
|
89
112
|
floating: HTMLElement | null;
|
|
90
|
-
} & import("@floating-ui/react").ExtendedElements<
|
|
113
|
+
} & import("@floating-ui/react").ExtendedElements<ReferenceType>;
|
|
91
114
|
context: {
|
|
92
115
|
x: number;
|
|
93
116
|
y: number;
|
|
@@ -103,8 +126,8 @@ export declare const usePopover: (options: PopoverOptions & PopoverInternalProps
|
|
|
103
126
|
dataRef: React.MutableRefObject<import("@floating-ui/react").ContextData>;
|
|
104
127
|
nodeId: string | undefined;
|
|
105
128
|
floatingId: string;
|
|
106
|
-
refs: import("@floating-ui/react").ExtendedRefs<
|
|
107
|
-
elements: import("@floating-ui/react").ExtendedElements<
|
|
129
|
+
refs: import("@floating-ui/react").ExtendedRefs<ReferenceType>;
|
|
130
|
+
elements: import("@floating-ui/react").ExtendedElements<ReferenceType>;
|
|
108
131
|
};
|
|
109
132
|
getFloatingProps: (userProps?: React.HTMLProps<HTMLElement>) => Record<string, unknown>;
|
|
110
133
|
getReferenceProps: (userProps?: React.HTMLProps<Element> | undefined) => Record<string, unknown>;
|
|
@@ -4,21 +4,35 @@
|
|
|
4
4
|
*--------------------------------------------------------------------------------------------*/
|
|
5
5
|
import * as React from 'react';
|
|
6
6
|
import cx from 'classnames';
|
|
7
|
-
import { useFloating, useClick, useDismiss, useInteractions, size, autoUpdate, offset, flip, shift, autoPlacement, inline, hide, FloatingFocusManager, useHover, useFocus, safePolygon, useRole, FloatingPortal, } from '@floating-ui/react';
|
|
8
|
-
import { Box, cloneElementWithRef, useControlledState, useId, useLayoutEffect, useMergedRefs, } from '../../utils/index.js';
|
|
9
|
-
import {
|
|
7
|
+
import { useFloating, useClick, useDismiss, useInteractions, size, autoUpdate, offset, flip, shift, autoPlacement, inline, hide, FloatingFocusManager, useHover, useFocus, safePolygon, useRole, FloatingPortal, useFloatingTree, useListNavigation, } from '@floating-ui/react';
|
|
8
|
+
import { Box, ShadowRoot, cloneElementWithRef, useControlledState, useId, useLayoutEffect, useMergedRefs, } from '../../utils/index.js';
|
|
9
|
+
import { usePortalTo } from '../../utils/components/Portal.js';
|
|
10
10
|
import { ThemeProvider } from '../ThemeProvider/ThemeProvider.js';
|
|
11
11
|
// ----------------------------------------------------------------------------
|
|
12
12
|
export const usePopover = (options) => {
|
|
13
|
-
const { placement = 'bottom-start', visible, onVisibleChange, closeOnOutsideClick, autoUpdateOptions, matchWidth,
|
|
14
|
-
const
|
|
13
|
+
const { placement = 'bottom-start', visible, onVisibleChange, closeOnOutsideClick, autoUpdateOptions, matchWidth, interactions: interactionsProp, role, ...rest } = options;
|
|
14
|
+
const mergedInteractions = {
|
|
15
|
+
...{
|
|
16
|
+
click: true,
|
|
17
|
+
dismiss: true,
|
|
18
|
+
hover: false,
|
|
19
|
+
focus: false,
|
|
20
|
+
listNavigation: undefined,
|
|
21
|
+
},
|
|
22
|
+
...interactionsProp,
|
|
23
|
+
};
|
|
24
|
+
const tree = useFloatingTree();
|
|
25
|
+
const middleware = React.useMemo(() => ({ flip: true, shift: true, ...options.middleware }), [options.middleware]);
|
|
15
26
|
const [open, onOpenChange] = useControlledState(false, visible, onVisibleChange);
|
|
16
27
|
const floating = useFloating({
|
|
17
28
|
placement,
|
|
18
29
|
open,
|
|
19
30
|
onOpenChange,
|
|
20
|
-
whileElementsMounted: (
|
|
21
|
-
|
|
31
|
+
whileElementsMounted: React.useMemo(() =>
|
|
32
|
+
// autoUpdate is expensive and should only be called when the popover is open
|
|
33
|
+
open ? (...args) => autoUpdate(...args, autoUpdateOptions) : undefined, [autoUpdateOptions, open]),
|
|
34
|
+
...rest,
|
|
35
|
+
middleware: React.useMemo(() => [
|
|
22
36
|
middleware.offset !== undefined && offset(middleware.offset),
|
|
23
37
|
middleware.flip && flip(),
|
|
24
38
|
middleware.shift && shift(),
|
|
@@ -31,18 +45,35 @@ export const usePopover = (options) => {
|
|
|
31
45
|
middleware.autoPlacement && autoPlacement(),
|
|
32
46
|
middleware.inline && inline(),
|
|
33
47
|
middleware.hide && hide(),
|
|
34
|
-
].filter(Boolean),
|
|
48
|
+
].filter(Boolean), [matchWidth, middleware]),
|
|
35
49
|
});
|
|
36
50
|
const interactions = useInteractions([
|
|
37
|
-
useClick(floating.context, {
|
|
38
|
-
|
|
51
|
+
useClick(floating.context, {
|
|
52
|
+
enabled: !!mergedInteractions.click,
|
|
53
|
+
...mergedInteractions.click,
|
|
54
|
+
}),
|
|
55
|
+
useDismiss(floating.context, {
|
|
56
|
+
enabled: !!mergedInteractions.dismiss,
|
|
57
|
+
outsidePress: closeOnOutsideClick,
|
|
58
|
+
bubbles: tree != null,
|
|
59
|
+
...mergedInteractions.dismiss,
|
|
60
|
+
}),
|
|
39
61
|
useHover(floating.context, {
|
|
40
|
-
enabled: !!
|
|
62
|
+
enabled: !!mergedInteractions.hover,
|
|
41
63
|
delay: 100,
|
|
42
|
-
handleClose: safePolygon({ buffer: 1 }),
|
|
64
|
+
handleClose: safePolygon({ buffer: 1, requireIntent: false }),
|
|
65
|
+
move: false,
|
|
66
|
+
...mergedInteractions.hover,
|
|
67
|
+
}),
|
|
68
|
+
useFocus(floating.context, {
|
|
69
|
+
enabled: !!mergedInteractions.focus,
|
|
70
|
+
...mergedInteractions.focus,
|
|
43
71
|
}),
|
|
44
|
-
useFocus(floating.context, { enabled: !!trigger.focus }),
|
|
45
72
|
useRole(floating.context, { role: 'dialog', enabled: !!role }),
|
|
73
|
+
useListNavigation(floating.context, {
|
|
74
|
+
enabled: !!mergedInteractions.listNavigation,
|
|
75
|
+
...mergedInteractions.listNavigation,
|
|
76
|
+
}),
|
|
46
77
|
]);
|
|
47
78
|
const [referenceWidth, setReferenceWidth] = React.useState();
|
|
48
79
|
const getFloatingProps = React.useCallback((userProps) => interactions.getFloatingProps({
|
|
@@ -114,11 +145,28 @@ export const Popover = React.forwardRef((props, forwardedRef) => {
|
|
|
114
145
|
...popover.getReferenceProps(children.props),
|
|
115
146
|
ref: popover.refs.setReference,
|
|
116
147
|
})),
|
|
117
|
-
popover.open ? (React.createElement(
|
|
118
|
-
React.createElement(
|
|
119
|
-
React.createElement(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
148
|
+
popover.open ? (React.createElement(PopoverPortal, { portal: portal },
|
|
149
|
+
React.createElement(ThemeProvider, { portalContainer: popoverElement },
|
|
150
|
+
React.createElement(DisplayContents, null),
|
|
151
|
+
React.createElement(FloatingFocusManager, { context: popover.context, modal: false, initialFocus: popover.refs.floating },
|
|
152
|
+
React.createElement(Box, { className: cx({ 'iui-popover-surface': applyBackground }, className), "aria-labelledby": !hasAriaLabel
|
|
153
|
+
? popover.refs.domReference.current?.id
|
|
154
|
+
: undefined, ...popover.getFloatingProps(rest), ref: popoverRef }, content))))) : null));
|
|
155
|
+
});
|
|
156
|
+
// ----------------------------------------------------------------------------
|
|
157
|
+
const PopoverPortal = ({ children, portal = true, }) => {
|
|
158
|
+
const portalTo = usePortalTo(portal);
|
|
159
|
+
return (React.createElement(FloatingPortal, { root: portalTo },
|
|
160
|
+
React.createElement(DisplayContents, null),
|
|
161
|
+
children));
|
|
162
|
+
};
|
|
163
|
+
// ----------------------------------------------------------------------------
|
|
164
|
+
/** Applies `display: contents` to the parent div. */
|
|
165
|
+
const DisplayContents = React.memo(() => {
|
|
166
|
+
return (React.createElement(ShadowRoot, { css: `
|
|
167
|
+
:host {
|
|
168
|
+
display: contents;
|
|
169
|
+
}
|
|
170
|
+
` },
|
|
171
|
+
React.createElement("slot", null)));
|
|
124
172
|
});
|