Skip to content

Commit 1b3ffd0

Browse files
committed
Fixes 11053: focus event causing jumpy scroll effect
1 parent 6f9fc9a commit 1b3ffd0

File tree

5 files changed

+39
-33
lines changed

5 files changed

+39
-33
lines changed

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ 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;
7476
}
7577

7678
const DropdownBase: React.FunctionComponent<DropdownProps> = ({
@@ -92,6 +94,7 @@ const DropdownBase: React.FunctionComponent<DropdownProps> = ({
9294
menuHeight,
9395
maxMenuHeight,
9496
shouldFocusFirstItemOnOpen = true,
97+
shouldPreventScrollOnItemFocus = true,
9598
...props
9699
}: DropdownProps) => {
97100
const localMenuRef = React.useRef<HTMLDivElement>();
@@ -114,20 +117,18 @@ const DropdownBase: React.FunctionComponent<DropdownProps> = ({
114117
) {
115118
if (onOpenChangeKeys.includes(event.key)) {
116119
onOpenChange(false);
117-
toggleRef.current?.focus();
120+
toggleRef.current?.focus({ preventScroll: shouldPreventScrollOnItemFocus });
118121
}
119122
}
120123
};
121124

122125
const handleClick = (event: MouseEvent) => {
123126
// toggle was opened, focus on first menu item
124127
if (isOpen && shouldFocusFirstItemOnOpen && toggleRef.current?.contains(event.target as Node)) {
125-
setTimeout(() => {
126-
const firstElement = menuRef?.current?.querySelector(
127-
'li button:not(:disabled),li input:not(:disabled),li a:not([aria-disabled="true"])'
128-
);
129-
firstElement && (firstElement as HTMLElement).focus();
130-
}, 10);
128+
const firstElement = menuRef?.current?.querySelector(
129+
'li button:not(:disabled),li input:not(:disabled),li a:not([aria-disabled="true"])'
130+
);
131+
firstElement && (firstElement as HTMLElement).focus({ preventScroll: shouldPreventScrollOnItemFocus });
131132
}
132133

133134
// If the event is not on the toggle and onOpenChange callback is provided, close the menu
@@ -155,7 +156,7 @@ const DropdownBase: React.FunctionComponent<DropdownProps> = ({
155156
ref={menuRef}
156157
onSelect={(event, value) => {
157158
onSelect && onSelect(event, value);
158-
shouldFocusToggleOnSelect && toggleRef.current.focus();
159+
shouldFocusToggleOnSelect && toggleRef.current?.focus();
159160
}}
160161
isPlain={isPlain}
161162
isScrollable={scrollable}

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ 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;
4042
}
4143

4244
/**
@@ -52,7 +54,8 @@ export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
5254
onOpenChange,
5355
zIndex = 9999,
5456
popperProps,
55-
onOpenChangeKeys = ['Escape', 'Tab']
57+
onOpenChangeKeys = ['Escape', 'Tab'],
58+
shouldPreventScrollOnItemFocus = true
5659
}: MenuContainerProps) => {
5760
React.useEffect(() => {
5861
const handleMenuKeys = (event: KeyboardEvent) => {
@@ -63,20 +66,18 @@ export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
6366
) {
6467
if (onOpenChangeKeys.includes(event.key)) {
6568
onOpenChange(false);
66-
toggleRef.current?.focus();
69+
toggleRef.current?.focus({ preventScroll: shouldPreventScrollOnItemFocus });
6770
}
6871
}
6972
};
7073

7174
const handleClick = (event: MouseEvent) => {
7275
// toggle was opened, focus on first menu item
7376
if (isOpen && toggleRef.current?.contains(event.target as Node)) {
74-
setTimeout(() => {
75-
const firstElement = menuRef?.current?.querySelector(
76-
'li button:not(:disabled),li input:not(:disabled),li a:not([aria-disabled="true"])'
77-
);
78-
firstElement && (firstElement as HTMLElement).focus();
79-
}, 0);
77+
const firstElement = menuRef?.current?.querySelector(
78+
'li button:not(:disabled),li input:not(:disabled),li a:not([aria-disabled="true"])'
79+
);
80+
firstElement && (firstElement as HTMLElement).focus({ preventScroll: shouldPreventScrollOnItemFocus });
8081
}
8182

8283
// If the event is not on the toggle and onOpenChange callback is provided, close the menu

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ 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;
5961
}
6062

6163
export const PaginationOptionsMenu: React.FunctionComponent<PaginationOptionsMenuProps> = ({
@@ -80,7 +82,8 @@ export const PaginationOptionsMenu: React.FunctionComponent<PaginationOptionsMen
8082
toggleTemplate,
8183
onPerPageSelect = () => null as any,
8284
containerRef,
83-
appendTo
85+
appendTo,
86+
shouldPreventScrollOnItemFocus = true
8487
}: PaginationOptionsMenuProps) => {
8588
const [isOpen, setIsOpen] = React.useState(false);
8689
const toggleRef = React.useRef<HTMLButtonElement>(null);
@@ -123,18 +126,16 @@ export const PaginationOptionsMenu: React.FunctionComponent<PaginationOptionsMen
123126
) {
124127
if (event.key === 'Escape' || event.key === 'Tab') {
125128
setIsOpen(false);
126-
toggleRef.current?.focus();
129+
toggleRef.current?.focus({ preventScroll: shouldPreventScrollOnItemFocus });
127130
}
128131
}
129132
};
130133

131134
const handleClick = (event: MouseEvent) => {
132135
// Focus the first non-disabled menu item on toggle 'click'
133136
if (isOpen && toggleRef.current?.contains(event.target as Node)) {
134-
setTimeout(() => {
135-
const firstElement = menuRef?.current?.querySelector('li button:not(:disabled)');
136-
firstElement && (firstElement as HTMLElement).focus();
137-
}, 0);
137+
const firstElement = menuRef?.current?.querySelector('li button:not(:disabled)');
138+
firstElement && (firstElement as HTMLElement).focus({ preventScroll: shouldPreventScrollOnItemFocus });
138139
}
139140

140141
// If the event is not on the toggle, close the menu

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ 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;
8183
}
8284

8385
const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
@@ -99,6 +101,7 @@ const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
99101
menuHeight,
100102
maxMenuHeight,
101103
isScrollable,
104+
shouldPreventScrollOnItemFocus = true,
102105
...props
103106
}: SelectProps & OUIAProps) => {
104107
const localMenuRef = React.useRef<HTMLDivElement>();
@@ -121,18 +124,16 @@ const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
121124
if (onOpenChangeKeys.includes(event.key)) {
122125
event.preventDefault();
123126
onOpenChange(false);
124-
toggleRef.current?.focus();
127+
toggleRef.current?.focus({ preventScroll: shouldPreventScrollOnItemFocus });
125128
}
126129
}
127130
};
128131

129132
const handleClick = (event: MouseEvent) => {
130133
// toggle was opened, focus on first menu item
131134
if (isOpen && shouldFocusFirstItemOnOpen && toggleRef.current?.contains(event.target as Node)) {
132-
setTimeout(() => {
133-
const firstElement = menuRef?.current?.querySelector('li button:not(:disabled),li input:not(:disabled)');
134-
firstElement && (firstElement as HTMLElement).focus();
135-
}, 10);
135+
const firstElement = menuRef?.current?.querySelector('li button:not(:disabled),li input:not(:disabled)');
136+
firstElement && (firstElement as HTMLElement).focus({ preventScroll: shouldPreventScrollOnItemFocus });
136137
}
137138

138139
// If the event is not on the toggle and onOpenChange callback is provided, close the menu

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ 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;
2426
}
2527

2628
export const OverflowTab: React.FunctionComponent<OverflowTabProps> = ({
@@ -30,6 +32,7 @@ export const OverflowTab: React.FunctionComponent<OverflowTabProps> = ({
3032
defaultTitleText = 'More',
3133
toggleAriaLabel,
3234
zIndex = 9999,
35+
shouldPreventScrollOnItemFocus = true,
3336
...props
3437
}: OverflowTabProps) => {
3538
const menuRef = React.useRef<HTMLDivElement>();
@@ -75,12 +78,11 @@ export const OverflowTab: React.FunctionComponent<OverflowTabProps> = ({
7578

7679
const toggleMenu = () => {
7780
setIsExpanded((prevIsExpanded) => !prevIsExpanded);
78-
setTimeout(() => {
79-
if (menuRef?.current) {
80-
const firstElement = menuRef.current.querySelector('li > button,input:not(:disabled)');
81-
firstElement && (firstElement as HTMLElement).focus();
82-
}
83-
}, 0);
81+
82+
if (menuRef?.current) {
83+
const firstElement = menuRef.current.querySelector('li > button,input:not(:disabled)');
84+
firstElement && (firstElement as HTMLElement).focus({ preventScroll: shouldPreventScrollOnItemFocus });
85+
}
8486
};
8587

8688
const overflowTab = (

0 commit comments

Comments
 (0)