Skip to content

Commit cc018c3

Browse files
authored
Merge pull request #83314 from software-mansion-labs/war-in/composable-button
[No QA] Add composed button
2 parents 92d7b7d + f443165 commit cc018c3

20 files changed

Lines changed: 1757 additions & 53 deletions

src/components/Button/index.tsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import useTheme from '@hooks/useTheme';
1717
import useThemeStyles from '@hooks/useThemeStyles';
1818
import HapticFeedback from '@libs/HapticFeedback';
1919
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
20+
import type {ButtonSizeValue} from '@styles/utils/types';
2021
import CONST from '@src/CONST';
2122
import type ChildrenProps from '@src/types/utils/ChildrenProps';
2223
import type IconAsset from '@src/types/utils/IconAsset';
@@ -158,9 +159,6 @@ type ButtonProps = Partial<ChildrenProps> &
158159
/** Boolean whether to display the right icon */
159160
shouldShowRightIcon?: boolean;
160161

161-
/** Whether button's content should be centered */
162-
isContentCentered?: boolean;
163-
164162
/** Whether the Enter keyboard listening is active whether or not the screen that contains the button is focused */
165163
isPressOnEnterActive?: boolean;
166164

@@ -288,7 +286,6 @@ function Button({
288286
testID = undefined,
289287
accessibilityLabel = '',
290288
link = false,
291-
isContentCentered = false,
292289
isPressOnEnterActive,
293290
isNested = false,
294291
secondLineText = '',
@@ -369,7 +366,7 @@ function Button({
369366

370367
if (icon || shouldShowRightIcon) {
371368
return (
372-
<View style={[isContentCentered ? styles.justifyContentCenter : styles.justifyContentBetween, styles.flexRow, iconWrapperStyles, styles.mw100]}>
369+
<View style={[styles.justifyContentBetween, styles.flexRow, iconWrapperStyles, styles.mw100]}>
373370
<View style={[styles.alignItemsCenter, styles.flexRow, styles.flexShrink1]}>
374371
{!!icon && (
375372
<View style={[extraSmall || small ? styles.mr1 : styles.mr2, !text && styles.mr0, iconStyles, isLoading && styles.opacity0]}>
@@ -406,10 +403,21 @@ function Button({
406403
return textComponent;
407404
};
408405

406+
let buttonSize: ButtonSizeValue;
407+
if (extraSmall) {
408+
buttonSize = CONST.DROPDOWN_BUTTON_SIZE.EXTRA_SMALL;
409+
} else if (small) {
410+
buttonSize = CONST.DROPDOWN_BUTTON_SIZE.SMALL;
411+
} else if (medium) {
412+
buttonSize = CONST.DROPDOWN_BUTTON_SIZE.MEDIUM;
413+
} else {
414+
buttonSize = CONST.DROPDOWN_BUTTON_SIZE.LARGE;
415+
}
416+
409417
const buttonStyles = useMemo<StyleProp<ViewStyle>>(
410418
() => [
411419
styles.button,
412-
StyleUtils.getButtonStyleWithIcon(styles, extraSmall, small, medium, large, !!icon, !!(text?.length > 0), shouldShowRightIcon),
420+
StyleUtils.getButtonStyleWithIcon(styles, buttonSize, !!icon, !!(text?.length > 0), shouldShowRightIcon),
413421
success ? styles.buttonSuccess : undefined,
414422
danger ? styles.buttonDanger : undefined,
415423
isDisabled && !shouldStayNormalOnDisable ? styles.buttonOpacityDisabled : undefined,
@@ -426,14 +434,11 @@ function Button({
426434
icon,
427435
innerStyles,
428436
isDisabled,
429-
large,
437+
buttonSize,
430438
link,
431-
medium,
432439
shouldRemoveLeftBorderRadius,
433440
shouldRemoveRightBorderRadius,
434441
shouldShowRightIcon,
435-
small,
436-
extraSmall,
437442
styles,
438443
success,
439444
text,
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import React, {useMemo, useState} from 'react';
2+
import type {StyleProp, ViewStyle} from 'react-native';
3+
import {StyleSheet, View} from 'react-native';
4+
import ActivityIndicator from '@components/ActivityIndicator';
5+
import {getButtonRole} from '@components/Button/utils';
6+
import type {PressableRef} from '@components/Pressable/GenericPressable/types';
7+
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
8+
import useStyleUtils from '@hooks/useStyleUtils';
9+
import useTheme from '@hooks/useTheme';
10+
import useThemeStyles from '@hooks/useThemeStyles';
11+
import HapticFeedback from '@libs/HapticFeedback';
12+
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
13+
import variables from '@styles/variables';
14+
import CONST from '@src/CONST';
15+
import {ButtonContext} from './context';
16+
import type {ButtonProps} from './types';
17+
18+
function Button({
19+
children,
20+
contentContainerStyle = [],
21+
size = CONST.DROPDOWN_BUTTON_SIZE.MEDIUM,
22+
isLoading = false,
23+
isDisabled = false,
24+
onLayout = () => {},
25+
onPress = () => {},
26+
onLongPress = () => {},
27+
onPressIn = () => {},
28+
onPressOut = () => {},
29+
onMouseDown = undefined,
30+
style = [],
31+
disabledStyle,
32+
innerStyles = [],
33+
shouldUseDefaultHover = true,
34+
variant,
35+
shouldRemoveBorderRadius,
36+
shouldEnableHapticFeedback = false,
37+
isLongPressDisabled = false,
38+
id = '',
39+
testID = undefined,
40+
accessibilityLabel = '',
41+
isNested = false,
42+
shouldBlendOpacity = false,
43+
shouldStayNormalOnDisable = false,
44+
sentryLabel,
45+
ref,
46+
accessibilityState,
47+
}: ButtonProps) {
48+
const theme = useTheme();
49+
const styles = useThemeStyles();
50+
const StyleUtils = useStyleUtils();
51+
const [isHovered, setIsHovered] = useState(false);
52+
const buttonLoadingReasonAttributes: SkeletonSpanReasonAttributes = {
53+
context: 'Button',
54+
};
55+
56+
const contextValue = useMemo(
57+
() => ({
58+
isHovered,
59+
isLoading,
60+
variant,
61+
size,
62+
}),
63+
[isHovered, isLoading, variant, size],
64+
);
65+
66+
const buttonVariantStyles = useMemo(() => {
67+
const shouldUseDisabledStyles = isDisabled && !shouldStayNormalOnDisable;
68+
if (!variant) {
69+
return shouldUseDisabledStyles ? [styles.buttonOpacityDisabled, styles.buttonDisabled] : undefined;
70+
}
71+
72+
const {normal: defaultStyles, disabled: disabledStyles} = StyleUtils.getButtonVariantStyles(styles);
73+
return [defaultStyles[variant], shouldUseDisabledStyles && disabledStyles[variant]];
74+
}, [isDisabled, shouldStayNormalOnDisable, styles, variant, StyleUtils]);
75+
76+
const borderRadiusStyles = useMemo<Record<'left' | 'right' | 'all', StyleProp<ViewStyle>>>(
77+
() => ({
78+
right: styles.noRightBorderRadius,
79+
left: styles.noLeftBorderRadius,
80+
all: [styles.noRightBorderRadius, styles.noLeftBorderRadius],
81+
}),
82+
[styles.noRightBorderRadius, styles.noLeftBorderRadius],
83+
);
84+
85+
const buttonStyles = useMemo<StyleProp<ViewStyle>>(
86+
() => [
87+
styles.button,
88+
StyleUtils.getButtonSizeStyle(styles, size),
89+
buttonVariantStyles,
90+
shouldRemoveBorderRadius ? borderRadiusStyles[shouldRemoveBorderRadius] : undefined,
91+
styles.alignItemsStretch,
92+
innerStyles,
93+
variant === 'link' && styles.bgTransparent,
94+
],
95+
[styles, StyleUtils, size, buttonVariantStyles, shouldRemoveBorderRadius, borderRadiusStyles, innerStyles, variant],
96+
);
97+
98+
const buttonContainerStyles = useMemo<StyleProp<ViewStyle>>(
99+
() => [buttonStyles, shouldBlendOpacity && styles.buttonBlendContainer],
100+
[buttonStyles, shouldBlendOpacity, styles.buttonBlendContainer],
101+
);
102+
103+
const buttonBlendForegroundStyle = useMemo<StyleProp<ViewStyle>>(() => {
104+
if (!shouldBlendOpacity) {
105+
return undefined;
106+
}
107+
108+
const {backgroundColor, opacity} = StyleSheet.flatten(buttonStyles);
109+
110+
return {
111+
backgroundColor,
112+
opacity,
113+
};
114+
}, [buttonStyles, shouldBlendOpacity]);
115+
116+
return (
117+
<PressableWithFeedback
118+
ref={ref as PressableRef}
119+
id={id}
120+
testID={testID}
121+
accessibilityLabel={accessibilityLabel}
122+
accessibilityState={accessibilityState}
123+
sentryLabel={sentryLabel}
124+
role={getButtonRole(isNested)}
125+
isNested={isNested}
126+
disabled={isLoading || isDisabled}
127+
disabledStyle={!shouldStayNormalOnDisable ? disabledStyle : undefined}
128+
shouldBlendOpacity={shouldBlendOpacity}
129+
style={buttonContainerStyles}
130+
wrapperStyle={[
131+
isDisabled && !shouldStayNormalOnDisable ? {...styles.cursorDisabled, ...styles.noSelect} : {},
132+
styles.buttonContainer,
133+
shouldRemoveBorderRadius ? borderRadiusStyles[shouldRemoveBorderRadius] : undefined,
134+
style,
135+
]}
136+
hoverDimmingValue={1}
137+
hoverStyle={
138+
!isDisabled || !shouldStayNormalOnDisable
139+
? [
140+
shouldUseDefaultHover && !isDisabled ? styles.buttonDefaultHovered : undefined,
141+
variant === 'success' && !isDisabled ? styles.buttonSuccessHovered : undefined,
142+
variant === 'danger' && !isDisabled ? styles.buttonDangerHovered : undefined,
143+
]
144+
: []
145+
}
146+
onLayout={onLayout}
147+
onPressIn={onPressIn}
148+
onPressOut={onPressOut}
149+
onMouseDown={onMouseDown}
150+
onHoverIn={!isDisabled || !shouldStayNormalOnDisable ? () => setIsHovered(true) : undefined}
151+
onHoverOut={!isDisabled || !shouldStayNormalOnDisable ? () => setIsHovered(false) : undefined}
152+
onPress={(event) => {
153+
if (event?.type === 'click') {
154+
const currentTarget = event?.currentTarget as HTMLElement;
155+
currentTarget?.blur();
156+
}
157+
158+
if (shouldEnableHapticFeedback) {
159+
HapticFeedback.press();
160+
}
161+
162+
if (isDisabled || isLoading) {
163+
return;
164+
}
165+
return onPress(event);
166+
}}
167+
onLongPress={(event) => {
168+
if (isLongPressDisabled) {
169+
return;
170+
}
171+
if (shouldEnableHapticFeedback) {
172+
HapticFeedback.longPress();
173+
}
174+
onLongPress(event);
175+
}}
176+
>
177+
{shouldBlendOpacity && <View style={[StyleSheet.absoluteFill, buttonBlendForegroundStyle]} />}
178+
<ButtonContext.Provider value={contextValue}>
179+
<View style={[styles.flexRow, styles.alignItemsCenter, styles.justifyContentCenter, contentContainerStyle, styles.mw100]}>{children}</View>
180+
</ButtonContext.Provider>
181+
{isLoading && (
182+
<ActivityIndicator
183+
color={variant === 'success' || variant === 'danger' ? theme.textLight : theme.text}
184+
style={[styles.pAbsolute, styles.l0, styles.r0]}
185+
size={size === CONST.DROPDOWN_BUTTON_SIZE.EXTRA_SMALL ? variables.iconSizeExtraSmall : undefined}
186+
reasonAttributes={buttonLoadingReasonAttributes}
187+
/>
188+
)}
189+
</PressableWithFeedback>
190+
);
191+
}
192+
193+
export default Button;

0 commit comments

Comments
 (0)