import { transitions } from 'polished'; import { forwardRefWithAs, PropsWithAs } from '@reach/utils'; import { variant as styledSystemVariant } from 'styled-system'; import { useReducedMotion } from 'framer-motion'; import VisuallyHidden from '@reach/visually-hidden'; import classNames from 'classnames'; import { Ref } from 'react'; import styled, { keyframes, useTheme } from 'styled-components'; import { Box, BoxStylingProps } from './Box'; import { ButtonVariantName } from './theme/variants/button'; import { LeftRight, SVGComponent } from './shared'; export type ButtonSize = 'extra-small' | 'small' | 'medium' | 'large'; /** * These represent props that are not already available via React.HTMLAttributes */ export type NonSemanticButtonProps = Pick & { size?: ButtonSize | ButtonSize[]; variant?: ButtonVariantName | ButtonVariantName[]; isLoading?: boolean; adornmentLeft?: SVGComponent; adornmentRight?: SVGComponent; }; export interface ButtonProps extends PropsWithAs<'button', NonSemanticButtonProps> {} const Dot = () => ( ); const wave = keyframes` 0% { transform: translate(0, -1.5px); } 50% { transform: translate(0, 1.5px); } 100% { transform: translate(0, -1.5px); } `; const Loader = styled(Box)<{ shouldReduceMotion: boolean }>` position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); display: flex; div { animation-name: ${({ shouldReduceMotion }) => (shouldReduceMotion ? 'none' : wave)}; animation-duration: 0.98s; animation-timing-function: ease-in-out; animation-delay: 0s; animation-fill-mode: both; animation-iteration-count: infinite; } div:nth-child(1) { animation-delay: 0s; } div:nth-child(2) { animation-delay: 0.14s; margin: 0 4px; } div:nth-child(3) { animation-delay: 0.28s; } `; export type AdornmentVariation = 'leftOnly' | 'rightOnly' | 'both' | 'none'; const getAdornmentVariant = (hasLeft: boolean, hasRight: boolean): AdornmentVariation => { if (hasLeft && hasRight) return 'both'; if (hasLeft && !hasRight) return 'leftOnly'; if (!hasLeft && hasRight) return 'rightOnly'; return 'none'; }; export type ButtonCompoundVariant = `${ButtonSize}_adorned-${AdornmentVariation}`; const getCompoundVariant = ( size: ButtonSize | ButtonSize[], adornmentVariant: AdornmentVariation, ) => { /* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ return Array.isArray(size) ? size.map((s) => `${s}_adorned-${adornmentVariant}` as ButtonCompoundVariant) : (`${size}_adorned-${adornmentVariant}` as ButtonCompoundVariant); /* eslint-enable @typescript-eslint/no-unnecessary-type-assertion */ }; interface ButtonAdornmentProps extends Required> { component: SVGComponent; side: LeftRight; } const ButtonAdornment = ({ component, side, size }: ButtonAdornmentProps) => { const theme = useTheme(); return ( ); }; const compoundVariantMap: { [key in ButtonCompoundVariant]: NonNullable } = { 'extra-small_adorned-none': { height: 24, px: 12, fontSize: 12, }, 'extra-small_adorned-leftOnly': { height: 24, pl: 4, pr: 8, fontSize: 12, }, 'extra-small_adorned-rightOnly': { height: 24, pl: 8, pr: 4, fontSize: 12, }, 'extra-small_adorned-both': { height: 24, px: 6, fontSize: 12, }, 'small_adorned-none': { height: 32, px: 16, fontSize: 12, }, 'small_adorned-leftOnly': { height: 32, pl: 6, pr: 10, fontSize: 12, }, 'small_adorned-rightOnly': { height: 32, pl: 10, pr: 6, fontSize: 12, }, 'small_adorned-both': { height: 32, px: 8, fontSize: 12, }, 'medium_adorned-none': { height: 40, px: 16, fontSize: 14, }, 'medium_adorned-leftOnly': { height: 40, pl: 12, pr: 16, fontSize: 14, }, 'medium_adorned-rightOnly': { height: 40, pl: 16, pr: 12, fontSize: 14, }, 'medium_adorned-both': { height: 40, px: 12, fontSize: 14, }, 'large_adorned-none': { height: 48, px: 24, fontSize: 16, }, 'large_adorned-leftOnly': { height: 48, pl: 12, pr: 20, fontSize: 16, }, 'large_adorned-rightOnly': { height: 48, pl: 20, pr: 12, fontSize: 16, }, 'large_adorned-both': { height: 48, px: 16, fontSize: 16, }, }; export const Button = forwardRefWithAs( ( { adornmentLeft, adornmentRight, as = 'button', disabled = false, size = 'medium', type = 'button', variant = 'button-filled-blue', isLoading = false, ref: _ref, // eslint-disable-line @typescript-eslint/no-unused-vars children, className, ...restOfProps }: ButtonProps, ref: Ref, ) => { const theme = useTheme(); const shouldReduceMotion = useReducedMotion(); const adornmentVariant = getAdornmentVariant(!!adornmentLeft, !!adornmentRight); const compoundVariant = getCompoundVariant(size, adornmentVariant); const content = adornmentVariant === 'none' ? ( <>{children} ) : ( {adornmentLeft && } {children} {adornmentRight && ( )} ); return ( {isLoading ? ( Loading... ) : ( content )} ); }, );