forked from patternfly/patternfly-react
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMenuContainer.tsx
More file actions
133 lines (123 loc) · 4.73 KB
/
MenuContainer.tsx
File metadata and controls
133 lines (123 loc) · 4.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import { useEffect, useRef } from 'react';
import { onToggleArrowKeydownDefault, Popper } from '../../helpers';
import type { PopperOptions } from '../../helpers/Popper/Popper';
/** @deprecated Use PopperOptions instead */
export type MenuPopperProps = PopperOptions;
export interface MenuContainerProps {
/** Menu to be rendered */
menu: React.ReactElement<any, string | React.JSXElementConstructor<any>>;
/** Reference to the menu */
menuRef: React.RefObject<any>;
/** Toggle to be rendered */
toggle: React.ReactNode;
/** Reference to the toggle */
toggleRef: React.RefObject<any>;
/** Flag to indicate if menu is opened.*/
isOpen: boolean;
/** Callback to change the open state of the menu.
* Triggered by clicking outside of the menu, or by pressing any keys specified in onOpenChangeKeys. */
onOpenChange?: (isOpen: boolean) => void;
/** Keys that trigger onOpenChange, defaults to tab and escape. It is highly recommended to include Escape in the array, while Tab may be omitted if the menu contains non-menu items that are focusable. */
onOpenChangeKeys?: string[];
/** Callback to override the toggle keydown behavior. By default, when the toggle has focus and the menu is open, pressing the up/down arrow keys will focus a valid non-disabled menu item - the first item for the down arrow key and last item for the up arrow key. */
onToggleKeydown?: (event: KeyboardEvent) => void;
/** z-index of the dropdown menu */
zIndex?: number;
/** Additional properties to pass to the Popper */
popperProps?: PopperOptions;
/** @beta Flag indicating the first menu item should be focused after opening the dropdown. */
shouldFocusFirstItemOnOpen?: boolean;
/** Flag indicating if scroll on focus of the first menu item should occur. */
shouldPreventScrollOnItemFocus?: boolean;
/** Time in ms to wait before firing the toggles' focus event. Defaults to 0 */
focusTimeoutDelay?: number;
}
/**
* Container that links a menu and menu toggle together, to handle basic keyboard input and control the opening and closing of a menu.
*/
export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
menu,
menuRef,
isOpen,
toggle,
toggleRef,
onOpenChange,
onToggleKeydown,
zIndex = 9999,
popperProps,
onOpenChangeKeys = ['Escape', 'Tab'],
shouldFocusFirstItemOnOpen = false,
shouldPreventScrollOnItemFocus = true,
focusTimeoutDelay = 0
}: MenuContainerProps) => {
const prevIsOpen = useRef<boolean>(isOpen);
useEffect(() => {
// menu was opened, focus on first menu item
if (prevIsOpen.current === false && isOpen === true && shouldFocusFirstItemOnOpen) {
setTimeout(() => {
const firstElement = menuRef?.current?.querySelector(
'li button:not(:disabled),li input:not(:disabled),li a:not([aria-disabled="true"])'
);
firstElement && (firstElement as HTMLElement).focus({ preventScroll: shouldPreventScrollOnItemFocus });
}, focusTimeoutDelay);
}
prevIsOpen.current = isOpen;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
useEffect(() => {
const handleMenuKeys = (event: KeyboardEvent) => {
// Close the menu on tab or escape if onOpenChange is provided
if (
(isOpen && onOpenChange && menuRef.current?.contains(event.target as Node)) ||
toggleRef.current?.contains(event.target as Node)
) {
if (onOpenChangeKeys.includes(event.key)) {
onOpenChange(false);
toggleRef.current?.focus();
}
}
if (toggleRef.current?.contains(event.target as Node)) {
if (onToggleKeydown) {
onToggleKeydown(event);
} else if (isOpen) {
onToggleArrowKeydownDefault(event, menuRef);
}
}
};
const handleClick = (event: MouseEvent) => {
// If the event is not on the toggle and onOpenChange callback is provided, close the menu
if (isOpen && onOpenChange && !toggleRef?.current?.contains(event.target as Node)) {
if (isOpen && !menuRef.current?.contains(event.target as Node)) {
onOpenChange(false);
}
}
};
window.addEventListener('keydown', handleMenuKeys);
window.addEventListener('click', handleClick);
return () => {
window.removeEventListener('keydown', handleMenuKeys);
window.removeEventListener('click', handleClick);
};
}, [
focusTimeoutDelay,
isOpen,
menuRef,
onOpenChange,
onOpenChangeKeys,
onToggleKeydown,
shouldPreventScrollOnItemFocus,
toggleRef
]);
return (
<Popper
trigger={toggle}
triggerRef={toggleRef}
popper={menu}
popperRef={menuRef}
isVisible={isOpen}
zIndex={zIndex}
{...popperProps}
/>
);
};
MenuContainer.displayName = 'MenuContainer';