Skip to content

Commit 01b3094

Browse files
Andrewgdewartlabaj
andauthored
Fixes 11053: focus event causing jumpy scroll effect (#11056)
* Fixes 11053: focus event causing jumpy scroll effect * revert prevent scroll on toggle --------- Co-authored-by: Titani <tlabaj@redhat.com>
1 parent 11da2f2 commit 01b3094

5 files changed

Lines changed: 65 additions & 17 deletions

File tree

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

Lines changed: 19 additions & 4 deletions
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>();
@@ -126,8 +132,8 @@ const DropdownBase: React.FunctionComponent<DropdownProps> = ({
126132
const firstElement = menuRef?.current?.querySelector(
127133
'li button:not(:disabled),li input:not(:disabled),li a:not([aria-disabled="true"])'
128134
);
129-
firstElement && (firstElement as HTMLElement).focus();
130-
}, 10);
135+
firstElement && (firstElement as HTMLElement).focus({ preventScroll: shouldPreventScrollOnItemFocus });
136+
}, focusTimeoutDelay);
131137
}
132138

133139
// If the event is not on the toggle and onOpenChange callback is provided, close the menu
@@ -145,7 +151,16 @@ const DropdownBase: React.FunctionComponent<DropdownProps> = ({
145151
window.removeEventListener('keydown', handleMenuKeys);
146152
window.removeEventListener('click', handleClick);
147153
};
148-
}, [isOpen, menuRef, toggleRef, onOpenChange, onOpenChangeKeys]);
154+
}, [
155+
isOpen,
156+
menuRef,
157+
toggleRef,
158+
onOpenChange,
159+
onOpenChangeKeys,
160+
shouldPreventScrollOnItemFocus,
161+
shouldFocusFirstItemOnOpen,
162+
focusTimeoutDelay
163+
]);
149164

150165
const scrollable = maxMenuHeight !== undefined || menuHeight !== undefined || isScrollable;
151166

@@ -155,7 +170,7 @@ const DropdownBase: React.FunctionComponent<DropdownProps> = ({
155170
ref={menuRef}
156171
onSelect={(event, value) => {
157172
onSelect && onSelect(event, value);
158-
shouldFocusToggleOnSelect && toggleRef.current.focus();
173+
shouldFocusToggleOnSelect && toggleRef.current?.focus();
159174
}}
160175
isPlain={isPlain}
161176
isScrollable={scrollable}

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ export interface MenuContainerProps {
3737
zIndex?: number;
3838
/** Additional properties to pass to the Popper */
3939
popperProps?: MenuPopperProps;
40+
/** Flag indicating if scroll on focus of the first menu item should occur. */
41+
shouldPreventScrollOnItemFocus?: boolean;
42+
/** Time in ms to wait before firing the toggles' focus event. Defaults to 0 */
43+
focusTimeoutDelay?: number;
4044
}
4145

4246
/**
@@ -52,7 +56,9 @@ export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
5256
onOpenChange,
5357
zIndex = 9999,
5458
popperProps,
55-
onOpenChangeKeys = ['Escape', 'Tab']
59+
onOpenChangeKeys = ['Escape', 'Tab'],
60+
shouldPreventScrollOnItemFocus = true,
61+
focusTimeoutDelay = 0
5662
}: MenuContainerProps) => {
5763
React.useEffect(() => {
5864
const handleMenuKeys = (event: KeyboardEvent) => {
@@ -75,8 +81,8 @@ export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
7581
const firstElement = menuRef?.current?.querySelector(
7682
'li button:not(:disabled),li input:not(:disabled),li a:not([aria-disabled="true"])'
7783
);
78-
firstElement && (firstElement as HTMLElement).focus();
79-
}, 0);
84+
firstElement && (firstElement as HTMLElement).focus({ preventScroll: shouldPreventScrollOnItemFocus });
85+
}, focusTimeoutDelay);
8086
}
8187

8288
// 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<MenuContainerProps> = ({
94100
window.removeEventListener('keydown', handleMenuKeys);
95101
window.removeEventListener('click', handleClick);
96102
};
97-
}, [isOpen, menuRef, onOpenChange, onOpenChangeKeys, toggleRef]);
103+
}, [focusTimeoutDelay, isOpen, menuRef, onOpenChange, onOpenChangeKeys, shouldPreventScrollOnItemFocus, toggleRef]);
98104

99105
return (
100106
<Popper

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

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

6165
export const PaginationOptionsMenu: React.FunctionComponent<PaginationOptionsMenuProps> = ({
@@ -80,7 +84,9 @@ export const PaginationOptionsMenu: React.FunctionComponent<PaginationOptionsMen
8084
toggleTemplate,
8185
onPerPageSelect = () => null as any,
8286
containerRef,
83-
appendTo
87+
appendTo,
88+
shouldPreventScrollOnItemFocus = true,
89+
focusTimeoutDelay = 0
8490
}: PaginationOptionsMenuProps) => {
8591
const [isOpen, setIsOpen] = React.useState(false);
8692
const toggleRef = React.useRef<HTMLButtonElement>(null);
@@ -133,8 +139,8 @@ export const PaginationOptionsMenu: React.FunctionComponent<PaginationOptionsMen
133139
if (isOpen && toggleRef.current?.contains(event.target as Node)) {
134140
setTimeout(() => {
135141
const firstElement = menuRef?.current?.querySelector('li button:not(:disabled)');
136-
firstElement && (firstElement as HTMLElement).focus();
137-
}, 0);
142+
firstElement && (firstElement as HTMLElement).focus({ preventScroll: shouldPreventScrollOnItemFocus });
143+
}, focusTimeoutDelay);
138144
}
139145

140146
// If the event is not on the toggle, close the menu
@@ -154,7 +160,7 @@ export const PaginationOptionsMenu: React.FunctionComponent<PaginationOptionsMen
154160
window.removeEventListener('keydown', handleMenuKeys);
155161
window.removeEventListener('click', handleClick);
156162
};
157-
}, [isOpen, menuRef]);
163+
}, [focusTimeoutDelay, isOpen, menuRef, shouldPreventScrollOnItemFocus]);
158164

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

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

Lines changed: 18 additions & 3 deletions
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>();
@@ -131,8 +137,8 @@ const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
131137
if (isOpen && shouldFocusFirstItemOnOpen && toggleRef.current?.contains(event.target as Node)) {
132138
setTimeout(() => {
133139
const firstElement = menuRef?.current?.querySelector('li button:not(:disabled),li input:not(:disabled)');
134-
firstElement && (firstElement as HTMLElement).focus();
135-
}, 10);
140+
firstElement && (firstElement as HTMLElement).focus({ preventScroll: shouldPreventScrollOnItemFocus });
141+
}, focusTimeoutDelay);
136142
}
137143

138144
// If the event is not on the toggle and onOpenChange callback is provided, close the menu
@@ -150,7 +156,16 @@ const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
150156
window.removeEventListener('keydown', handleMenuKeys);
151157
window.removeEventListener('click', handleClick);
152158
};
153-
}, [isOpen, menuRef, toggleRef, onOpenChange, onOpenChangeKeys]);
159+
}, [
160+
isOpen,
161+
menuRef,
162+
toggleRef,
163+
onOpenChange,
164+
onOpenChangeKeys,
165+
shouldPreventScrollOnItemFocus,
166+
shouldFocusFirstItemOnOpen,
167+
focusTimeoutDelay
168+
]);
154169

155170
const menu = (
156171
<Menu

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

Lines changed: 8 additions & 2 deletions
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)