Skip to content

Commit

Permalink
fix(Menus): added new fix for scroll jump bug (#11119)
Browse files Browse the repository at this point in the history
Co-authored-by: Donald Labaj <[email protected]>
  • Loading branch information
thatblindgeye and dlabaj authored Oct 23, 2024
1 parent e6c87a6 commit f19a7a5
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 17 deletions.
23 changes: 19 additions & 4 deletions packages/react-core/src/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<DropdownProps> = ({
Expand All @@ -92,6 +96,8 @@ const DropdownBase: React.FunctionComponent<DropdownProps> = ({
menuHeight,
maxMenuHeight,
shouldFocusFirstItemOnOpen = false,
shouldPreventScrollOnItemFocus = true,
focusTimeoutDelay = 0,
...props
}: DropdownProps) => {
const localMenuRef = React.useRef<HTMLDivElement>();
Expand All @@ -112,8 +118,8 @@ const DropdownBase: React.FunctionComponent<DropdownProps> = ({
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);
}

prevIsOpen.current = isOpen;
Expand Down Expand Up @@ -151,7 +157,16 @@ const DropdownBase: React.FunctionComponent<DropdownProps> = ({
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;

Expand All @@ -161,7 +176,7 @@ const DropdownBase: React.FunctionComponent<DropdownProps> = ({
ref={menuRef}
onSelect={(event, value) => {
onSelect && onSelect(event, value);
shouldFocusToggleOnSelect && toggleRef.current.focus();
shouldFocusToggleOnSelect && toggleRef.current?.focus();
}}
isPlain={isPlain}
isScrollable={scrollable}
Expand Down
14 changes: 10 additions & 4 deletions packages/react-core/src/components/Menu/MenuContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export interface MenuContainerProps {
popperProps?: MenuPopperProps;
/** @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;
}

/**
Expand All @@ -54,7 +58,9 @@ export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
zIndex = 9999,
popperProps,
onOpenChangeKeys = ['Escape', 'Tab'],
shouldFocusFirstItemOnOpen = true
shouldFocusFirstItemOnOpen = true,
shouldPreventScrollOnItemFocus = true,
focusTimeoutDelay = 0
}: MenuContainerProps) => {
const prevIsOpen = React.useRef<boolean>(isOpen);
React.useEffect(() => {
Expand All @@ -64,8 +70,8 @@ export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
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);
}

prevIsOpen.current = isOpen;
Expand Down Expand Up @@ -102,7 +108,7 @@ export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
window.removeEventListener('keydown', handleMenuKeys);
window.removeEventListener('click', handleClick);
};
}, [isOpen, menuRef, onOpenChange, onOpenChangeKeys, toggleRef]);
}, [focusTimeoutDelay, isOpen, menuRef, onOpenChange, onOpenChangeKeys, shouldPreventScrollOnItemFocus, toggleRef]);

return (
<Popper
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export interface PaginationOptionsMenuProps extends React.HTMLProps<HTMLDivEleme
containerRef?: React.RefObject<HTMLDivElement>;
/** @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<PaginationOptionsMenuProps> = ({
Expand All @@ -81,7 +85,9 @@ export const PaginationOptionsMenu: React.FunctionComponent<PaginationOptionsMen
toggleTemplate,
onPerPageSelect = () => null as any,
containerRef,
appendTo
appendTo,
shouldPreventScrollOnItemFocus = true,
focusTimeoutDelay = 0
}: PaginationOptionsMenuProps) => {
const [isOpen, setIsOpen] = React.useState(false);
const toggleRef = React.useRef<HTMLButtonElement>(null);
Expand Down Expand Up @@ -134,8 +140,8 @@ export const PaginationOptionsMenu: React.FunctionComponent<PaginationOptionsMen
if (isOpen && toggleRef.current?.contains(event.target as Node)) {
setTimeout(() => {
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
Expand All @@ -155,7 +161,7 @@ export const PaginationOptionsMenu: React.FunctionComponent<PaginationOptionsMen
window.removeEventListener('keydown', handleMenuKeys);
window.removeEventListener('click', handleClick);
};
}, [isOpen, menuRef]);
}, [focusTimeoutDelay, isOpen, menuRef, shouldPreventScrollOnItemFocus]);

const renderItems = () =>
perPageOptions.map(({ value, title }) => (
Expand Down
21 changes: 18 additions & 3 deletions packages/react-core/src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<SelectProps & OUIAProps> = ({
Expand All @@ -99,6 +103,8 @@ const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
menuHeight,
maxMenuHeight,
isScrollable,
shouldPreventScrollOnItemFocus = true,
focusTimeoutDelay = 0,
...props
}: SelectProps & OUIAProps) => {
const localMenuRef = React.useRef<HTMLDivElement>();
Expand All @@ -116,8 +122,8 @@ const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
if (prevIsOpen.current === false && isOpen === true && shouldFocusFirstItemOnOpen) {
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);
}

prevIsOpen.current = isOpen;
Expand Down Expand Up @@ -156,7 +162,16 @@ const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
window.removeEventListener('keydown', handleMenuKeys);
window.removeEventListener('click', handleClick);
};
}, [isOpen, menuRef, toggleRef, onOpenChange, onOpenChangeKeys]);
}, [
isOpen,
menuRef,
toggleRef,
onOpenChange,
onOpenChangeKeys,
shouldPreventScrollOnItemFocus,
shouldFocusFirstItemOnOpen,
focusTimeoutDelay
]);

const menu = (
<Menu
Expand Down
10 changes: 8 additions & 2 deletions packages/react-core/src/components/Tabs/OverflowTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export interface OverflowTabProps extends React.HTMLProps<HTMLLIElement> {
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<OverflowTabProps> = ({
Expand All @@ -30,6 +34,8 @@ export const OverflowTab: React.FunctionComponent<OverflowTabProps> = ({
defaultTitleText = 'More',
toggleAriaLabel,
zIndex = 9999,
shouldPreventScrollOnItemFocus = true,
focusTimeoutDelay = 0,
...props
}: OverflowTabProps) => {
const menuRef = React.useRef<HTMLDivElement>();
Expand Down Expand Up @@ -78,9 +84,9 @@ export const OverflowTab: React.FunctionComponent<OverflowTabProps> = ({
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 = (
Expand Down

0 comments on commit f19a7a5

Please sign in to comment.