diff options
Diffstat (limited to 'src/components/Button.tsx')
-rw-r--r-- | src/components/Button.tsx | 189 |
1 files changed, 109 insertions, 80 deletions
diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 68cee4374..ece1ad6b0 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,20 +1,22 @@ import React from 'react' import { + AccessibilityProps, Pressable, - Text, PressableProps, + StyleProp, + StyleSheet, + Text, TextProps, - ViewStyle, - AccessibilityProps, - View, TextStyle, - StyleSheet, - StyleProp, + View, + ViewStyle, } from 'react-native' import LinearGradient from 'react-native-linear-gradient' +import {Trans} from '@lingui/macro' -import {useTheme, atoms as a, tokens, android, flatten} from '#/alf' +import {android, atoms as a, flatten, tokens, useTheme} from '#/alf' import {Props as SVGIconProps} from '#/components/icons/common' +import {normalizeTextStyles} from '#/components/Typography' export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient' export type ButtonColor = @@ -27,7 +29,7 @@ export type ButtonColor = | 'gradient_sunset' | 'gradient_nordic' | 'gradient_bonfire' -export type ButtonSize = 'small' | 'large' +export type ButtonSize = 'tiny' | 'small' | 'medium' | 'large' export type ButtonShape = 'round' | 'square' | 'default' export type VariantProps = { /** @@ -48,25 +50,32 @@ export type VariantProps = { shape?: ButtonShape } -export type ButtonProps = React.PropsWithChildren< - Pick<PressableProps, 'disabled' | 'onPress'> & - AccessibilityProps & - VariantProps & { - testID?: string - label: string - style?: StyleProp<ViewStyle> - } -> -export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean} +export type ButtonState = { + hovered: boolean + focused: boolean + pressed: boolean + disabled: boolean +} -const Context = React.createContext< +export type ButtonContext = VariantProps & ButtonState + +export type ButtonProps = Pick< + PressableProps, + 'disabled' | 'onPress' | 'testID' +> & + AccessibilityProps & VariantProps & { - hovered: boolean - focused: boolean - pressed: boolean - disabled: boolean + testID?: string + label: string + style?: StyleProp<ViewStyle> + children: + | React.ReactNode + | string + | ((context: ButtonContext) => React.ReactNode | string) } ->({ +export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean} + +const Context = React.createContext<VariantProps & ButtonState>({ hovered: false, focused: false, pressed: false, @@ -132,7 +141,7 @@ export function Button({ })) }, [setState]) - const {baseStyles, hoverStyles, focusStyles} = React.useMemo(() => { + const {baseStyles, hoverStyles} = React.useMemo(() => { const baseStyles: ViewStyle[] = [] const hoverStyles: ViewStyle[] = [] const light = t.name === 'light' @@ -158,7 +167,7 @@ export function Button({ if (!disabled) { baseStyles.push(a.border, { - borderColor: tokens.color.blue_500, + borderColor: t.palette.primary_500, }) hoverStyles.push(a.border, { backgroundColor: light @@ -167,7 +176,7 @@ export function Button({ }) } else { baseStyles.push(a.border, { - borderColor: light ? tokens.color.blue_200 : tokens.color.blue_900, + borderColor: light ? t.palette.primary_200 : t.palette.primary_900, }) } } else if (variant === 'ghost') { @@ -184,20 +193,14 @@ export function Button({ if (variant === 'solid') { if (!disabled) { baseStyles.push({ - backgroundColor: light - ? tokens.color.gray_50 - : tokens.color.gray_900, + backgroundColor: t.palette.contrast_25, }) hoverStyles.push({ - backgroundColor: light - ? tokens.color.gray_100 - : tokens.color.gray_950, + backgroundColor: t.palette.contrast_50, }) } else { baseStyles.push({ - backgroundColor: light - ? tokens.color.gray_200 - : tokens.color.gray_950, + backgroundColor: t.palette.contrast_100, }) } } else if (variant === 'outline') { @@ -207,21 +210,19 @@ export function Button({ if (!disabled) { baseStyles.push(a.border, { - borderColor: light ? tokens.color.gray_300 : tokens.color.gray_700, + borderColor: t.palette.contrast_300, }) - hoverStyles.push(a.border, t.atoms.bg_contrast_50) + hoverStyles.push(t.atoms.bg_contrast_50) } else { baseStyles.push(a.border, { - borderColor: light ? tokens.color.gray_200 : tokens.color.gray_800, + borderColor: t.palette.contrast_200, }) } } else if (variant === 'ghost') { if (!disabled) { baseStyles.push(t.atoms.bg) hoverStyles.push({ - backgroundColor: light - ? tokens.color.gray_100 - : tokens.color.gray_900, + backgroundColor: t.palette.contrast_100, }) } } @@ -229,14 +230,14 @@ export function Button({ if (variant === 'solid') { if (!disabled) { baseStyles.push({ - backgroundColor: t.palette.negative_400, + backgroundColor: t.palette.negative_500, }) hoverStyles.push({ - backgroundColor: t.palette.negative_500, + backgroundColor: t.palette.negative_600, }) } else { baseStyles.push({ - backgroundColor: t.palette.negative_600, + backgroundColor: t.palette.negative_700, }) } } else if (variant === 'outline') { @@ -246,7 +247,7 @@ export function Button({ if (!disabled) { baseStyles.push(a.border, { - borderColor: t.palette.negative_400, + borderColor: t.palette.negative_500, }) hoverStyles.push(a.border, { backgroundColor: light @@ -266,7 +267,7 @@ export function Button({ hoverStyles.push({ backgroundColor: light ? t.palette.negative_100 - : t.palette.negative_950, + : t.palette.negative_975, }) } } @@ -275,8 +276,12 @@ export function Button({ if (shape === 'default') { if (size === 'large') { baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md) + } else if (size === 'medium') { + baseStyles.push({paddingVertical: 12}, a.px_2xl, a.rounded_sm, a.gap_md) } else if (size === 'small') { baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm) + } else if (size === 'tiny') { + baseStyles.push({paddingVertical: 4}, a.px_sm, a.rounded_xs, a.gap_xs) } } else if (shape === 'round' || shape === 'square') { if (size === 'large') { @@ -287,24 +292,24 @@ export function Button({ } } else if (size === 'small') { baseStyles.push({height: 40, width: 40}) + } else if (size === 'tiny') { + baseStyles.push({height: 20, width: 20}) } if (shape === 'round') { baseStyles.push(a.rounded_full) } else if (shape === 'square') { - baseStyles.push(a.rounded_sm) + if (size === 'tiny') { + baseStyles.push(a.rounded_xs) + } else { + baseStyles.push(a.rounded_sm) + } } } return { baseStyles, hoverStyles, - focusStyles: [ - ...hoverStyles, - { - outline: 0, - } as ViewStyle, - ], } }, [t, variant, color, size, shape, disabled]) @@ -338,7 +343,7 @@ export function Button({ } }, [variant, color]) - const context = React.useMemo( + const context = React.useMemo<ButtonContext>( () => ({ ...state, variant, @@ -349,6 +354,8 @@ export function Button({ [state, variant, color, size, disabled], ) + const flattenedBaseStyles = flatten(baseStyles) + return ( <Pressable role="button" @@ -362,15 +369,12 @@ export function Button({ disabled: disabled || false, }} style={[ - flatten(style), a.flex_row, a.align_center, a.justify_center, - a.overflow_hidden, - a.justify_center, - ...baseStyles, + flattenedBaseStyles, ...(state.hovered || state.pressed ? hoverStyles : []), - ...(state.focused ? focusStyles : []), + flatten(style), ]} onPressIn={onPressIn} onPressOut={onPressOut} @@ -379,21 +383,33 @@ export function Button({ onFocus={onFocus} onBlur={onBlur}> {variant === 'gradient' && ( - <LinearGradient - colors={ - state.hovered || state.pressed || state.focused - ? gradientHoverColors - : gradientColors - } - locations={gradientLocations} - start={{x: 0, y: 0}} - end={{x: 1, y: 1}} - style={[a.absolute, a.inset_0]} - /> + <View + style={[ + a.absolute, + a.inset_0, + a.overflow_hidden, + {borderRadius: flattenedBaseStyles.borderRadius}, + ]}> + <LinearGradient + colors={ + state.hovered || state.pressed + ? gradientHoverColors + : gradientColors + } + locations={gradientLocations} + start={{x: 0, y: 0}} + end={{x: 1, y: 1}} + style={[a.absolute, a.inset_0]} + /> + </View> )} <Context.Provider value={context}> - {typeof children === 'string' ? ( + {/* @ts-ignore */} + {typeof children === 'string' || children?.type === Trans ? ( + /* @ts-ignore */ <ButtonText>{children}</ButtonText> + ) : typeof children === 'function' ? ( + children(context) ) : ( children )} @@ -435,31 +451,31 @@ export function useSharedButtonTextStyles() { if (variant === 'solid' || variant === 'gradient') { if (!disabled) { baseStyles.push({ - color: light ? tokens.color.gray_700 : tokens.color.gray_100, + color: t.palette.contrast_700, }) } else { baseStyles.push({ - color: light ? tokens.color.gray_400 : tokens.color.gray_700, + color: t.palette.contrast_400, }) } } else if (variant === 'outline') { if (!disabled) { baseStyles.push({ - color: light ? tokens.color.gray_600 : tokens.color.gray_300, + color: t.palette.contrast_600, }) } else { baseStyles.push({ - color: light ? tokens.color.gray_400 : tokens.color.gray_700, + color: t.palette.contrast_300, }) } } else if (variant === 'ghost') { if (!disabled) { baseStyles.push({ - color: light ? tokens.color.gray_600 : tokens.color.gray_300, + color: t.palette.contrast_600, }) } else { baseStyles.push({ - color: light ? tokens.color.gray_400 : tokens.color.gray_600, + color: t.palette.contrast_300, }) } } @@ -493,6 +509,8 @@ export function useSharedButtonTextStyles() { if (size === 'large') { baseStyles.push(a.text_md, android({paddingBottom: 1})) + } else if (size === 'tiny') { + baseStyles.push(a.text_xs, android({paddingBottom: 1})) } else { baseStyles.push(a.text_sm, android({paddingBottom: 1})) } @@ -505,7 +523,14 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) { const textStyles = useSharedButtonTextStyles() return ( - <Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}> + <Text + {...rest} + style={normalizeTextStyles([ + a.font_bold, + a.text_center, + textStyles, + style, + ])}> {children} </Text> ) @@ -514,9 +539,11 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) { export function ButtonIcon({ icon: Comp, position, + size: iconSize, }: { icon: React.ComponentType<SVGIconProps> position?: 'left' | 'right' + size?: SVGIconProps['size'] }) { const {size, disabled} = useButtonContext() const textStyles = useSharedButtonTextStyles() @@ -532,7 +559,9 @@ export function ButtonIcon({ }, ]}> <Comp - size={size === 'large' ? 'md' : 'sm'} + size={ + iconSize ?? (size === 'large' ? 'md' : size === 'tiny' ? 'xs' : 'sm') + } style={[{color: textStyles.color, pointerEvents: 'none'}]} /> </View> |