forked from primer/react
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ActionMenu.tsx
147 lines (133 loc) · 4.95 KB
/
ActionMenu.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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import React from 'react'
import {useSSRSafeId} from '@react-aria/ssr'
import {TriangleDownIcon} from '@primer/octicons-react'
import {AnchoredOverlay, AnchoredOverlayProps} from './AnchoredOverlay'
import {OverlayProps} from './Overlay'
import {useProvidedRefOrCreate, useProvidedStateOrCreate, useMenuInitialFocus, useTypeaheadFocus} from './hooks'
import {Divider} from './ActionList/Divider'
import {ActionListContainerContext} from './ActionList/ActionListContainerContext'
import {Button, ButtonProps} from './Button'
import {MandateProps} from './utils/types'
import {SxProp, merge} from './sx'
type MenuContextProps = Pick<
AnchoredOverlayProps,
'anchorRef' | 'renderAnchor' | 'open' | 'onOpen' | 'onClose' | 'anchorId'
>
const MenuContext = React.createContext<MenuContextProps>({renderAnchor: null, open: false})
export type ActionMenuProps = {
/**
* Recommended: `ActionMenu.Button` or `ActionMenu.Anchor` with `ActionMenu.Overlay`
*/
children: React.ReactElement[] | React.ReactElement
/**
* If defined, will control the open/closed state of the overlay. Must be used in conjunction with `onOpenChange`.
*/
open?: boolean
/**
* If defined, will control the open/closed state of the overlay. Must be used in conjunction with `open`.
*/
onOpenChange?: (s: boolean) => void
} & Pick<AnchoredOverlayProps, 'anchorRef'>
const Menu: React.FC<ActionMenuProps> = ({
anchorRef: externalAnchorRef,
open,
onOpenChange,
children
}: ActionMenuProps) => {
const [combinedOpenState, setCombinedOpenState] = useProvidedStateOrCreate(open, onOpenChange, false)
const onOpen = React.useCallback(() => setCombinedOpenState(true), [setCombinedOpenState])
const onClose = React.useCallback(() => setCombinedOpenState(false), [setCombinedOpenState])
const anchorRef = useProvidedRefOrCreate(externalAnchorRef)
const anchorId = useSSRSafeId()
let renderAnchor: AnchoredOverlayProps['renderAnchor'] = null
// 🚨 Hack for good API!
// we strip out Anchor from children and pass it to AnchoredOverlay to render
// with additional props for accessibility
const contents = React.Children.map(children, child => {
if (child.type === MenuButton || child.type === Anchor) {
renderAnchor = anchorProps => React.cloneElement(child, anchorProps)
return null
}
return child
})
return (
<MenuContext.Provider value={{anchorRef, renderAnchor, anchorId, open: combinedOpenState, onOpen, onClose}}>
{contents}
</MenuContext.Provider>
)
}
export type ActionMenuAnchorProps = {children: React.ReactElement}
const Anchor = React.forwardRef<AnchoredOverlayProps['anchorRef'], ActionMenuAnchorProps>(
({children, ...anchorProps}, anchorRef) => {
return React.cloneElement(children, {...anchorProps, ref: anchorRef})
}
)
/** this component is syntactical sugar 🍭 */
export type ActionMenuButtonProps = ButtonProps
const MenuButton = React.forwardRef<AnchoredOverlayProps['anchorRef'], ButtonProps>(
({sx: sxProp = {}, ...props}, anchorRef) => {
return (
<Anchor ref={anchorRef}>
<Button
type="button"
trailingIcon={TriangleDownIcon}
sx={merge(
{
// override the margin on caret for optical alignment
'[data-component=trailingIcon]': {marginX: -1}
},
sxProp as SxProp
)}
{...props}
/>
</Anchor>
)
}
)
type MenuOverlayProps = Partial<OverlayProps> &
Pick<AnchoredOverlayProps, 'align'> & {
/**
* Recommended: `ActionList`
*/
children: React.ReactElement[] | React.ReactElement
}
const Overlay: React.FC<MenuOverlayProps> = ({children, align = 'start', ...overlayProps}) => {
// we typecast anchorRef as required instead of optional
// because we know that we're setting it in context in Menu
const {anchorRef, renderAnchor, anchorId, open, onOpen, onClose} = React.useContext(MenuContext) as MandateProps<
MenuContextProps,
'anchorRef'
>
const containerRef = React.createRef<HTMLDivElement>()
const {openWithFocus} = useMenuInitialFocus(open, onOpen, containerRef)
useTypeaheadFocus(open, containerRef)
return (
<AnchoredOverlay
anchorRef={anchorRef}
renderAnchor={renderAnchor}
anchorId={anchorId}
open={open}
onOpen={openWithFocus}
onClose={onClose}
align={align}
overlayProps={overlayProps}
focusZoneSettings={{focusOutBehavior: 'wrap'}}
>
<div ref={containerRef}>
<ActionListContainerContext.Provider
value={{
container: 'ActionMenu',
listRole: 'menu',
listLabelledBy: anchorId,
selectionAttribute: 'aria-checked', // Should this be here?
afterSelect: onClose
}}
>
{children}
</ActionListContainerContext.Provider>
</div>
</AnchoredOverlay>
)
}
Menu.displayName = 'ActionMenu'
export const ActionMenu = Object.assign(Menu, {Button: MenuButton, Anchor, Overlay, Divider})