From 6a5a30c71f48f0a24314b2aa130fc52347a5bfbe Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 19 Mar 2024 13:05:47 +0700 Subject: [PATCH 1/4] Fix three-dot menu overlaps with emoji menu --- src/components/ContextMenuItem.tsx | 7 +++++- src/components/EmojiPicker/EmojiPicker.js | 23 +++++++++++++++++-- src/components/PopoverWithMeasuredContent.tsx | 12 ++++++++-- src/libs/PopoverWithMeasuredContentUtils.ts | 5 ++-- src/libs/calculateAnchorPosition.ts | 10 ++++---- .../BaseReportActionContextMenu.tsx | 16 ++++++++++--- .../report/ContextMenu/ContextMenuActions.tsx | 7 +++--- .../PopoverReportActionContextMenu.tsx | 12 ++++++++-- .../ContextMenu/ReportActionContextMenu.ts | 3 +++ src/styles/index.ts | 7 +++++- 10 files changed, 81 insertions(+), 21 deletions(-) diff --git a/src/components/ContextMenuItem.tsx b/src/components/ContextMenuItem.tsx index b80d6a138c9e..a2e610c9e8d6 100644 --- a/src/components/ContextMenuItem.tsx +++ b/src/components/ContextMenuItem.tsx @@ -1,6 +1,6 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useImperativeHandle} from 'react'; -import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; +import type {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useThrottledButtonState from '@hooks/useThrottledButtonState'; @@ -46,6 +46,9 @@ type ContextMenuItemProps = { wrapperStyle?: StyleProp; shouldPreventDefaultFocusOnPress?: boolean; + + /** The ref of mini context menu item */ + buttonRef: React.RefObject; }; type ContextMenuItemHandle = { @@ -66,6 +69,7 @@ function ContextMenuItem( shouldLimitWidth = true, wrapperStyle, shouldPreventDefaultFocusOnPress = true, + buttonRef, }: ContextMenuItemProps, ref: ForwardedRef, ) { @@ -94,6 +98,7 @@ function ContextMenuItem( return isMini ? ( { const [emojiPopoverAnchorOrigin, setEmojiPopoverAnchorOrigin] = useState(DEFAULT_ANCHOR_ORIGIN); const [activeID, setActiveID] = useState(); const emojiPopoverAnchorRef = useRef(null); + const emojiAnchorDimission = useRef({ + width: 0, + height: 0, + }); const onModalHide = useRef(() => {}); const onEmojiSelected = useRef(() => {}); const activeEmoji = useRef(); @@ -75,7 +79,14 @@ const EmojiPicker = forwardRef((props, ref) => { // eslint-disable-next-line es/no-optional-chaining onWillShow?.(); setIsEmojiPickerVisible(true); - setEmojiPopoverAnchorPosition(value); + setEmojiPopoverAnchorPosition({ + horizontal: value.horizontal, + vertical: value.vertical, + }); + emojiAnchorDimission.current = { + width: value.width, + height: value.height, + }; setEmojiPopoverAnchorOrigin(anchorOriginValue); setActiveID(id); }); @@ -154,7 +165,14 @@ const EmojiPicker = forwardRef((props, ref) => { return; } calculateAnchorPosition(emojiPopoverAnchor.current, emojiPopoverAnchorOrigin).then((value) => { - setEmojiPopoverAnchorPosition(value); + setEmojiPopoverAnchorPosition({ + horizontal: value.horizontal, + vertical: value.vertical, + }); + emojiAnchorDimission.current = { + width: value.width, + height: value.height, + }; }); }); return () => { @@ -190,6 +208,7 @@ const EmojiPicker = forwardRef((props, ref) => { anchorAlignment={emojiPopoverAnchorOrigin} outerStyle={StyleUtils.getOuterModalStyle(windowHeight, props.viewportOffsetTop)} innerContainerStyle={styles.popoverInnerContainer} + anchorDimensions={emojiAnchorDimission.current} avoidKeyboard > & { /** The horizontal and vertical anchors points for the popover */ anchorPosition: AnchorPosition; + + /** The dimension of anchor component */ + anchorDimensions: AnchorDimensions; }; /** @@ -42,6 +45,10 @@ function PopoverWithMeasuredContent({ statusBarTranslucent = true, avoidKeyboard = false, hideModalContentWhileAnimating = false, + anchorDimensions = { + height: 0, + width: 0, + }, ...props }: PopoverWithMeasuredContentProps) { const styles = useThemeStyles(); @@ -110,11 +117,12 @@ function PopoverWithMeasuredContent({ }, [anchorPosition, anchorAlignment, popoverWidth, popoverHeight]); const horizontalShift = PopoverWithMeasuredContentUtils.computeHorizontalShift(adjustedAnchorPosition.left, popoverWidth, windowWidth); - const verticalShift = PopoverWithMeasuredContentUtils.computeVerticalShift(adjustedAnchorPosition.top, popoverHeight, windowHeight); + const verticalShift = PopoverWithMeasuredContentUtils.computeVerticalShift(adjustedAnchorPosition.top, popoverHeight, windowHeight, anchorDimensions.height); const shiftedAnchorPosition = { left: adjustedAnchorPosition.left + horizontalShift, bottom: windowHeight - (adjustedAnchorPosition.top + popoverHeight) - verticalShift, }; + return isContentMeasured ? ( windowHeight) { diff --git a/src/libs/calculateAnchorPosition.ts b/src/libs/calculateAnchorPosition.ts index 9fe6e8f018d8..365bd1ece28b 100644 --- a/src/libs/calculateAnchorPosition.ts +++ b/src/libs/calculateAnchorPosition.ts @@ -2,7 +2,7 @@ import type {ValueOf} from 'type-fest'; import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {AnchorPosition} from '@src/styles'; +import type {AnchorDimensions, AnchorPosition} from '@src/styles'; type AnchorOrigin = { horizontal: ValueOf; @@ -13,18 +13,18 @@ type AnchorOrigin = { /** * Gets the x,y position of the passed in component for the purpose of anchoring another component to it. */ -export default function calculateAnchorPosition(anchorComponent: ContextMenuAnchor, anchorOrigin?: AnchorOrigin): Promise { +export default function calculateAnchorPosition(anchorComponent: ContextMenuAnchor, anchorOrigin?: AnchorOrigin): Promise { return new Promise((resolve) => { if (!anchorComponent || !('measureInWindow' in anchorComponent)) { - resolve({horizontal: 0, vertical: 0}); + resolve({horizontal: 0, vertical: 0, width: 0, height: 0}); return; } anchorComponent.measureInWindow((x, y, width, height) => { if (anchorOrigin?.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP && anchorOrigin?.horizontal === CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT) { - resolve({horizontal: x, vertical: y + height + (anchorOrigin?.shiftVertical ?? 0)}); + resolve({horizontal: x, vertical: y + height + (anchorOrigin?.shiftVertical ?? 0), width, height}); return; } - resolve({horizontal: x + width, vertical: y + (anchorOrigin?.shiftVertical ?? 0)}); + resolve({horizontal: x + width, vertical: y + (anchorOrigin?.shiftVertical ?? 0), width, height}); }); }); } diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index 4a5948545345..71c58c3796b8 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -120,6 +120,7 @@ function BaseReportActionContextMenu({ const [shouldKeepOpen, setShouldKeepOpen] = useState(false); const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(isMini, isSmallScreenWidth); const {isOffline} = useNetwork(); + const threedotRef = useRef(null); const reportAction: OnyxEntry = useMemo(() => { if (isEmptyObject(reportActions) || reportActionID === '0') { @@ -193,14 +194,14 @@ function BaseReportActionContextMenu({ {isActive: shouldEnableArrowNavigation}, ); - const openOverflowMenu = (event: GestureResponderEvent | MouseEvent) => { + const openOverflowMenu = (event: GestureResponderEvent | MouseEvent, anchorRef: MutableRefObject) => { const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction); const originalReport = ReportUtils.getReport(originalReportID); showContextMenu( CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, event, selection, - anchor?.current as ViewType | RNText | null, + anchorRef?.current as ViewType | RNText | null, reportID, reportAction?.reportActionID, originalReportID, @@ -216,6 +217,8 @@ function BaseReportActionContextMenu({ undefined, filteredContextMenuActions, true, + () => {}, + true, ); }; @@ -249,19 +252,26 @@ function BaseReportActionContextMenu({ textTranslateKey === 'reportActionContextMenu.deleteAction' || textTranslateKey === 'reportActionContextMenu.deleteConfirmation'; const text = textTranslateKey && (isKeyInActionUpdateKeys ? translate(textTranslateKey, {action: reportAction}) : translate(textTranslateKey)); + const isMenuAction = textTranslateKey === 'reportActionContextMenu.menu'; return ( { menuItemRefs.current[index] = ref; }} + buttonRef={isMenuAction ? threedotRef : {current: null}} icon={contextAction.icon} text={text ?? ''} successIcon={contextAction.successIcon} successText={contextAction.successTextTranslateKey ? translate(contextAction.successTextTranslateKey) : undefined} isMini={isMini} key={contextAction.textTranslateKey} - onPress={(event) => interceptAnonymousUser(() => contextAction.onPress?.(closePopup, {...payload, event}), contextAction.isAnonymousAction)} + onPress={(event) => + interceptAnonymousUser( + () => contextAction.onPress?.(closePopup, {...payload, event, ...(isMenuAction ? {anchorRef: threedotRef} : {})}), + contextAction.isAnonymousAction, + ) + } description={contextAction.getDescription?.(selection) ?? ''} isAnonymousAction={contextAction.isAnonymousAction} isFocused={focusedIndex === index} diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index c5ab9bbff1f5..dc4f9a05d49e 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -73,9 +73,10 @@ type ContextMenuActionPayload = { interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; anchor?: MutableRefObject; checkIfContextMenuActive?: () => void; - openOverflowMenu: (event: GestureResponderEvent | MouseEvent) => void; + openOverflowMenu: (event: GestureResponderEvent | MouseEvent, anchorRef: MutableRefObject) => void; event?: GestureResponderEvent | MouseEvent | KeyboardEvent; setIsEmojiPickerActive?: (state: boolean) => void; + anchorRef?: MutableRefObject; }; type OnPress = (closePopover: boolean, payload: ContextMenuActionPayload, selection?: string, reportID?: string, draftMessage?: string) => void; @@ -489,8 +490,8 @@ const ContextMenuActions: ContextMenuAction[] = [ textTranslateKey: 'reportActionContextMenu.menu', icon: Expensicons.ThreeDots, shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline, isMini) => isMini, - onPress: (closePopover, {openOverflowMenu, event, openContextMenu}) => { - openOverflowMenu(event as GestureResponderEvent | MouseEvent); + onPress: (closePopover, {openOverflowMenu, event, openContextMenu, anchorRef}) => { + openOverflowMenu(event as GestureResponderEvent | MouseEvent, anchorRef ?? {current: null}); openContextMenu(); }, getDescription: () => {}, diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 793fbf9b1e7e..250d7f8adbbe 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -13,6 +13,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as IOU from '@userActions/IOU'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; +import type {AnchorDimensions} from '@src/styles'; import type {ReportAction} from '@src/types/onyx'; import BaseReportActionContextMenu from './BaseReportActionContextMenu'; import type {ContextMenuAction} from './ContextMenuActions'; @@ -68,6 +69,10 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef(null); const contextMenuAnchorRef = useRef(null); const contextMenuTargetNode = useRef(null); + const contextMenuDimensions = useRef({ + width: 0, + height: 0, + }); const onPopoverShow = useRef(() => {}); const onPopoverHide = useRef(() => {}); @@ -165,6 +170,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef {}, + isOverflowMenu = false, ) => { const {pageX = 0, pageY = 0} = extractPointerEvent(event); contextMenuAnchorRef.current = contextMenuAnchor; @@ -181,9 +187,10 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef((resolve) => { - if (!pageX && !pageY && contextMenuAnchorRef.current) { + if (Boolean(!pageX && !pageY && contextMenuAnchorRef.current) || isOverflowMenu) { calculateAnchorPosition(contextMenuAnchorRef.current).then((position) => { - popoverAnchorPosition.current = position; + popoverAnchorPosition.current = {horizontal: position.horizontal, vertical: position.vertical}; + contextMenuDimensions.current = {width: position.vertical, height: position.height}; resolve(); }); } else { @@ -316,6 +323,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef void, + isOverflowMenu?: boolean, ) => void; type ReportActionContextMenu = { @@ -117,6 +118,7 @@ function showContextMenu( disabledActions: ContextMenuAction[] = [], shouldCloseOnTarget = false, setIsEmojiPickerActive = () => {}, + isOverflowMenu = false, ) { if (!contextMenuRef.current) { return; @@ -146,6 +148,7 @@ function showContextMenu( disabledActions, shouldCloseOnTarget, setIsEmojiPickerActive, + isOverflowMenu, ); } diff --git a/src/styles/index.ts b/src/styles/index.ts index 8a91291a0c71..49f6098cb6d8 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -44,6 +44,11 @@ import variables from './variables'; type ColorScheme = (typeof CONST.COLOR_SCHEME)[keyof typeof CONST.COLOR_SCHEME]; type StatusBarStyle = (typeof CONST.STATUS_BAR_STYLE)[keyof typeof CONST.STATUS_BAR_STYLE]; +type AnchorDimensions = { + width: number; + height: number; +}; + type AnchorPosition = { horizontal: number; vertical: number; @@ -4702,4 +4707,4 @@ const defaultStyles = styles(defaultTheme); export default styles; export {defaultStyles}; -export type {Styles, ThemeStyles, StatusBarStyle, ColorScheme, AnchorPosition}; +export type {Styles, ThemeStyles, StatusBarStyle, ColorScheme, AnchorPosition, AnchorDimensions}; From 89b2287e6d1e51bb088b66cea7e556ef3b0fce86 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 19 Mar 2024 13:30:07 +0700 Subject: [PATCH 2/4] fix lint --- src/components/ContextMenuItem.tsx | 4 ++-- src/components/PopoverWithMeasuredContent.tsx | 2 +- .../home/report/ContextMenu/BaseReportActionContextMenu.tsx | 2 +- src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/ContextMenuItem.tsx b/src/components/ContextMenuItem.tsx index a2e610c9e8d6..453e72dc761f 100644 --- a/src/components/ContextMenuItem.tsx +++ b/src/components/ContextMenuItem.tsx @@ -48,7 +48,7 @@ type ContextMenuItemProps = { shouldPreventDefaultFocusOnPress?: boolean; /** The ref of mini context menu item */ - buttonRef: React.RefObject; + buttonRef?: React.RefObject; }; type ContextMenuItemHandle = { @@ -69,7 +69,7 @@ function ContextMenuItem( shouldLimitWidth = true, wrapperStyle, shouldPreventDefaultFocusOnPress = true, - buttonRef, + buttonRef = {current: null}, }: ContextMenuItemProps, ref: ForwardedRef, ) { diff --git a/src/components/PopoverWithMeasuredContent.tsx b/src/components/PopoverWithMeasuredContent.tsx index 4c0197c348a9..a57ed01ab27f 100644 --- a/src/components/PopoverWithMeasuredContent.tsx +++ b/src/components/PopoverWithMeasuredContent.tsx @@ -16,7 +16,7 @@ type PopoverWithMeasuredContentProps = Omit) => { + const openOverflowMenu = (event: GestureResponderEvent | MouseEvent, anchorRef: MutableRefObject) => { const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction); const originalReport = ReportUtils.getReport(originalReportID); showContextMenu( diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index dc4f9a05d49e..5ce213a8c006 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -73,10 +73,10 @@ type ContextMenuActionPayload = { interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; anchor?: MutableRefObject; checkIfContextMenuActive?: () => void; - openOverflowMenu: (event: GestureResponderEvent | MouseEvent, anchorRef: MutableRefObject) => void; + openOverflowMenu: (event: GestureResponderEvent | MouseEvent, anchorRef: MutableRefObject) => void; event?: GestureResponderEvent | MouseEvent | KeyboardEvent; setIsEmojiPickerActive?: (state: boolean) => void; - anchorRef?: MutableRefObject; + anchorRef?: MutableRefObject; }; type OnPress = (closePopover: boolean, payload: ContextMenuActionPayload, selection?: string, reportID?: string, draftMessage?: string) => void; From e103c673d22907ec80217f820d5ceb543f92c973 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 19 Mar 2024 17:33:55 +0700 Subject: [PATCH 3/4] add shoudSwitchPositionIfOverflow props --- src/components/EmojiPicker/EmojiPicker.js | 1 + src/components/PopoverWithMeasuredContent.tsx | 12 +++++++++++- src/libs/PopoverWithMeasuredContentUtils.ts | 7 ++++--- .../ContextMenu/PopoverReportActionContextMenu.tsx | 1 + 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPicker.js b/src/components/EmojiPicker/EmojiPicker.js index 025aa8740eb0..e8f201700635 100644 --- a/src/components/EmojiPicker/EmojiPicker.js +++ b/src/components/EmojiPicker/EmojiPicker.js @@ -210,6 +210,7 @@ const EmojiPicker = forwardRef((props, ref) => { innerContainerStyle={styles.popoverInnerContainer} anchorDimensions={emojiAnchorDimission.current} avoidKeyboard + shoudSwitchPositionIfOverflow > windowHeight) { // Anchor is in Bottom window Edge, shift top by a multiple of four. - return roundToNearestMultipleOfFour(windowHeight - popoverBottomEdge); + return roundToNearestMultipleOfFour(shoudSwitchPositionIfOverflow ? -(menuHeight + anchorHeight) : windowHeight - popoverBottomEdge); } // Anchor is not in the gutter, so no need to shift it vertically diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 250d7f8adbbe..08ad2a27f726 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -325,6 +325,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef Date: Tue, 26 Mar 2024 15:45:47 +0700 Subject: [PATCH 4/4] fix typo --- src/components/EmojiPicker/EmojiPicker.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPicker.js b/src/components/EmojiPicker/EmojiPicker.js index e17d828e3188..fbfa4563d70e 100644 --- a/src/components/EmojiPicker/EmojiPicker.js +++ b/src/components/EmojiPicker/EmojiPicker.js @@ -32,7 +32,7 @@ const EmojiPicker = forwardRef((props, ref) => { const [emojiPopoverAnchorOrigin, setEmojiPopoverAnchorOrigin] = useState(DEFAULT_ANCHOR_ORIGIN); const [activeID, setActiveID] = useState(); const emojiPopoverAnchorRef = useRef(null); - const emojiAnchorDimission = useRef({ + const emojiAnchorDimension = useRef({ width: 0, height: 0, }); @@ -84,7 +84,7 @@ const EmojiPicker = forwardRef((props, ref) => { horizontal: value.horizontal, vertical: value.vertical, }); - emojiAnchorDimission.current = { + emojiAnchorDimension.current = { width: value.width, height: value.height, }; @@ -170,7 +170,7 @@ const EmojiPicker = forwardRef((props, ref) => { horizontal: value.horizontal, vertical: value.vertical, }); - emojiAnchorDimission.current = { + emojiAnchorDimension.current = { width: value.width, height: value.height, }; @@ -210,7 +210,7 @@ const EmojiPicker = forwardRef((props, ref) => { anchorAlignment={emojiPopoverAnchorOrigin} outerStyle={StyleUtils.getOuterModalStyle(windowHeight, props.viewportOffsetTop)} innerContainerStyle={styles.popoverInnerContainer} - anchorDimensions={emojiAnchorDimission.current} + anchorDimensions={emojiAnchorDimension.current} avoidKeyboard shoudSwitchPositionIfOverflow >