1
- import React , { useEffect , useState } from 'react'
2
- import { TriangleDownIcon } from '@primer/octicons-react'
1
+ import React , { useCallback , useContext , useMemo , useEffect , useState } from 'react'
2
+ import { TriangleDownIcon , ChevronRightIcon } from '@primer/octicons-react'
3
3
import type { AnchoredOverlayProps } from '../AnchoredOverlay'
4
4
import { AnchoredOverlay } from '../AnchoredOverlay'
5
5
import type { OverlayProps } from '../Overlay'
@@ -13,11 +13,16 @@ import type {MandateProps} from '../utils/types'
13
13
import type { ForwardRefComponent as PolymorphicForwardRefComponent } from '../utils/polymorphic'
14
14
import { Tooltip } from '../TooltipV2/Tooltip'
15
15
16
+ export type MenuCloseHandler = (
17
+ gesture : 'anchor-click' | 'click-outside' | 'escape' | 'tab' | 'item-select' | 'arrow-left' ,
18
+ ) => void
19
+
16
20
export type MenuContextProps = Pick <
17
21
AnchoredOverlayProps ,
18
22
'anchorRef' | 'renderAnchor' | 'open' | 'onOpen' | 'anchorId'
19
23
> & {
20
- onClose ?: ( gesture : 'anchor-click' | 'click-outside' | 'escape' | 'tab' ) => void
24
+ onClose ?: MenuCloseHandler
25
+ isSubmenu ?: boolean
21
26
}
22
27
const MenuContext = React . createContext < MenuContextProps > ( { renderAnchor : null , open : false } )
23
28
@@ -44,9 +49,23 @@ const Menu: React.FC<React.PropsWithChildren<ActionMenuProps>> = ({
44
49
onOpenChange,
45
50
children,
46
51
} : ActionMenuProps ) => {
52
+ const parentMenuContext = useContext ( MenuContext )
53
+
47
54
const [ combinedOpenState , setCombinedOpenState ] = useProvidedStateOrCreate ( open , onOpenChange , false )
48
55
const onOpen = React . useCallback ( ( ) => setCombinedOpenState ( true ) , [ setCombinedOpenState ] )
49
- const onClose = React . useCallback ( ( ) => setCombinedOpenState ( false ) , [ setCombinedOpenState ] )
56
+ const onClose : MenuCloseHandler = React . useCallback (
57
+ gesture => {
58
+ setCombinedOpenState ( false )
59
+
60
+ // Close the parent stack when an item is selected or the user tabs out of the menu entirely
61
+ switch ( gesture ) {
62
+ case 'tab' :
63
+ case 'item-select' :
64
+ parentMenuContext . onClose ?.( gesture )
65
+ }
66
+ } ,
67
+ [ setCombinedOpenState , parentMenuContext ] ,
68
+ )
50
69
51
70
const menuButtonChild = React . Children . toArray ( children ) . find (
52
71
child => React . isValidElement < ActionMenuButtonProps > ( child ) && ( child . type === MenuButton || child . type === Anchor ) ,
@@ -100,15 +119,59 @@ const Menu: React.FC<React.PropsWithChildren<ActionMenuProps>> = ({
100
119
} )
101
120
102
121
return (
103
- < MenuContext . Provider value = { { anchorRef, renderAnchor, anchorId, open : combinedOpenState , onOpen, onClose} } >
122
+ < MenuContext . Provider
123
+ value = { {
124
+ anchorRef,
125
+ renderAnchor,
126
+ anchorId,
127
+ open : combinedOpenState ,
128
+ onOpen,
129
+ onClose,
130
+ // will be undefined for the outermost level, then false for the top menu, then true inside that
131
+ isSubmenu : parentMenuContext . isSubmenu !== undefined ,
132
+ } }
133
+ >
104
134
{ contents }
105
135
</ MenuContext . Provider >
106
136
)
107
137
}
108
138
109
139
export type ActionMenuAnchorProps = { children : React . ReactElement ; id ?: string }
110
140
const Anchor = React . forwardRef < HTMLElement , ActionMenuAnchorProps > ( ( { children, ...anchorProps } , anchorRef ) => {
111
- return React . cloneElement ( children , { ...anchorProps , ref : anchorRef } )
141
+ const { onOpen, isSubmenu} = React . useContext ( MenuContext )
142
+
143
+ const openSubmenuOnRightArrow : React . KeyboardEventHandler < HTMLElement > = useCallback (
144
+ event => {
145
+ children . props . onKeyDown ?.( event )
146
+ if ( isSubmenu && event . key === 'ArrowRight' && ! event . defaultPrevented ) onOpen ?.( 'anchor-key-press' )
147
+ } ,
148
+ [ children , isSubmenu , onOpen ] ,
149
+ )
150
+
151
+ // Add right chevron icon to submenu anchors rendered using `ActionList.Item`
152
+ const parentActionListContext = useContext ( ActionListContainerContext )
153
+ const thisActionListContext = useMemo (
154
+ ( ) =>
155
+ isSubmenu
156
+ ? {
157
+ ...parentActionListContext ,
158
+ defaultTrailingVisual : < ChevronRightIcon /> ,
159
+ // Default behavior is to close after selecting; we want to open the submenu instead
160
+ afterSelect : ( ) => onOpen ?.( 'anchor-click' ) ,
161
+ }
162
+ : parentActionListContext ,
163
+ [ isSubmenu , onOpen , parentActionListContext ] ,
164
+ )
165
+
166
+ return (
167
+ < ActionListContainerContext . Provider value = { thisActionListContext } >
168
+ { React . cloneElement ( children , {
169
+ ...anchorProps ,
170
+ ref : anchorRef ,
171
+ onKeyDown : openSubmenuOnRightArrow ,
172
+ } ) }
173
+ </ ActionListContainerContext . Provider >
174
+ )
112
175
} )
113
176
114
177
/** this component is syntactical sugar 🍭 */
@@ -133,19 +196,24 @@ type MenuOverlayProps = Partial<OverlayProps> &
133
196
const Overlay : React . FC < React . PropsWithChildren < MenuOverlayProps > > = ( {
134
197
children,
135
198
align = 'start' ,
136
- side = 'outside-bottom' ,
199
+ side,
137
200
'aria-labelledby' : ariaLabelledby ,
138
201
...overlayProps
139
202
} ) => {
140
203
// we typecast anchorRef as required instead of optional
141
204
// because we know that we're setting it in context in Menu
142
- const { anchorRef, renderAnchor, anchorId, open, onOpen, onClose} = React . useContext ( MenuContext ) as MandateProps <
143
- MenuContextProps ,
144
- 'anchorRef'
145
- >
205
+ const {
206
+ anchorRef,
207
+ renderAnchor,
208
+ anchorId,
209
+ open,
210
+ onOpen,
211
+ onClose,
212
+ isSubmenu = false ,
213
+ } = React . useContext ( MenuContext ) as MandateProps < MenuContextProps , 'anchorRef' >
146
214
147
215
const containerRef = React . useRef < HTMLDivElement > ( null )
148
- useMenuKeyboardNavigation ( open , onClose , containerRef , anchorRef )
216
+ useMenuKeyboardNavigation ( open , onClose , containerRef , anchorRef , isSubmenu )
149
217
150
218
// If the menu anchor is an icon button, we need to label the menu by tooltip that also labelled the anchor.
151
219
const [ anchorAriaLabelledby , setAnchorAriaLabelledby ] = useState < null | string > ( null )
@@ -167,7 +235,7 @@ const Overlay: React.FC<React.PropsWithChildren<MenuOverlayProps>> = ({
167
235
onOpen = { onOpen }
168
236
onClose = { onClose }
169
237
align = { align }
170
- side = { side }
238
+ side = { side ?? ( isSubmenu ? 'outside-right' : 'outside-bottom' ) }
171
239
overlayProps = { overlayProps }
172
240
focusZoneSettings = { { focusOutBehavior : 'wrap' } }
173
241
>
@@ -179,7 +247,7 @@ const Overlay: React.FC<React.PropsWithChildren<MenuOverlayProps>> = ({
179
247
// If there is a custom aria-labelledby, use that. Otherwise, if exists, use the id that labels the anchor such as tooltip. If none of them exist, use anchor id.
180
248
listLabelledBy : ariaLabelledby || anchorAriaLabelledby || anchorId ,
181
249
selectionAttribute : 'aria-checked' , // Should this be here?
182
- afterSelect : onClose ,
250
+ afterSelect : ( ) => onClose ?. ( 'item-select' ) ,
183
251
} }
184
252
>
185
253
{ children }
0 commit comments