import type { ReactElement, ReactNode } from 'react'; import React, { useCallback, useMemo, useState } from 'react'; import type { StyleProp, ViewStyle } from 'react-native'; import type { Theme } from '../../theme'; import { useTheme } from '../../theme'; import { useDeprecation } from '../../utils/hooks'; import type { IconName } from '../Icon'; import LoadingIndicator from './LoadingIndicator'; import type { Intent, ThemeVariant, CamelCase } from './StyledButton'; import { StyledButtonContainer, StyledButtonIcon, StyledButtonIconWrapper, StyledButtonText, StyledButtonTitleOfVariantText, StyledSmallButtonText, } from './StyledButton'; /** * @deprecated Use 'primary' | 'secondary' | 'danger' | 'inverted' instead. */ type DeprecatedIntent = 'white'; type ValidIntent = 'primary' | 'secondary' | 'danger' | 'inverted'; export interface ButtonProps { /** * Helps users understand what will happen when they perform an action on the accessibility element when that result is not clear from the accessibility label. */ accessibilityHint?: string; /** * A succinct label in a localized string that identifies the accessibility element */ accessibilityLabel?: string; /** * Disable state of button. */ disabled?: boolean; /** * Places an icon within the button, before the button's text */ icon?: IconName | ReactNode; /** * Visual intent color to apply to button. It is required for `filled`, `outlined` and `text` variants. * * ⚠️ 'white' intent is deprecated and will be removed in the next major release. Please use 'primary' | 'secondary' | 'danger' | 'inverted' instead. */ intent?: ValidIntent | DeprecatedIntent; /** * Loading state of button. */ loading?: boolean; /** * Set the handler to handle press event. */ onPress: () => void; /** * Places an icon within the button, after the button's text */ rightIcon?: IconName | ReactNode; /** * Additional style. */ style?: StyleProp; /** * Testing id of the component. */ testID?: string; /** * Button label. */ text?: ReactNode; /** * Button type. */ variant?: | 'filled' | 'outlined' | 'text' | 'inline-text' | 'filled-medium' | 'outlined-medium' | 'text-medium' | 'filled-compact' | 'outlined-compact' | 'text-compact' | 'inline-text-compact'; } const isIconName = (icon: IconName | ReactNode): icon is IconName => { return typeof icon === 'string'; }; const FILLED_VARIANTS = { primary: 'filled-primary', secondary: 'filled-secondary', danger: 'filled-danger', white: 'filled-white', inverted: 'filled-inverted', } as const; const OUTLINED_VARIANTS = { primary: 'outlined-primary', secondary: 'outlined-secondary', danger: 'outlined-danger', white: 'outlined-white', inverted: 'outlined-inverted', } as const; const TEXT_VARIANTS = { primary: 'text-primary', secondary: 'text-secondary', danger: 'text-danger', white: 'text-white', inverted: 'text-inverted', } as const; export const getThemeVariant = ( variant: | 'filled' | 'outlined' | 'text' | 'filled-medium' | 'outlined-medium' | 'text-medium' | 'filled-compact' | 'outlined-compact' | 'text-compact', intent: Intent ): ThemeVariant => { switch (variant) { case 'filled': case 'filled-medium': case 'filled-compact': return FILLED_VARIANTS[intent]; case 'outlined': case 'outlined-medium': case 'outlined-compact': return OUTLINED_VARIANTS[intent]; case 'text': case 'text-medium': case 'text-compact': return TEXT_VARIANTS[intent]; } }; const getUnderlayColor = (theme: Theme, themeVariant: ThemeVariant) => { const colorKey = themeVariant.replace(/-([a-z])/g, (_, letter) => { return letter.toUpperCase(); }) as CamelCase; return theme.__hd__.button.colors.pressedBackground[colorKey]; }; const deprecatedVariants: ThemeVariant[] = [ 'filled-secondary', 'filled-danger', 'outlined-secondary', 'outlined-danger', ]; function isTextVariant( themeVariant: ThemeVariant ): themeVariant is | 'text-primary' | 'text-secondary' | 'text-danger' | 'text-white' | 'text-inverted' { return [ 'text-primary', 'text-secondary', 'text-danger', 'text-white', 'text-inverted', ].includes(themeVariant); } const Button = ({ accessibilityHint, accessibilityLabel, disabled = false, icon, intent = 'primary', loading = false, onPress, rightIcon, style, testID, text, variant = 'filled', }: ButtonProps): ReactElement => { const isInlineText = variant === 'inline-text' || variant === 'inline-text-compact'; const isIconButtonOnly = !text; const themeVariant = getThemeVariant(isInlineText ? 'text' : variant, intent); const theme = useTheme(); const underlayColor = useMemo(() => { return isInlineText ? 'transparent' : getUnderlayColor(theme, themeVariant); }, [theme, themeVariant, isInlineText]); const [isPressed, setIsPressed] = useState(false); const handlePress = useCallback(() => onPress(), [onPress]); useDeprecation( `Button variant ${deprecatedVariants.join(', ')} are deprecated.`, deprecatedVariants.includes(themeVariant) ); const isCompactVariant = [ 'filled-compact', 'outlined-compact', 'text-compact', 'inline-text-compact', ].includes(variant); const isMediumVariant = [ 'filled-medium', 'outlined-medium', 'text-medium', ].includes(variant); const isRenderTextVariant = isTextVariant(themeVariant); const renderTextVariantTitle = () => { if (!isRenderTextVariant || isIconButtonOnly) return null; return ( {text} ); }; const renderTitle = () => { if (isIconButtonOnly) return null; if (isCompactVariant) { return ( {text} ); } if (isMediumVariant) { return ( {text} ); } return ( {text} ); }; return ( isInlineText && setIsPressed(true)} onPressOut={() => isInlineText && setIsPressed(false)} themeIsCompact={isCompactVariant} themeIsMedium={isMediumVariant} themeIsIconOnly={isIconButtonOnly} > {loading === true ? ( ) : ( <> {icon !== undefined && ( {isIconName(icon) ? ( ) : ( icon )} )} {isRenderTextVariant ? renderTextVariantTitle() : renderTitle()} {!isIconButtonOnly && rightIcon !== undefined && ( {isIconName(rightIcon) ? ( ) : ( rightIcon )} )} )} ); }; export default Button;