react-native-ui-lib
Version:
[](https://stand-with-ukraine.pp.ua)
472 lines (469 loc) • 13.5 kB
JavaScript
import _isEqual from "lodash/isEqual";
import React, { PureComponent } from 'react';
import { Platform, StyleSheet, LayoutAnimation } from 'react-native';
import { asBaseComponent, forwardRef, Constants } from "../../commons/new";
import { Colors, Typography, BorderRadiuses } from "../../style";
import TouchableOpacity from "../touchableOpacity";
import View from "../view";
import Text from "../text";
import Icon from "../icon";
import { ButtonSize, ButtonAnimationDirection, ButtonProps, DEFAULT_PROPS } from "./types";
import { PADDINGS, HORIZONTAL_PADDINGS, MIN_WIDTH, DEFAULT_SIZE, SIZE_TO_VERTICAL_HITSLOP } from "./ButtonConstants";
export { ButtonSize, ButtonAnimationDirection, ButtonProps };
class Button extends PureComponent {
static displayName = 'Button';
static defaultProps = DEFAULT_PROPS;
static sizes = ButtonSize;
static animationDirection = ButtonAnimationDirection;
// This redundant constructor for some reason fix tests :/
// eslint-disable-next-line
constructor(props) {
super(props);
}
state = {
size: undefined,
measuredSize: undefined,
borderRadius: undefined
};
styles = createStyles();
componentDidUpdate(prevProps) {
if (this.props.animateLayout && !_isEqual(prevProps, this.props)) {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
}
}
// This method will be called more than once in case of layout change!
onLayout = event => {
const {
width,
height
} = event.nativeEvent.layout;
if (this.props.round) {
const size = height >= width ? height : width;
this.setState({
size
});
}
if (this.needsHitslopMeasurement()) {
this.setState({
measuredSize: {
width,
height
}
});
}
if (Constants.isAndroid && Platform.Version <= 17) {
this.setState({
borderRadius: height / 2
});
}
};
needsHitslopMeasurement() {
const {
avoidMinWidth,
avoidInnerPadding,
round
} = this.props;
return this.isLink || this.isIconButton && !round || avoidMinWidth || avoidInnerPadding;
}
get isLink() {
const {
link,
hyperlink
} = this.props;
return link || hyperlink;
}
get isFilled() {
return this.getBackgroundColor() !== 'transparent';
}
get isIconButton() {
const {
iconSource,
label
} = this.props;
return iconSource && !label;
}
getBackgroundColor() {
const {
disabled,
outline,
disabledBackgroundColor,
backgroundColor,
modifiers
} = this.props;
const {
backgroundColor: modifiersBackgroundColor
} = modifiers;
if (!outline && !this.isLink) {
if (disabled) {
return disabledBackgroundColor || Colors.$backgroundDisabled;
}
return backgroundColor || modifiersBackgroundColor || Colors.$backgroundPrimaryHeavy;
}
return 'transparent';
}
getActiveBackgroundColor() {
const {
getActiveBackgroundColor
} = this.props;
if (getActiveBackgroundColor) {
return getActiveBackgroundColor(this.getBackgroundColor(), this.props);
}
}
getLabelColor() {
const {
linkColor,
outline,
outlineColor,
disabled,
color: propsColor,
backgroundColor,
modifiers
} = this.props;
const {
color: modifiersColor
} = modifiers;
const isLink = this.isLink;
let color = Colors.$textDefaultLight;
if (isLink) {
color = linkColor || Colors.$textPrimary;
} else if (outline) {
color = outlineColor || Colors.$textPrimary;
} else if (this.isIconButton) {
color = backgroundColor === 'transparent' ? undefined : Colors.$iconDefaultLight;
}
if (disabled && !this.isFilled) {
return Colors.$textDisabled;
}
color = propsColor || modifiersColor || color;
return color;
}
getIconColor() {
const {
disabled
} = this.props;
let tintColor = this.getLabelColor();
if (disabled && !this.isFilled) {
tintColor = Colors.$iconDisabled;
}
return tintColor;
}
getLabelSizeStyle() {
const size = this.props.size || DEFAULT_SIZE;
const LABEL_STYLE_BY_SIZE = {};
LABEL_STYLE_BY_SIZE[Button.sizes.xSmall] = Typography.text80;
LABEL_STYLE_BY_SIZE[Button.sizes.small] = Typography.text80;
LABEL_STYLE_BY_SIZE[Button.sizes.medium] = Typography.text80;
LABEL_STYLE_BY_SIZE[Button.sizes.large] = undefined;
const labelSizeStyle = LABEL_STYLE_BY_SIZE[size];
return labelSizeStyle;
}
getContainerSizeStyle() {
const {
avoidMinWidth,
avoidInnerPadding,
round,
size: propsSize
} = this.props;
const size = propsSize || DEFAULT_SIZE;
const CONTAINER_STYLE_BY_SIZE = {};
CONTAINER_STYLE_BY_SIZE[Button.sizes.xSmall] = round ? {
height: this.state.size,
width: this.state.size,
padding: PADDINGS.XSMALL
} : {
paddingVertical: PADDINGS.XSMALL,
paddingHorizontal: HORIZONTAL_PADDINGS.XSMALL,
minWidth: MIN_WIDTH.XSMALL
};
CONTAINER_STYLE_BY_SIZE[Button.sizes.small] = round ? {
height: this.state.size,
width: this.state.size,
padding: PADDINGS.SMALL
} : {
paddingVertical: PADDINGS.SMALL,
paddingHorizontal: HORIZONTAL_PADDINGS.SMALL,
minWidth: MIN_WIDTH.SMALL
};
CONTAINER_STYLE_BY_SIZE[Button.sizes.medium] = round ? {
height: this.state.size,
width: this.state.size,
padding: PADDINGS.MEDIUM
} : {
paddingVertical: PADDINGS.MEDIUM,
paddingHorizontal: HORIZONTAL_PADDINGS.MEDIUM,
minWidth: MIN_WIDTH.MEDIUM
};
CONTAINER_STYLE_BY_SIZE[Button.sizes.large] = round ? {
height: this.state.size,
width: this.state.size,
padding: PADDINGS.LARGE
} : {
paddingVertical: PADDINGS.LARGE,
paddingHorizontal: HORIZONTAL_PADDINGS.LARGE,
minWidth: MIN_WIDTH.LARGE
};
const containerSizeStyle = CONTAINER_STYLE_BY_SIZE[size];
if (this.isLink || this.isIconButton && !round) {
containerSizeStyle.paddingVertical = undefined;
containerSizeStyle.paddingHorizontal = undefined;
containerSizeStyle.minWidth = undefined;
}
if (avoidMinWidth) {
containerSizeStyle.minWidth = undefined;
}
if (avoidInnerPadding) {
containerSizeStyle.paddingVertical = undefined;
containerSizeStyle.paddingHorizontal = undefined;
}
return containerSizeStyle;
}
getOutlineStyle() {
const {
outline,
outlineColor,
outlineWidth,
disabled
} = this.props;
let outlineStyle;
if ((outline || outlineColor) && !this.isLink) {
outlineStyle = {
borderWidth: outlineWidth ?? 1,
borderColor: outlineColor || Colors.$outlinePrimary
};
if (disabled) {
outlineStyle.borderColor = Colors.$outlineDisabled;
}
}
return outlineStyle;
}
getBorderRadiusStyle() {
const {
fullWidth,
borderRadius: propsBorderRadius,
modifiers
} = this.props;
const {
borderRadius: modifiersBorderRadius
} = modifiers;
if (this.isLink || fullWidth || propsBorderRadius === 0) {
return {
borderRadius: 0
};
}
const borderRadius = propsBorderRadius || modifiersBorderRadius || BorderRadiuses.br100;
return {
borderRadius
};
}
getShadowStyle() {
const backgroundColor = this.getBackgroundColor();
const {
enableShadow
} = this.props;
if (enableShadow) {
return [this.styles.shadowStyle, {
shadowColor: backgroundColor
}];
}
}
getIconStyle() {
const {
iconStyle: propsIconStyle,
iconOnRight,
size: propsSize,
link
} = this.props;
const size = propsSize || DEFAULT_SIZE;
const iconStyle = {};
const marginSide = link ? 4 : [Button.sizes.large, Button.sizes.medium].includes(size) ? 8 : 4;
if (!this.isIconButton) {
if (iconOnRight) {
iconStyle.marginLeft = marginSide;
} else {
iconStyle.marginRight = marginSide;
}
}
return [iconStyle, propsIconStyle];
}
getAnimationDirectionStyle() {
const {
animateTo
} = this.props;
let style;
switch (animateTo) {
case 'left':
style = {
alignSelf: 'flex-start'
};
break;
case 'right':
style = {
alignSelf: 'flex-end'
};
break;
default:
// 'center' is the default
break;
}
return style;
}
renderIcon() {
const {
iconSource,
supportRTL,
testID,
iconProps
} = this.props;
if (iconSource) {
const iconColor = this.getIconColor();
const iconStyle = this.getIconStyle();
if (typeof iconSource === 'function') {
return iconSource([{
tintColor: iconColor
}, this.getIconStyle()]);
} else {
// if (Constants.isWeb) {
return <Icon style={iconStyle} source={iconSource} supportRTL={supportRTL} testID={`${testID}.icon`}
// Note: Passing tintColor as prop is required for Web
tintColor={iconColor} {...iconProps} />;
// }
// return (
// <Image
// source={iconSource}
// supportRTL={supportRTL}
// style={iconStyle}
// testID={`${testID}.icon`}
// {...iconProps}
// />
// );
}
}
return null;
}
renderLabel() {
const {
label,
labelStyle,
labelProps,
hyperlink,
testID,
modifiers
} = this.props;
const color = this.getLabelColor();
const labelSizeStyle = this.getLabelSizeStyle();
const {
typography
} = modifiers;
if (label) {
return <Text style={[this.styles.text, !!color && {
color
}, labelSizeStyle, typography, labelStyle]} underline={hyperlink} numberOfLines={1} testID={`${testID}.label`} recorderTag={'unmask'} {...labelProps}>
{label}
</Text>;
}
return null;
}
getAccessibleHitSlop() {
const {
measuredSize
} = this.state;
const containerStyle = this.getContainerSizeStyle();
const buttonWidth = measuredSize?.width ?? containerStyle.width ?? containerStyle.minWidth ?? 0;
const buttonHeight = measuredSize?.height ?? containerStyle.height ?? 0;
const horizontalHitslop = Math.max(0, (48 - buttonWidth) / 2);
const verticalHitslop = buttonHeight ? Math.max(0, (48 - buttonHeight) / 2) : SIZE_TO_VERTICAL_HITSLOP[this.props.size || DEFAULT_SIZE] / 2;
return {
top: verticalHitslop,
bottom: verticalHitslop,
left: horizontalHitslop,
right: horizontalHitslop
};
}
renderCustomBackground() {
const {
customBackground
} = this.props;
if (customBackground) {
const borderRadiusStyle = this.getBorderRadiusStyle();
return <View absF style={[this.styles.backgroundElement, borderRadiusStyle]}>
{customBackground}
</View>;
}
}
render() {
const {
onPress,
disabled,
style,
testID,
animateLayout,
modifiers,
forwardedRef,
hitSlop: hitSlopProp,
customBackground,
...others
} = this.props;
const shadowStyle = this.getShadowStyle();
const {
margins,
paddings
} = modifiers;
const backgroundColor = customBackground ? undefined : this.getBackgroundColor();
const outlineStyle = this.getOutlineStyle();
const containerSizeStyle = this.getContainerSizeStyle();
const borderRadiusStyle = this.getBorderRadiusStyle();
return <TouchableOpacity row centerV style={[this.styles.container, animateLayout && this.getAnimationDirectionStyle(), containerSizeStyle, this.isLink && this.styles.innerContainerLink, shadowStyle, margins, paddings, {
backgroundColor
}, borderRadiusStyle, outlineStyle, style]} activeOpacity={0.6} activeBackgroundColor={this.getActiveBackgroundColor()} onLayout={this.onLayout} onPress={onPress} disabled={disabled} testID={testID} hitSlop={hitSlopProp ?? this.getAccessibleHitSlop()} {...others} ref={forwardedRef}>
{this.renderCustomBackground()}
{this.props.children}
{this.props.iconOnRight ? this.renderLabel() : this.renderIcon()}
{this.props.iconOnRight ? this.renderIcon() : this.renderLabel()}
</TouchableOpacity>;
}
}
function createStyles() {
return StyleSheet.create({
container: {
backgroundColor: 'transparent',
justifyContent: 'center',
alignItems: 'center'
},
innerContainerLink: {
minWidth: undefined,
paddingHorizontal: undefined,
paddingVertical: undefined,
borderRadius: BorderRadiuses.br0,
backgroundColor: undefined
},
shadowStyle: {
shadowOffset: {
height: 5,
width: 0
},
shadowOpacity: 0.35,
shadowRadius: 9.5,
elevation: 2
},
text: {
backgroundColor: 'transparent',
flexDirection: 'row',
...Typography.text70
},
backgroundElement: {
overflow: 'hidden'
}
});
}
export { Button }; // For tests
const modifiersOptions = {
paddings: true,
margins: true,
borderRadius: true,
backgroundColor: true,
typography: true,
color: true
};
export default asBaseComponent(forwardRef(Button), {
modifiersOptions
});