diff --git a/src/components/ContextMenuItem.tsx b/src/components/ContextMenuItem.tsx
index 453e72dc761f..f252cc5b734f 100644
--- a/src/components/ContextMenuItem.tsx
+++ b/src/components/ContextMenuItem.tsx
@@ -8,8 +8,8 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
import getButtonState from '@libs/getButtonState';
import type IconAsset from '@src/types/utils/IconAsset';
import BaseMiniContextMenuItem from './BaseMiniContextMenuItem';
+import FocusableMenuItem from './FocusableMenuItem';
import Icon from './Icon';
-import MenuItem from './MenuItem';
type ContextMenuItemProps = {
/** Icon Component */
@@ -49,6 +49,9 @@ type ContextMenuItemProps = {
/** The ref of mini context menu item */
buttonRef?: React.RefObject;
+
+ /** Handles what to do when the item is focused */
+ onFocus?: () => void;
};
type ContextMenuItemHandle = {
@@ -70,6 +73,7 @@ function ContextMenuItem(
wrapperStyle,
shouldPreventDefaultFocusOnPress = true,
buttonRef = {current: null},
+ onFocus = () => {},
}: ContextMenuItemProps,
ref: ForwardedRef,
) {
@@ -113,7 +117,7 @@ function ContextMenuItem(
)}
) : (
-
);
}
diff --git a/src/components/FocusableMenuItem.tsx b/src/components/FocusableMenuItem.tsx
new file mode 100644
index 000000000000..e3ec8394dfa0
--- /dev/null
+++ b/src/components/FocusableMenuItem.tsx
@@ -0,0 +1,24 @@
+import React, {useRef} from 'react';
+import type {View} from 'react-native';
+import useSyncFocus from '@hooks/useSyncFocus';
+import type {MenuItemProps} from './MenuItem';
+import MenuItem from './MenuItem';
+
+function FocusableMenuItem(props: MenuItemProps) {
+ const ref = useRef(null);
+
+ // Sync focus on an item
+ useSyncFocus(ref, Boolean(props.focused));
+
+ return (
+
+ );
+}
+
+FocusableMenuItem.displayName = 'FocusableMenuItem';
+
+export default FocusableMenuItem;
diff --git a/src/components/Hoverable/index.tsx b/src/components/Hoverable/index.tsx
index 3729ee380b34..e3357fd963c4 100644
--- a/src/components/Hoverable/index.tsx
+++ b/src/components/Hoverable/index.tsx
@@ -1,6 +1,7 @@
import type {Ref} from 'react';
import React, {cloneElement, forwardRef} from 'react';
import {hasHoverSupport} from '@libs/DeviceCapabilities';
+import mergeRefs from '@libs/mergeRefs';
import {getReturnValue} from '@libs/ValueUtils';
import ActiveHoverable from './ActiveHoverable';
import type HoverableProps from './types';
@@ -14,7 +15,8 @@ function Hoverable({isDisabled, ...props}: HoverableProps, ref: Ref
// If Hoverable is disabled, just render the child without additional logic or event listeners.
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (isDisabled || !hasHoverSupport()) {
- return cloneElement(getReturnValue(props.children, false), {ref});
+ const child = getReturnValue(props.children, false);
+ return cloneElement(child, {ref: mergeRefs(ref, child.ref)});
}
return (
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index 12ddf04658f4..f23c8db97f47 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -1,6 +1,6 @@
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import type {ImageContentFit} from 'expo-image';
-import type {ForwardedRef, ReactNode} from 'react';
+import type {ReactNode} from 'react';
import React, {forwardRef, useContext, useMemo} from 'react';
import type {GestureResponderEvent, StyleProp, TextStyle, ViewStyle} from 'react-native';
import {View} from 'react-native';
@@ -32,6 +32,7 @@ import * as Expensicons from './Icon/Expensicons';
import * as defaultWorkspaceAvatars from './Icon/WorkspaceDefaultAvatars';
import {MenuItemGroupContext} from './MenuItemGroup';
import MultipleAvatars from './MultipleAvatars';
+import type {PressableRef} from './Pressable/GenericPressable/types';
import PressableWithSecondaryInteraction from './PressableWithSecondaryInteraction';
import RenderHTML from './RenderHTML';
import SelectCircle from './SelectCircle';
@@ -249,6 +250,9 @@ type MenuItemBaseProps = {
/** Adds padding to the left of the text when there is no icon. */
shouldPutLeftPaddingWhenNoIcon?: boolean;
+
+ /** Handles what to do when the item is focused */
+ onFocus?: () => void;
};
type MenuItemProps = (IconProps | AvatarProps | NoIcon) & MenuItemBaseProps;
@@ -321,8 +325,9 @@ function MenuItem(
contentFit = 'cover',
isPaneMenu = false,
shouldPutLeftPaddingWhenNoIcon = false,
+ onFocus,
}: MenuItemProps,
- ref: ForwardedRef,
+ ref: PressableRef,
) {
const theme = useTheme();
const styles = useThemeStyles();
@@ -449,6 +454,7 @@ function MenuItem(
role={CONST.ROLE.MENUITEM}
accessibilityLabel={title ? title.toString() : ''}
accessible
+ onFocus={onFocus}
>
{({pressed}) => (
<>
@@ -678,5 +684,5 @@ function MenuItem(
MenuItem.displayName = 'MenuItem';
-export type {IconProps, AvatarProps, NoIcon, MenuItemBaseProps, MenuItemProps};
+export type {AvatarProps, IconProps, MenuItemBaseProps, MenuItemProps, NoIcon};
export default forwardRef(MenuItem);
diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx
index cf77ac7c4fd6..7baf8bb8830e 100644
--- a/src/components/PopoverMenu.tsx
+++ b/src/components/PopoverMenu.tsx
@@ -9,6 +9,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
import CONST from '@src/CONST';
import type {AnchorPosition} from '@src/styles';
import type AnchorAlignment from '@src/types/utils/AnchorAlignment';
+import FocusableMenuItem from './FocusableMenuItem';
import * as Expensicons from './Icon/Expensicons';
import type {MenuItemProps} from './MenuItem';
import MenuItem from './MenuItem';
@@ -193,7 +194,7 @@ function PopoverMenu({
{!!headerText && {headerText}}
{enteredSubMenuIndexes.length > 0 && renderBackButtonItem()}
{currentMenuItems.map((item, menuIndex) => (
-
diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx
index 0343ffa9826b..5f4438f18f60 100644
--- a/src/components/SelectionList/BaseListItem.tsx
+++ b/src/components/SelectionList/BaseListItem.tsx
@@ -1,10 +1,11 @@
-import React from 'react';
+import React, {useRef} from 'react';
import {View} from 'react-native';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import useHover from '@hooks/useHover';
+import useSyncFocus from '@hooks/useSyncFocus';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
@@ -26,11 +27,18 @@ function BaseListItem({
pendingAction,
FooterComponent,
children,
+ isFocused,
+ onFocus = () => {},
}: BaseListItemProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {hovered, bind} = useHover();
+ const pressableRef = useRef(null);
+
+ // Sync focus on an item
+ useSyncFocus(pressableRef, Boolean(isFocused));
+
const rightHandSideComponentRender = () => {
if (canSelectMultiple || !rightHandSideComponent) {
return null;
@@ -54,6 +62,7 @@ function BaseListItem({
onSelectRow(item)}
disabled={isDisabled}
accessibilityLabel={item.text ?? ''}
@@ -64,6 +73,7 @@ function BaseListItem({
onMouseDown={shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined}
nativeID={keyForList ?? ''}
style={pressableStyle}
+ onFocus={onFocus}
>
{typeof children === 'function' ? children(hovered) : children}
diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx
index 4c6979f1a53e..62f098e76228 100644
--- a/src/components/SelectionList/BaseSelectionList.tsx
+++ b/src/components/SelectionList/BaseSelectionList.tsx
@@ -4,7 +4,6 @@ import type {ForwardedRef} from 'react';
import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import type {LayoutChangeEvent, SectionList as RNSectionList, TextInput as RNTextInput, SectionListRenderItemInfo} from 'react-native';
import {View} from 'react-native';
-import ArrowKeyFocusManager from '@components/ArrowKeyFocusManager';
import Button from '@components/Button';
import Checkbox from '@components/Checkbox';
import FixedFooter from '@components/FixedFooter';
@@ -16,6 +15,7 @@ import ShowMoreButton from '@components/ShowMoreButton';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import useActiveElementRole from '@hooks/useActiveElementRole';
+import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager';
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
import useKeyboardState from '@hooks/useKeyboardState';
import useLocalize from '@hooks/useLocalize';
@@ -167,9 +167,6 @@ function BaseSelectionList(
};
}, [canSelectMultiple, sections]);
- // If `initiallyFocusedOptionKey` is not passed, we fall back to `-1`, to avoid showing the highlight on the first member
- const [focusedIndex, setFocusedIndex] = useState(() => flattenedSections.allOptions.findIndex((option) => option.keyForList === initiallyFocusedOptionKey));
-
const [slicedSections, ShowMoreButtonInstance] = useMemo(() => {
let remainingOptionsLimit = CONST.MAX_OPTIONS_SELECTOR_PAGE_LENGTH * currentPage;
const processedSections = getSectionsWithIndexOffset(
@@ -226,6 +223,17 @@ function BaseSelectionList(
[flattenedSections.allOptions],
);
+ // If `initiallyFocusedOptionKey` is not passed, we fall back to `-1`, to avoid showing the highlight on the first member
+ const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({
+ initialFocusedIndex: flattenedSections.allOptions.findIndex((option) => option.keyForList === initiallyFocusedOptionKey),
+ maxIndex: flattenedSections.allOptions.length - 1,
+ isActive: true,
+ onFocusedIndexChange: (index: number) => {
+ scrollToIndex(index, true);
+ },
+ isFocused,
+ });
+
/**
* Logic to run when a row is selected, either with click/press or keyboard hotkeys.
*
@@ -341,6 +349,7 @@ function BaseSelectionList(
rightHandSideComponent={rightHandSideComponent}
keyForList={item.keyForList ?? ''}
isMultilineSupported={isRowMultilineSupported}
+ onFocus={() => setFocusedIndex(index)}
/>
);
};
@@ -375,7 +384,7 @@ function BaseSelectionList(
setFocusedIndex(newFocusedIndex);
scrollToIndex(newFocusedIndex, true);
},
- [scrollToIndex],
+ [scrollToIndex, setFocusedIndex],
);
/** Focuses the text input when the component comes into focus and after any navigation animations finish. */
@@ -461,7 +470,7 @@ function BaseSelectionList(
setItemsToHighlight(null);
}, timeout);
},
- [flattenedSections.allOptions, updateAndScrollToFocusedIndex],
+ [flattenedSections.allOptions, setFocusedIndex, updateAndScrollToFocusedIndex],
);
useImperativeHandle(ref, () => ({scrollAndHighlightItem}), [scrollAndHighlightItem]);
@@ -493,136 +502,129 @@ function BaseSelectionList(
);
return (
- section.data).length - 1}
- onFocusedIndexChanged={updateAndScrollToFocusedIndex}
- >
-
- {({safeAreaPaddingBottomStyle}) => (
-
- {shouldShowTextInput && (
-
- {
- innerTextInputRef.current = element as RNTextInput;
-
- if (!textInputRef) {
- return;
- }
-
- if (typeof textInputRef === 'function') {
- textInputRef(element as RNTextInput);
- } else {
- // eslint-disable-next-line no-param-reassign
- textInputRef.current = element as RNTextInput;
- }
- }}
- label={textInputLabel}
- accessibilityLabel={textInputLabel}
- hint={textInputHint}
- role={CONST.ROLE.PRESENTATION}
- value={textInputValue}
- placeholder={textInputPlaceholder}
- maxLength={textInputMaxLength}
- onChangeText={onChangeText}
- inputMode={inputMode}
- selectTextOnFocus
- spellCheck={false}
- iconLeft={textInputIconLeft}
- onSubmitEditing={selectFocusedOption}
- blurOnSubmit={!!flattenedSections.allOptions.length}
- isLoading={isLoadingNewOptions}
- testID="selection-list-text-input"
- />
-
- )}
- {/* If we are loading new options we will avoid showing any header message. This is mostly because one of the header messages says there are no options. */}
- {/* This is misleading because we might be in the process of loading fresh options from the server. */}
- {!isLoadingNewOptions && !!headerMessage && (
-
- {headerMessage}
-
- )}
- {!!headerContent && headerContent}
- {flattenedSections.allOptions.length === 0 && showLoadingPlaceholder ? (
-
- ) : (
- <>
- {!headerMessage && canSelectMultiple && shouldShowSelectAll && (
-
-
-
+ {({safeAreaPaddingBottomStyle}) => (
+
+ {shouldShowTextInput && (
+
+ {
+ innerTextInputRef.current = element as RNTextInput;
+
+ if (!textInputRef) {
+ return;
+ }
+
+ if (typeof textInputRef === 'function') {
+ textInputRef(element as RNTextInput);
+ } else {
+ // eslint-disable-next-line no-param-reassign
+ textInputRef.current = element as RNTextInput;
+ }
+ }}
+ label={textInputLabel}
+ accessibilityLabel={textInputLabel}
+ hint={textInputHint}
+ role={CONST.ROLE.PRESENTATION}
+ value={textInputValue}
+ placeholder={textInputPlaceholder}
+ maxLength={textInputMaxLength}
+ onChangeText={onChangeText}
+ inputMode={inputMode}
+ selectTextOnFocus
+ spellCheck={false}
+ iconLeft={textInputIconLeft}
+ onSubmitEditing={selectFocusedOption}
+ blurOnSubmit={!!flattenedSections.allOptions.length}
+ isLoading={isLoadingNewOptions}
+ testID="selection-list-text-input"
+ />
+
+ )}
+ {/* If we are loading new options we will avoid showing any header message. This is mostly because one of the header messages says there are no options. */}
+ {/* This is misleading because we might be in the process of loading fresh options from the server. */}
+ {!isLoadingNewOptions && !!headerMessage && (
+
+ {headerMessage}
+
+ )}
+ {!!headerContent && headerContent}
+ {flattenedSections.allOptions.length === 0 && showLoadingPlaceholder ? (
+
+ ) : (
+ <>
+ {!headerMessage && canSelectMultiple && shouldShowSelectAll && (
+
+
+
+ {!customListHeader && (
+
- {!customListHeader && (
- e.preventDefault() : undefined}
- >
- {translate('workspace.people.selectAll')}
-
- )}
-
- {customListHeader}
+ dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
+ onMouseDown={shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined}
+ >
+ {translate('workspace.people.selectAll')}
+
+ )}
- )}
- {!headerMessage && !canSelectMultiple && customListHeader}
- item.keyForList ?? `${index}`}
- extraData={focusedIndex}
- // the only valid values on the new arch are "white", "black", and "default", other values will cause a crash
- indicatorStyle="white"
- keyboardShouldPersistTaps="always"
- showsVerticalScrollIndicator={showScrollIndicator}
- initialNumToRender={12}
- maxToRenderPerBatch={maxToRenderPerBatch}
- windowSize={5}
- viewabilityConfig={{viewAreaCoveragePercentThreshold: 95}}
- testID="selection-list"
- onLayout={onSectionListLayout}
- style={(!maxToRenderPerBatch || (shouldHideListOnInitialRender && isInitialSectionListRender)) && styles.opacity0}
- ListFooterComponent={ShowMoreButtonInstance}
- />
- {children}
- >
- )}
- {showConfirmButton && (
-
-
-
- )}
- {!!footerContent && {footerContent}}
-
- )}
-
-
+ {customListHeader}
+
+ )}
+ {!headerMessage && !canSelectMultiple && customListHeader}
+ item.keyForList ?? `${index}`}
+ extraData={focusedIndex}
+ // the only valid values on the new arch are "white", "black", and "default", other values will cause a crash
+ indicatorStyle="white"
+ keyboardShouldPersistTaps="always"
+ showsVerticalScrollIndicator={showScrollIndicator}
+ initialNumToRender={12}
+ maxToRenderPerBatch={maxToRenderPerBatch}
+ windowSize={5}
+ viewabilityConfig={{viewAreaCoveragePercentThreshold: 95}}
+ testID="selection-list"
+ onLayout={onSectionListLayout}
+ style={(!maxToRenderPerBatch || (shouldHideListOnInitialRender && isInitialSectionListRender)) && styles.opacity0}
+ ListFooterComponent={ShowMoreButtonInstance}
+ />
+ {children}
+ >
+ )}
+ {showConfirmButton && (
+
+
+
+ )}
+ {!!footerContent && {footerContent}}
+
+ )}
+
);
}
diff --git a/src/components/SelectionList/RadioListItem.tsx b/src/components/SelectionList/RadioListItem.tsx
index b6b11c2f273c..808fa740bfb3 100644
--- a/src/components/SelectionList/RadioListItem.tsx
+++ b/src/components/SelectionList/RadioListItem.tsx
@@ -16,6 +16,7 @@ function RadioListItem({
shouldPreventDefaultFocusOnSelectRow,
rightHandSideComponent,
isMultilineSupported = false,
+ onFocus,
}: RadioListItemProps) {
const styles = useThemeStyles();
const fullTitle = isMultilineSupported ? item.text?.trimStart() : item.text;
@@ -34,6 +35,7 @@ function RadioListItem({
shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow}
rightHandSideComponent={rightHandSideComponent}
keyForList={item.keyForList}
+ onFocus={onFocus}
>
<>
diff --git a/src/components/SelectionList/TableListItem.tsx b/src/components/SelectionList/TableListItem.tsx
index a233f5dd83fd..cc87d84baf03 100644
--- a/src/components/SelectionList/TableListItem.tsx
+++ b/src/components/SelectionList/TableListItem.tsx
@@ -23,6 +23,7 @@ function TableListItem({
onDismissError,
shouldPreventDefaultFocusOnSelectRow,
rightHandSideComponent,
+ onFocus,
}: TableListItemProps) {
const styles = useThemeStyles();
const theme = useTheme();
@@ -56,6 +57,7 @@ function TableListItem({
errors={item.errors}
pendingAction={item.pendingAction}
keyForList={item.keyForList}
+ onFocus={onFocus}
>
{(hovered) => (
<>
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index af2ea3469408..e41fa22ea4e9 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -51,6 +51,9 @@ type CommonListItemProps = {
/** Whether to wrap long text up to 2 lines */
isMultilineSupported?: boolean;
+
+ /** Handles what to do when the item is focused */
+ onFocus?: () => void;
};
type ListItem = {
diff --git a/src/hooks/useArrowKeyFocusManager.ts b/src/hooks/useArrowKeyFocusManager.ts
index a6882c600f2c..28a137656cfa 100644
--- a/src/hooks/useArrowKeyFocusManager.ts
+++ b/src/hooks/useArrowKeyFocusManager.ts
@@ -13,6 +13,7 @@ type Config = {
disableCyclicTraversal?: boolean;
allowHorizontalArrowKeys?: boolean;
allowNegativeIndexes?: boolean;
+ isFocused?: boolean;
};
type UseArrowKeyFocusManager = [number, (index: number) => void];
@@ -31,6 +32,7 @@ type UseArrowKeyFocusManager = [number, (index: number) => void];
* @param [config.itemsPerRow] – The number of items per row. If provided, the arrow keys will move focus horizontally as well as vertically
* @param [config.disableCyclicTraversal] – Whether to disable cyclic traversal of the list. If true, the arrow keys will have no effect when the first or last item is focused
* @param [config.allowHorizontalArrowKeys] – Whether to enable the right/left keys
+ * @param [config.isFocused] Whether navigation is focused
*/
export default function useArrowKeyFocusManager({
maxIndex,
@@ -46,6 +48,7 @@ export default function useArrowKeyFocusManager({
disableCyclicTraversal = false,
allowHorizontalArrowKeys = false,
allowNegativeIndexes = false,
+ isFocused = true,
}: Config): UseArrowKeyFocusManager {
const [focusedIndex, setFocusedIndex] = useState(initialFocusedIndex);
const arrowConfig = useMemo(
@@ -68,7 +71,7 @@ export default function useArrowKeyFocusManager({
useEffect(() => onFocusedIndexChange(focusedIndex), [focusedIndex]);
const arrowUpCallback = useCallback(() => {
- if (maxIndex < 0) {
+ if (maxIndex < 0 || !isFocused) {
return;
}
const nextIndex = disableCyclicTraversal ? -1 : maxIndex;
@@ -95,12 +98,12 @@ export default function useArrowKeyFocusManager({
}
return newFocusedIndex;
});
- }, [disableCyclicTraversal, disabledIndexes, itemsPerRow, maxIndex, allowNegativeIndexes]);
+ }, [maxIndex, isFocused, disableCyclicTraversal, itemsPerRow, disabledIndexes, allowNegativeIndexes]);
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_UP, arrowUpCallback, arrowConfig);
const arrowDownCallback = useCallback(() => {
- if (maxIndex < 0) {
+ if (maxIndex < 0 || !isFocused) {
return;
}
@@ -140,7 +143,7 @@ export default function useArrowKeyFocusManager({
}
return newFocusedIndex;
});
- }, [disableCyclicTraversal, disabledIndexes, itemsPerRow, maxIndex]);
+ }, [disableCyclicTraversal, disabledIndexes, isFocused, itemsPerRow, maxIndex]);
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_DOWN, arrowDownCallback, arrowConfig);
diff --git a/src/hooks/useSyncFocus/index.native.ts b/src/hooks/useSyncFocus/index.native.ts
new file mode 100644
index 000000000000..1a1718bd3826
--- /dev/null
+++ b/src/hooks/useSyncFocus/index.native.ts
@@ -0,0 +1,5 @@
+// The .focus() method is not supported for Pressable component on mobile platforms.
+
+const useSyncFocus = () => {};
+
+export default useSyncFocus;
diff --git a/src/hooks/useSyncFocus/index.ts b/src/hooks/useSyncFocus/index.ts
new file mode 100644
index 000000000000..bdc4a6a876da
--- /dev/null
+++ b/src/hooks/useSyncFocus/index.ts
@@ -0,0 +1,20 @@
+import {useLayoutEffect} from 'react';
+import type {RefObject} from 'react';
+import type {View} from 'react-native';
+
+/**
+ * Custom React hook created to handle sync of focus on an element when the user navigates through the app with keyboard.
+ * When the user navigates through the app using the arrows and then the tab button, the focus on the element and the native focus of the browser differs.
+ * To maintain consistency when an element is focused in the app, the focus() method is additionally called on the focused element to eliminate the difference between native browser focus and application focus.
+ */
+const useSyncFocus = (ref: RefObject, isFocused: boolean) => {
+ useLayoutEffect(() => {
+ if (!isFocused) {
+ return;
+ }
+
+ ref.current?.focus();
+ }, [isFocused, ref]);
+};
+
+export default useSyncFocus;
diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
index 07a894251fd4..46ebdd751762 100755
--- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
+++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
@@ -282,6 +282,7 @@ function BaseReportActionContextMenu({
isAnonymousAction={contextAction.isAnonymousAction}
isFocused={focusedIndex === index}
shouldPreventDefaultFocusOnPress={contextAction.shouldPreventDefaultFocusOnPress}
+ onFocus={() => setFocusedIndex(index)}
/>
);
})}