Skip to content

Commit

Permalink
fix context menu when long press on url and email link
Browse files Browse the repository at this point in the history
  • Loading branch information
b1tjoy committed Nov 24, 2022
1 parent 02fa245 commit ad78781
Show file tree
Hide file tree
Showing 12 changed files with 280 additions and 110 deletions.
24 changes: 21 additions & 3 deletions src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.js
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
28 changes: 22 additions & 6 deletions src/components/AnchorForCommentsOnly/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
import React from 'react';
import * as anchorForCommentsOnlyPropTypes from './anchorForCommentsOnlyPropTypes';
import _ from 'underscore';
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
onPressIn={() => props.isSmallScreenWidth && canUseTouchScreen() && ControlSelection.block()}
onPressOut={() => ControlSelection.unblock()}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(_.omit(props, ['onPressIn', 'onPressOut']))}
/>
);

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 @@ -55,6 +64,8 @@ class PreRenderer extends React.Component {
render() {
return (
<BasePreRenderer
onPressIn={() => this.props.isSmallScreenWidth && canUseTouchScreen() && ControlSelection.block()}
onPressOut={() => ControlSelection.unblock()}
// eslint-disable-next-line react/jsx-props-no-spreading
{...this.props}
ref={el => this.ref = el}
Expand All @@ -63,6 +74,6 @@ class PreRenderer extends React.Component {
}
}

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

0 comments on commit ad78781

Please sign in to comment.