From 03a3ff23deb43e217a6d1082e2cd2ae8a93746a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Wed, 6 Aug 2025 16:00:41 +0200 Subject: [PATCH 01/24] unify 3 dot button anchor algorithm, dissapear 3 dot popover on help panel transition --- src/components/AttachmentModal.tsx | 1 - src/components/HeaderWithBackButton/index.tsx | 49 ++++++++++--------- src/components/HeaderWithBackButton/types.ts | 4 -- src/components/ThreeDotsMenu/index.tsx | 9 ++++ .../ValidateCodeActionModal/index.tsx | 3 -- src/hooks/usePopoverPosition.ts | 31 ++++++++++++ src/hooks/useThreeDotsAnchorPosition.ts | 17 ------- src/pages/EditReportFieldPage.tsx | 5 -- .../Search/SavedSearchItemThreeDotMenu.tsx | 22 ++------- .../step/IOURequestStepConfirmation.tsx | 3 -- .../request/step/IOURequestStepSubrate.tsx | 3 -- .../request/step/IOURequestStepWaypoint.tsx | 3 -- .../AttachmentModalBaseContent.tsx | 3 -- src/pages/settings/AboutPage/ConsolePage.tsx | 3 -- .../Contacts/ContactMethodDetailsPage.tsx | 3 -- .../CardSection/CardSectionActions/index.tsx | 22 ++------- .../Subscription/TaxExemptActions/index.tsx | 22 ++------- .../workspace/WorkspacePageWithSections.tsx | 4 +- src/pages/workspace/WorkspacesListRow.tsx | 20 ++------ .../accounting/PolicyAccountingPage.tsx | 37 +++++--------- src/styles/index.ts | 12 ----- 21 files changed, 93 insertions(+), 183 deletions(-) create mode 100644 src/hooks/usePopoverPosition.ts delete mode 100644 src/hooks/useThreeDotsAnchorPosition.ts diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 188c4ab4a00a..cd65b0c60579 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -474,7 +474,6 @@ function AttachmentModal({ onBackButtonPress={closeModal} onCloseButtonPress={closeModal} shouldShowThreeDotsButton={shouldShowThreeDotsButton} - threeDotsAnchorPosition={styles.threeDotsPopoverOffsetAttachmentModal(windowWidth)} threeDotsAnchorAlignment={{ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index 5ebd0c54cf49..dbe9c4cebd01 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -1,4 +1,4 @@ -import React, {useMemo} from 'react'; +import React, {useMemo, useRef} from 'react'; import {ActivityIndicator, Keyboard, StyleSheet, View} from 'react-native'; import type {SvgProps} from 'react-native-svg'; import Avatar from '@components/Avatar'; @@ -13,6 +13,7 @@ import HelpButton from '@components/SidePanel/HelpComponents/HelpButton'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; import Tooltip from '@components/Tooltip'; import useLocalize from '@hooks/useLocalize'; +import usePopoverPosition from '@hooks/usePopoverPosition'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -51,10 +52,6 @@ function HeaderWithBackButton({ subtitle = '', title = '', titleColor, - threeDotsAnchorPosition = { - vertical: 0, - horizontal: 0, - }, threeDotsAnchorAlignment = { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, @@ -80,6 +77,8 @@ function HeaderWithBackButton({ const StyleUtils = useStyleUtils(); const [isDownloadButtonActive, temporarilyDisableDownloadButton] = useThrottledButtonState(); const {translate} = useLocalize(); + const threeDotContainerRef = useRef(null); + const {calculatePopoverPosition} = usePopoverPosition(); const middleContent = useMemo(() => { if (progressBarPercentage) { @@ -153,34 +152,36 @@ function HeaderWithBackButton({ ) : ( - + + calculatePopoverPosition(threeDotContainerRef)} + icon={threeDotsMenuIcon} + iconFill={threeDotsMenuIconFill} + disabled={shouldDisableThreeDotsButton} + menuItems={threeDotsMenuItems} + onIconPress={onThreeDotsButtonPress} + shouldOverlay={shouldOverlayDots} + anchorAlignment={threeDotsAnchorAlignment} + shouldSetModalVisibility={shouldSetModalVisibility} + /> + ); } return null; }, [ - onThreeDotsButtonPress, - shouldDisableThreeDotsButton, - shouldOverlayDots, - shouldSetModalVisibility, shouldShowThreeDotsButton, + threeDotsMenuItems, + shouldMinimizeMenuButton, styles.touchableButtonImage, theme.icon, - threeDotsAnchorAlignment, - threeDotsAnchorPosition, threeDotsMenuIcon, threeDotsMenuIconFill, - threeDotsMenuItems, - shouldMinimizeMenuButton, + shouldDisableThreeDotsButton, + onThreeDotsButtonPress, + shouldOverlayDots, + threeDotsAnchorAlignment, + shouldSetModalVisibility, + calculatePopoverPosition, ]); return ( diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts index fcb0541f1542..1125ece7f7f9 100644 --- a/src/components/HeaderWithBackButton/types.ts +++ b/src/components/HeaderWithBackButton/types.ts @@ -5,7 +5,6 @@ import type {PopoverMenuItem} from '@components/PopoverMenu'; import type {Action} from '@hooks/useSingleExecution'; import type {StepCounterParams} from '@src/languages/params'; import type {TranslationPaths} from '@src/languages/types'; -import type {AnchorPosition} from '@src/styles'; import type {Report} from '@src/types/onyx'; import type {Icon} from '@src/types/onyx/OnyxCommon'; import type AnchorAlignment from '@src/types/utils/AnchorAlignment'; @@ -85,9 +84,6 @@ type HeaderWithBackButtonProps = Partial & { /** List of menu items for more(three dots) menu */ threeDotsMenuItems?: PopoverMenuItem[]; - /** The anchor position of the menu */ - threeDotsAnchorPosition?: AnchorPosition; - /** The anchor alignment of the menu */ threeDotsAnchorAlignment?: AnchorAlignment; diff --git a/src/components/ThreeDotsMenu/index.tsx b/src/components/ThreeDotsMenu/index.tsx index 7ba8be2987d3..8519853049d3 100644 --- a/src/components/ThreeDotsMenu/index.tsx +++ b/src/components/ThreeDotsMenu/index.tsx @@ -11,6 +11,7 @@ import EducationalTooltip from '@components/Tooltip/EducationalTooltip'; import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import useSidePanel from '@hooks/useSidePanel'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {isMobile} from '@libs/Browser'; @@ -52,6 +53,7 @@ function ThreeDotsMenu({ const buttonRef = useRef(null); const {translate} = useLocalize(); const isBehindModal = modal?.willAlertModalBecomeVisible && !modal?.isPopover && !shouldOverlay; + const {isSidePanelTransitionEnded} = useSidePanel(); const showPopoverMenu = () => { setPopupMenuVisible(true); @@ -76,6 +78,13 @@ function ThreeDotsMenu({ hidePopoverMenu(); }, [hidePopoverMenu, isBehindModal, isPopupMenuVisible]); + useEffect(() => { + if (isSidePanelTransitionEnded) { + return; + } + hidePopoverMenu(); + }, [hidePopoverMenu, isSidePanelTransitionEnded]); + const onThreeDotsPress = () => { if (isPopupMenuVisible) { hidePopoverMenu(); diff --git a/src/components/ValidateCodeActionModal/index.tsx b/src/components/ValidateCodeActionModal/index.tsx index 17c54f4bc4b7..f176a91a32af 100644 --- a/src/components/ValidateCodeActionModal/index.tsx +++ b/src/components/ValidateCodeActionModal/index.tsx @@ -7,7 +7,6 @@ import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import useThreeDotsAnchorPosition from '@hooks/useThreeDotsAnchorPosition'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -39,7 +38,6 @@ function ValidateCodeActionModal({ const firstRenderRef = useRef(true); const validateCodeFormRef = useRef(null); const styles = useThemeStyles(); - const threeDotsAnchorPosition = useThreeDotsAnchorPosition(styles.threeDotsPopoverOffset); const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE, {canBeMissing: true}); const hide = useCallback(() => { @@ -85,7 +83,6 @@ function ValidateCodeActionModal({ threeDotsMenuItems={threeDotsMenuItems} shouldShowThreeDotsButton={threeDotsMenuItems.length > 0} shouldOverlayDots - threeDotsAnchorPosition={threeDotsAnchorPosition} onThreeDotsButtonPress={onThreeDotsButtonPress} /> diff --git a/src/hooks/usePopoverPosition.ts b/src/hooks/usePopoverPosition.ts new file mode 100644 index 000000000000..42b848597b80 --- /dev/null +++ b/src/hooks/usePopoverPosition.ts @@ -0,0 +1,31 @@ +import {useCallback} from 'react'; +import type {View} from 'react-native'; +import type {AnchorPosition} from '@styles/index'; +import useResponsiveLayout from './useResponsiveLayout'; + +function usePopoverPosition() { + // Popovers are not used on small screen widths, but can be present in RHP + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {isSmallScreenWidth} = useResponsiveLayout(); + + const calculatePopoverPosition = useCallback( + (anchorRef: React.RefObject) => { + if (isSmallScreenWidth) { + return Promise.resolve({horizontal: 0, vertical: 0}); + } + return new Promise((resolve) => { + anchorRef.current?.measureInWindow((x, y, width, height) => { + resolve({ + horizontal: x + width, + vertical: y + height, + }); + }); + }); + }, + [isSmallScreenWidth], + ); + + return {calculatePopoverPosition}; +} + +export default usePopoverPosition; diff --git a/src/hooks/useThreeDotsAnchorPosition.ts b/src/hooks/useThreeDotsAnchorPosition.ts deleted file mode 100644 index ae17b3a0ef7e..000000000000 --- a/src/hooks/useThreeDotsAnchorPosition.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type {AnchorPosition} from '@styles/index'; -import variables from '@styles/variables'; -import {useSidePanelDisplayStatus} from './useSidePanel'; -import useWindowDimensions from './useWindowDimensions'; - -/** - * Hook that calculates the anchor position for the three dots menu - * based on the current screen width and the visibility of a Side Panel. - */ -function useThreeDotsAnchorPosition(anchorPositionStyle: (screenWidth: number) => AnchorPosition) { - const {windowWidth} = useWindowDimensions(); - const {shouldHideSidePanel} = useSidePanelDisplayStatus(); - - return anchorPositionStyle(shouldHideSidePanel ? windowWidth : windowWidth - variables.sideBarWidth); -} - -export default useThreeDotsAnchorPosition; diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx index 966ccfafd3aa..ac54f6a5797e 100644 --- a/src/pages/EditReportFieldPage.tsx +++ b/src/pages/EditReportFieldPage.tsx @@ -9,8 +9,6 @@ import type {PopoverMenuItem} from '@components/PopoverMenu'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useThreeDotsAnchorPosition from '@hooks/useThreeDotsAnchorPosition'; import {deleteReportField, updateReportField, updateReportName} from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -27,8 +25,6 @@ import EditReportFieldText from './EditReportFieldText'; type EditReportFieldPageProps = PlatformStackScreenProps; function EditReportFieldPage({route}: EditReportFieldPageProps) { - const styles = useThemeStyles(); - const threeDotsAnchorPosition = useThreeDotsAnchorPosition(styles.threeDotsPopoverOffsetNoCloseButton); const {backTo, reportID, policyID} = route.params; const fieldKey = getReportFieldKey(route.params.fieldID); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); @@ -102,7 +98,6 @@ function EditReportFieldPage({route}: EditReportFieldPageProps) { title={fieldName} threeDotsMenuItems={menuItems} shouldShowThreeDotsButton={!!menuItems?.length} - threeDotsAnchorPosition={threeDotsAnchorPosition} onBackButtonPress={goBack} /> diff --git a/src/pages/Search/SavedSearchItemThreeDotMenu.tsx b/src/pages/Search/SavedSearchItemThreeDotMenu.tsx index e5475feaadd5..d0b19d430e32 100644 --- a/src/pages/Search/SavedSearchItemThreeDotMenu.tsx +++ b/src/pages/Search/SavedSearchItemThreeDotMenu.tsx @@ -1,10 +1,9 @@ -import React, {useCallback, useRef} from 'react'; +import React, {useRef} from 'react'; import {View} from 'react-native'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import usePopoverPosition from '@hooks/usePopoverPosition'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {AnchorPosition} from '@styles/index'; import CONST from '@src/CONST'; type SavedSearchItemThreeDotMenuProps = { @@ -17,22 +16,9 @@ type SavedSearchItemThreeDotMenuProps = { function SavedSearchItemThreeDotMenu({menuItems, isDisabledItem, hideProductTrainingTooltip, renderTooltipContent, shouldRenderTooltip}: SavedSearchItemThreeDotMenuProps) { const threeDotsMenuContainerRef = useRef(null); - const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); - const calculateAndSetThreeDotsMenuPosition = useCallback(() => { - if (shouldUseNarrowLayout) { - return Promise.resolve({horizontal: 0, vertical: 0}); - } - return new Promise((resolve) => { - threeDotsMenuContainerRef.current?.measureInWindow((x, y, width) => { - resolve({ - horizontal: x + width, - vertical: y, - }); - }); - }); - }, [shouldUseNarrowLayout]); + const {calculatePopoverPosition} = usePopoverPosition(); return ( calculatePopoverPosition(threeDotsMenuContainerRef)} renderProductTrainingTooltipContent={renderTooltipContent} shouldShowProductTrainingTooltip={shouldRenderTooltip} anchorAlignment={{ diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index a97fba62af73..f24cea5bc37f 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -21,7 +21,6 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useThreeDotsAnchorPosition from '@hooks/useThreeDotsAnchorPosition'; import {completeTestDriveTask} from '@libs/actions/Task'; import DateUtils from '@libs/DateUtils'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; @@ -147,7 +146,6 @@ function IOURequestStepConfirmation({ const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); - const threeDotsAnchorPosition = useThreeDotsAnchorPosition(styles.threeDotsPopoverOffsetNoCloseButton); const {isOffline} = useNetwork(); const [startLocationPermissionFlow, setStartLocationPermissionFlow] = useState(false); const [selectedParticipantList, setSelectedParticipantList] = useState([]); @@ -1050,7 +1048,6 @@ function IOURequestStepConfirmation({ subtitle={hasMultipleTransactions ? `${currentTransactionIndex + 1} ${translate('common.of')} ${transactions.length}` : undefined} onBackButtonPress={navigateBack} shouldShowThreeDotsButton={shouldShowThreeDotsButton} - threeDotsAnchorPosition={threeDotsAnchorPosition} threeDotsMenuItems={[ { icon: Expensicons.Receipt, diff --git a/src/pages/iou/request/step/IOURequestStepSubrate.tsx b/src/pages/iou/request/step/IOURequestStepSubrate.tsx index 117b4bd50d48..f84143569a79 100644 --- a/src/pages/iou/request/step/IOURequestStepSubrate.tsx +++ b/src/pages/iou/request/step/IOURequestStepSubrate.tsx @@ -18,7 +18,6 @@ import ValuePicker from '@components/ValuePicker'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; -import useThreeDotsAnchorPosition from '@hooks/useThreeDotsAnchorPosition'; import {addErrorMessage} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getPerDiemCustomUnit} from '@libs/PolicyUtils'; @@ -70,7 +69,6 @@ function IOURequestStepSubrate({ const styles = useThemeStyles(); const policy = usePolicy(report?.policyID); const customUnit = getPerDiemCustomUnit(policy); - const threeDotsAnchorPosition = useThreeDotsAnchorPosition(styles.threeDotsPopoverOffsetNoCloseButton); const [isDeleteStopModalOpen, setIsDeleteStopModalOpen] = useState(false); const [restoreFocusType, setRestoreFocusType] = useState(); const navigation = useNavigation(); @@ -185,7 +183,6 @@ function IOURequestStepSubrate({ onBackButtonPress={goBack} shouldShowThreeDotsButton={shouldShowThreeDotsButton} shouldSetModalVisibility={false} - threeDotsAnchorPosition={threeDotsAnchorPosition} threeDotsMenuItems={[ { icon: Expensicons.Trashcan, diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx index f433e54e5c7d..c14c32c80a3f 100644 --- a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx +++ b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx @@ -18,7 +18,6 @@ import useLocationBias from '@hooks/useLocationBias'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import useThreeDotsAnchorPosition from '@hooks/useThreeDotsAnchorPosition'; import {isSafari} from '@libs/Browser'; import {addErrorMessage} from '@libs/ErrorUtils'; import {shouldUseTransactionDraft} from '@libs/IOUUtils'; @@ -67,7 +66,6 @@ function IOURequestStepWaypoint({ transaction, }: IOURequestStepWaypointProps) { const styles = useThemeStyles(); - const threeDotsAnchorPosition = useThreeDotsAnchorPosition(styles.threeDotsPopoverOffsetNoCloseButton); const [isDeleteStopModalOpen, setIsDeleteStopModalOpen] = useState(false); const [restoreFocusType, setRestoreFocusType] = useState(); const navigation = useNavigation(); @@ -204,7 +202,6 @@ function IOURequestStepWaypoint({ onBackButtonPress={goBack} shouldShowThreeDotsButton={shouldShowThreeDotsButton} shouldSetModalVisibility={false} - threeDotsAnchorPosition={threeDotsAnchorPosition} threeDotsMenuItems={[ { icon: Expensicons.Trashcan, diff --git a/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent.tsx b/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent.tsx index ab12e8ba0a0f..551d427091d6 100644 --- a/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent.tsx +++ b/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent.tsx @@ -24,7 +24,6 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import fileDownload from '@libs/fileDownload'; import {getFileName} from '@libs/fileDownload/FileUtils'; @@ -209,7 +208,6 @@ function AttachmentModalBaseContent({ onValidateFile, }: AttachmentModalBaseContentProps) { const styles = useThemeStyles(); - const {windowWidth} = useWindowDimensions(); const {shouldUseNarrowLayout} = useResponsiveLayout(); // This logic is used to ensure that the source is updated when the source changes and @@ -458,7 +456,6 @@ function AttachmentModalBaseContent({ onBackButtonPress={onClose} onCloseButtonPress={onClose} shouldShowThreeDotsButton={shouldShowThreeDotsButton} - threeDotsAnchorPosition={styles.threeDotsPopoverOffsetAttachmentModal(windowWidth)} threeDotsAnchorAlignment={{ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, diff --git a/src/pages/settings/AboutPage/ConsolePage.tsx b/src/pages/settings/AboutPage/ConsolePage.tsx index 9a7ed0a89d67..84d263b5472d 100644 --- a/src/pages/settings/AboutPage/ConsolePage.tsx +++ b/src/pages/settings/AboutPage/ConsolePage.tsx @@ -19,7 +19,6 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useThreeDotsAnchorPosition from '@hooks/useThreeDotsAnchorPosition'; import {addLog} from '@libs/actions/Console'; import {createLog, parseStringifiedMessages, sanitizeConsoleInput} from '@libs/Console'; import type {Log} from '@libs/Console'; @@ -50,7 +49,6 @@ function ConsolePage() { const {translate} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); - const threeDotsAnchorPosition = useThreeDotsAnchorPosition(styles.threeDotsPopoverOffsetNoCloseButton); const route = useRoute>(); const isAuthenticated = useIsAuthenticated(); @@ -163,7 +161,6 @@ function ConsolePage() { onBackButtonPress={() => Navigation.goBack(route.params?.backTo)} shouldShowThreeDotsButton threeDotsMenuItems={menuItems} - threeDotsAnchorPosition={threeDotsAnchorPosition} threeDotsMenuIcon={Expensicons.Filter} threeDotsMenuIconFill={theme.icon} /> diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx index 745fcc570288..2f7e8c114e9e 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx @@ -21,7 +21,6 @@ import useOnyx from '@hooks/useOnyx'; import usePrevious from '@hooks/usePrevious'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import blurActiveElement from '@libs/Accessibility/blurActiveElement'; import { clearContactMethod, @@ -65,7 +64,6 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { const {formatPhoneNumber, translate} = useLocalize(); const theme = useTheme(); const themeStyles = useThemeStyles(); - const {windowWidth} = useWindowDimensions(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const validateCodeFormRef = useRef(null); @@ -315,7 +313,6 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { threeDotsMenuItems={getThreeDotsMenuItems()} shouldShowThreeDotsButton={getThreeDotsMenuItems().length > 0} shouldOverlayDots - threeDotsAnchorPosition={themeStyles.threeDotsPopoverOffset(windowWidth)} onThreeDotsButtonPress={() => { // Hide the keyboard when the user clicks the three-dot menu. // Use blurActiveElement() for mWeb and KeyboardUtils.dismiss() for native apps. diff --git a/src/pages/settings/Subscription/CardSection/CardSectionActions/index.tsx b/src/pages/settings/Subscription/CardSection/CardSectionActions/index.tsx index 2563f2955b60..af0c7daf94b6 100644 --- a/src/pages/settings/Subscription/CardSection/CardSectionActions/index.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSectionActions/index.tsx @@ -1,12 +1,11 @@ -import React, {useCallback, useMemo, useRef} from 'react'; +import React, {useMemo, useRef} from 'react'; import {View} from 'react-native'; import * as Expensicons from '@components/Icon/Expensicons'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; import type ThreeDotsMenuProps from '@components/ThreeDotsMenu/types'; import useLocalize from '@hooks/useLocalize'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import usePopoverPosition from '@hooks/usePopoverPosition'; import Navigation from '@navigation/Navigation'; -import type {AnchorPosition} from '@styles/index'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -16,7 +15,6 @@ const anchorAlignment = { }; function CardSectionActions() { - const {shouldUseNarrowLayout} = useResponsiveLayout(); const {translate} = useLocalize(); const threeDotsMenuContainerRef = useRef(null); @@ -36,24 +34,12 @@ function CardSectionActions() { [translate], ); - const calculateAndSetThreeDotsMenuPosition = useCallback(() => { - if (shouldUseNarrowLayout) { - return Promise.resolve({horizontal: 0, vertical: 0}); - } - return new Promise((resolve) => { - threeDotsMenuContainerRef.current?.measureInWindow((x, y, width, height) => { - resolve({ - horizontal: x + width, - vertical: y + height, - }); - }); - }); - }, [shouldUseNarrowLayout]); + const {calculatePopoverPosition} = usePopoverPosition(); return ( calculatePopoverPosition(threeDotsMenuContainerRef)} menuItems={overflowMenu} anchorAlignment={anchorAlignment} shouldOverlay diff --git a/src/pages/settings/Subscription/TaxExemptActions/index.tsx b/src/pages/settings/Subscription/TaxExemptActions/index.tsx index 919e97c1d447..e45df1e3a545 100644 --- a/src/pages/settings/Subscription/TaxExemptActions/index.tsx +++ b/src/pages/settings/Subscription/TaxExemptActions/index.tsx @@ -1,12 +1,11 @@ -import React, {useCallback, useMemo, useRef} from 'react'; +import React, {useMemo, useRef} from 'react'; import {View} from 'react-native'; import * as Expensicons from '@components/Icon/Expensicons'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; import type ThreeDotsMenuProps from '@components/ThreeDotsMenu/types'; import useLocalize from '@hooks/useLocalize'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import usePopoverPosition from '@hooks/usePopoverPosition'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {AnchorPosition} from '@styles/index'; import {navigateToConciergeChat} from '@userActions/Report'; import {requestTaxExempt} from '@userActions/Subscription'; import CONST from '@src/CONST'; @@ -17,7 +16,6 @@ const anchorAlignment = { }; function TaxExemptActions() { - const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); const {translate} = useLocalize(); const threeDotsMenuContainerRef = useRef(null); @@ -37,19 +35,7 @@ function TaxExemptActions() { [translate], ); - const calculateAndSetThreeDotsMenuPosition = useCallback(() => { - if (shouldUseNarrowLayout) { - return Promise.resolve({horizontal: 0, vertical: 0}); - } - return new Promise((resolve) => { - threeDotsMenuContainerRef.current?.measureInWindow((x, y, width, height) => { - resolve({ - horizontal: x + width, - vertical: y + height, - }); - }); - }); - }, [shouldUseNarrowLayout]); + const {calculatePopoverPosition} = usePopoverPosition(); return ( calculatePopoverPosition(threeDotsMenuContainerRef)} menuItems={overflowMenu} anchorAlignment={anchorAlignment} shouldOverlay diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 26f1ff373306..78cb678f155b 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -30,7 +30,7 @@ import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscree import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; type WorkspacePageWithSectionsProps = WithPolicyAndFullscreenLoadingProps & - Pick & { + Pick & { shouldSkipVBBACall?: boolean; /** The text to display in the header */ @@ -122,7 +122,6 @@ function WorkspacePageWithSections({ onBackButtonPress, shouldShowThreeDotsButton, threeDotsMenuItems, - threeDotsAnchorPosition, shouldUseHeadlineHeader = true, addBottomSafeAreaPadding = false, }: WorkspacePageWithSectionsProps) { @@ -213,7 +212,6 @@ function WorkspacePageWithSections({ icon={icon ?? undefined} shouldShowThreeDotsButton={shouldShowThreeDotsButton} threeDotsMenuItems={threeDotsMenuItems} - threeDotsAnchorPosition={threeDotsAnchorPosition} shouldUseHeadlineHeader={shouldUseHeadlineHeader} > {headerContent} diff --git a/src/pages/workspace/WorkspacesListRow.tsx b/src/pages/workspace/WorkspacesListRow.tsx index 4588b3235ad3..19243181cdd5 100644 --- a/src/pages/workspace/WorkspacesListRow.tsx +++ b/src/pages/workspace/WorkspacesListRow.tsx @@ -1,5 +1,5 @@ import {Str} from 'expensify-common'; -import React, {useCallback, useEffect, useRef} from 'react'; +import React, {useEffect, useRef} from 'react'; import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; import type {ValueOf} from 'type-fest'; @@ -17,13 +17,13 @@ import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentU import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import WorkspacesListRowDisplayName from '@components/WorkspacesListRowDisplayName'; import useLocalize from '@hooks/useLocalize'; +import usePopoverPosition from '@hooks/usePopoverPosition'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {getDisplayNameOrDefault, getPersonalDetailsByIDs} from '@libs/PersonalDetailsUtils'; import {getUserFriendlyWorkspaceType} from '@libs/PolicyUtils'; import type {AvatarSource} from '@libs/UserUtils'; -import type {AnchorPosition} from '@styles/index'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -145,19 +145,7 @@ function WorkspacesListRow({ threeDotsMenuRef?.current?.hidePopoverMenu(); }, [isLoadingBill, resetLoadingSpinnerIconIndex]); - const calculateAndSetThreeDotsMenuPosition = useCallback(() => { - if (shouldUseNarrowLayout) { - return Promise.resolve({horizontal: 0, vertical: 0}); - } - return new Promise((resolve) => { - threeDotsMenuContainerRef.current?.measureInWindow((x, y, width, height) => { - resolve({ - horizontal: x + width, - vertical: y + height, - }); - }); - }); - }, [shouldUseNarrowLayout]); + const {calculatePopoverPosition} = usePopoverPosition(); if (layoutWidth === CONST.LAYOUT_WIDTH.NONE) { // To prevent layout from jumping or rendering for a split second, when @@ -203,7 +191,7 @@ function WorkspacesListRow({ calculatePopoverPosition(threeDotsMenuContainerRef)} menuItems={menuItems} anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}} shouldOverlay diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index f22e5bb4554a..c7e53c308c6f 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -25,10 +25,10 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; +import usePopoverPosition from '@hooks/usePopoverPosition'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useThreeDotsAnchorPosition from '@hooks/useThreeDotsAnchorPosition'; import {isAuthenticationError, isConnectionInProgress, isConnectionUnverified, removePolicyConnection, syncConnection} from '@libs/actions/connections'; import {shouldShowQBOReimbursableExportDestinationAccountError} from '@libs/actions/connections/QuickbooksOnline'; import {isExpensifyCardFullySetUp} from '@libs/CardUtils'; @@ -49,7 +49,6 @@ import { import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; -import type {AnchorPosition} from '@styles/index'; import {openOldDotLink} from '@userActions/Link'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -74,7 +73,6 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { const {translate, datetimeToRelative: getDatetimeToRelative, getLocalDateFromDatetime} = useLocalize(); const {isOffline} = useNetwork(); const {isBetaEnabled} = usePermissions(); - const threeDotsAnchorPosition = useThreeDotsAnchorPosition(styles.threeDotsPopoverOffsetNoCloseButton); const {shouldUseNarrowLayout} = useResponsiveLayout(); const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false); const [datetimeToRelative, setDateTimeToRelative] = useState(''); @@ -178,19 +176,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { setDateTimeToRelative(''); }, [getDatetimeToRelative, successfulDate]); - const calculateAndSetThreeDotsMenuPosition = useCallback(() => { - if (shouldUseNarrowLayout) { - return Promise.resolve({horizontal: 0, vertical: 0}); - } - return new Promise((resolve) => { - threeDotsMenuContainerRef.current?.measureInWindow((x, y, width, height) => { - resolve({ - horizontal: x + width, - vertical: y + height, - }); - }); - }); - }, [shouldUseNarrowLayout]); + const {calculatePopoverPosition} = usePopoverPosition(); const integrationSpecificMenuItems = useMemo(() => { const sageIntacctEntityList = policy?.connections?.intacct?.data?.entities ?? []; @@ -401,7 +387,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { ) : ( calculatePopoverPosition(threeDotsMenuContainerRef)} menuItems={overflowMenu} anchorAlignment={{ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, @@ -417,28 +403,28 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { }, [ policy, isSyncInProgress, - connectedIntegration, - synchronizationError, - shouldShowSynchronizationError, policyID, + connectedIntegration, translate, + isBetaEnabled, + connectionSyncProgress?.stageInProgress, styles.sectionMenuItemTopDescription, styles.pb0, styles.mt5, styles.popoverMenuIcon, styles.justifyContentCenter, - connectionSyncProgress?.stageInProgress, - datetimeToRelative, + shouldShowCardReconciliationOption, + shouldShowSynchronizationError, + synchronizationError, theme.spinner, + calculatePopoverPosition, overflowMenu, - calculateAndSetThreeDotsMenuPosition, integrationSpecificMenuItems, accountingIntegrations, isOffline, startIntegrationFlow, popoverAnchorRefs, - isBetaEnabled, - shouldShowCardReconciliationOption, + datetimeToRelative, ]); const otherIntegrationsItems = useMemo(() => { @@ -531,7 +517,6 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { shouldShowBackButton={shouldUseNarrowLayout} icon={Illustrations.Accounting} shouldUseHeadlineHeader - threeDotsAnchorPosition={threeDotsAnchorPosition} onBackButtonPress={Navigation.popToSidebar} /> horizontal: windowWidth - 60, }) satisfies AnchorPosition, - threeDotsPopoverOffsetNoCloseButton: (windowWidth: number) => - ({ - ...getPopOverVerticalOffset(60), - horizontal: windowWidth - 10, - }) satisfies AnchorPosition, - - threeDotsPopoverOffsetAttachmentModal: (windowWidth: number) => - ({ - ...getPopOverVerticalOffset(80), - horizontal: windowWidth - 140, - }) satisfies AnchorPosition, - popoverMenuOffset: (windowWidth: number) => ({ ...getPopOverVerticalOffset(180), From b8f27913e71da72549f47bf529eead8be4d232a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Thu, 7 Aug 2025 18:37:55 +0200 Subject: [PATCH 02/24] make useSidePanel a context refactor Popovers to (mostly, use same logic for popover anchor) --- src/App.tsx | 2 + src/components/AttachmentModal.tsx | 2 - src/components/AvatarWithImagePicker.tsx | 43 +++++-------- .../ButtonWithDropdownMenu/index.tsx | 35 +++-------- .../ButtonWithDropdownMenu/types.ts | 11 ---- src/components/Popover/index.tsx | 9 +++ .../Search/FilterDropdowns/DropdownButton.tsx | 46 +++++++------- .../SearchPageHeader/SearchFiltersBar.tsx | 1 - src/components/ThreeDotsMenu/index.tsx | 9 --- src/hooks/usePopoverPosition.ts | 34 +++++++--- .../{useSidePanel.ts => useSidePanel.tsx} | 63 ++++++++++++++----- src/pages/NewChatConfirmPage.tsx | 1 - src/pages/ReportDetailsPage.tsx | 1 - 13 files changed, 134 insertions(+), 123 deletions(-) rename src/hooks/{useSidePanel.ts => useSidePanel.tsx} (74%) diff --git a/src/App.tsx b/src/App.tsx index a7510270bdb9..e49ab6374460 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -39,6 +39,7 @@ import CONFIG from './CONFIG'; import Expensify from './Expensify'; import {CurrentReportIDContextProvider} from './hooks/useCurrentReportID'; import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; +import {SidePanelProvider} from './hooks/useSidePanel'; import HybridAppHandler from './HybridAppHandler'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; import {AttachmentModalContextProvider} from './pages/media/AttachmentModalScreen/AttachmentModalContext'; @@ -100,6 +101,7 @@ function App() { InputBlurContextProvider, FullScreenBlockingViewContextProvider, FullScreenLoaderContextProvider, + SidePanelProvider, ]} > diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index cd65b0c60579..9b9df2df57f7 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -9,7 +9,6 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import attachmentModalHandler from '@libs/AttachmentModalHandler'; import fileDownload from '@libs/fileDownload'; @@ -203,7 +202,6 @@ function AttachmentModal({ const [isDownloadButtonReadyToBeShown, setIsDownloadButtonReadyToBeShown] = React.useState(true); const isPDFLoadError = useRef(false); const isReplaceReceipt = useRef(false); - const {windowWidth} = useWindowDimensions(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const nope = useSharedValue(false); const isOverlayModalVisible = (isReceiptAttachment && isDeleteReceiptConfirmModalVisible) || (!isReceiptAttachment && isAttachmentInvalid); diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx index f39af201d70f..c115927495bc 100644 --- a/src/components/AvatarWithImagePicker.tsx +++ b/src/components/AvatarWithImagePicker.tsx @@ -1,11 +1,11 @@ import {useIsFocused} from '@react-navigation/native'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react'; import {StyleSheet, View} from 'react-native'; import type {ImageStyle, StyleProp, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; +import usePopoverPosition from '@hooks/usePopoverPosition'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import {isSafari} from '@libs/Browser'; import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import {splitExtensionFromFileName, validateImageForCorruption} from '@libs/fileDownload/FileUtils'; @@ -124,11 +124,10 @@ type AvatarWithImagePickerProps = { /** Optionally override the default "Edit" icon */ editIcon?: IconAsset; - - /** Determines if a style utility function should be used for calculating the PopoverMenu anchor position. */ - shouldUseStyleUtilityForAnchorPosition?: boolean; }; +const anchorAlignment = {horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}; + function AvatarWithImagePicker({ DefaultAvatar = () => null, style, @@ -156,12 +155,10 @@ function AvatarWithImagePicker({ enablePreview = false, shouldDisableViewPhoto = false, editIcon = Expensicons.Pencil, - shouldUseStyleUtilityForAnchorPosition = false, }: AvatarWithImagePickerProps) { const theme = useTheme(); const styles = useThemeStyles(); const isFocused = useIsFocused(); - const {windowWidth} = useWindowDimensions(); const [popoverPosition, setPopoverPosition] = useState({horizontal: 0, vertical: 0}); const [isMenuVisible, setIsMenuVisible] = useState(false); const [errorData, setErrorData] = useState({validationError: null, phraseParam: {}}); @@ -171,6 +168,7 @@ function AvatarWithImagePicker({ name: '', type: '', }); + const {calculatePopoverPosition} = usePopoverPosition(); const anchorRef = useRef(null); const {translate} = useLocalize(); @@ -296,23 +294,6 @@ function AvatarWithImagePicker({ return menuItems; }; - useEffect(() => { - if (!anchorRef.current) { - return; - } - - if (!isMenuVisible) { - return; - } - - anchorRef.current.measureInWindow((x, y, width, height) => { - setPopoverPosition({ - horizontal: x + (width - variables.photoUploadPopoverWidth) / 2, - vertical: y + height + variables.spacing2, - }); - }); - }, [isMenuVisible, windowWidth]); - const onPressAvatar = useCallback( (openPicker: OpenPicker) => { if (disabled && enablePreview && onViewPhotoPress) { @@ -330,6 +311,16 @@ function AvatarWithImagePicker({ [disabled, enablePreview, isUsingDefaultAvatar, onViewPhotoPress, showAvatarCropModal], ); + useLayoutEffect(() => { + if (!anchorRef.current || !isMenuVisible) { + return; + } + + calculatePopoverPosition(anchorRef, anchorAlignment).then(({vertical, horizontal, width}) => { + setPopoverPosition({ vertical: vertical + variables.spacing2, horizontal: horizontal - width + (width - variables.photoUploadPopoverWidth) / 2}); + }); + }, [calculatePopoverPosition, isMenuVisible]); + return ( @@ -414,6 +405,7 @@ function AvatarWithImagePicker({ setIsMenuVisible(false)} onItemSelected={(item, index) => { @@ -428,8 +420,7 @@ function AvatarWithImagePicker({ } }} menuItems={menuItems} - anchorPosition={shouldUseStyleUtilityForAnchorPosition ? styles.popoverMenuOffset(windowWidth) : popoverPosition} - anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}} + anchorAlignment={anchorAlignment} anchorRef={anchorRef} /> diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index ee0badb49dab..40089ec6d0c4 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -7,12 +7,12 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PopoverMenu from '@components/PopoverMenu'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; +import usePopoverPosition from '@hooks/usePopoverPosition'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import mergeRefs from '@libs/mergeRefs'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -40,7 +40,6 @@ function ButtonWithDropdownMenuInner(props: ButtonWithDropdownMenuPr horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, // we assume that popover menu opens below the button, anchor is at TOP }, - popoverHorizontalOffsetType, buttonRef, onPress, options, @@ -51,7 +50,6 @@ function ButtonWithDropdownMenuInner(props: ButtonWithDropdownMenuPr enterKeyEventListenerPriority = 0, wrapperStyle, useKeyboardShortcuts = false, - shouldUseStyleUtilityForAnchorPosition = false, defaultSelectedIndex = 0, shouldShowSelectedItemCheck = false, testID, @@ -72,7 +70,6 @@ function ButtonWithDropdownMenuInner(props: ButtonWithDropdownMenuPr // 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(defaultPopoverAnchorPosition); - const {windowWidth, windowHeight} = useWindowDimensions(); const dropdownAnchor = useRef(null); // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply correct popover styles // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth @@ -93,33 +90,15 @@ function ButtonWithDropdownMenuInner(props: ButtonWithDropdownMenuPr const {paddingBottom} = useSafeAreaPaddings(true); + const {calculatePopoverPosition} = usePopoverPosition(); + useEffect(() => { - if (!dropdownAnchor.current) { - return; - } - if (!isMenuVisible) { + if (!dropdownAnchor.current || !isMenuVisible) { return; } - if ('measureInWindow' in dropdownAnchor.current) { - dropdownAnchor.current.measureInWindow((x, y, w, h) => { - let horizontalPosition = x + w; - if (popoverHorizontalOffsetType === 'left') { - horizontalPosition = x; - } else if (popoverHorizontalOffsetType === 'center') { - horizontalPosition = x + w / 2; - } - - setPopoverAnchorPosition({ - horizontal: horizontalPosition, - vertical: - anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP - ? y + h + CONST.MODAL.POPOVER_MENU_PADDING // if vertical anchorAlignment is TOP, menu will open below the button and we need to add the height of button and padding - : y - CONST.MODAL.POPOVER_MENU_PADDING, // if it is BOTTOM, menu will open above the button so NO need to add height but DO subtract padding - }); - }); - } - }, [windowWidth, windowHeight, isMenuVisible, anchorAlignment.vertical, popoverHorizontalOffsetType]); + calculatePopoverPosition(dropdownAnchor, anchorAlignment).then(setPopoverAnchorPosition); + }, [isMenuVisible, calculatePopoverPosition, anchorAlignment]); const handleSingleOptionPress = useCallback( (event: GestureResponderEvent | KeyboardEvent | undefined) => { @@ -282,7 +261,7 @@ function ButtonWithDropdownMenuInner(props: ButtonWithDropdownMenuPr setIsMenuVisible(false); } }} - anchorPosition={shouldUseStyleUtilityForAnchorPosition ? styles.popoverButtonDropdownMenuOffset(windowWidth) : popoverAnchorPosition} + anchorPosition={popoverAnchorPosition} shouldShowSelectedItemCheck={shouldShowSelectedItemCheck} // eslint-disable-next-line react-compiler/react-compiler anchorRef={nullCheckRef(dropdownAnchor)} diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts index 85a06bfa5e88..606d42ed5274 100644 --- a/src/components/ButtonWithDropdownMenu/types.ts +++ b/src/components/ButtonWithDropdownMenu/types.ts @@ -95,14 +95,6 @@ type ButtonWithDropdownMenuProps = { /** The anchor alignment of the popover menu */ anchorAlignment?: AnchorAlignment; - /** - * Determines how the popover menu should be horizontally positioned relative to the button. - * - 'right': Anchors to the right edge of the button (default) - * - 'left': Anchors to the left edge of the button - * - 'center': Anchors to the center of the button - */ - popoverHorizontalOffsetType?: ValueOf; - /* ref for the button */ buttonRef?: RefObject; @@ -124,9 +116,6 @@ type ButtonWithDropdownMenuProps = { /** Whether to use keyboard shortcuts for confirmation or not */ useKeyboardShortcuts?: boolean; - /** Determines if a style utility function should be used for calculating the PopoverMenu anchor position. */ - shouldUseStyleUtilityForAnchorPosition?: boolean; - /** Decides which index in menuItems should be selected */ defaultSelectedIndex?: number; diff --git a/src/components/Popover/index.tsx b/src/components/Popover/index.tsx index 1d685bea5d2f..aabcad2538d6 100644 --- a/src/components/Popover/index.tsx +++ b/src/components/Popover/index.tsx @@ -4,6 +4,7 @@ import Modal from '@components/Modal'; import {PopoverContext} from '@components/PopoverProvider'; import PopoverWithoutOverlay from '@components/PopoverWithoutOverlay'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSidePanel from '@hooks/useSidePanel'; import TooltipRefManager from '@libs/TooltipRefManager'; import CONST from '@src/CONST'; import type PopoverProps from './types'; @@ -35,6 +36,14 @@ function Popover(props: PopoverProps) { const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); const withoutOverlayRef = useRef(null); const {close, popover} = React.useContext(PopoverContext); + const {isSidePanelTransitionEnded} = useSidePanel(); + + React.useEffect(() => { + if (isSidePanelTransitionEnded || isSmallScreenWidth || !isVisible) { + return; + } + onClose?.(); + }, [onClose, isSidePanelTransitionEnded]); // Not adding this inside the PopoverProvider // because this is an issue on smaller screens as well. diff --git a/src/components/Search/FilterDropdowns/DropdownButton.tsx b/src/components/Search/FilterDropdowns/DropdownButton.tsx index 32bdcc3094c4..912d96cd18c0 100644 --- a/src/components/Search/FilterDropdowns/DropdownButton.tsx +++ b/src/components/Search/FilterDropdowns/DropdownButton.tsx @@ -1,5 +1,5 @@ import type {ReactNode} from 'react'; -import React, {useCallback, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {View} from 'react-native'; import Button from '@components/Button'; import CaretWrapper from '@components/CaretWrapper'; @@ -7,7 +7,9 @@ import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; import Text from '@components/Text'; import withViewportOffsetTop from '@components/withViewportOffsetTop'; import useOnyx from '@hooks/useOnyx'; +import usePopoverPosition from '@hooks/usePopoverPosition'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import {useSidePanelDisplayStatus} from '@hooks/useSidePanel'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -34,21 +36,20 @@ type DropdownButtonProps = { const PADDING_MODAL = 8; -const ANCHOR_ORIGIN = { - horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, - vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, -}; - function DropdownButton({label, value, viewportOffsetTop, PopoverComponent}: DropdownButtonProps) { // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to distinguish RHL and narrow layout // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth} = useResponsiveLayout(); + const {shouldHideSidePanel} = useSidePanelDisplayStatus(); + const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {windowHeight} = useWindowDimensions(); const triggerRef = useRef(null); const [isOverlayVisible, setIsOverlayVisible] = useState(false); + const {calculatePopoverPosition} = usePopoverPosition(); + const [popoverTriggerPosition, setPopoverTriggerPosition] = useState({ horizontal: 0, vertical: 0, @@ -69,18 +70,21 @@ function DropdownButton({label, value, viewportOffsetTop, PopoverComponent}: Dro }); }, [willAlertModalBecomeVisible]); - /** - * Calculate popover position and toggle overlay - */ - const calculatePopoverPositionAndToggleOverlay = useCallback(() => { - triggerRef.current?.measureInWindow((x, y, _, height) => { - setPopoverTriggerPosition({ - horizontal: x, - vertical: y + height + PADDING_MODAL, - }); - toggleOverlay(); - }); - }, [toggleOverlay]); + const anchorAlignment = useMemo( + () => ({ + horizontal: shouldHideSidePanel ? CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT : CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, + }), + [shouldHideSidePanel], + ); + + useEffect(() => { + if (!triggerRef.current) { + return; + } + + calculatePopoverPosition(triggerRef, anchorAlignment).then((pos) => setPopoverTriggerPosition({...pos, vertical: pos.vertical + PADDING_MODAL})); + }, [isOverlayVisible, calculatePopoverPosition, anchorAlignment]); /** * When no items are selected, render the label, otherwise, render the @@ -106,7 +110,7 @@ function DropdownButton({label, value, viewportOffsetTop, PopoverComponent}: Dro return PopoverComponent({closeOverlay: toggleOverlay}); // PopoverComponent is stable so we don't need it here as a dep. // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [isOverlayVisible, toggleOverlay]); + }, [toggleOverlay]); return ( <> @@ -115,7 +119,7 @@ function DropdownButton({label, value, viewportOffsetTop, PopoverComponent}: Dro small ref={triggerRef} innerStyles={[isOverlayVisible && styles.buttonHoveredBG, {maxWidth: 256}]} - onPress={calculatePopoverPositionAndToggleOverlay} + onPress={toggleOverlay} > {!areAllMatchingItemsSelected && showSelectAllMatchingItems && (