Skip to content

Commit f19a7a5

Browse files
fix(Menus): added new fix for scroll jump bug (#11119)
Co-authored-by: Donald Labaj <[email protected]>
1 parent e6c87a6 commit f19a7a5

File tree

5 files changed

+65
-17
lines changed

5 files changed

+65
-17
lines changed

packages/react-core/src/components/Dropdown/Dropdown.tsx

+19-4
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ export interface DropdownProps extends MenuProps, OUIAProps {
7171
maxMenuHeight?: string;
7272
/** @beta Flag indicating the first menu item should be focused after opening the dropdown. */
7373
shouldFocusFirstItemOnOpen?: boolean;
74+
/** Flag indicating if scroll on focus of the first menu item should occur. */
75+
shouldPreventScrollOnItemFocus?: boolean;
76+
/** Time in ms to wait before firing the toggles' focus event. Defaults to 0 */
77+
focusTimeoutDelay?: number;
7478
}
7579

7680
const DropdownBase: React.FunctionComponent<DropdownProps> = ({
@@ -92,6 +96,8 @@ const DropdownBase: React.FunctionComponent<DropdownProps> = ({
9296
menuHeight,
9397
maxMenuHeight,
9498
shouldFocusFirstItemOnOpen = false,
99+
shouldPreventScrollOnItemFocus = true,
100+
focusTimeoutDelay = 0,
95101
...props
96102
}: DropdownProps) => {
97103
const localMenuRef = React.useRef<HTMLDivElement>();
@@ -112,8 +118,8 @@ const DropdownBase: React.FunctionComponent<DropdownProps> = ({
112118
const firstElement = menuRef?.current?.querySelector(
113119
'li button:not(:disabled),li input:not(:disabled),li a:not([aria-disabled="true"])'
114120
);
115-
firstElement && (firstElement as HTMLElement).focus();
116-
}, 10);
121+
firstElement && (firstElement as HTMLElement).focus({ preventScroll: shouldPreventScrollOnItemFocus });
122+
}, focusTimeoutDelay);
117123
}
118124

119125
prevIsOpen.current = isOpen;
@@ -151,7 +157,16 @@ const DropdownBase: React.FunctionComponent<DropdownProps> = ({
151157
window.removeEventListener('keydown', handleMenuKeys);
152158
window.removeEventListener('click', handleClick);
153159
};
154-
}, [isOpen, menuRef, toggleRef, onOpenChange, onOpenChangeKeys]);
160+
}, [
161+
isOpen,
162+
menuRef,
163+
toggleRef,
164+
onOpenChange,
165+
onOpenChangeKeys,
166+
shouldPreventScrollOnItemFocus,
167+
shouldFocusFirstItemOnOpen,
168+
focusTimeoutDelay
169+
]);
155170

156171
const scrollable = maxMenuHeight !== undefined || menuHeight !== undefined || isScrollable;
157172

@@ -161,7 +176,7 @@ const DropdownBase: React.FunctionComponent<DropdownProps> = ({
161176
ref={menuRef}
162177
onSelect={(event, value) => {
163178
onSelect && onSelect(event, value);
164-
shouldFocusToggleOnSelect && toggleRef.current.focus();
179+
shouldFocusToggleOnSelect && toggleRef.current?.focus();
165180
}}
166181
isPlain={isPlain}
167182
isScrollable={scrollable}

packages/react-core/src/components/Menu/MenuContainer.tsx

+10-4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ export interface MenuContainerProps {
3939
popperProps?: MenuPopperProps;
4040
/** @beta Flag indicating the first menu item should be focused after opening the dropdown. */
4141
shouldFocusFirstItemOnOpen?: boolean;
42+
/** Flag indicating if scroll on focus of the first menu item should occur. */
43+
shouldPreventScrollOnItemFocus?: boolean;
44+
/** Time in ms to wait before firing the toggles' focus event. Defaults to 0 */
45+
focusTimeoutDelay?: number;
4246
}
4347

4448
/**
@@ -54,7 +58,9 @@ export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
5458
zIndex = 9999,
5559
popperProps,
5660
onOpenChangeKeys = ['Escape', 'Tab'],
57-
shouldFocusFirstItemOnOpen = true
61+
shouldFocusFirstItemOnOpen = true,
62+
shouldPreventScrollOnItemFocus = true,
63+
focusTimeoutDelay = 0
5864
}: MenuContainerProps) => {
5965
const prevIsOpen = React.useRef<boolean>(isOpen);
6066
React.useEffect(() => {
@@ -64,8 +70,8 @@ export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
6470
const firstElement = menuRef?.current?.querySelector(
6571
'li button:not(:disabled),li input:not(:disabled),li a:not([aria-disabled="true"])'
6672
);
67-
firstElement && (firstElement as HTMLElement).focus();
68-
}, 10);
73+
firstElement && (firstElement as HTMLElement).focus({ preventScroll: shouldPreventScrollOnItemFocus });
74+
}, focusTimeoutDelay);
6975
}
7076

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

107113
return (
108114
<Popper

packages/react-core/src/components/Pagination/PaginationOptionsMenu.tsx

+10-4
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ export interface PaginationOptionsMenuProps extends React.HTMLProps<HTMLDivEleme
5757
containerRef?: React.RefObject<HTMLDivElement>;
5858
/** @beta The container to append the pagination options menu to. Overrides the containerRef prop. */
5959
appendTo?: HTMLElement | (() => HTMLElement) | 'inline';
60+
/** Flag indicating if scroll on focus of the first menu item should occur. */
61+
shouldPreventScrollOnItemFocus?: boolean;
62+
/** Time in ms to wait before firing the toggles' focus event. Defaults to 0 */
63+
focusTimeoutDelay?: number;
6064
}
6165

6266
export const PaginationOptionsMenu: React.FunctionComponent<PaginationOptionsMenuProps> = ({
@@ -81,7 +85,9 @@ export const PaginationOptionsMenu: React.FunctionComponent<PaginationOptionsMen
8185
toggleTemplate,
8286
onPerPageSelect = () => null as any,
8387
containerRef,
84-
appendTo
88+
appendTo,
89+
shouldPreventScrollOnItemFocus = true,
90+
focusTimeoutDelay = 0
8591
}: PaginationOptionsMenuProps) => {
8692
const [isOpen, setIsOpen] = React.useState(false);
8793
const toggleRef = React.useRef<HTMLButtonElement>(null);
@@ -134,8 +140,8 @@ export const PaginationOptionsMenu: React.FunctionComponent<PaginationOptionsMen
134140
if (isOpen && toggleRef.current?.contains(event.target as Node)) {
135141
setTimeout(() => {
136142
const firstElement = menuRef?.current?.querySelector('li button:not(:disabled)');
137-
firstElement && (firstElement as HTMLElement).focus();
138-
}, 0);
143+
firstElement && (firstElement as HTMLElement).focus({ preventScroll: shouldPreventScrollOnItemFocus });
144+
}, focusTimeoutDelay);
139145
}
140146

141147
// If the event is not on the toggle, close the menu
@@ -155,7 +161,7 @@ export const PaginationOptionsMenu: React.FunctionComponent<PaginationOptionsMen
155161
window.removeEventListener('keydown', handleMenuKeys);
156162
window.removeEventListener('click', handleClick);
157163
};
158-
}, [isOpen, menuRef]);
164+
}, [focusTimeoutDelay, isOpen, menuRef, shouldPreventScrollOnItemFocus]);
159165

160166
const renderItems = () =>
161167
perPageOptions.map(({ value, title }) => (

packages/react-core/src/components/Select/Select.tsx

+18-3
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ export interface SelectProps extends MenuProps, OUIAProps {
7878
maxMenuHeight?: string;
7979
/** Indicates if the select menu should be scrollable */
8080
isScrollable?: boolean;
81+
/** Flag indicating if scroll on focus of the first menu item should occur. */
82+
shouldPreventScrollOnItemFocus?: boolean;
83+
/** Time in ms to wait before firing the toggles' focus event. Defaults to 0 */
84+
focusTimeoutDelay?: number;
8185
}
8286

8387
const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
@@ -99,6 +103,8 @@ const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
99103
menuHeight,
100104
maxMenuHeight,
101105
isScrollable,
106+
shouldPreventScrollOnItemFocus = true,
107+
focusTimeoutDelay = 0,
102108
...props
103109
}: SelectProps & OUIAProps) => {
104110
const localMenuRef = React.useRef<HTMLDivElement>();
@@ -116,8 +122,8 @@ const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
116122
if (prevIsOpen.current === false && isOpen === true && shouldFocusFirstItemOnOpen) {
117123
setTimeout(() => {
118124
const firstElement = menuRef?.current?.querySelector('li button:not(:disabled),li input:not(:disabled)');
119-
firstElement && (firstElement as HTMLElement).focus();
120-
}, 10);
125+
firstElement && (firstElement as HTMLElement).focus({ preventScroll: shouldPreventScrollOnItemFocus });
126+
}, focusTimeoutDelay);
121127
}
122128

123129
prevIsOpen.current = isOpen;
@@ -156,7 +162,16 @@ const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
156162
window.removeEventListener('keydown', handleMenuKeys);
157163
window.removeEventListener('click', handleClick);
158164
};
159-
}, [isOpen, menuRef, toggleRef, onOpenChange, onOpenChangeKeys]);
165+
}, [
166+
isOpen,
167+
menuRef,
168+
toggleRef,
169+
onOpenChange,
170+
onOpenChangeKeys,
171+
shouldPreventScrollOnItemFocus,
172+
shouldFocusFirstItemOnOpen,
173+
focusTimeoutDelay
174+
]);
160175

161176
const menu = (
162177
<Menu

packages/react-core/src/components/Tabs/OverflowTab.tsx

+8-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export interface OverflowTabProps extends React.HTMLProps<HTMLLIElement> {
2121
toggleAriaLabel?: string;
2222
/** z-index of the overflow tab */
2323
zIndex?: number;
24+
/** Flag indicating if scroll on focus of the first menu item should occur. */
25+
shouldPreventScrollOnItemFocus?: boolean;
26+
/** Time in ms to wait before firing the toggles' focus event. Defaults to 0 */
27+
focusTimeoutDelay?: number;
2428
}
2529

2630
export const OverflowTab: React.FunctionComponent<OverflowTabProps> = ({
@@ -30,6 +34,8 @@ export const OverflowTab: React.FunctionComponent<OverflowTabProps> = ({
3034
defaultTitleText = 'More',
3135
toggleAriaLabel,
3236
zIndex = 9999,
37+
shouldPreventScrollOnItemFocus = true,
38+
focusTimeoutDelay = 0,
3339
...props
3440
}: OverflowTabProps) => {
3541
const menuRef = React.useRef<HTMLDivElement>();
@@ -78,9 +84,9 @@ export const OverflowTab: React.FunctionComponent<OverflowTabProps> = ({
7884
setTimeout(() => {
7985
if (menuRef?.current) {
8086
const firstElement = menuRef.current.querySelector('li > button,input:not(:disabled)');
81-
firstElement && (firstElement as HTMLElement).focus();
87+
firstElement && (firstElement as HTMLElement).focus({ preventScroll: shouldPreventScrollOnItemFocus });
8288
}
83-
}, 0);
89+
}, focusTimeoutDelay);
8490
};
8591

8692
const overflowTab = (

0 commit comments

Comments
 (0)