diff --git a/packages/react-core/src/components/Dropdown/Dropdown.tsx b/packages/react-core/src/components/Dropdown/Dropdown.tsx index 00c13f9782a..66a9597cb96 100644 --- a/packages/react-core/src/components/Dropdown/Dropdown.tsx +++ b/packages/react-core/src/components/Dropdown/Dropdown.tsx @@ -51,6 +51,8 @@ export interface DropdownProps extends MenuProps, OUIAProps { 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[]; + /** 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). */ + onArrowUpDownKeyDown?: (event: KeyboardEvent) => void; /** Indicates if the menu should be without the outer box-shadow. */ isPlain?: boolean; /** Indicates if the menu should be scrollable. */ @@ -85,6 +87,7 @@ const DropdownBase: React.FunctionComponent = ({ toggle, shouldFocusToggleOnSelect = false, onOpenChange, + onArrowUpDownKeyDown, isPlain, isScrollable, innerRef, @@ -127,6 +130,23 @@ const DropdownBase: React.FunctionComponent = ({ }, [isOpen]); React.useEffect(() => { + const onArrowUpDownKeyDownDefault = (event: KeyboardEvent) => { + event.preventDefault(); + + let listItem: HTMLLIElement; + if (event.key === 'ArrowDown') { + listItem = menuRef.current?.querySelector('li'); + } else { + const allItems = menuRef.current?.querySelectorAll('li'); + listItem = allItems ? allItems[allItems.length - 1] : null; + } + + const focusableElement = listItem?.querySelector( + 'button:not(:disabled),input:not(:disabled),a:not([aria-disabled="true"])' + ); + focusableElement && (focusableElement as HTMLElement).focus(); + }; + const handleMenuKeys = (event: KeyboardEvent) => { // Close the menu on tab or escape if onOpenChange is provided if ( @@ -139,6 +159,14 @@ const DropdownBase: React.FunctionComponent = ({ toggleRef.current?.focus(); } } + + if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { + if (onArrowUpDownKeyDown) { + onArrowUpDownKeyDown(event); + } else { + onArrowUpDownKeyDownDefault(event); + } + } }; const handleClick = (event: MouseEvent) => { @@ -163,6 +191,7 @@ const DropdownBase: React.FunctionComponent = ({ toggleRef, onOpenChange, onOpenChangeKeys, + onArrowUpDownKeyDown, shouldPreventScrollOnItemFocus, shouldFocusFirstItemOnOpen, focusTimeoutDelay diff --git a/packages/react-core/src/components/Menu/MenuContainer.tsx b/packages/react-core/src/components/Menu/MenuContainer.tsx index c1b96273b90..103346ccce8 100644 --- a/packages/react-core/src/components/Menu/MenuContainer.tsx +++ b/packages/react-core/src/components/Menu/MenuContainer.tsx @@ -17,6 +17,7 @@ export interface MenuPopperProps { /** Flag to prevent the popper from overflowing its container and becoming partially obscured. */ preventOverflow?: boolean; } + export interface MenuContainerProps { /** Menu to be rendered */ menu: React.ReactElement>; @@ -33,6 +34,8 @@ export interface MenuContainerProps { 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[]; + /** 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). */ + onArrowUpDownKeyDown?: (event: KeyboardEvent) => void; /** z-index of the dropdown menu */ zIndex?: number; /** Additional properties to pass to the Popper */ @@ -55,10 +58,11 @@ export const MenuContainer: React.FunctionComponent = ({ toggle, toggleRef, onOpenChange, + onArrowUpDownKeyDown, zIndex = 9999, popperProps, onOpenChangeKeys = ['Escape', 'Tab'], - shouldFocusFirstItemOnOpen = true, + shouldFocusFirstItemOnOpen = false, shouldPreventScrollOnItemFocus = true, focusTimeoutDelay = 0 }: MenuContainerProps) => { @@ -79,6 +83,23 @@ export const MenuContainer: React.FunctionComponent = ({ }, [isOpen]); React.useEffect(() => { + const onArrowUpDownKeyDownDefault = (event: KeyboardEvent) => { + event.preventDefault(); + + let listItem: HTMLLIElement; + if (event.key === 'ArrowDown') { + listItem = menuRef.current?.querySelector('li'); + } else { + const allItems = menuRef.current?.querySelectorAll('li'); + listItem = allItems ? allItems[allItems.length - 1] : null; + } + + const focusableElement = listItem?.querySelector( + 'button:not(:disabled),input:not(:disabled),a:not([aria-disabled="true"])' + ); + focusableElement && (focusableElement as HTMLElement).focus(); + }; + const handleMenuKeys = (event: KeyboardEvent) => { // Close the menu on tab or escape if onOpenChange is provided if ( @@ -90,6 +111,14 @@ export const MenuContainer: React.FunctionComponent = ({ toggleRef.current?.focus(); } } + + if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { + if (onArrowUpDownKeyDown) { + onArrowUpDownKeyDown(event); + } else { + onArrowUpDownKeyDownDefault(event); + } + } }; const handleClick = (event: MouseEvent) => { @@ -108,7 +137,16 @@ export const MenuContainer: React.FunctionComponent = ({ window.removeEventListener('keydown', handleMenuKeys); window.removeEventListener('click', handleClick); }; - }, [focusTimeoutDelay, isOpen, menuRef, onOpenChange, onOpenChangeKeys, shouldPreventScrollOnItemFocus, toggleRef]); + }, [ + focusTimeoutDelay, + isOpen, + menuRef, + onOpenChange, + onOpenChangeKeys, + onArrowUpDownKeyDown, + shouldPreventScrollOnItemFocus, + toggleRef + ]); return ( 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[]; + /** 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). */ + onArrowUpDownKeyDown?: (event: KeyboardEvent) => void; + /** Indicates that the Select is used as a typeahead (combobox). Focus won't shift to menu items when pressing up/down arrows. */ + isTypeahead?: boolean; /** Indicates if the select should be without the outer box-shadow */ isPlain?: boolean; /** @hide Forwarded ref */ @@ -95,6 +99,8 @@ const SelectBase: React.FunctionComponent = ({ shouldFocusFirstItemOnOpen = false, onOpenChange, onOpenChangeKeys = ['Escape', 'Tab'], + onArrowUpDownKeyDown, + isTypeahead, isPlain, innerRef, zIndex = 9999, @@ -131,6 +137,21 @@ const SelectBase: React.FunctionComponent = ({ }, [isOpen]); React.useEffect(() => { + const onArrowUpDownKeyDownDefault = (event: KeyboardEvent) => { + event.preventDefault(); + + let listItem: HTMLLIElement; + if (event.key === 'ArrowDown') { + listItem = menuRef.current?.querySelector('li'); + } else { + const allItems = menuRef.current?.querySelectorAll('li'); + listItem = allItems ? allItems[allItems.length - 1] : null; + } + + const focusableElement = listItem?.querySelector('button:not(:disabled),input:not(:disabled)'); + focusableElement && (focusableElement as HTMLElement).focus(); + }; + const handleMenuKeys = (event: KeyboardEvent) => { // Close the menu on tab or escape if onOpenChange is provided if ( @@ -144,6 +165,14 @@ const SelectBase: React.FunctionComponent = ({ toggleRef.current?.focus(); } } + + if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { + if (onArrowUpDownKeyDown) { + onArrowUpDownKeyDown(event); + } else if (!isTypeahead) { + onArrowUpDownKeyDownDefault(event); + } + } }; const handleClick = (event: MouseEvent) => { @@ -168,6 +197,7 @@ const SelectBase: React.FunctionComponent = ({ toggleRef, onOpenChange, onOpenChangeKeys, + onArrowUpDownKeyDown, shouldPreventScrollOnItemFocus, shouldFocusFirstItemOnOpen, focusTimeoutDelay diff --git a/packages/react-core/src/components/Select/examples/SelectMultiTypeahead.tsx b/packages/react-core/src/components/Select/examples/SelectMultiTypeahead.tsx index 41707fd1136..2ee90fd6460 100644 --- a/packages/react-core/src/components/Select/examples/SelectMultiTypeahead.tsx +++ b/packages/react-core/src/components/Select/examples/SelectMultiTypeahead.tsx @@ -247,7 +247,7 @@ export const SelectMultiTypeahead: React.FunctionComponent = () => { !isOpen && closeMenu(); }} toggle={toggle} - shouldFocusFirstItemOnOpen={false} + isTypeahead > {selectOptions.map((option, index) => ( diff --git a/packages/react-core/src/components/Select/examples/SelectMultiTypeaheadCheckbox.tsx b/packages/react-core/src/components/Select/examples/SelectMultiTypeaheadCheckbox.tsx index 030b62cc93b..fb22d42ac97 100644 --- a/packages/react-core/src/components/Select/examples/SelectMultiTypeaheadCheckbox.tsx +++ b/packages/react-core/src/components/Select/examples/SelectMultiTypeaheadCheckbox.tsx @@ -241,7 +241,7 @@ export const SelectMultiTypeaheadCheckbox: React.FunctionComponent = () => { !isOpen && closeMenu(); }} toggle={toggle} - shouldFocusFirstItemOnOpen={false} + isTypeahead > {selectOptions.map((option, index) => ( diff --git a/packages/react-core/src/components/Select/examples/SelectMultiTypeaheadCreatable.tsx b/packages/react-core/src/components/Select/examples/SelectMultiTypeaheadCreatable.tsx index e43c1afe1dd..e7906e1ceef 100644 --- a/packages/react-core/src/components/Select/examples/SelectMultiTypeaheadCreatable.tsx +++ b/packages/react-core/src/components/Select/examples/SelectMultiTypeaheadCreatable.tsx @@ -260,7 +260,7 @@ export const SelectMultiTypeaheadCreatable: React.FunctionComponent = () => { !isOpen && closeMenu(); }} toggle={toggle} - shouldFocusFirstItemOnOpen={false} + isTypeahead > {selectOptions.map((option, index) => ( diff --git a/packages/react-core/src/components/Select/examples/SelectTypeahead.tsx b/packages/react-core/src/components/Select/examples/SelectTypeahead.tsx index 12ca075992d..4562dd901c6 100644 --- a/packages/react-core/src/components/Select/examples/SelectTypeahead.tsx +++ b/packages/react-core/src/components/Select/examples/SelectTypeahead.tsx @@ -244,7 +244,7 @@ export const SelectTypeahead: React.FunctionComponent = () => { !isOpen && closeMenu(); }} toggle={toggle} - shouldFocusFirstItemOnOpen={false} + isTypeahead > {selectOptions.map((option, index) => ( diff --git a/packages/react-core/src/components/Select/examples/SelectTypeaheadCreatable.tsx b/packages/react-core/src/components/Select/examples/SelectTypeaheadCreatable.tsx index fda4f11bdfd..fa8ad00ff71 100644 --- a/packages/react-core/src/components/Select/examples/SelectTypeaheadCreatable.tsx +++ b/packages/react-core/src/components/Select/examples/SelectTypeaheadCreatable.tsx @@ -251,7 +251,7 @@ export const SelectTypeaheadCreatable: React.FunctionComponent = () => { !isOpen && closeMenu(); }} toggle={toggle} - shouldFocusFirstItemOnOpen={false} + isTypeahead > {selectOptions.map((option, index) => ( diff --git a/packages/react-templates/src/components/Select/MultiTypeaheadSelect.tsx b/packages/react-templates/src/components/Select/MultiTypeaheadSelect.tsx index 5beb2b9d7b5..0895a5d60bd 100644 --- a/packages/react-templates/src/components/Select/MultiTypeaheadSelect.tsx +++ b/packages/react-templates/src/components/Select/MultiTypeaheadSelect.tsx @@ -320,7 +320,7 @@ export const MultiTypeaheadSelectBase: React.FunctionComponent diff --git a/packages/react-templates/src/components/Select/TypeaheadSelect.tsx b/packages/react-templates/src/components/Select/TypeaheadSelect.tsx index b674a227d5d..64d96621880 100644 --- a/packages/react-templates/src/components/Select/TypeaheadSelect.tsx +++ b/packages/react-templates/src/components/Select/TypeaheadSelect.tsx @@ -361,7 +361,7 @@ export const TypeaheadSelectBase: React.FunctionComponent !isOpen && closeMenu(); }} toggle={toggle} - shouldFocusFirstItemOnOpen={false} + isTypeahead ref={innerRef} {...props} >