diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 06226ad1e0..a37017decc 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -6,10 +6,14 @@ import { TextProps, ViewStyle, AccessibilityProps, + View, + TextStyle, + StyleSheet, } from 'react-native' import LinearGradient from 'react-native-linear-gradient' -import {useTheme, atoms, tokens, web, native} from '#/alf' +import {useTheme, atoms as a, tokens, web, native} from '#/alf' +import {Props as SVGIconProps} from '#/components/icons/common' export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient' export type ButtonColor = @@ -38,26 +42,33 @@ export type VariantProps = { size?: ButtonSize } -export type ButtonProps = Pick & - AccessibilityProps & - VariantProps & { - children: - | ((props: { - state: { - pressed: boolean - hovered: boolean - focused: boolean - } - props: VariantProps & { - disabled?: boolean - } - }) => React.ReactNode) - | React.ReactNode - | string - label: string - } +export type ButtonProps = React.PropsWithChildren< + Pick & + AccessibilityProps & + VariantProps & { + label: string + } +> export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean} +const Context = React.createContext< + VariantProps & { + hovered: boolean + focused: boolean + pressed: boolean + disabled: boolean + } +>({ + hovered: false, + focused: false, + pressed: false, + disabled: false, +}) + +export function useButtonContext() { + return React.useContext(Context) +} + export function Button({ children, variant, @@ -131,21 +142,21 @@ export function Button({ }) } } else if (variant === 'outline') { - baseStyles.push(atoms.border, t.atoms.bg, { + baseStyles.push(a.border, t.atoms.bg, { borderWidth: 1, }) if (!disabled) { - baseStyles.push(atoms.border, { + baseStyles.push(a.border, { borderColor: tokens.color.blue_500, }) - hoverStyles.push(atoms.border, { + hoverStyles.push(a.border, { backgroundColor: light ? t.palette.primary_100 : t.palette.primary_900, }) } else { - baseStyles.push(atoms.border, { + baseStyles.push(a.border, { borderColor: light ? tokens.color.blue_200 : tokens.color.blue_900, }) } @@ -180,17 +191,17 @@ export function Button({ }) } } else if (variant === 'outline') { - baseStyles.push(atoms.border, t.atoms.bg, { + baseStyles.push(a.border, t.atoms.bg, { borderWidth: 1, }) if (!disabled) { - baseStyles.push(atoms.border, { + baseStyles.push(a.border, { borderColor: light ? tokens.color.gray_500 : tokens.color.gray_500, }) - hoverStyles.push(atoms.border, t.atoms.bg_contrast_50) + hoverStyles.push(a.border, t.atoms.bg_contrast_50) } else { - baseStyles.push(atoms.border, { + baseStyles.push(a.border, { borderColor: light ? tokens.color.gray_200 : tokens.color.gray_800, }) } @@ -219,19 +230,19 @@ export function Button({ }) } } else if (variant === 'outline') { - baseStyles.push(atoms.border, t.atoms.bg, { + baseStyles.push(a.border, t.atoms.bg, { borderWidth: 1, }) if (!disabled) { - baseStyles.push(atoms.border, { + baseStyles.push(a.border, { borderColor: t.palette.negative_600, }) - hoverStyles.push(atoms.border, { + hoverStyles.push(a.border, { backgroundColor: light ? t.palette.negative_50 : '#2D0614', // darker red }) } else { - baseStyles.push(atoms.border, { + baseStyles.push(a.border, { borderColor: light ? t.palette.negative_200 : t.palette.negative_900, @@ -248,19 +259,9 @@ export function Button({ } if (size === 'large') { - baseStyles.push( - {paddingVertical: 15}, - atoms.px_2xl, - atoms.rounded_sm, - atoms.gap_sm, - ) + baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_sm) } else if (size === 'small') { - baseStyles.push( - {paddingVertical: 9}, - atoms.px_md, - atoms.rounded_sm, - atoms.gap_xs, - ) + baseStyles.push({paddingVertical: 9}, a.px_md, a.rounded_sm, a.gap_sm) } return { @@ -305,15 +306,13 @@ export function Button({ } }, [variant, color]) - const childProps = React.useMemo( + const context = React.useMemo( () => ({ - state, - props: { - variant, - color, - size, - disabled: disabled || false, - }, + ...state, + variant, + color, + size, + disabled: disabled || false, }), [state, variant, color, size, disabled], ) @@ -331,9 +330,9 @@ export function Button({ disabled: disabled || false, }} style={[ - atoms.flex_row, - atoms.align_center, - atoms.overflow_hidden, + a.flex_row, + a.align_center, + a.overflow_hidden, ...baseStyles, ...(state.hovered || state.pressed ? hoverStyles : []), ...(state.focused ? focusStyles : []), @@ -354,39 +353,25 @@ export function Button({ locations={gradientLocations} start={{x: 0, y: 0}} end={{x: 1, y: 1}} - style={[atoms.absolute, atoms.inset_0]} + style={[a.absolute, a.inset_0]} /> )} - {typeof children === 'string' ? ( - - {children} - - ) : typeof children === 'function' ? ( - children(childProps) - ) : ( - children - )} + + {typeof children === 'string' ? ( + {children} + ) : ( + children + )} + ) } -export function ButtonText({ - children, - style, - variant, - color, - size, - disabled, - ...rest -}: ButtonTextProps) { +export function useSharedButtonTextStyles() { const t = useTheme() - - const textStyles = React.useMemo(() => { - const baseStyles = [] + const {color, variant, disabled, size} = useButtonContext() + return React.useMemo(() => { + const baseStyles: TextStyle[] = [] const light = t.name === 'light' if (color === 'primary') { @@ -473,26 +458,46 @@ export function ButtonText({ if (size === 'large') { baseStyles.push( - atoms.text_md, + a.text_md, web({paddingBottom: 1}), native({marginTop: 2}), ) } else { baseStyles.push( - atoms.text_md, + a.text_md, web({paddingBottom: 1}), native({marginTop: 2}), ) } - return baseStyles + return StyleSheet.flatten(baseStyles) }, [t, variant, color, size, disabled]) +} + +export function ButtonText({children, style, ...rest}: ButtonTextProps) { + const textStyles = useSharedButtonTextStyles() return ( - + {children} ) } + +export function ButtonIcon({ + icon: Comp, +}: { + icon: React.ComponentType +}) { + const {size} = useButtonContext() + const textStyles = useSharedButtonTextStyles() + + return ( + + + + ) +} diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 2eb7b6bffd..8f686f3c4c 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -14,8 +14,8 @@ import { import {sanitizeUrl} from '@braintree/sanitize-url' import {isWeb} from '#/platform/detection' -import {useTheme, web} from '#/alf' -import {Button, ButtonProps} from '#/components/Button' +import {useTheme, web, flatten} from '#/alf' +import {Button, ButtonProps, useButtonContext} from '#/components/Button' import {AllNavigatorParams, NavigationProp} from '#/lib/routes/types' import { convertBskyAppUrlIfNeeded, @@ -25,7 +25,10 @@ import { import {useModalControls} from '#/state/modals' import {router} from '#/routes' -export type LinkProps = Omit & { +export type LinkProps = Omit< + ButtonProps, + 'style' | 'onPress' | 'disabled' | 'label' +> & { /** * `TextStyle` to apply to the anchor element itself. Does not apply to any children. */ @@ -39,6 +42,7 @@ export type LinkProps = Omit & { * works for Links with children that are strings i.e. text links. */ warnOnMismatchingTextChild?: boolean + label?: ButtonProps['label'] } & Pick>[0], 'to'> /** @@ -52,12 +56,11 @@ export type LinkProps = Omit & { export function Link({ children, to, - style, action = 'push', warnOnMismatchingTextChild, + style, ...rest }: LinkProps) { - const t = useTheme() const navigation = useNavigation() const {href} = useLinkProps({ to: @@ -67,12 +70,12 @@ export function Link({ const {openModal, closeModal} = useModalControls() const onPress = React.useCallback( (e: GestureResponderEvent) => { - const label = typeof children === 'string' ? children : '' + const stringChildren = typeof children === 'string' ? children : '' const requiresWarning = Boolean( warnOnMismatchingTextChild && - label && + stringChildren && isExternal && - linkRequiresWarning(href, label), + linkRequiresWarning(href, stringChildren), ) if (requiresWarning) { @@ -80,7 +83,7 @@ export function Link({ openModal({ name: 'link-warning', - text: label, + text: stringChildren, href: href, }) } else { @@ -139,6 +142,7 @@ export function Link({ return ( ) } + +function LinkText({ + children, + style, +}: React.PropsWithChildren<{ + style?: StyleProp +}>) { + const t = useTheme() + const {hovered} = useButtonContext() + return ( + + {children as string} + + ) +} diff --git a/src/view/screens/Storybook/Buttons.tsx b/src/view/screens/Storybook/Buttons.tsx index 168d088408..fbdc84eb4b 100644 --- a/src/view/screens/Storybook/Buttons.tsx +++ b/src/view/screens/Storybook/Buttons.tsx @@ -2,8 +2,16 @@ import React from 'react' import {View} from 'react-native' import {atoms as a} from '#/alf' -import {Button, ButtonVariant, ButtonColor} from '#/components/Button' +import { + Button, + ButtonVariant, + ButtonColor, + ButtonIcon, + ButtonText, +} from '#/components/Button' import {H1} from '#/components/Typography' +import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight' +import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' export function Buttons() { return ( @@ -83,6 +91,33 @@ export function Buttons() { )} + + + + + + ) diff --git a/src/view/screens/Storybook/Links.tsx b/src/view/screens/Storybook/Links.tsx index 2b9da2a9de..c3b1c0e0f3 100644 --- a/src/view/screens/Storybook/Links.tsx +++ b/src/view/screens/Storybook/Links.tsx @@ -13,27 +13,21 @@ export function Links() { External - +

External with custom children

+ style={[a.text_lg]}> https://blueskyweb.xyz @@ -41,12 +35,12 @@ export function Links() { - {({props}) => Link as a button} + Link as a button