UNPKG

@patreon/studio

Version:

Patreon Studio Design System

152 lines (151 loc) 7.25 kB
'use client'; import cx from 'classnames'; import React, { useMemo } from 'react'; import { useLogger } from '../../hooks/useLogger'; import { classNameForBodyText } from '../../styles/classNameForBodyText'; import devWarn from '../../utilities/dev-warn'; import { mapResponsive, wrapResponsive } from '../../utilities/opaque-responsive'; import { classNameForResponsiveValue, createResponsiveClassNameLookup } from '../../utilities/responsive-style'; import { LoadingSpinner } from '../LoadingSpinner'; import styles from './Button.module.css'; import { getSpinnerColor, getIconSize } from './theme'; const classNameFluidLookup = createResponsiveClassNameLookup(styles, { true: 'fluidSet', false: 'fluidUnset', }); const useButtonClassList = ({ variant, size, unfilled, isLoading, disabled, textAlign, weight, padding, corners, fluid, className, hasChildren, hasIcon, hasDisclosureIcon, }) => { return useMemo(() => { const responsiveFluid = mapResponsive(wrapResponsive(fluid), (value) => (value ? 'true' : 'false')); return cx(styles.root, { [styles.themePrimary]: variant === 'primary', [styles.themeSecondary]: variant === 'secondary', [styles.themeTertiary]: variant === 'tertiary', [styles.themeCritical]: variant === 'critical', [styles.themeFloating]: variant === 'floating', [styles.themeInsetWhite]: variant === 'insetWhite', [styles.themeInsetBlack]: variant === 'insetBlack', [styles.sizeXs]: size === 'xs', [styles.sizeSm]: size === 'sm', [styles.sizeMd]: size === 'md', [styles.sizeLg]: size === 'lg', [styles.cornersPill]: corners === 'pill', [styles.hasDimensions]: padding === 'default', [styles.variantIconOnly]: hasIcon && !hasDisclosureIcon && !hasChildren, [styles.variantLabelOnly]: !hasIcon && !hasDisclosureIcon && hasChildren, [styles.variantLabelAndIcon]: !hasDisclosureIcon && hasChildren && hasIcon, [styles.variantDisclosureAndLabel]: !hasIcon && hasDisclosureIcon && hasChildren, [styles.variantIconAndDisclosureOnly]: hasIcon && hasDisclosureIcon && !hasChildren, [styles.variantIconLabelAndDisclosure]: hasIcon && hasDisclosureIcon && hasChildren, [styles.hasTextAlign]: textAlign !== undefined, [styles.textAlignLeft]: textAlign === 'left', [styles.textAlignRight]: textAlign === 'right', [styles.isFilled]: !unfilled, [styles.isUnfilled]: unfilled, [styles.isActive]: !(disabled || isLoading), [styles.isInactive]: disabled || isLoading, [styles.isLoading]: isLoading, }, classNameForResponsiveValue(responsiveFluid, classNameFluidLookup), classNameForBodyText({ size: size === 'xs' ? 'sm' : size, weight }), className); }, [ fluid, variant, size, corners, padding, hasIcon, hasDisclosureIcon, hasChildren, textAlign, unfilled, disabled, isLoading, weight, className, ]); }; export const Button = React.forwardRef(function Button({ variant = 'secondary', unfilled = false, children, size = 'md', disabled = false, onClick, icon, fluid = false, iconProps, isLoading = false, disclosureIcon, href, target, type = 'button', corners = 'rounded', padding: userPadding, textAlign, weight = 'bold', className, style, loggerId, loggerProps, id, 'data-tag': dataTag, ...props // aria props }, ref) { const log = useLogger(); const hasChildren = !!children; const hasIcon = !!icon; const hasDisclosureIcon = !!disclosureIcon; const iconSize = getIconSize(size); const LeftIcon = icon; const RightIcon = disclosureIcon; // we're not setting a default value for padding in props because we want to // check if the user has passed in a value for padding with a button using // a label. If they have, we want to warn them that they should not use // padding with a button using a label. const iconOnly = hasIcon && !hasDisclosureIcon && !hasChildren; const padding = iconOnly ? userPadding ?? 'default' : 'default'; const classList = useButtonClassList({ children, icon, size, unfilled, variant, isLoading, disabled, textAlign, weight, corners, padding, fluid, className, hasChildren, hasIcon, hasDisclosureIcon, }); // Ideally we would catch both of these warnings in the type definition, but we extend the // ButtonProps type which means we can't add a intersection type easily. We should revisit // this in the future and consider adding a `IconButton` component for icon only buttons // instead of using allowing button to have so many responsibilities. if (iconOnly && !props['aria-label']) { devWarn('`aria-label` must be defined in icon only buttons for accessibility.'); } if (padding !== undefined && !iconOnly) { devWarn('`padding` prop is only supported when using an icon only button.'); } const content = (<> <div className={styles.contentContainer}> {LeftIcon && <LeftIcon size={iconSize} color="currentColor" {...iconProps}/>} {children && <div className={styles.labelContainer}>{children}</div>} {RightIcon && <RightIcon size={iconSize} color="currentColor" {...iconProps}/>} </div> {isLoading && (<div className={styles.loadingWrapper}> <LoadingSpinner color={getSpinnerColor({ variant, unfilled })} size={size === 'lg' ? 'sm' : 'xs'}/> </div>)} </>); if (href || target) { const computedHref = isLoading || disabled ? undefined : href ?? '#'; return (<a onClick={(e) => { if (disabled) { e.preventDefault(); return; } if (computedHref?.length && e.target instanceof HTMLElement) { e.target.style.cursor = 'default'; } log('buttonClick', loggerId, loggerProps); onClick?.(e); }} onMouseDown={(e) => { if (computedHref?.length && e.target instanceof HTMLElement) { e.target.style.cursor = 'default'; } e.preventDefault(); }} aria-disabled={disabled} ref={ref} rel={target === '_blank' ? 'noopener noreferrer' : undefined} role={disabled ? 'link' : undefined} href={computedHref} target={target} className={classList} style={style} id={id} data-tag={dataTag} {...props}> {content} </a>); } const onClickOverride = (e) => { if (isLoading || disabled) { e.preventDefault(); return; } log('buttonClick', loggerId, loggerProps); onClick?.(e); }; return (<button onClick={onClickOverride} aria-disabled={disabled} type={type} ref={ref} className={classList} style={style} id={id} data-tag={dataTag} {...props}> {content} </button>); }); //# sourceMappingURL=index.jsx.map