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

Migrate Button.js to function component #30349

303 changes: 167 additions & 136 deletions src/components/Button/index.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import React, {Component} from 'react';
import {ActivityIndicator, View} from 'react-native';
import PropTypes from 'prop-types';
import styles from '../../styles/styles';
import themeColors from '../../styles/themes/default';
import Text from '../Text';
import KeyboardShortcut from '../../libs/KeyboardShortcut';
import Icon from '../Icon';
import React, {useEffect} from 'react';
import {ActivityIndicator, View} from 'react-native';
import CONST from '../../CONST';
import * as StyleUtils from '../../styles/StyleUtils';
import HapticFeedback from '../../libs/HapticFeedback';
import withNavigationFallback from '../withNavigationFallback';
import KeyboardShortcut from '../../libs/KeyboardShortcut';
import compose from '../../libs/compose';
import * as StyleUtils from '../../styles/StyleUtils';
import styles from '../../styles/styles';
import themeColors from '../../styles/themes/default';
import Icon from '../Icon';
import * as Expensicons from '../Icon/Expensicons';
import withNavigationFocus from '../withNavigationFocus';
import validateSubmitShortcut from './validateSubmitShortcut';
import PressableWithFeedback from '../Pressable/PressableWithFeedback';
import Text from '../Text';
import refPropTypes from '../refPropTypes';
import withNavigationFallback from '../withNavigationFallback';
import withNavigationFocus from '../withNavigationFocus';
import validateSubmitShortcut from './validateSubmitShortcut';

const propTypes = {
/** Should the press event bubble across multiple instances when Enter key triggers it. */
Expand Down Expand Up @@ -161,94 +161,127 @@ const defaultProps = {
forwardedRef: undefined,
};

class Button extends Component {
constructor(props) {
super(props);

this.renderContent = this.renderContent.bind(this);
}

componentDidMount() {
if (!this.props.pressOnEnter) {
function Button({
allowBubble,
text,
shouldShowRightIcon,

icon,
iconRight,
iconFill,
iconStyles,
iconRightStyles,

small,
large,
medium,

isLoading,
isDisabled,

onPress,
onLongPress,
onPressIn,
onPressOut,
onMouseDown,

pressOnEnter,
enterKeyEventListenerPriority,

style,
innerStyles,
textStyles,

shouldUseDefaultHover,
success,
danger,
children,

shouldRemoveRightBorderRadius,
shouldRemoveLeftBorderRadius,
shouldEnableHapticFeedback,

isFocused,
fabioh8010 marked this conversation as resolved.
Show resolved Hide resolved
nativeID,
accessibilityLabel,
forwardedRef,
}) {
useEffect(() => {
if (!pressOnEnter) {
return;
}

const shortcutConfig = CONST.KEYBOARD_SHORTCUTS.ENTER;

// Setup and attach keypress handler for pressing the button with Enter key
this.unsubscribe = KeyboardShortcut.subscribe(
return KeyboardShortcut.subscribe(
shortcutConfig.shortcutKey,
fabioh8010 marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

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

use useKeyboardShortcut hook instead.

Copy link
Member

Choose a reason for hiding this comment

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

We can use isActive param to control dynamic subscription.

(e) => {
if (!validateSubmitShortcut(this.props.isFocused, this.props.isDisabled, this.props.isLoading, e)) {
(event) => {
if (!validateSubmitShortcut(isFocused, isDisabled, isLoading, event)) {
return;
}
this.props.onPress();
onPress();
},
shortcutConfig.descriptionKey,
shortcutConfig.modifiers,
true,
this.props.allowBubble,
this.props.enterKeyEventListenerPriority,
allowBubble,
enterKeyEventListenerPriority,
false,
);
}

componentWillUnmount() {
// Cleanup event listeners
if (!this.unsubscribe) {
return;
}
this.unsubscribe();
}
// This effect should run only once during mounting
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

renderContent() {
if (this.props.children) {
return this.props.children;
const renderContent = () => {
if (children) {
fabioh8010 marked this conversation as resolved.
Show resolved Hide resolved
return children;
}

const textComponent = (
<Text
numberOfLines={1}
selectable={false}
style={[
this.props.isLoading && styles.opacity0,
isLoading && styles.opacity0,
styles.pointerEventsNone,
styles.buttonText,
this.props.small && styles.buttonSmallText,
this.props.medium && styles.buttonMediumText,
this.props.large && styles.buttonLargeText,
this.props.success && styles.buttonSuccessText,
this.props.danger && styles.buttonDangerText,
this.props.icon && styles.textAlignLeft,
...this.props.textStyles,
small && styles.buttonSmallText,
medium && styles.buttonMediumText,
large && styles.buttonLargeText,
success && styles.buttonSuccessText,
danger && styles.buttonDangerText,
icon && styles.textAlignLeft,
...textStyles,
]}
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
>
{this.props.text}
{text}
</Text>
);

if (this.props.icon || this.props.shouldShowRightIcon) {
if (icon || shouldShowRightIcon) {
return (
<View style={[styles.justifyContentBetween, styles.flexRow]}>
<View style={[styles.alignItemsCenter, styles.flexRow, styles.flexShrink1]}>
{this.props.icon && (
<View style={[styles.mr1, ...this.props.iconStyles]}>
{icon && (
<View style={[styles.mr1, ...iconStyles]}>
<Icon
src={this.props.icon}
fill={this.props.iconFill}
small={this.props.small}
src={icon}
fill={iconFill}
small={small}
/>
</View>
)}
{textComponent}
</View>
{this.props.shouldShowRightIcon && (
<View style={[styles.justifyContentCenter, styles.ml1, ...this.props.iconRightStyles]}>
{shouldShowRightIcon && (
<View style={[styles.justifyContentCenter, styles.ml1, ...iconRightStyles]}>
<Icon
src={this.props.iconRight}
fill={this.props.iconFill}
small={this.props.small}
src={iconRight}
fill={iconFill}
small={small}
/>
</View>
)}
Expand All @@ -257,87 +290,85 @@ class Button extends Component {
}

return textComponent;
}

render() {
return (
<PressableWithFeedback
ref={this.props.forwardedRef}
onPress={(e) => {
if (e && e.type === 'click') {
e.currentTarget.blur();
}

if (this.props.shouldEnableHapticFeedback) {
HapticFeedback.press();
}
return this.props.onPress(e);
}}
onLongPress={(e) => {
if (this.props.shouldEnableHapticFeedback) {
HapticFeedback.longPress();
}
this.props.onLongPress(e);
}}
onPressIn={this.props.onPressIn}
onPressOut={this.props.onPressOut}
onMouseDown={this.props.onMouseDown}
disabled={this.props.isLoading || this.props.isDisabled}
wrapperStyle={[
this.props.isDisabled ? {...styles.cursorDisabled, ...styles.noSelect} : {},
styles.buttonContainer,
this.props.shouldRemoveRightBorderRadius ? styles.noRightBorderRadius : undefined,
this.props.shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined,
...StyleUtils.parseStyleAsArray(this.props.style),
]}
style={[
styles.button,
this.props.small ? styles.buttonSmall : undefined,
this.props.medium ? styles.buttonMedium : undefined,
this.props.large ? styles.buttonLarge : undefined,
this.props.success ? styles.buttonSuccess : undefined,
this.props.danger ? styles.buttonDanger : undefined,
this.props.isDisabled && (this.props.success || this.props.danger) ? styles.buttonOpacityDisabled : undefined,
this.props.isDisabled && !this.props.danger && !this.props.success ? styles.buttonDisabled : undefined,
this.props.shouldRemoveRightBorderRadius ? styles.noRightBorderRadius : undefined,
this.props.shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined,
this.props.icon || this.props.shouldShowRightIcon ? styles.alignItemsStretch : undefined,
...this.props.innerStyles,
]}
hoverStyle={[
this.props.shouldUseDefaultHover && !this.props.isDisabled ? styles.buttonDefaultHovered : undefined,
this.props.success && !this.props.isDisabled ? styles.buttonSuccessHovered : undefined,
this.props.danger && !this.props.isDisabled ? styles.buttonDangerHovered : undefined,
]}
nativeID={this.props.nativeID}
accessibilityLabel={this.props.accessibilityLabel}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON}
hoverDimmingValue={1}
>
{this.renderContent()}
{this.props.isLoading && (
<ActivityIndicator
color={this.props.success || this.props.danger ? themeColors.textLight : themeColors.text}
style={[styles.pAbsolute, styles.l0, styles.r0]}
/>
)}
</PressableWithFeedback>
);
}
};

return (
<PressableWithFeedback
ref={forwardedRef}
onPress={(event) => {
if (event && event.type === 'click') {
event.currentTarget.blur();
}

if (shouldEnableHapticFeedback) {
HapticFeedback.press();
}
return onPress(event);
}}
onLongPress={(event) => {
if (shouldEnableHapticFeedback) {
HapticFeedback.longPress();
}
onLongPress(event);
}}
onPressIn={onPressIn}
onPressOut={onPressOut}
onMouseDown={onMouseDown}
disabled={isLoading || isDisabled}
wrapperStyle={[
isDisabled ? {...styles.cursorDisabled, ...styles.noSelect} : {},
styles.buttonContainer,
shouldRemoveRightBorderRadius ? styles.noRightBorderRadius : undefined,
shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined,
...StyleUtils.parseStyleAsArray(style),
]}
style={[
styles.button,
small ? styles.buttonSmall : undefined,
medium ? styles.buttonMedium : undefined,
large ? styles.buttonLarge : undefined,
success ? styles.buttonSuccess : undefined,
danger ? styles.buttonDanger : undefined,
isDisabled && (success || danger) ? styles.buttonOpacityDisabled : undefined,
isDisabled && !danger && !success ? styles.buttonDisabled : undefined,
shouldRemoveRightBorderRadius ? styles.noRightBorderRadius : undefined,
shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined,
icon || shouldShowRightIcon ? styles.alignItemsStretch : undefined,
...innerStyles,
]}
hoverStyle={[
shouldUseDefaultHover && !isDisabled ? styles.buttonDefaultHovered : undefined,
success && !isDisabled ? styles.buttonSuccessHovered : undefined,
danger && !isDisabled ? styles.buttonDangerHovered : undefined,
]}
nativeID={nativeID}
accessibilityLabel={accessibilityLabel}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON}
hoverDimmingValue={1}
>
{renderContent()}
{isLoading && (
<ActivityIndicator
color={success || danger ? themeColors.textLight : themeColors.text}
style={[styles.pAbsolute, styles.l0, styles.r0]}
/>
)}
</PressableWithFeedback>
);
}

Button.propTypes = propTypes;
Button.defaultProps = defaultProps;
Button.displayName = 'Button';

const ButtonWithRef = React.forwardRef((props, ref) => (
<Button
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
forwardedRef={ref}
/>
));

ButtonWithRef.displayName = 'ButtonWithRef';

export default compose(
withNavigationFallback,
withNavigationFocus,
)(
React.forwardRef((props, ref) => (
<Button
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
forwardedRef={ref}
/>
)),
);
export default compose(withNavigationFallback, withNavigationFocus)(ButtonWithRef);
Loading