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

162 changes: 91 additions & 71 deletions src/components/PressableWithSecondaryInteraction/index.js
Original file line number Diff line number Diff line change
@@ -1,103 +1,123 @@
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.current) {
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.current) {
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);
};
}, [forwardedRef, onSecondaryInteraction, preventDefaultContextMenu, withoutFocusOnSecondaryInteraction]);

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

// On Web, Text does not support LongPress events thus manage inline mode with styling instead of using Text.
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ const propTypes = {
/** The function that should be called when this pressable is pressedOut */
onPressOut: PropTypes.func,

/** The function that should be called when this pressable is LongPressed or right-clicked. */
/**
* The function that should be called when this pressable is LongPressed or right-clicked.
*
* This function should be stable, preferably wrapped in a `useCallback` so that it does not
* cause several re-renders.
*/
onSecondaryInteraction: PropTypes.func,

/** The children which should be contained in this wrapper component. */
Expand Down
Loading