UNPKG

@spaced-out/ui-design-system

Version:
287 lines (269 loc) 8.68 kB
// @flow strict import * as React from 'react'; import {classify} from '../../utils/classify'; import {CircularLoader} from '../CircularLoader'; import type {IconType} from '../Icon'; import {Icon} from '../Icon'; import {Truncate} from '../Truncate'; import css from './Button.module.css'; type ClassNames = $ReadOnly<{wrapper?: string, icon?: string, text?: string}>; /** * Note(Nishant): Although Button supports gradient as a type, its not currently customizable really. * This only supports pre-defined gradient that moves from left to right. * If someone wants to add more gradients, the expectation is that they would add it through a wrapper className. * * We could have taken an extra prop to take in the Gradient colors but that should not be encouraged * as it would add an additional overhead on the component to figure out exact color values from string tokens * and since this is rarely used type anyway, it should be avoided. */ export const BUTTON_TYPES = Object.freeze({ primary: 'primary', secondary: 'secondary', tertiary: 'tertiary', ghost: 'ghost', danger: 'danger', gradient: 'gradient', }); export const BUTTON_ACTION_TYPE = Object.freeze({ button: 'button', submit: 'submit', reset: 'reset', }); export const BUTTON_SIZE = Object.freeze({ small: 'small', medium: 'medium', }); export type ButtonType = $Values<typeof BUTTON_TYPES>; export type ButtonActionType = $Values<typeof BUTTON_ACTION_TYPE>; export type ButtonSize = $Keys<typeof BUTTON_SIZE>; export type BaseButtonProps = { children?: React.Node, disabled?: mixed, actionType?: ButtonActionType, onClick?: ?(SyntheticEvent<HTMLElement>) => mixed, ariaLabel?: string, tabIndex?: number, isLoading?: boolean, role?: string, ... }; export type UnstyledButtonProps = { ...BaseButtonProps, className?: string, ... }; export type ButtonProps = { ...BaseButtonProps, classNames?: ClassNames, iconLeftName?: string, iconLeftType?: IconType, iconRightName?: string, iconRightType?: IconType, type?: ButtonType, isFluid?: boolean, size?: ButtonSize, ... }; const ButtonTypeToIconColorMap = { primary: 'inversePrimary', secondary: 'clickable', tertiary: 'primary', ghost: 'primary', danger: 'inversePrimary', gradient: 'inversePrimary', }; const ButtonTypeToLoaderColorMap = { primary: 'colorTextInversePrimary', secondary: 'colorTextClickable', tertiary: 'colorTextPrimary', ghost: 'colorTextPrimary', danger: 'colorTextInversePrimary', gradient: 'colorTextInversePrimary', }; export const UnstyledButton: React$AbstractComponent< UnstyledButtonProps, HTMLButtonElement, > = React.forwardRef<UnstyledButtonProps, HTMLButtonElement>( ( { disabled, onClick, className, ariaLabel, actionType, tabIndex = 0, isLoading, role = 'button', ...props }: UnstyledButtonProps, ref, ) => ( <button {...props} {...(ariaLabel ? {'aria-label': ariaLabel} : {})} className={className} ref={ref} role={role} disabled={disabled} tabIndex={disabled ? -1 : tabIndex} type={actionType} onClick={(event) => { if (disabled || isLoading) { event.preventDefault(); } else if (onClick) { onClick(event); } }} /> ), ); export const Button: React$AbstractComponent<ButtonProps, HTMLButtonElement> = React.forwardRef<ButtonProps, HTMLButtonElement>( ( { classNames, children, iconLeftName = '', iconLeftType = 'regular', iconRightName = '', iconRightType = 'regular', type = 'primary', isFluid = false, disabled = false, actionType = 'button', size = 'medium', isLoading, ...props }: ButtonProps, ref, ) => ( <UnstyledButton {...props} actionType={actionType} disabled={disabled} isLoading={isLoading} className={classify( css.button, { [css.primary]: type === 'primary', [css.secondary]: type === 'secondary', [css.tertiary]: type === 'tertiary', [css.ghost]: type === 'ghost', [css.danger]: type === 'danger', [css.gradient]: type === 'gradient', [css.disabled]: disabled, [css.small]: size === 'small', [css.medium]: size === 'medium', [css.isFluid]: isFluid === true, [css.withIconLeft]: !!iconLeftName, [css.withIconRight]: !!iconRightName, [css.withBothIcon]: !!(iconLeftName && iconRightName), [css.onlyIcon]: (iconLeftName || iconRightName) && !children, }, classNames?.wrapper, )} ref={ref} > <div className={css.buttonRow}> {/* Has no icon, only child */} {!(iconLeftName || iconRightName) ? ( <div className={classify(css.textContainer, classNames?.text)}> {isLoading && ( <div className={css.loader}> <CircularLoader size={size} colorToken={ disabled ? 'colorTextDisabled' : ButtonTypeToLoaderColorMap[type] } /> </div> )} <Truncate className={classify({[css.hidden]: isLoading})}> {children} </Truncate> </div> ) : // has icon, but no child children == null ? ( <> {isLoading && ( <div className={css.loader}> <CircularLoader size={size} colorToken={ disabled ? 'colorTextDisabled' : ButtonTypeToLoaderColorMap[type] } /> </div> )} <Icon name={iconLeftName || iconRightName} color={disabled ? 'disabled' : ButtonTypeToIconColorMap[type]} size={size === 'medium' ? 'medium' : 'small'} type={iconLeftName ? iconLeftType : iconRightType} className={classify( {[css.hidden]: isLoading}, classNames?.icon, )} /> </> ) : ( // has icon _and_ child (iconLeftName || iconRightName) && ( <> {iconLeftName && ( <Icon name={iconLeftName} color={ disabled ? 'disabled' : ButtonTypeToIconColorMap[type] } size={size === 'medium' ? 'medium' : 'small'} type={iconLeftType} className={classify( {[css.hidden]: isLoading}, classNames?.icon, )} /> )} <div className={classify(css.textContainer, classNames?.text)}> {isLoading && ( <div className={css.loader}> <CircularLoader size={size} colorToken={ disabled ? 'colorTextDisabled' : ButtonTypeToLoaderColorMap[type] } /> </div> )} <Truncate className={classify({[css.hidden]: isLoading})}> {children} </Truncate> </div> {iconRightName && ( <Icon name={iconRightName} color={ disabled ? 'disabled' : ButtonTypeToIconColorMap[type] } size={size === 'medium' ? 'medium' : 'small'} type={iconRightType} className={classify( {[css.hidden]: isLoading}, classNames?.icon, )} /> )} </> ) )} </div> </UnstyledButton> ), ); Button.name = Button.displayName = 'Button';