diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js index dec809e1266b..a1abfb0326f3 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js @@ -1,9 +1,30 @@ import React from 'react'; import {Pressable} from 'react-native'; -import * as anchorForAttachmentsOnlyPropTypes from './anchorForAttachmentsOnlyPropTypes'; +import PropTypes from 'prop-types'; +import { + propTypes as anchorForAttachmentsOnlyPropTypes, + defaultProps as anchorForAttachmentsOnlyDefaultProps, +} from './anchorForAttachmentsOnlyPropTypes'; import AttachmentView from '../AttachmentView'; import fileDownload from '../../libs/fileDownload'; import addEncryptedAuthTokenToURL from '../../libs/addEncryptedAuthTokenToURL'; +import {ShowContextMenuContext, showContextMenuForReport} from '../ShowContextMenuContext'; + +const propTypes = { + /** Press in handler for the link */ + onPressIn: PropTypes.func, + + /** Press out handler for the link */ + onPressOut: PropTypes.func, + + ...anchorForAttachmentsOnlyPropTypes, +}; + +const defaultProps = { + onPressIn: undefined, + onPressOut: undefined, + ...anchorForAttachmentsOnlyDefaultProps, +}; class BaseAnchorForAttachmentsOnly extends React.Component { constructor(props) { @@ -30,27 +51,45 @@ class BaseAnchorForAttachmentsOnly extends React.Component { const source = addEncryptedAuthTokenToURL(this.props.source); return ( - { - if (this.state.isDownloading) { - return; - } - this.processDownload(source, this.props.displayName); - }} - > - - + + {({ + anchor, + reportID, + action, + checkIfContextMenuActive, + }) => ( + { + if (this.state.isDownloading) { + return; + } + this.processDownload(source, this.props.displayName); + }} + onPressIn={this.props.onPressIn} + onPressOut={this.props.onPressOut} + onLongPress={event => showContextMenuForReport( + event, + anchor, + reportID, + action, + checkIfContextMenuActive, + )} + > + + + )} + ); } } -BaseAnchorForAttachmentsOnly.propTypes = anchorForAttachmentsOnlyPropTypes.propTypes; -BaseAnchorForAttachmentsOnly.defaultProps = anchorForAttachmentsOnlyPropTypes.defaultProps; +BaseAnchorForAttachmentsOnly.propTypes = propTypes; +BaseAnchorForAttachmentsOnly.defaultProps = defaultProps; export default BaseAnchorForAttachmentsOnly; diff --git a/src/components/AnchorForAttachmentsOnly/index.js b/src/components/AnchorForAttachmentsOnly/index.js index a71d65e969cd..9a0fb4be1237 100644 --- a/src/components/AnchorForAttachmentsOnly/index.js +++ b/src/components/AnchorForAttachmentsOnly/index.js @@ -1,9 +1,17 @@ import React from 'react'; import * as anchorForAttachmentsOnlyPropTypes from './anchorForAttachmentsOnlyPropTypes'; import BaseAnchorForAttachmentsOnly from './BaseAnchorForAttachmentsOnly'; +import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; +import ControlSelection from '../../libs/ControlSelection'; -// eslint-disable-next-line react/jsx-props-no-spreading -const AnchorForAttachmentsOnly = props => ; +const AnchorForAttachmentsOnly = props => ( + DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPressOut={() => ControlSelection.unblock()} + /> +); AnchorForAttachmentsOnly.propTypes = anchorForAttachmentsOnlyPropTypes.propTypes; AnchorForAttachmentsOnly.defaultProps = anchorForAttachmentsOnlyPropTypes.defaultProps; diff --git a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.js b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.js index e48e68e329f5..18d3fbf25960 100644 --- a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.js +++ b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.js @@ -1,6 +1,7 @@ import _ from 'underscore'; import React from 'react'; import {StyleSheet} from 'react-native'; +import PropTypes from 'prop-types'; import lodashGet from 'lodash/get'; import Str from 'expensify-common/lib/str'; import Text from '../Text'; @@ -11,13 +12,28 @@ import Tooltip from '../Tooltip'; import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; import styles from '../../styles/styles'; import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions'; -import {propTypes as anchorForCommentsOnlyPropTypes, defaultProps} from './anchorForCommentsOnlyPropTypes'; +import { + propTypes as anchorForCommentsOnlyPropTypes, + defaultProps as anchorForCommentsOnlyDefaultProps, +} from './anchorForCommentsOnlyPropTypes'; const propTypes = { + /** Press in handler for the link */ + onPressIn: PropTypes.func, + + /** Press out handler for the link */ + onPressOut: PropTypes.func, + ...anchorForCommentsOnlyPropTypes, ...windowDimensionsPropTypes, }; +const defaultProps = { + onPressIn: undefined, + onPressOut: undefined, + ...anchorForCommentsOnlyDefaultProps, +}; + /* * This is a default anchor component for regular links. */ @@ -45,6 +61,9 @@ const BaseAnchorForCommentsOnly = (props) => { ); } } + onPress={linkProps.onPress} + onPressIn={props.onPressIn} + onPressOut={props.onPressOut} > { rel: props.rel, target: props.target, }} - // eslint-disable-next-line react/jsx-props-no-spreading - {...linkProps} + href={linkProps.href} // eslint-disable-next-line react/jsx-props-no-spreading {...rest} > diff --git a/src/components/AnchorForCommentsOnly/index.js b/src/components/AnchorForCommentsOnly/index.js index 1526e78007fe..d25b819cddba 100644 --- a/src/components/AnchorForCommentsOnly/index.js +++ b/src/components/AnchorForCommentsOnly/index.js @@ -1,9 +1,18 @@ import React from 'react'; import * as anchorForCommentsOnlyPropTypes from './anchorForCommentsOnlyPropTypes'; import BaseAnchorForCommentsOnly from './BaseAnchorForCommentsOnly'; +import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; +import ControlSelection from '../../libs/ControlSelection'; + +const AnchorForCommentsOnly = props => ( + DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPressOut={() => ControlSelection.unblock()} + /> +); -// eslint-disable-next-line react/jsx-props-no-spreading -const AnchorForCommentsOnly = props => ; AnchorForCommentsOnly.propTypes = anchorForCommentsOnlyPropTypes.propTypes; AnchorForCommentsOnly.defaultProps = anchorForCommentsOnlyPropTypes.defaultProps; AnchorForCommentsOnly.displayName = 'AnchorForCommentsOnly'; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js index f3e9009a72ca..cef91ba9a9e2 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js @@ -6,6 +6,7 @@ import styles from '../../../styles/styles'; import ThumbnailImage from '../../ThumbnailImage'; import PressableWithoutFocus from '../../PressableWithoutFocus'; import CONST from '../../../CONST'; +import {ShowContextMenuContext, showContextMenuForReport} from '../../ShowContextMenuContext'; const ImageRenderer = (props) => { const htmlAttribs = props.tnode.attributes; @@ -57,27 +58,37 @@ const ImageRenderer = (props) => { imageHeight={imageHeight} /> ) : ( - - {({show}) => ( - + {({ + anchor, + reportID, + action, + checkIfContextMenuActive, + }) => ( + - - + {({show}) => ( + showContextMenuForReport(event, anchor, reportID, action, checkIfContextMenuActive)} + > + + + )} + )} - + ); }; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/BasePreRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/BasePreRenderer.js index 5edafb4dc04d..25ea94e0ae6b 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/BasePreRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/BasePreRenderer.js @@ -1,28 +1,59 @@ import React, {forwardRef} from 'react'; import {ScrollView} from 'react-native-gesture-handler'; -import {View} from 'react-native'; +import {Pressable} from 'react-native'; +import PropTypes from 'prop-types'; import _ from 'underscore'; import htmlRendererPropTypes from '../htmlRendererPropTypes'; import withLocalize from '../../../withLocalize'; +import {ShowContextMenuContext, showContextMenuForReport} from '../../../ShowContextMenuContext'; + +const propTypes = { + /** Press in handler for the code block */ + onPressIn: PropTypes.func, + + /** Press out handler for the code block */ + onPressOut: PropTypes.func, + + ...htmlRendererPropTypes, +}; + +const defaultProps = { + onPressIn: undefined, + onPressOut: undefined, +}; const BasePreRenderer = forwardRef((props, ref) => { const TDefaultRenderer = props.TDefaultRenderer; - const defaultRendererProps = _.omit(props, ['TDefaultRenderer']); + const defaultRendererProps = _.omit(props, ['TDefaultRenderer', 'onPressIn', 'onPressOut', 'onLongPress']); return ( - true}> - {/* eslint-disable-next-line react/jsx-props-no-spreading */} - - + + {({ + anchor, + reportID, + action, + checkIfContextMenuActive, + }) => ( + showContextMenuForReport(event, anchor, reportID, action, checkIfContextMenuActive)} + > + {/* eslint-disable-next-line react/jsx-props-no-spreading */} + + + )} + ); }); BasePreRenderer.displayName = 'BasePreRenderer'; -BasePreRenderer.propTypes = htmlRendererPropTypes; +BasePreRenderer.propTypes = propTypes; +BasePreRenderer.defaultProps = defaultProps; export default withLocalize(BasePreRenderer); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js index 59c05e73ed97..9e158ede05f0 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js @@ -3,6 +3,8 @@ import _ from 'underscore'; import withLocalize from '../../../withLocalize'; import htmlRendererPropTypes from '../htmlRendererPropTypes'; import BasePreRenderer from './BasePreRenderer'; +import * as DeviceCapabilities from '../../../../libs/DeviceCapabilities'; +import ControlSelection from '../../../../libs/ControlSelection'; class PreRenderer extends React.Component { constructor(props) { @@ -58,6 +60,8 @@ class PreRenderer extends React.Component { // eslint-disable-next-line react/jsx-props-no-spreading {...this.props} ref={el => this.ref = el} + onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPressOut={() => ControlSelection.unblock()} /> ); } diff --git a/src/components/PressableWithSecondaryInteraction/index.js b/src/components/PressableWithSecondaryInteraction/index.js index d9e1629d33d6..7b971e726867 100644 --- a/src/components/PressableWithSecondaryInteraction/index.js +++ b/src/components/PressableWithSecondaryInteraction/index.js @@ -1,7 +1,6 @@ import _ from 'underscore'; import React, {Component} from 'react'; import {Pressable} from 'react-native'; -import {LongPressGestureHandler, State} from 'react-native-gesture-handler'; import * as pressableWithSecondaryInteractionPropTypes from './pressableWithSecondaryInteractionPropTypes'; import styles from '../../styles/styles'; import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; @@ -12,7 +11,6 @@ import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; class PressableWithSecondaryInteraction extends Component { constructor(props) { super(props); - this.callSecondaryInteractionWithMappedEvent = this.callSecondaryInteractionWithMappedEvent.bind(this); this.executeSecondaryInteractionOnContextMenu = this.executeSecondaryInteractionOnContextMenu.bind(this); } @@ -27,27 +25,6 @@ class PressableWithSecondaryInteraction extends Component { this.pressableRef.removeEventListener('contextmenu', this.executeSecondaryInteractionOnContextMenu); } - /** - * @param {Object} e - */ - callSecondaryInteractionWithMappedEvent(e) { - if ((e.nativeEvent.state !== State.ACTIVE) || DeviceCapabilities.hasHoverSupport()) { - return; - } - - // Map gesture event to normal Responder event - const { - absoluteX, absoluteY, locationX, locationY, - } = e.nativeEvent; - const mapEvent = { - ...e, - nativeEvent: { - ...e.nativeEvent, pageX: absoluteX, pageY: absoluteY, x: locationX, y: locationY, - }, - }; - this.props.onSecondaryInteraction(mapEvent); - } - /** * @param {contextmenu} e - A right-click MouseEvent. * https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event @@ -65,19 +42,23 @@ class PressableWithSecondaryInteraction extends Component { // On Web, Text does not support LongPress events thus manage inline mode with styling instead of using Text. return ( - - this.pressableRef = el} - // eslint-disable-next-line react/jsx-props-no-spreading - {...defaultPressableProps} - > - {this.props.children} - - + { + if (DeviceCapabilities.hasHoverSupport()) { + return; + } + this.props.onSecondaryInteraction(e); + }} + onPressOut={this.props.onPressOut} + onPress={this.props.onPress} + ref={el => this.pressableRef = el} + // eslint-disable-next-line react/jsx-props-no-spreading + {...defaultPressableProps} + > + {this.props.children} + ); } } diff --git a/src/components/PressableWithSecondaryInteraction/index.native.js b/src/components/PressableWithSecondaryInteraction/index.native.js index 3b6edfc92198..f186146b4134 100644 --- a/src/components/PressableWithSecondaryInteraction/index.native.js +++ b/src/components/PressableWithSecondaryInteraction/index.native.js @@ -1,7 +1,6 @@ import _ from 'underscore'; import React, {forwardRef} from 'react'; import {Pressable} from 'react-native'; -import {LongPressGestureHandler, State} from 'react-native-gesture-handler'; import * as pressableWithSecondaryInteractionPropTypes from './pressableWithSecondaryInteractionPropTypes'; import Text from '../Text'; import HapticFeedback from '../../libs/HapticFeedback'; @@ -16,40 +15,21 @@ const PressableWithSecondaryInteraction = (props) => { // Use Text node for inline mode to prevent content overflow. const Node = props.inline ? Text : Pressable; return ( - { - if (e.nativeEvent.state !== State.ACTIVE) { - return; - } - - // Map gesture event to normal Responder event - const { - absoluteX, absoluteY, locationX, locationY, - } = e.nativeEvent; - const mapEvent = { - ...e, - nativeEvent: { - ...e.nativeEvent, pageX: absoluteX, pageY: absoluteY, x: locationX, y: locationY, - }, - }; - + { e.preventDefault(); HapticFeedback.trigger(); - props.onSecondaryInteraction(mapEvent); + props.onSecondaryInteraction(e); }} - > - - {props.children} - - - + {...(_.omit(props, 'onLongPress'))} + > + {props.children} + ); }; diff --git a/src/components/PressableWithoutFocus.js b/src/components/PressableWithoutFocus.js index 30668608496f..10ea16c71d06 100644 --- a/src/components/PressableWithoutFocus.js +++ b/src/components/PressableWithoutFocus.js @@ -12,6 +12,9 @@ const propTypes = { /** Callback for onPress event */ onPress: PropTypes.func.isRequired, + /** Callback for onLongPress event */ + onLongPress: PropTypes.func, + /** Styles that should be passed to touchable container */ // eslint-disable-next-line react/forbid-prop-types styles: PropTypes.arrayOf(PropTypes.object), @@ -19,6 +22,7 @@ const propTypes = { const defaultProps = { styles: [], + onLongPress: undefined, }; /** @@ -41,7 +45,12 @@ class PressableWithoutFocus extends React.Component { render() { return ( - this.pressableRef = el} style={this.props.styles}> + this.pressableRef = el} + style={this.props.styles} + > {this.props.children} ); diff --git a/src/components/ReportActionItem/IOUAction.js b/src/components/ReportActionItem/IOUAction.js index 8896b4a41b58..ba78f8cd9cb8 100644 --- a/src/components/ReportActionItem/IOUAction.js +++ b/src/components/ReportActionItem/IOUAction.js @@ -25,6 +25,12 @@ const propTypes = { /** Is this IOUACTION the most recent? */ isMostRecentIOUReportAction: PropTypes.bool.isRequired, + /** Popover context menu anchor, used for showing context menu */ + contextMenuAnchor: PropTypes.shape({current: PropTypes.elementType}), + + /** Callback for updating context menu active state, used for showing context menu */ + checkIfContextMenuActive: PropTypes.func, + /* Onyx Props */ /** chatReport associated with iouReport */ chatReport: PropTypes.shape({ @@ -48,6 +54,8 @@ const propTypes = { }; const defaultProps = { + contextMenuAnchor: undefined, + checkIfContextMenuActive: () => {}, chatReport: { participants: [], }, @@ -79,13 +87,19 @@ const IOUAction = (props) => { <> {shouldShowIOUPreview && ( {}, + action: undefined, + contextMenuAnchor: undefined, + checkIfContextMenuActive: () => {}, containerStyles: [], walletTerms: {}, pendingAction: null, @@ -129,8 +145,30 @@ const IOUPreview = (props) => { {style: 'currency', currency: props.iouReport.currency}, ) : ''; const avatarTooltip = [Str.removeSMSDomain(managerEmail), Str.removeSMSDomain(ownerEmail)]; + + const showContextMenu = (event) => { + // Use action and shouldHidePayButton props to check if we are in IOUDetailsModal, + // if it's true, do nothing when user long press, otherwise show context menu. + if (!props.action && props.shouldHidePayButton) { + return; + } + + showContextMenuForReport( + event, + props.contextMenuAnchor, + props.chatReportID, + props.action, + props.checkIfContextMenuActive, + ); + }; + return ( - + DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPressOut={() => ControlSelection.unblock()} + onLongPress={showContextMenu} + > {