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

feat: migrate PressableWithSecondaryInteraction to function component #25398

163 changes: 92 additions & 71 deletions src/components/PressableWithSecondaryInteraction/index.js
Original file line number Diff line number Diff line change
@@ -1,103 +1,124 @@
import _ from 'underscore';
import React, {Component} from 'react';
import * as pressableWithSecondaryInteractionPropTypes from './pressableWithSecondaryInteractionPropTypes';
import React, {forwardRef, useEffect, useRef} from 'react';
import styles from '../../styles/styles';
import * as DeviceCapabilities from '../../libs/DeviceCapabilities';
import * as StyleUtils from '../../styles/StyleUtils';
import PressableWithFeedback from '../Pressable/PressableWithFeedback';
import * as pressableWithSecondaryInteractionPropTypes from './pressableWithSecondaryInteractionPropTypes';

/**
* This is a special Pressable that calls onSecondaryInteraction when LongPressed, or right-clicked.
*/
class PressableWithSecondaryInteraction extends Component {
constructor(props) {
super(props);
this.executeSecondaryInteraction = this.executeSecondaryInteraction.bind(this);
this.executeSecondaryInteractionOnContextMenu = this.executeSecondaryInteractionOnContextMenu.bind(this);
}

componentDidMount() {
if (this.props.forwardedRef) {
if (_.isFunction(this.props.forwardedRef)) {
this.props.forwardedRef(this.pressableRef);
} else if (_.isObject(this.props.forwardedRef)) {
this.props.forwardedRef.current = this.pressableRef;
}
}
this.pressableRef.addEventListener('contextmenu', this.executeSecondaryInteractionOnContextMenu);
}

componentWillUnmount() {
this.pressableRef.removeEventListener('contextmenu', this.executeSecondaryInteractionOnContextMenu);
}
function PressableWithSecondaryInteraction({
children,
inline,
style,
enableLongPressWithHover,
withoutFocusOnSecondaryInteraction,
preventDefaultContextMenu,
onSecondaryInteraction,
onPressIn,
onPress,
onPressOut,
activeOpacity,
forwardedRef,
...rest
}) {
const pressableRef = useRef(null);

/**
* @param {Event} e - the secondary interaction event
*/
executeSecondaryInteraction(e) {
if (DeviceCapabilities.hasHoverSupport() && !this.props.enableLongPressWithHover) {
const executeSecondaryInteraction = (e) => {
if (DeviceCapabilities.hasHoverSupport() && !enableLongPressWithHover) {
return;
}
if (this.props.withoutFocusOnSecondaryInteraction && this.pressableRef) {
this.pressableRef.blur();
if (withoutFocusOnSecondaryInteraction && pressableRef && pressableRef.current) {
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved
pressableRef.current.blur();
}
this.props.onSecondaryInteraction(e);
}
onSecondaryInteraction(e);
};

/**
* @param {contextmenu} e - A right-click MouseEvent.
* https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event
*/
executeSecondaryInteractionOnContextMenu(e) {
if (!this.props.onSecondaryInteraction) {
useEffect(() => {
if (!pressableRef || !pressableRef.current) {
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved
return;
}

e.stopPropagation();
if (this.props.preventDefaultContextMenu) {
e.preventDefault();
if (forwardedRef) {
if (_.isFunction(forwardedRef)) {
forwardedRef(pressableRef);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
forwardedRef(pressableRef);
forwardedRef(pressableRef.current);

I think this will fix both issues

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raised PR for this fix: #27576

} else if (_.isObject(forwardedRef)) {
// eslint-disable-next-line no-param-reassign
forwardedRef.current = pressableRef.current;
}
}

this.props.onSecondaryInteraction(e);
const element = pressableRef.current;

/**
* This component prevents the tapped element from capturing focus.
* We need to blur this element when clicked as it opens modal that implements focus-trapping.
* When the modal is closed it focuses back to the last active element.
* Therefore it shifts the element to bring it back to focus.
* https://github.com/Expensify/App/issues/14148
* @param {contextmenu} e - A right-click MouseEvent.
* https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event
*/
if (this.props.withoutFocusOnSecondaryInteraction && this.pressableRef) {
this.pressableRef.blur();
}
}

render() {
const defaultPressableProps = _.omit(this.props, ['onSecondaryInteraction', 'children', 'onLongPress']);
const inlineStyle = this.props.inline ? styles.dInline : {};

// On Web, Text does not support LongPress events thus manage inline mode with styling instead of using Text.
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved
return (
<PressableWithFeedback
wrapperStyle={StyleUtils.combineStyles(DeviceCapabilities.canUseTouchScreen() ? [styles.userSelectNone, styles.noSelect] : [], inlineStyle)}
onPressIn={this.props.onPressIn}
onLongPress={this.props.onSecondaryInteraction ? this.executeSecondaryInteraction : undefined}
pressDimmingValue={this.props.activeOpacity}
onPressOut={this.props.onPressOut}
onPress={this.props.onPress}
ref={(el) => (this.pressableRef = el)}
// eslint-disable-next-line react/jsx-props-no-spreading
{...defaultPressableProps}
style={(state) => [StyleUtils.parseStyleFromFunction(this.props.style, state), inlineStyle]}
>
{this.props.children}
</PressableWithFeedback>
);
}
const executeSecondaryInteractionOnContextMenu = (e) => {
if (!onSecondaryInteraction) {
return;
}

e.stopPropagation();
if (preventDefaultContextMenu) {
e.preventDefault();
}

onSecondaryInteraction(e);

/**
* This component prevents the tapped element from capturing focus.
* We need to blur this element when clicked as it opens modal that implements focus-trapping.
* When the modal is closed it focuses back to the last active element.
* Therefore it shifts the element to bring it back to focus.
* https://github.com/Expensify/App/issues/14148
*/
if (withoutFocusOnSecondaryInteraction) {
element.blur();
}
};

element.addEventListener('contextmenu', executeSecondaryInteractionOnContextMenu);

return () => {
element.removeEventListener('contextmenu', executeSecondaryInteractionOnContextMenu);
};
// We only want this to run on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved

const defaultPressableProps = _.omit(rest, ['onLongPress']);
const inlineStyle = inline ? styles.dInline : {};

return (
<PressableWithFeedback
wrapperStyle={StyleUtils.combineStyles(DeviceCapabilities.canUseTouchScreen() ? [styles.userSelectNone, styles.noSelect] : [], inlineStyle)}
onPressIn={onPressIn}
onLongPress={onSecondaryInteraction ? executeSecondaryInteraction : undefined}
pressDimmingValue={activeOpacity}
onPressOut={onPressOut}
onPress={onPress}
ref={pressableRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...defaultPressableProps}
style={(state) => [StyleUtils.parseStyleFromFunction(style, state), inlineStyle]}
>
{children}
</PressableWithFeedback>
);
}

PressableWithSecondaryInteraction.propTypes = pressableWithSecondaryInteractionPropTypes.propTypes;
PressableWithSecondaryInteraction.defaultProps = pressableWithSecondaryInteractionPropTypes.defaultProps;
export default React.forwardRef((props, ref) => (
PressableWithSecondaryInteraction.displayName = 'PressableWithSecondaryInteraction';

export default forwardRef((props, ref) => (
<PressableWithSecondaryInteraction
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
Expand Down