@@ -17,6 +17,7 @@ export interface MenuPopperProps {
1717 /** Flag to prevent the popper from overflowing its container and becoming partially obscured. */
1818 preventOverflow ?: boolean ;
1919}
20+
2021export interface MenuContainerProps {
2122 /** Menu to be rendered */
2223 menu : React . ReactElement < any , string | React . JSXElementConstructor < any > > ;
@@ -33,6 +34,8 @@ export interface MenuContainerProps {
3334 onOpenChange ?: ( isOpen : boolean ) => void ;
3435 /** 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. */
3536 onOpenChangeKeys ?: string [ ] ;
37+ /** Custom callback to override the default behaviour when pressing up/down arrows. Default is focusing the menu items (first item on arrow down, last item on arrow up). */
38+ onArrowUpDownKeyDown ?: ( event : KeyboardEvent ) => void ;
3639 /** z-index of the dropdown menu */
3740 zIndex ?: number ;
3841 /** Additional properties to pass to the Popper */
@@ -55,10 +58,11 @@ export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
5558 toggle,
5659 toggleRef,
5760 onOpenChange,
61+ onArrowUpDownKeyDown,
5862 zIndex = 9999 ,
5963 popperProps,
6064 onOpenChangeKeys = [ 'Escape' , 'Tab' ] ,
61- shouldFocusFirstItemOnOpen = true ,
65+ shouldFocusFirstItemOnOpen = false ,
6266 shouldPreventScrollOnItemFocus = true ,
6367 focusTimeoutDelay = 0
6468} : MenuContainerProps ) => {
@@ -79,6 +83,23 @@ export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
7983 } , [ isOpen ] ) ;
8084
8185 React . useEffect ( ( ) => {
86+ const onArrowUpDownKeyDownDefault = ( event : KeyboardEvent ) => {
87+ event . preventDefault ( ) ;
88+
89+ let listItem : HTMLLIElement ;
90+ if ( event . key === 'ArrowDown' ) {
91+ listItem = menuRef . current ?. querySelector ( 'li' ) ;
92+ } else {
93+ const allItems = menuRef . current ?. querySelectorAll ( 'li' ) ;
94+ listItem = allItems ? allItems [ allItems . length - 1 ] : null ;
95+ }
96+
97+ const focusableElement = listItem ?. querySelector (
98+ 'button:not(:disabled),input:not(:disabled),a:not([aria-disabled="true"])'
99+ ) ;
100+ focusableElement && ( focusableElement as HTMLElement ) . focus ( ) ;
101+ } ;
102+
82103 const handleMenuKeys = ( event : KeyboardEvent ) => {
83104 // Close the menu on tab or escape if onOpenChange is provided
84105 if (
@@ -90,6 +111,14 @@ export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
90111 toggleRef . current ?. focus ( ) ;
91112 }
92113 }
114+
115+ if ( event . key === 'ArrowDown' || event . key === 'ArrowUp' ) {
116+ if ( onArrowUpDownKeyDown ) {
117+ onArrowUpDownKeyDown ( event ) ;
118+ } else {
119+ onArrowUpDownKeyDownDefault ( event ) ;
120+ }
121+ }
93122 } ;
94123
95124 const handleClick = ( event : MouseEvent ) => {
@@ -108,7 +137,16 @@ export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
108137 window . removeEventListener ( 'keydown' , handleMenuKeys ) ;
109138 window . removeEventListener ( 'click' , handleClick ) ;
110139 } ;
111- } , [ focusTimeoutDelay , isOpen , menuRef , onOpenChange , onOpenChangeKeys , shouldPreventScrollOnItemFocus , toggleRef ] ) ;
140+ } , [
141+ focusTimeoutDelay ,
142+ isOpen ,
143+ menuRef ,
144+ onOpenChange ,
145+ onOpenChangeKeys ,
146+ onArrowUpDownKeyDown ,
147+ shouldPreventScrollOnItemFocus ,
148+ toggleRef
149+ ] ) ;
112150
113151 return (
114152 < Popper
0 commit comments