-
Notifications
You must be signed in to change notification settings - Fork 39
/
DropdownMenu.tsx
123 lines (114 loc) · 3.4 KB
/
DropdownMenu.tsx
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
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import * as React from 'react';
import { Popover, mergeRefs } from '../utils/index.js';
import type {
CommonProps,
PopoverProps,
PopoverInstance,
} from '../utils/index.js';
import { Menu } from '../Menu/index.js';
export type DropdownMenuProps = {
/**
* List of menu items. Recommended to use MenuItem component.
* You can pass function that takes argument `close` that closes the dropdown menu.
*/
menuItems: (close: () => void) => JSX.Element[];
/**
* ARIA role. Role of menu. For menu use 'menu', for select use 'listbox'.
* @default 'menu'
*/
role?: string;
/**
* Child element to wrap dropdown with.
*/
children: React.ReactNode;
} & Omit<PopoverProps, 'content'> &
Omit<CommonProps, 'title'>;
/**
* Dropdown menu component.
* Uses the {@link Popover} component, which is a wrapper around [tippy.js](https://atomiks.github.io/tippyjs).
* @example
* const menuItems = (close: () => void) => [
* <MenuItem key={1} onClick={onClick(1, close)}>
* Item #1
* </MenuItem>,
* <MenuItem key={2} onClick={onClick(2, close)}>
* Item #2
* </MenuItem>,
* <MenuItem key={3} onClick={onClick(3, close)}>
* Item #3
* </MenuItem>,
* ];
* <DropdownMenu menuItems={menuItems}>
* <Button>Menu</Button>
* </DropdownMenu>
*/
export const DropdownMenu = (props: DropdownMenuProps) => {
const {
menuItems,
children,
className,
style,
role = 'menu',
visible,
placement = 'bottom-start',
onShow,
onHide,
trigger,
id,
...rest
} = props;
const [isVisible, setIsVisible] = React.useState(visible ?? false);
React.useEffect(() => {
setIsVisible(visible ?? false);
}, [visible]);
const open = React.useCallback(() => setIsVisible(true), []);
const close = React.useCallback(() => setIsVisible(false), []);
const targetRef = React.useRef<HTMLElement>(null);
const onShowHandler = React.useCallback(
(instance: PopoverInstance) => {
setIsVisible(true);
onShow?.(instance);
},
[onShow],
);
const onHideHandler = React.useCallback(
(instance: PopoverInstance) => {
setIsVisible(false);
targetRef.current?.focus();
onHide?.(instance);
},
[onHide],
);
return (
<Popover
content={
<Menu className={className} style={style} role={role} id={id}>
{React.useMemo(() => menuItems(close), [menuItems, close])}
</Menu>
}
visible={trigger === undefined ? isVisible : undefined}
onClickOutside={close}
placement={placement}
onShow={onShowHandler}
onHide={onHideHandler}
trigger={visible === undefined ? trigger : undefined}
{...rest}
>
{React.cloneElement(children as JSX.Element, {
ref: mergeRefs(
targetRef,
(props.children as React.FunctionComponentElement<HTMLElement>).ref,
),
onClick: (args: unknown) => {
trigger === undefined && (isVisible ? close() : open());
(children as JSX.Element).props.onClick?.(args);
},
})}
</Popover>
);
};
export default DropdownMenu;