diff --git a/__tests__/components/Button/Button.test.tsx b/__tests__/components/Button/Button.test.tsx new file mode 100644 index 00000000..4ef4e298 --- /dev/null +++ b/__tests__/components/Button/Button.test.tsx @@ -0,0 +1,133 @@ +import { render } from '@testing-library/react' + +import Button from 'components/Button' +import { + buttonColorClasses, + buttonSizeClasses, + buttonVariantClasses, + focusClasses, +} from 'components/Button/constants' + +describe(') + + expect(getByTestId('test-id')).toBeInTheDocument() + }) + + it('should handle `className` prop correctly', () => { + const testClass = 'test-class' + const { container } = render( - ) -}) diff --git a/src/components/Button/constants.ts b/src/components/Button/constants.ts new file mode 100644 index 00000000..06d738c7 --- /dev/null +++ b/src/components/Button/constants.ts @@ -0,0 +1,55 @@ +export const buttonColorClasses = { + primary: + 'font-bold gradient-primary-to-secondary hover:bg-white/20 active:bg-white/40 focus:bg-white/20', + secondary: + 'border border-white/30 bg-transparent hover:bg-white/20 active:bg-white/40 focus:bg-white/20', + tertiary: 'bg-white/10 hover:bg-white/20 active:bg-white/40 focus:bg-white/20', + quaternary: 'bg-transparent text-white/60 hover:text-white active:text-white', +} + +export const focusClasses = { + primary: 'bg-white/20', + secondary: 'bg-white/20', + tertiary: 'bg-white/20', + quaternary: 'text-white', +} + +export const buttonBorderClasses = + 'before:content-[" "] before:absolute before:inset-0 before:rounded-sm before:p-[1px] before:border-glas before:-z-1' + +export const buttonGradientClasses = [ + 'before:content-[" "] before:absolute before:inset-0 before:rounded-sm before:-z-1 before:opacity-0', + 'before:gradient-secondary-to-primary before:transition-opacity before:ease-in', + 'hover:before:opacity-100', +] + +export const buttonTransparentColorClasses = { + primary: 'hover:text-primary active:text-primary focus:text-primary', + secondary: 'hover:text-secondary active:text-secondary focus:text-secondary', + tertiary: 'hover:text-white/80 active:text-white/80 focus:text-white/80', + quaternary: 'text-white/60 hover:text-white active:text-white', +} + +export const buttonRoundSizeClasses = { + small: 'h-[32px] w-[32px]', + medium: 'h-[40px] w-[40px]', + large: 'h-[56px] w-[56px]', +} + +export const buttonSizeClasses = { + small: 'text-sm', + medium: 'text-base', + large: 'text-lg', +} + +export const buttonPaddingClasses = { + small: 'px-2.5 py-1.5 min-h-[32px]', + medium: 'px-3 py-2 min-h-[40px]', + large: 'px-3.5 py-2.5 min-h-[56px]', +} + +export const buttonVariantClasses = { + solid: 'rounded-sm text-white shadow-button justify-center group', + transparent: 'rounded-sm bg-transparent p-0 transition duration-200 ease-in', + round: 'rounded-full p-0', +} diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx new file mode 100644 index 00000000..59e18845 --- /dev/null +++ b/src/components/Button/index.tsx @@ -0,0 +1,127 @@ +import classNames from 'classnames' +import React, { LegacyRef, ReactElement, ReactNode, useMemo } from 'react' + +import { CircularProgress } from 'components/CircularProgress' +import { + buttonBorderClasses, + buttonColorClasses, + buttonGradientClasses, + buttonPaddingClasses, + buttonRoundSizeClasses, + buttonSizeClasses, + buttonTransparentColorClasses, + buttonVariantClasses, + focusClasses, +} from 'components/Button/constants' +import { glowElement } from 'components/Button/utils' +import { ChevronDown } from 'components/Icons' +import useStore from 'store' + +interface Props { + children?: string | ReactNode + className?: string + color?: 'primary' | 'secondary' | 'tertiary' | 'quaternary' + disabled?: boolean + id?: string + showProgressIndicator?: boolean + size?: 'small' | 'medium' | 'large' + text?: string | ReactNode + variant?: 'solid' | 'transparent' | 'round' + onClick?: (e: React.MouseEvent) => void + leftIcon?: ReactElement + rightIcon?: ReactElement + iconClassName?: string + hasSubmenu?: boolean + hasFocus?: boolean +} + +const Button = React.forwardRef(function Button( + { + children, + className = '', + color = 'primary', + disabled, + id = '', + showProgressIndicator, + size = 'small', + text, + variant = 'solid', + onClick, + leftIcon, + rightIcon, + iconClassName, + hasSubmenu, + hasFocus, + }: Props, + ref, +) { + const enableAnimations = useStore((s) => s.enableAnimations) + const isDisabled = disabled || showProgressIndicator + const shouldShowText = text && !children + const shouldShowGlowElement = variant === 'solid' && !isDisabled + + const buttonClassNames = useMemo(() => { + const buttonClasses = [ + buttonSizeClasses[size], + buttonPaddingClasses[size], + buttonColorClasses[color], + ] + + if (variant === 'round') { + buttonClasses.push(buttonColorClasses[color], buttonRoundSizeClasses[size]) + } else if (variant === 'transparent') { + buttonClasses.push(buttonTransparentColorClasses[color]) + } + + return classNames( + 'relative z-1 flex items-center', + 'cursor-pointer appearance-none break-normal outline-none', + 'text-white transition-all', + enableAnimations && 'transition-color', + buttonClasses, + buttonVariantClasses[variant], + variant === 'solid' && color === 'tertiary' && buttonBorderClasses, + variant === 'solid' && color === 'primary' && buttonGradientClasses, + isDisabled && 'pointer-events-none opacity-50', + hasFocus && focusClasses[color], + className, + ) + }, [className, color, enableAnimations, hasFocus, isDisabled, size, variant]) + + const [leftIconClassNames, rightIconClassNames] = useMemo(() => { + const hasContent = !!(text || children) + const iconClasses = ['flex items-center justify-center', iconClassName ?? 'h-4 w-4'] + const leftIconClasses = [iconClasses, hasContent && 'mr-2'] + const rightIconClasses = [iconClasses, hasContent && 'ml-2'] + + return [leftIconClasses, rightIconClasses].map(classNames) + }, [children, iconClassName, text]) + + return ( + + ) +}) + +export default Button diff --git a/src/components/Button/utils.tsx b/src/components/Button/utils.tsx new file mode 100644 index 00000000..dc3e42d3 --- /dev/null +++ b/src/components/Button/utils.tsx @@ -0,0 +1,22 @@ +import classNames from 'classnames' + +export function glowElement(enableAnimations: boolean) { + return ( + + + + ) +} diff --git a/src/components/CircularProgress.tsx b/src/components/CircularProgress.tsx index 15fd9844..946a5105 100644 --- a/src/components/CircularProgress.tsx +++ b/src/components/CircularProgress.tsx @@ -31,7 +31,11 @@ export const CircularProgress = ({ color = '#FFFFFF', size = 20, className }: Pr ) return ( -
+