diff --git a/src/CONST.ts b/src/CONST.ts index 0b23b8d4b956..5af4b05dca81 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5192,6 +5192,7 @@ const CONST = { REPORT_ACTION: 'REPORT_ACTION', EMAIL: 'EMAIL', REPORT: 'REPORT', + TEXT: 'TEXT', }, PROMOTED_ACTIONS: { diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 32f9f7d5a827..6858b8289fe7 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -1,6 +1,6 @@ import type {ImageContentFit} from 'expo-image'; import type {ReactElement, ReactNode, Ref} from 'react'; -import React, {forwardRef, useContext, useMemo} from 'react'; +import React, {forwardRef, useContext, useMemo, useRef} from 'react'; import type {GestureResponderEvent, StyleProp, TextStyle, ViewStyle} from 'react-native'; import {ActivityIndicator, View} from 'react-native'; import type {ValueOf} from 'type-fest'; @@ -12,8 +12,10 @@ import ControlSelection from '@libs/ControlSelection'; import convertToLTR from '@libs/convertToLTR'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import getButtonState from '@libs/getButtonState'; +import mergeRefs from '@libs/mergeRefs'; import Parser from '@libs/Parser'; import type {AvatarSource} from '@libs/UserUtils'; +import {showContextMenu} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import variables from '@styles/variables'; import {callFunctionIfActionIsAllowed} from '@userActions/Session'; import CONST from '@src/CONST'; @@ -358,6 +360,9 @@ type MenuItemBaseProps = { /** Whether to teleport the portal to the modal layer */ shouldTeleportPortalToModalLayer?: boolean; + + /** The value to copy on secondary interaction */ + copyValue?: string; }; type MenuItemProps = (IconProps | AvatarProps | NoIcon) & MenuItemBaseProps; @@ -473,6 +478,7 @@ function MenuItem( shouldBreakWord = false, pressableTestID, shouldTeleportPortalToModalLayer, + copyValue, }: MenuItemProps, ref: PressableRef, ) { @@ -482,6 +488,7 @@ function MenuItem( const combinedStyle = [styles.popoverMenuItem, style]; const {shouldUseNarrowLayout} = useResponsiveLayout(); const {isExecuting, singleExecution, waitForNavigate} = useContext(MenuItemGroupContext) ?? {}; + const popoverAnchor = useRef(null); const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedback.deleted) : false; const descriptionVerticalMargin = shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1; @@ -594,6 +601,19 @@ function MenuItem( } }; + const secondaryInteraction = (event: GestureResponderEvent | MouseEvent) => { + if (!copyValue) { + return; + } + showContextMenu({ + type: CONST.CONTEXT_MENU_TYPES.TEXT, + event, + selection: copyValue, + contextMenuAnchor: popoverAnchor.current, + }); + onSecondaryInteraction?.(event); + }; + return ( {!!label && !isLabelHoverable && ( @@ -618,7 +638,7 @@ function MenuItem( onPress={shouldCheckActionAllowedOnPress ? callFunctionIfActionIsAllowed(onPressAction, isAnonymousAction) : onPressAction} onPressIn={() => shouldBlockSelection && shouldUseNarrowLayout && canUseTouchScreen() && ControlSelection.block()} onPressOut={ControlSelection.unblock} - onSecondaryInteraction={onSecondaryInteraction} + onSecondaryInteraction={copyValue ? secondaryInteraction : onSecondaryInteraction} wrapperStyle={outerWrapperStyle} activeOpacity={!interactive ? 1 : variables.pressDimValue} opacityAnimationDuration={0} @@ -637,7 +657,7 @@ function MenuItem( } disabledStyle={shouldUseDefaultCursorWhenDisabled && [styles.cursorDefault]} disabled={disabled || isExecuting} - ref={ref} + ref={mergeRefs(ref, popoverAnchor)} role={CONST.ROLE.MENUITEM} accessibilityLabel={title ? title.toString() : ''} accessible diff --git a/src/pages/Travel/CarTripDetails.tsx b/src/pages/Travel/CarTripDetails.tsx index e840e1a56da7..544cbe0f5177 100644 --- a/src/pages/Travel/CarTripDetails.tsx +++ b/src/pages/Travel/CarTripDetails.tsx @@ -77,7 +77,7 @@ function CarTripDetails({reservation, personalDetails}: CarTripDetailsProps) { )} {!!displayName && ( diff --git a/src/pages/Travel/FlightTripDetails.tsx b/src/pages/Travel/FlightTripDetails.tsx index 901286ef33b8..388b00b2c591 100644 --- a/src/pages/Travel/FlightTripDetails.tsx +++ b/src/pages/Travel/FlightTripDetails.tsx @@ -57,7 +57,7 @@ function FlightTripDetails({reservation, prevReservation, personalDetails}: Flig )} diff --git a/src/pages/Travel/HotelTripDetails.tsx b/src/pages/Travel/HotelTripDetails.tsx index 6ad9f0010488..81de1937063f 100644 --- a/src/pages/Travel/HotelTripDetails.tsx +++ b/src/pages/Travel/HotelTripDetails.tsx @@ -43,9 +43,9 @@ function HotelTripDetails({reservation, personalDetails}: HotelTripDetailsProps) )} {!!displayName && ( diff --git a/src/pages/Travel/TrainTripDetails.tsx b/src/pages/Travel/TrainTripDetails.tsx index c83245981321..1a3ae939a921 100644 --- a/src/pages/Travel/TrainTripDetails.tsx +++ b/src/pages/Travel/TrainTripDetails.tsx @@ -37,7 +37,7 @@ function TrainTripDetails({reservation, personalDetails}: TrainTripDetailsProps) )} diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 4735921d1425..069691662b2e 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -433,6 +433,19 @@ const ContextMenuActions: ContextMenuAction[] = [ }, getDescription: (selection) => selection, }, + { + isAnonymousAction: true, + textTranslateKey: 'reportActionContextMenu.copyToClipboard', + icon: Expensicons.Copy, + successTextTranslateKey: 'reportActionContextMenu.copied', + successIcon: Expensicons.Checkmark, + shouldShow: ({type}) => type === CONST.CONTEXT_MENU_TYPES.TEXT, + onPress: (closePopover, {selection}) => { + Clipboard.setString(selection); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, + getDescription: () => undefined, + }, { isAnonymousAction: true, textTranslateKey: 'reportActionContextMenu.copyEmailToClipboard', diff --git a/src/styles/index.ts b/src/styles/index.ts index d1e34225d17f..9561407db29b 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4281,6 +4281,7 @@ const styles = (theme: ThemeColors) => }, contextMenuItemPopoverMaxWidth: { + minWidth: 320, maxWidth: 375, },