Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix context menu when long press on url and email link #12987

Merged
merged 12 commits into from
Jan 12, 2023
Merged
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -11,13 +12,28 @@ import Tooltip from '../Tooltip';
import canUseTouchScreen from '../../libs/canUseTouchscreen';
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.
*/
Expand Down Expand Up @@ -45,6 +61,9 @@ const BaseAnchorForCommentsOnly = (props) => {
);
}
}
onPress={linkProps.onPress}
onPressIn={props.onPressIn}
onPressOut={props.onPressOut}
>
<Tooltip containerStyles={[styles.dInline]} text={Str.isValidEmail(props.displayName) ? '' : props.href}>
<Text
Expand All @@ -55,8 +74,7 @@ const BaseAnchorForCommentsOnly = (props) => {
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}
>
Expand Down
27 changes: 21 additions & 6 deletions src/components/AnchorForCommentsOnly/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import React from 'react';
import * as anchorForCommentsOnlyPropTypes from './anchorForCommentsOnlyPropTypes';
import {propTypes as anchorForCommentsOnlyPropTypes, defaultProps} from './anchorForCommentsOnlyPropTypes';
import BaseAnchorForCommentsOnly from './BaseAnchorForCommentsOnly';
import canUseTouchScreen from '../../libs/canUseTouchscreen';
import ControlSelection from '../../libs/ControlSelection';
import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions';

// eslint-disable-next-line react/jsx-props-no-spreading
const AnchorForCommentsOnly = props => <BaseAnchorForCommentsOnly {...props} />;
AnchorForCommentsOnly.propTypes = anchorForCommentsOnlyPropTypes.propTypes;
AnchorForCommentsOnly.defaultProps = anchorForCommentsOnlyPropTypes.defaultProps;
const propTypes = {
...anchorForCommentsOnlyPropTypes,
...windowDimensionsPropTypes,
};

const AnchorForCommentsOnly = props => (
<BaseAnchorForCommentsOnly
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
onPressIn={() => props.isSmallScreenWidth && canUseTouchScreen() && ControlSelection.block()}
b1tjoy marked this conversation as resolved.
Show resolved Hide resolved
onPressOut={() => ControlSelection.unblock()}
/>
);

AnchorForCommentsOnly.propTypes = propTypes;
AnchorForCommentsOnly.defaultProps = defaultProps;
AnchorForCommentsOnly.displayName = 'AnchorForCommentsOnly';

export default AnchorForCommentsOnly;
export default withWindowDimensions(AnchorForCommentsOnly);
49 changes: 30 additions & 19 deletions src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -57,27 +58,37 @@ const ImageRenderer = (props) => {
imageHeight={imageHeight}
/>
) : (
<AttachmentModal
allowDownload
sourceURL={source}
isAuthTokenRequired={isAttachment}
originalFileName={originalFileName}
>
{({show}) => (
<PressableWithoutFocus
style={styles.noOutline}
onPress={show}
<ShowContextMenuContext.Consumer>
{({
anchor,
reportID,
action,
checkIfContextMenuActive,
}) => (
<AttachmentModal
allowDownload
sourceURL={source}
isAuthTokenRequired={isAttachment}
originalFileName={originalFileName}
>
<ThumbnailImage
previewSourceURL={previewSource}
style={styles.webViewStyles.tagStyles.img}
isAuthTokenRequired={isAttachment}
imageWidth={imageWidth}
imageHeight={imageHeight}
/>
</PressableWithoutFocus>
{({show}) => (
<PressableWithoutFocus
style={styles.noOutline}
onPress={show}
onLongPress={event => showContextMenuForReport(event, anchor, reportID, action, checkIfContextMenuActive)}
>
<ThumbnailImage
previewSourceURL={previewSource}
style={styles.webViewStyles.tagStyles.img}
isAuthTokenRequired={isAttachment}
imageWidth={imageWidth}
imageHeight={imageHeight}
/>
</PressableWithoutFocus>
)}
</AttachmentModal>
)}
</AttachmentModal>
</ShowContextMenuContext.Consumer>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<ScrollView
ref={ref}
horizontal
>
<View onStartShouldSetResponder={() => true}>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<TDefaultRenderer {...defaultRendererProps} />
</View>
<ShowContextMenuContext.Consumer>
{({
anchor,
reportID,
action,
checkIfContextMenuActive,
}) => (
<Pressable
onPressIn={props.onPressIn}
onPressOut={props.onPressOut}
onLongPress={event => showContextMenuForReport(event, anchor, reportID, action, checkIfContextMenuActive)}
>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<TDefaultRenderer {...defaultRendererProps} />
</Pressable>
)}
</ShowContextMenuContext.Consumer>
</ScrollView>
);
});

BasePreRenderer.displayName = 'BasePreRenderer';
BasePreRenderer.propTypes = htmlRendererPropTypes;
BasePreRenderer.propTypes = propTypes;
BasePreRenderer.defaultProps = defaultProps;

export default withLocalize(BasePreRenderer);
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ import _ from 'underscore';
import withLocalize from '../../../withLocalize';
import htmlRendererPropTypes from '../htmlRendererPropTypes';
import BasePreRenderer from './BasePreRenderer';
import compose from '../../../../libs/compose';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../../withWindowDimensions';
import canUseTouchScreen from '../../../../libs/canUseTouchscreen';
import ControlSelection from '../../../../libs/ControlSelection';

const propTypes = {
...htmlRendererPropTypes,
...windowDimensionsPropTypes,
};

class PreRenderer extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -58,11 +67,13 @@ class PreRenderer extends React.Component {
// eslint-disable-next-line react/jsx-props-no-spreading
{...this.props}
ref={el => this.ref = el}
onPressIn={() => this.props.isSmallScreenWidth && canUseTouchScreen() && ControlSelection.block()}
b1tjoy marked this conversation as resolved.
Show resolved Hide resolved
onPressOut={() => ControlSelection.unblock()}
/>
);
}
}

PreRenderer.propTypes = htmlRendererPropTypes;
PreRenderer.propTypes = propTypes;

export default withLocalize(PreRenderer);
export default compose(withLocalize, withWindowDimensions)(PreRenderer);
53 changes: 17 additions & 36 deletions src/components/PressableWithSecondaryInteraction/index.js
Original file line number Diff line number Diff line change
@@ -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 hasHoverSupport from '../../libs/hasHoverSupport';
Expand All @@ -12,7 +11,6 @@ import hasHoverSupport from '../../libs/hasHoverSupport';
class PressableWithSecondaryInteraction extends Component {
constructor(props) {
super(props);
this.callSecondaryInteractionWithMappedEvent = this.callSecondaryInteractionWithMappedEvent.bind(this);
this.executeSecondaryInteractionOnContextMenu = this.executeSecondaryInteractionOnContextMenu.bind(this);
}

Expand All @@ -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) || 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
Expand All @@ -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 (
<LongPressGestureHandler onHandlerStateChange={this.callSecondaryInteractionWithMappedEvent}>
<Pressable
style={this.props.inline && styles.dInline}
onPressIn={this.props.onPressIn}
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}
</Pressable>
</LongPressGestureHandler>
<Pressable
style={this.props.inline && styles.dInline}
onPressIn={this.props.onPressIn}
onLongPress={(e) => {
if (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}
</Pressable>
);
}
}
Expand Down
28 changes: 10 additions & 18 deletions src/components/PressableWithSecondaryInteraction/index.native.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -16,28 +15,21 @@ const PressableWithSecondaryInteraction = (props) => {
// Use Text node for inline mode to prevent content overflow.
const Node = props.inline ? Text : Pressable;
return (
<LongPressGestureHandler
onHandlerStateChange={(e) => {
if (e.nativeEvent.state !== State.ACTIVE) {
return;
}
<Node
ref={props.forwardedRef}
onPress={props.onPress}
onLongPress={(e) => {
e.preventDefault();
HapticFeedback.trigger();
props.onSecondaryInteraction(e);
}}
>
<Node
ref={props.forwardedRef}
onPress={props.onPress}
onPressIn={props.onPressIn}
onPressOut={props.onPressOut}
onPressIn={props.onPressIn}
onPressOut={props.onPressOut}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(_.omit(props, 'onLongPress'))}
>
{props.children}
</Node>
</LongPressGestureHandler>

{...(_.omit(props, 'onLongPress'))}
>
{props.children}
</Node>
);
};

Expand Down
Loading