diff --git a/packages/react-core/src/components/Dropdown/Dropdown.tsx b/packages/react-core/src/components/Dropdown/Dropdown.tsx index 2aaa1c38b44..5f6beaeed3a 100644 --- a/packages/react-core/src/components/Dropdown/Dropdown.tsx +++ b/packages/react-core/src/components/Dropdown/Dropdown.tsx @@ -71,6 +71,10 @@ export interface DropdownProps extends MenuProps, OUIAProps { maxMenuHeight?: string; /** @beta Flag indicating the first menu item should be focused after opening the dropdown. */ shouldFocusFirstItemOnOpen?: boolean; + /** Flag indicating if scroll on focus of the first menu item should occur. */ + shouldPreventScrollOnItemFocus?: boolean; + /** Time in ms to wait before firing the toggles' focus event. Defaults to 0 */ + focusTimeoutDelay?: number; } const DropdownBase: React.FunctionComponent = ({ @@ -92,6 +96,8 @@ const DropdownBase: React.FunctionComponent = ({ menuHeight, maxMenuHeight, shouldFocusFirstItemOnOpen = true, + shouldPreventScrollOnItemFocus = true, + focusTimeoutDelay = 0, ...props }: DropdownProps) => { const localMenuRef = React.useRef(); @@ -114,7 +120,7 @@ const DropdownBase: React.FunctionComponent = ({ ) { if (onOpenChangeKeys.includes(event.key)) { onOpenChange(false); - toggleRef.current?.focus(); + toggleRef.current?.focus({ preventScroll: shouldPreventScrollOnItemFocus }); } } }; @@ -126,8 +132,8 @@ const DropdownBase: React.FunctionComponent = ({ const firstElement = menuRef?.current?.querySelector( 'li button:not(:disabled),li input:not(:disabled),li a:not([aria-disabled="true"])' ); - firstElement && (firstElement as HTMLElement).focus(); - }, 10); + firstElement && (firstElement as HTMLElement).focus({ preventScroll: shouldPreventScrollOnItemFocus }); + }, focusTimeoutDelay); } // If the event is not on the toggle and onOpenChange callback is provided, close the menu @@ -145,7 +151,16 @@ const DropdownBase: React.FunctionComponent = ({ window.removeEventListener('keydown', handleMenuKeys); window.removeEventListener('click', handleClick); }; - }, [isOpen, menuRef, toggleRef, onOpenChange, onOpenChangeKeys]); + }, [ + isOpen, + menuRef, + toggleRef, + onOpenChange, + onOpenChangeKeys, + shouldPreventScrollOnItemFocus, + shouldFocusFirstItemOnOpen, + focusTimeoutDelay + ]); const scrollable = maxMenuHeight !== undefined || menuHeight !== undefined || isScrollable; @@ -155,7 +170,7 @@ const DropdownBase: React.FunctionComponent = ({ ref={menuRef} onSelect={(event, value) => { onSelect && onSelect(event, value); - shouldFocusToggleOnSelect && toggleRef.current.focus(); + shouldFocusToggleOnSelect && toggleRef.current?.focus(); }} isPlain={isPlain} isScrollable={scrollable} diff --git a/packages/react-core/src/components/Menu/MenuContainer.tsx b/packages/react-core/src/components/Menu/MenuContainer.tsx index 6208fb02222..2a839ffd453 100644 --- a/packages/react-core/src/components/Menu/MenuContainer.tsx +++ b/packages/react-core/src/components/Menu/MenuContainer.tsx @@ -37,6 +37,10 @@ export interface MenuContainerProps { zIndex?: number; /** Additional properties to pass to the Popper */ popperProps?: MenuPopperProps; + /** Flag indicating if scroll on focus of the first menu item should occur. */ + shouldPreventScrollOnItemFocus?: boolean; + /** Time in ms to wait before firing the toggles' focus event. Defaults to 0 */ + focusTimeoutDelay?: number; } /** @@ -52,7 +56,9 @@ export const MenuContainer: React.FunctionComponent = ({ onOpenChange, zIndex = 9999, popperProps, - onOpenChangeKeys = ['Escape', 'Tab'] + onOpenChangeKeys = ['Escape', 'Tab'], + shouldPreventScrollOnItemFocus = true, + focusTimeoutDelay = 0 }: MenuContainerProps) => { React.useEffect(() => { const handleMenuKeys = (event: KeyboardEvent) => { @@ -63,7 +69,7 @@ export const MenuContainer: React.FunctionComponent = ({ ) { if (onOpenChangeKeys.includes(event.key)) { onOpenChange(false); - toggleRef.current?.focus(); + toggleRef.current?.focus({ preventScroll: shouldPreventScrollOnItemFocus }); } } }; @@ -75,8 +81,8 @@ export const MenuContainer: React.FunctionComponent = ({ const firstElement = menuRef?.current?.querySelector( 'li button:not(:disabled),li input:not(:disabled),li a:not([aria-disabled="true"])' ); - firstElement && (firstElement as HTMLElement).focus(); - }, 0); + firstElement && (firstElement as HTMLElement).focus({ preventScroll: shouldPreventScrollOnItemFocus }); + }, focusTimeoutDelay); } // If the event is not on the toggle and onOpenChange callback is provided, close the menu @@ -94,7 +100,7 @@ export const MenuContainer: React.FunctionComponent = ({ window.removeEventListener('keydown', handleMenuKeys); window.removeEventListener('click', handleClick); }; - }, [isOpen, menuRef, onOpenChange, onOpenChangeKeys, toggleRef]); + }, [focusTimeoutDelay, isOpen, menuRef, onOpenChange, onOpenChangeKeys, shouldPreventScrollOnItemFocus, toggleRef]); return ( ; /** @beta The container to append the pagination options menu to. Overrides the containerRef prop. */ appendTo?: HTMLElement | (() => HTMLElement) | 'inline'; + /** Flag indicating if scroll on focus of the first menu item should occur. */ + shouldPreventScrollOnItemFocus?: boolean; + /** Time in ms to wait before firing the toggles' focus event. Defaults to 0 */ + focusTimeoutDelay?: number; } export const PaginationOptionsMenu: React.FunctionComponent = ({ @@ -80,7 +84,9 @@ export const PaginationOptionsMenu: React.FunctionComponent null as any, containerRef, - appendTo + appendTo, + shouldPreventScrollOnItemFocus = true, + focusTimeoutDelay = 0 }: PaginationOptionsMenuProps) => { const [isOpen, setIsOpen] = React.useState(false); const toggleRef = React.useRef(null); @@ -123,7 +129,7 @@ export const PaginationOptionsMenu: React.FunctionComponent { const firstElement = menuRef?.current?.querySelector('li button:not(:disabled)'); - firstElement && (firstElement as HTMLElement).focus(); - }, 0); + firstElement && (firstElement as HTMLElement).focus({ preventScroll: shouldPreventScrollOnItemFocus }); + }, focusTimeoutDelay); } // If the event is not on the toggle, close the menu @@ -154,7 +160,7 @@ export const PaginationOptionsMenu: React.FunctionComponent perPageOptions.map(({ value, title }) => ( diff --git a/packages/react-core/src/components/Select/Select.tsx b/packages/react-core/src/components/Select/Select.tsx index 21179505802..a0ab2146003 100644 --- a/packages/react-core/src/components/Select/Select.tsx +++ b/packages/react-core/src/components/Select/Select.tsx @@ -78,6 +78,10 @@ export interface SelectProps extends MenuProps, OUIAProps { maxMenuHeight?: string; /** Indicates if the select menu should be scrollable */ isScrollable?: boolean; + /** Flag indicating if scroll on focus of the first menu item should occur. */ + shouldPreventScrollOnItemFocus?: boolean; + /** Time in ms to wait before firing the toggles' focus event. Defaults to 0 */ + focusTimeoutDelay?: number; } const SelectBase: React.FunctionComponent = ({ @@ -99,6 +103,8 @@ const SelectBase: React.FunctionComponent = ({ menuHeight, maxMenuHeight, isScrollable, + shouldPreventScrollOnItemFocus = true, + focusTimeoutDelay = 0, ...props }: SelectProps & OUIAProps) => { const localMenuRef = React.useRef(); @@ -121,7 +127,7 @@ const SelectBase: React.FunctionComponent = ({ if (onOpenChangeKeys.includes(event.key)) { event.preventDefault(); onOpenChange(false); - toggleRef.current?.focus(); + toggleRef.current?.focus({ preventScroll: shouldPreventScrollOnItemFocus }); } } }; @@ -131,8 +137,8 @@ const SelectBase: React.FunctionComponent = ({ if (isOpen && shouldFocusFirstItemOnOpen && toggleRef.current?.contains(event.target as Node)) { setTimeout(() => { const firstElement = menuRef?.current?.querySelector('li button:not(:disabled),li input:not(:disabled)'); - firstElement && (firstElement as HTMLElement).focus(); - }, 10); + firstElement && (firstElement as HTMLElement).focus({ preventScroll: shouldPreventScrollOnItemFocus }); + }, focusTimeoutDelay); } // If the event is not on the toggle and onOpenChange callback is provided, close the menu @@ -150,7 +156,16 @@ const SelectBase: React.FunctionComponent = ({ window.removeEventListener('keydown', handleMenuKeys); window.removeEventListener('click', handleClick); }; - }, [isOpen, menuRef, toggleRef, onOpenChange, onOpenChangeKeys]); + }, [ + isOpen, + menuRef, + toggleRef, + onOpenChange, + onOpenChangeKeys, + shouldPreventScrollOnItemFocus, + shouldFocusFirstItemOnOpen, + focusTimeoutDelay + ]); const menu = ( { toggleAriaLabel?: string; /** z-index of the overflow tab */ zIndex?: number; + /** Flag indicating if scroll on focus of the first menu item should occur. */ + shouldPreventScrollOnItemFocus?: boolean; + /** Time in ms to wait before firing the toggles' focus event. Defaults to 0 */ + focusTimeoutDelay?: number; } export const OverflowTab: React.FunctionComponent = ({ @@ -30,6 +34,8 @@ export const OverflowTab: React.FunctionComponent = ({ defaultTitleText = 'More', toggleAriaLabel, zIndex = 9999, + shouldPreventScrollOnItemFocus = true, + focusTimeoutDelay = 0, ...props }: OverflowTabProps) => { const menuRef = React.useRef(); @@ -78,9 +84,9 @@ export const OverflowTab: React.FunctionComponent = ({ setTimeout(() => { if (menuRef?.current) { const firstElement = menuRef.current.querySelector('li > button,input:not(:disabled)'); - firstElement && (firstElement as HTMLElement).focus(); + firstElement && (firstElement as HTMLElement).focus({ preventScroll: shouldPreventScrollOnItemFocus }); } - }, 0); + }, focusTimeoutDelay); }; const overflowTab = (