diff --git a/packages/react-core/src/components/Dropdown/Dropdown.tsx b/packages/react-core/src/components/Dropdown/Dropdown.tsx index 00c13f9782a..c556937a095 100644 --- a/packages/react-core/src/components/Dropdown/Dropdown.tsx +++ b/packages/react-core/src/components/Dropdown/Dropdown.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { css } from '@patternfly/react-styles'; import { Menu, MenuContent, MenuProps } from '../Menu'; import { Popper } from '../../helpers/Popper/Popper'; -import { useOUIAProps, OUIAProps } from '../../helpers'; +import { useOUIAProps, OUIAProps, onToggleArrowKeydownDefault } from '../../helpers'; export interface DropdownPopperProps { /** Vertical direction of the popper. If enableFlip is set to true, this will set the initial direction before the popper flips. */ @@ -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[]; + /** Callback to override the toggle keydown behavior. By default, when the toggle has focus and the menu is open, pressing the up/down arrow keys will focus a valid non-disabled menu item - the first item for the down arrow key and last item for the up arrow key. */ + onToggleKeydown?: (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, + onToggleKeydown, isPlain, isScrollable, innerRef, @@ -139,6 +142,14 @@ const DropdownBase: React.FunctionComponent = ({ toggleRef.current?.focus(); } } + + if (toggleRef.current?.contains(event.target as Node)) { + if (onToggleKeydown) { + onToggleKeydown(event); + } else if (isOpen) { + onToggleArrowKeydownDefault(event, menuRef); + } + } }; const handleClick = (event: MouseEvent) => { @@ -163,6 +174,7 @@ const DropdownBase: React.FunctionComponent = ({ toggleRef, onOpenChange, onOpenChangeKeys, + onToggleKeydown, 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..888e7c7dbe4 100644 --- a/packages/react-core/src/components/Menu/MenuContainer.tsx +++ b/packages/react-core/src/components/Menu/MenuContainer.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Popper } from '../../helpers/Popper/Popper'; +import { onToggleArrowKeydownDefault, Popper } from '../../helpers'; export interface MenuPopperProps { /** Vertical direction of the popper. If enableFlip is set to true, this will set the initial direction before the popper flips. */ @@ -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[]; + /** Callback to override the toggle keydown behavior. By default, when the toggle has focus and the menu is open, pressing the up/down arrow keys will focus a valid non-disabled menu item - the first item for the down arrow key and last item for the up arrow key. */ + onToggleKeydown?: (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, + onToggleKeydown, zIndex = 9999, popperProps, onOpenChangeKeys = ['Escape', 'Tab'], - shouldFocusFirstItemOnOpen = true, + shouldFocusFirstItemOnOpen = false, shouldPreventScrollOnItemFocus = true, focusTimeoutDelay = 0 }: MenuContainerProps) => { @@ -90,6 +94,14 @@ export const MenuContainer: React.FunctionComponent = ({ toggleRef.current?.focus(); } } + + if (toggleRef.current?.contains(event.target as Node)) { + if (onToggleKeydown) { + onToggleKeydown(event); + } else if (isOpen) { + onToggleArrowKeydownDefault(event, menuRef); + } + } }; const handleClick = (event: MouseEvent) => { @@ -108,7 +120,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, + onToggleKeydown, + 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[]; + /** Callback to override the toggle keydown behavior. By default, when the toggle has focus and the menu is open, pressing the up/down arrow keys will focus a valid non-disabled menu item - the first item for the down arrow key and last item for the up arrow key. */ + onToggleKeydown?: (event: KeyboardEvent) => void; + /** Select variant. For typeahead variant focus won't shift to menu items when pressing up/down arrows. */ + variant?: 'default' | 'typeahead'; /** 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'], + onToggleKeydown, + variant, isPlain, innerRef, zIndex = 9999, @@ -144,6 +150,14 @@ const SelectBase: React.FunctionComponent = ({ toggleRef.current?.focus(); } } + + if (toggleRef.current?.contains(event.target as Node)) { + if (onToggleKeydown) { + onToggleKeydown(event); + } else if (isOpen && variant !== 'typeahead') { + onToggleArrowKeydownDefault(event, menuRef); + } + } }; const handleClick = (event: MouseEvent) => { @@ -168,6 +182,7 @@ const SelectBase: React.FunctionComponent = ({ toggleRef, onOpenChange, onOpenChangeKeys, + onToggleKeydown, 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..e4a1538a0ca 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} + variant="typeahead" > {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..2b46979959f 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} + variant="typeahead" > {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..256a7ee7afb 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} + variant="typeahead" > {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..e682de837fa 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} + variant="typeahead" > {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..bd6cf9a3f8b 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} + variant="typeahead" > {selectOptions.map((option, index) => ( diff --git a/packages/react-core/src/helpers/KeyboardHandler.tsx b/packages/react-core/src/helpers/KeyboardHandler.tsx index afdaf7b5fb0..716e0703098 100644 --- a/packages/react-core/src/helpers/KeyboardHandler.tsx +++ b/packages/react-core/src/helpers/KeyboardHandler.tsx @@ -176,6 +176,33 @@ export const setTabIndex = (options: HTMLElement[]) => { } }; +/** + * This function is used in Dropdown, Select and MenuContainer as a default toggle keydown behavior. When the toggle has focus and the menu is open, pressing the up/down arrow keys will focus a valid non-disabled menu item - the first item for the down arrow key and last item for the up arrow key. + * + * @param event Event triggered by the keyboard + * @param menuRef Menu reference + */ +export const onToggleArrowKeydownDefault = (event: KeyboardEvent, menuRef: React.RefObject) => { + if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') { + return; + } + + event.preventDefault(); + + const listItems = Array.from(menuRef.current?.querySelectorAll('li')); + const focusableElements = listItems + .map((li) => li.querySelector('button:not(:disabled),input:not(:disabled),a:not([aria-disabled="true"])')) + .filter((el) => el !== null); + + let focusableElement: Element; + if (event.key === 'ArrowDown') { + focusableElement = focusableElements[0]; + } else { + focusableElement = focusableElements[focusableElements.length - 1]; + } + focusableElement && (focusableElement as HTMLElement).focus(); +}; + class KeyboardHandler extends React.Component { static displayName = 'KeyboardHandler'; static defaultProps: KeyboardHandlerProps = { diff --git a/packages/react-templates/src/components/Select/MultiTypeaheadSelect.tsx b/packages/react-templates/src/components/Select/MultiTypeaheadSelect.tsx index 5beb2b9d7b5..552b4594d51 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..b0e14b5282b 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} + variant="typeahead" ref={innerRef} {...props} >