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: use pressable with press feedback #19391

Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions src/components/Button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,24 +213,27 @@ class Button extends Component {
</Text>
);

if (this.props.icon) {
if (this.props.icon || this.props.shouldShowRightIcon) {
return (
<View style={[styles.justifyContentBetween, styles.flexRow]}>
<View style={[styles.alignItemsCenter, styles.flexRow, styles.flexShrink1]}>
<View style={[styles.mr1, ...this.props.iconStyles]}>
<Icon
src={this.props.icon}
fill={this.props.iconFill}
small={this.props.small}
/>
</View>
{this.props.icon && (
<View style={[styles.mr1, ...this.props.iconStyles]}>
<Icon
src={this.props.icon}
fill={this.props.iconFill}
small={this.props.small}
/>
</View>
)}
{textComponent}
</View>
{this.props.shouldShowRightIcon && (
<View style={styles.justifyContentCenter}>
<View style={[styles.justifyContentCenter, styles.ml1, ...this.props.iconStyles]}>
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like this has caused issues in such button components

Screenshot 2023-05-31 at 1 02 54 PM

Copy link
Contributor

Choose a reason for hiding this comment

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

@MonilBhavsar Seems like the left icon styles were also being applied to the right icon. Fixed ✅

<Icon
src={this.props.iconRight}
fill={this.props.iconFill}
small={this.props.small}
/>
</View>
)}
Expand Down
45 changes: 9 additions & 36 deletions src/components/CopyTextToClipboard.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
import React from 'react';
import {Pressable} from 'react-native';
import PropTypes from 'prop-types';
import Text from './Text';
import * as Expensicons from './Icon/Expensicons';
import compose from '../libs/compose';
import PressableWithDelayToggle from './PressableWithDelayToggle';
import Clipboard from '../libs/Clipboard';
import getButtonState from '../libs/getButtonState';
import Icon from './Icon';
import Tooltip from './Tooltip';
import styles from '../styles/styles';
import * as StyleUtils from '../styles/StyleUtils';
import variables from '../styles/variables';
import withLocalize, {withLocalizePropTypes} from './withLocalize';
import withDelayToggleButtonState, {withDelayToggleButtonStatePropTypes} from './withDelayToggleButtonState';

const propTypes = {
/** The text to display and copy to the clipboard */
Expand All @@ -23,8 +14,6 @@ const propTypes = {
textStyles: PropTypes.arrayOf(PropTypes.object),

...withLocalizePropTypes,

...withDelayToggleButtonStatePropTypes,
};

const defaultProps = {
Expand All @@ -39,40 +28,24 @@ class CopyTextToClipboard extends React.Component {
}

copyToClipboard() {
if (this.props.isDelayButtonStateComplete) {
return;
}
Clipboard.setString(this.props.text);
this.props.toggleDelayButtonState(true);
}

render() {
return (
<Text
<PressableWithDelayToggle
text={this.props.text}
tooltipText={this.props.translate('reportActionContextMenu.copyToClipboard')}
tooltipTextChecked={this.props.translate('reportActionContextMenu.copied')}
icon={Expensicons.Copy}
textStyles={this.props.textStyles}
onPress={this.copyToClipboard}
style={[styles.flexRow, styles.cursorPointer]}
suppressHighlighting
Copy link
Contributor

Choose a reason for hiding this comment

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

Coming from #24591

We are missing the prop suppressHighlighting if the PressableWithDelayToggle uses the Text component.

>
<Text style={this.props.textStyles}>{`${this.props.text} `}</Text>
<Tooltip text={this.props.translate(`reportActionContextMenu.${this.props.isDelayButtonStateComplete ? 'copied' : 'copyToClipboard'}`)}>
<Pressable onPress={this.copyToClipboard}>
{({hovered, pressed}) => (
<Icon
src={this.props.isDelayButtonStateComplete ? Expensicons.Checkmark : Expensicons.Copy}
fill={StyleUtils.getIconFillColor(getButtonState(hovered, pressed, this.props.isDelayButtonStateComplete))}
width={variables.iconSizeSmall}
height={variables.iconSizeSmall}
inline
/>
)}
</Pressable>
</Tooltip>
</Text>
/>
);
}
}

CopyTextToClipboard.propTypes = propTypes;
CopyTextToClipboard.defaultProps = defaultProps;

export default compose(withLocalize, withDelayToggleButtonState)(CopyTextToClipboard);
export default withLocalize(CopyTextToClipboard);
143 changes: 143 additions & 0 deletions src/components/PressableWithDelayToggle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Pressable} from 'react-native';
import * as Expensicons from './Icon/Expensicons';
import compose from '../libs/compose';
import Icon from './Icon';
import Tooltip from './Tooltip';
import Text from './Text';
import styles from '../styles/styles';
import variables from '../styles/variables';
import getButtonState from '../libs/getButtonState';
import * as StyleUtils from '../styles/StyleUtils';
import withLocalize, {withLocalizePropTypes} from './withLocalize';
import withDelayToggleButtonState, {withDelayToggleButtonStatePropTypes} from './withDelayToggleButtonState';

const propTypes = {
...withLocalizePropTypes,

...withDelayToggleButtonStatePropTypes,

/** The text to display */
text: PropTypes.string,

/** The text to display once the pressable is pressed */
textChecked: PropTypes.string,

/** The tooltip text to display */
tooltipText: PropTypes.string,

/** The tooltip text to display once the pressable is pressed */
tooltipTextChecked: PropTypes.string,

/** Styles to apply to the container */
// eslint-disable-next-line react/forbid-prop-types
styles: PropTypes.arrayOf(PropTypes.object),

/** Styles to apply to the text */
// eslint-disable-next-line react/forbid-prop-types
textStyles: PropTypes.arrayOf(PropTypes.object),

/** Styles to apply to the icon */
// eslint-disable-next-line react/forbid-prop-types
iconStyles: PropTypes.arrayOf(PropTypes.object),

/** Callback to be called on onPress */
onPress: PropTypes.func.isRequired,

/** The icon to display */
icon: PropTypes.func,

/** The icon to display once the pressable is pressed */
iconChecked: PropTypes.func,

/**
* Should be set to `true` if this component is being rendered inline in
* another `Text`. This is due to limitations in RN regarding the
* vertical text alignment of non-Text elements
*/
inline: PropTypes.bool,
};

const defaultProps = {
text: '',
textChecked: '',
tooltipText: '',
tooltipTextChecked: '',
styles: [],
textStyles: [],
iconStyles: [],
icon: null,
inline: true,
iconChecked: Expensicons.Checkmark,
};

function PressableWithDelayToggle(props) {
const updatePressState = () => {
if (props.isDelayButtonStateComplete) {
return;
}
props.toggleDelayButtonState(true);
props.onPress();
};

// Due to limitations in RN regarding the vertical text alignment of non-Text elements,
// for elements that are supposed to be inline, we need to use a Text element instead
// of a Pressable
const PressableView = props.inline ? Text : Pressable;

return (
<PressableView
style={[styles.flexRow, ...props.styles]}
onPress={updatePressState}
>
<Tooltip
containerStyles={[styles.flexRow]}
text={props.isDelayButtonStateComplete ? props.tooltipTextChecked : props.tooltipText}
>
<Text
Copy link
Member

Choose a reason for hiding this comment

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

There is a console error caused by this line -

image

Failed prop type: Invalid prop containerStyles of type object supplied to Tooltip, expected an array.
at Tooltip (webpack-internal:///./src/components/Tooltip/index.js:49:86)
at withWindowDimensions(Tooltip)
at div
at eval (webpack-internal:///./node_modules/@expensify/react-native-web/dist/exports/View/index.js:52:25)
at Pressable (webpack-internal:///./node_modules/@expensify/react-native-web/dist/exports/Pressable/index.js:40:24)
at PressableWithDelayToggle (webpack-internal:///./src/components/PressableWithDelayToggle.js:94:29)

suppressHighlighting
style={[styles.mr1, ...props.textStyles]}
>
{props.isDelayButtonStateComplete && props.textChecked ? props.textChecked : props.text}
</Text>
<Pressable
ref={props.innerRef}
focusable
accessibilityLabel={props.isDelayButtonStateComplete ? props.tooltipTextChecked : props.tooltipText}
onPress={updatePressState}
>
{({hovered, pressed}) => (
<>
{props.icon && (
<Icon
src={props.isDelayButtonStateComplete ? props.iconChecked : props.icon}
fill={StyleUtils.getIconFillColor(getButtonState(hovered, pressed, props.isDelayButtonStateComplete))}
style={props.iconStyles}
width={variables.iconSizeSmall}
height={variables.iconSizeSmall}
/>
)}
</>
)}
</Pressable>
</Tooltip>
</PressableView>
);
}

PressableWithDelayToggle.propTypes = propTypes;
PressableWithDelayToggle.defaultProps = defaultProps;

export default compose(
withLocalize,
withDelayToggleButtonState,
)(
React.forwardRef((props, ref) => (
<PressableWithDelayToggle
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
innerRef={ref}
/>
)),
);
20 changes: 13 additions & 7 deletions src/pages/settings/Security/TwoFactorAuth/CodesPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
import HeaderWithCloseButton from '../../../../components/HeaderWithCloseButton';
import Navigation from '../../../../libs/Navigation/Navigation';
import ScreenWrapper from '../../../../components/ScreenWrapper';
import * as Expensicons from '../../../../components/Icon/Expensicons';
import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize';
import compose from '../../../../libs/compose';
import ROUTES from '../../../../ROUTES';
Expand All @@ -14,6 +15,7 @@ import * as Illustrations from '../../../../components/Icon/Illustrations';
import styles from '../../../../styles/styles';
import FixedFooter from '../../../../components/FixedFooter';
import Button from '../../../../components/Button';
import PressableWithDelayToggle from '../../../../components/PressableWithDelayToggle';
import Text from '../../../../components/Text';
import Section from '../../../../components/Section';
import ONYXKEYS from '../../../../ONYXKEYS';
Expand Down Expand Up @@ -92,23 +94,27 @@ function CodesPage(props) {
))}
</View>
<View style={styles.twoFactorAuthCodesButtonsContainer}>
<Button
<PressableWithDelayToggle
text={props.translate('twoFactorAuth.copyCodes')}
medium
icon={Expensicons.Copy}
inline={false}
onPress={() => {
Clipboard.setString(props.account.recoveryCodes);
setIsNextButtonDisabled(false);
}}
style={styles.twoFactorAuthCodesButton}
styles={[styles.button, styles.buttonMedium, styles.twoFactorAuthCodesButton]}
textStyles={[styles.buttonMediumText]}
/>
<Button
text="Download"
medium
<PressableWithDelayToggle
text={props.translate('common.download')}
icon={Expensicons.Download}
onPress={() => {
localFileDownload('two-factor-auth-codes', props.account.recoveryCodes);
setIsNextButtonDisabled(false);
}}
style={styles.twoFactorAuthCodesButton}
inline={false}
styles={[styles.button, styles.buttonMedium, styles.twoFactorAuthCodesButton]}
textStyles={[styles.buttonMediumText]}
/>
</View>
</>
Expand Down