Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 1 addition & 12 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {PortalProvider} from '@gorhom/portal';
import * as Sentry from '@sentry/react-native';
import React, {useEffect} from 'react';
import React from 'react';
import {LogBox, View} from 'react-native';
import {GestureHandlerRootView} from 'react-native-gesture-handler';
import {PickerStateProvider} from 'react-native-picker-select';
Expand Down Expand Up @@ -51,7 +51,6 @@ import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop';
import HybridAppHandler from './HybridAppHandler';
import OnyxUpdateManager from './libs/actions/OnyxUpdateManager';
import './libs/HybridApp';
import NavigationFocusManager from './libs/NavigationFocusManager';
import {AttachmentModalContextProvider} from './pages/media/AttachmentModalScreen/AttachmentModalContext';
import ExpensifyCardContextProvider from './pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardContextProvider';
import TravelCVVContextProvider from './pages/settings/Wallet/TravelCVVPage/TravelCVVContextProvider';
Expand All @@ -76,16 +75,6 @@ function App() {
useDefaultDragAndDrop();
OnyxUpdateManager();

// Initialize NavigationFocusManager for web focus restoration during back navigation
// This captures focus on pointerdown/keydown before navigation changes focus to body
useEffect(() => {
NavigationFocusManager.initialize();

return () => {
NavigationFocusManager.destroy();
};
}, []);

return (
<StrictModeWrapper>
<SplashScreenStateContextProvider>
Expand Down
6 changes: 2 additions & 4 deletions src/components/ApprovalWorkflowSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,6 @@ function ApprovalWorkflowSection({approvalWorkflow, onPress, currency = CONST.CU
</Text>
</View>
)}
{/* MenuItems are display-only (interactive={false}) because the outer
PressableWithoutFeedback handles all click interactions. */}
<MenuItem
title={translate('workflowsExpensesFromPage.title')}
style={styles.p0}
Expand All @@ -86,7 +84,7 @@ function ApprovalWorkflowSection({approvalWorkflow, onPress, currency = CONST.CU
iconHeight={20}
iconWidth={20}
iconFill={theme.icon}
interactive={false}
onPress={onPress}
shouldRemoveBackground
sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.WORKFLOWS.APPROVAL_SECTION_EXPENSES_FROM}
/>
Expand All @@ -108,7 +106,7 @@ function ApprovalWorkflowSection({approvalWorkflow, onPress, currency = CONST.CU
iconWidth={20}
numberOfLinesDescription={1}
iconFill={theme.icon}
interactive={false}
onPress={onPress}
shouldRemoveBackground
helperText={getApprovalLimitDescription({approver, currency, translate, personalDetailsByEmail})}
helperTextStyle={styles.workflowApprovalLimitText}
Expand Down
58 changes: 11 additions & 47 deletions src/components/ButtonWithDropdownMenu/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {RefCallback} from 'react';
import type {RefObject} from 'react';
import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
import {View} from 'react-native';
import type {GestureResponderEvent} from 'react-native';
Expand All @@ -14,11 +14,9 @@ import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import mergeRefs from '@libs/mergeRefs';
import NavigationFocusManager from '@libs/NavigationFocusManager';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import type {AnchorPosition} from '@src/styles';
import viewRef from '@src/types/utils/viewRef';
import type {ButtonWithDropdownMenuProps} from './types';

const defaultAnchorAlignment = {
Expand All @@ -27,8 +25,6 @@ const defaultAnchorAlignment = {
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP,
};

type DropdownAnchor = View | HTMLDivElement | null;

function ButtonWithDropdownMenu<IValueType>({ref, ...props}: ButtonWithDropdownMenuProps<IValueType>) {
const {
success = true,
Expand Down Expand Up @@ -78,22 +74,18 @@ function ButtonWithDropdownMenu<IValueType>({ref, ...props}: ButtonWithDropdownM
// In tests, skip the popover anchor position calculation. The default values are needed for popover menu to be rendered in tests.
const defaultPopoverAnchorPosition = process.env.NODE_ENV === 'test' ? {horizontal: 100, vertical: 100} : null;
const [popoverAnchorPosition, setPopoverAnchorPosition] = useState<AnchorPosition | null>(defaultPopoverAnchorPosition);
const dropdownAnchor = useRef<DropdownAnchor>(null);
const [wasOpenedViaKeyboard, setWasOpenedViaKeyboard] = useState(false);
const dropdownAnchor = useRef<View | null>(null);
// We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply correct popover styles
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth} = useResponsiveLayout();
const setDropdownAnchor: RefCallback<View> = useCallback((node: DropdownAnchor) => {
dropdownAnchor.current = node;
}, []);
// eslint-disable-next-line react-hooks/refs -- mergeRefs creates a ref callback and does not read ref.current during render
const dropdownButtonRef = isSplitButton ? buttonRef : mergeRefs(buttonRef, setDropdownAnchor);
const dropdownButtonRef = isSplitButton ? buttonRef : mergeRefs(buttonRef, dropdownAnchor);
const selectedItem = options.at(selectedItemIndex) ?? options.at(0);
const areAllOptionsDisabled = options.every((option) => option.disabled);
const innerStyleDropButton = StyleUtils.getDropDownButtonHeight(buttonSize);
const isButtonSizeLarge = buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE;
const isButtonSizeSmall = buttonSize === CONST.DROPDOWN_BUTTON_SIZE.SMALL;
const isButtonSizeExtraSmall = buttonSize === CONST.DROPDOWN_BUTTON_SIZE.EXTRA_SMALL;
const nullCheckRef = (refParam: RefObject<View | null>) => refParam ?? null;
const shouldShowButtonRightIcon = !!options.at(0)?.shouldShowButtonRightIcon;

useEffect(() => {
Expand All @@ -109,7 +101,7 @@ function ButtonWithDropdownMenu<IValueType>({ref, ...props}: ButtonWithDropdownM
return;
}

calculatePopoverPosition(viewRef(dropdownAnchor), anchorAlignment).then(setPopoverAnchorPosition);
calculatePopoverPosition(dropdownAnchor, anchorAlignment).then(setPopoverAnchorPosition);
}, [isMenuVisible, calculatePopoverPosition, anchorAlignment]);

const handleSingleOptionPress = useCallback(
Expand All @@ -131,27 +123,12 @@ function ButtonWithDropdownMenu<IValueType>({ref, ...props}: ButtonWithDropdownM
[options, onPress, onOptionSelected, onSubItemSelected],
);

/** Opens or closes the menu with keyboard tracking */
const toggleMenu = useCallback(() => {
if (!isMenuVisible) {
// Capture keyboard state BEFORE menu opens
const wasKeyboardInteraction = NavigationFocusManager.wasRecentKeyboardInteraction();
setWasOpenedViaKeyboard(wasKeyboardInteraction);
if (wasKeyboardInteraction) {
NavigationFocusManager.clearKeyboardInteractionFlag();
}
} else {
setWasOpenedViaKeyboard(false);
}
setIsMenuVisible(!isMenuVisible);
}, [isMenuVisible]);

useKeyboardShortcut(
CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER,
(e) => {
if (shouldAlwaysShowDropdownMenu || options.length) {
if (!isSplitButton) {
toggleMenu();
setIsMenuVisible(!isMenuVisible);
return;
}
if (selectedItem?.value) {
Expand All @@ -173,28 +150,18 @@ function ButtonWithDropdownMenu<IValueType>({ref, ...props}: ButtonWithDropdownM
const handlePress = useCallback(
(event?: GestureResponderEvent | KeyboardEvent) => {
if (!isSplitButton) {
toggleMenu();
setIsMenuVisible(!isMenuVisible);
} else if (selectedItem?.value) {
onPress(event, selectedItem.value);
}
},
[isSplitButton, onPress, selectedItem?.value, toggleMenu],
[isMenuVisible, isSplitButton, onPress, selectedItem?.value],
);

useImperativeHandle(ref, () => ({
setIsMenuVisible,
}));

const focusDropdownAnchor = useCallback(() => {
const anchor = dropdownAnchor.current;

if (!anchor || !('focus' in anchor) || typeof anchor.focus !== 'function') {
return;
}

anchor.focus();
}, []);

return (
<View style={wrapperStyle}>
{shouldAlwaysShowDropdownMenu || options.length > 1 ? (
Expand Down Expand Up @@ -232,12 +199,12 @@ function ButtonWithDropdownMenu<IValueType>({ref, ...props}: ButtonWithDropdownM

{isSplitButton && (
<Button
ref={setDropdownAnchor}
ref={dropdownAnchor}
success={success}
isDisabled={isDisabled}
shouldStayNormalOnDisable={shouldStayNormalOnDisable}
style={[styles.pl0]}
onPress={toggleMenu}
onPress={() => setIsMenuVisible(!isMenuVisible)}
shouldRemoveLeftBorderRadius
extraSmall={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.EXTRA_SMALL}
large={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE}
Expand Down Expand Up @@ -304,12 +271,9 @@ function ButtonWithDropdownMenu<IValueType>({ref, ...props}: ButtonWithDropdownM
isVisible={isMenuVisible}
onClose={() => {
setIsMenuVisible(false);
setWasOpenedViaKeyboard(false);
onOptionsMenuHide?.();
}}
wasOpenedViaKeyboard={wasOpenedViaKeyboard}
onModalShow={onOptionsMenuShow}
onModalHide={focusDropdownAnchor}
onItemSelected={(selectedSubitem, index, event) => {
onSubItemSelected?.(selectedSubitem, index, event);
if (selectedSubitem.shouldCloseModalOnSelect !== false) {
Expand All @@ -318,7 +282,7 @@ function ButtonWithDropdownMenu<IValueType>({ref, ...props}: ButtonWithDropdownM
}}
anchorPosition={popoverAnchorPosition}
shouldShowSelectedItemCheck={shouldShowSelectedItemCheck}
anchorRef={dropdownAnchor}
anchorRef={nullCheckRef(dropdownAnchor)}
scrollContainerStyle={!shouldUseModalPaddingStyle && isSmallScreenWidth && {...styles.pt4, paddingBottom}}
anchorAlignment={anchorAlignment}
shouldUseModalPaddingStyle={shouldUseModalPaddingStyle}
Expand Down
Loading
Loading