feat: finalize button component with tests (#224)
* feat: finalize button component with tests * fix: import * fix: import
This commit is contained in:
parent
999d936f85
commit
ee20c2fde2
133
__tests__/components/Button/Button.test.tsx
Normal file
133
__tests__/components/Button/Button.test.tsx
Normal file
@ -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('<Button />', () => {
|
||||||
|
afterAll(() => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render', () => {
|
||||||
|
const { container } = render(<Button />)
|
||||||
|
expect(container).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render `children` when its passed', () => {
|
||||||
|
const children = <span data-testid='test-id'>Hello World!</span>
|
||||||
|
const { getByTestId } = render(<Button>{children}</Button>)
|
||||||
|
|
||||||
|
expect(getByTestId('test-id')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle `className` prop correctly', () => {
|
||||||
|
const testClass = 'test-class'
|
||||||
|
const { container } = render(<Button className={testClass} />)
|
||||||
|
|
||||||
|
expect(container.querySelector('button')).toHaveClass(testClass)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle `color` prop correctly', () => {
|
||||||
|
const colors = Object.keys(buttonColorClasses) as [keyof typeof buttonColorClasses]
|
||||||
|
|
||||||
|
colors.forEach((color) => {
|
||||||
|
const { container } = render(<Button color={color} />)
|
||||||
|
|
||||||
|
expect(container.querySelector('button')).toHaveClass(buttonColorClasses[color])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle `disabled=true` prop correctly', () => {
|
||||||
|
const testFunction = jest.fn()
|
||||||
|
const { container } = render(<Button disabled={true} onClick={testFunction} />)
|
||||||
|
const button = container.querySelector('button')
|
||||||
|
|
||||||
|
button?.click()
|
||||||
|
|
||||||
|
expect(button).toHaveClass('pointer-events-none')
|
||||||
|
expect(testFunction).not.toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle `disabled=false` prop correctly', () => {
|
||||||
|
const testFunction = jest.fn()
|
||||||
|
const { container } = render(<Button disabled={false} onClick={testFunction} />)
|
||||||
|
const button = container.querySelector('button')
|
||||||
|
|
||||||
|
button?.click()
|
||||||
|
|
||||||
|
expect(button).not.toHaveClass('pointer-events-none')
|
||||||
|
expect(testFunction).toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show progress indicator when `showProgressIndicator=true`', () => {
|
||||||
|
const { getByTestId } = render(<Button showProgressIndicator={true} />)
|
||||||
|
|
||||||
|
expect(getByTestId('circular-progress-component')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle `size` prop correctly', () => {
|
||||||
|
const sizes = Object.keys(buttonSizeClasses) as [keyof typeof buttonSizeClasses]
|
||||||
|
|
||||||
|
sizes.forEach((size) => {
|
||||||
|
const { container } = render(<Button size={size} />)
|
||||||
|
|
||||||
|
expect(container.querySelector('button')).toHaveClass(buttonSizeClasses[size])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show `text` when its passed', () => {
|
||||||
|
const text = 'Hello!'
|
||||||
|
const { getByText } = render(<Button text={text} />)
|
||||||
|
|
||||||
|
expect(getByText(text)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle `variant` prop correctly', () => {
|
||||||
|
const variants = Object.keys(buttonVariantClasses) as [keyof typeof buttonVariantClasses]
|
||||||
|
|
||||||
|
variants.forEach((variant) => {
|
||||||
|
const { container } = render(<Button variant={variant} />)
|
||||||
|
|
||||||
|
expect(container.querySelector('button')).toHaveClass(buttonVariantClasses[variant])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show left icon when `leftIcon` prop is passed', () => {
|
||||||
|
const icon = <span data-testid='left-icon'>this is the left icon</span>
|
||||||
|
const { getByTestId } = render(<Button leftIcon={icon} />)
|
||||||
|
|
||||||
|
expect(getByTestId('left-icon')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show right icon when `rightIcon` prop is passed', () => {
|
||||||
|
const icon = <span data-testid='right-icon'>this is the right icon</span>
|
||||||
|
const { getByTestId } = render(<Button rightIcon={icon} />)
|
||||||
|
|
||||||
|
expect(getByTestId('right-icon')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle `iconClassName` prop correctly', () => {
|
||||||
|
const icon = <span data-testid='icon'>just an icon</span>
|
||||||
|
const { getByTestId } = render(<Button rightIcon={icon} iconClassName='test-icon-class' />)
|
||||||
|
|
||||||
|
expect(getByTestId('icon').parentElement).toHaveClass('test-icon-class')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show submenu indicator when `hasSubmenu=true`', () => {
|
||||||
|
const { getByTestId } = render(<Button hasSubmenu={true} />)
|
||||||
|
|
||||||
|
expect(getByTestId('button-submenu-indicator')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set focus classes when `hasFocus=true`', () => {
|
||||||
|
const { container } = render(<Button hasFocus={true} color='primary' />)
|
||||||
|
const button = container.querySelector('button')
|
||||||
|
|
||||||
|
expect(button).toHaveClass(focusClasses['primary'])
|
||||||
|
})
|
||||||
|
})
|
@ -9,6 +9,7 @@ module.exports = {
|
|||||||
'!<rootDir>/.next/**',
|
'!<rootDir>/.next/**',
|
||||||
'!<rootDir>/*.config.js',
|
'!<rootDir>/*.config.js',
|
||||||
'!<rootDir>/coverage/**',
|
'!<rootDir>/coverage/**',
|
||||||
|
'!<rootDir>/src/types/**',
|
||||||
],
|
],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
// Handle CSS imports (with CSS modules)
|
// Handle CSS imports (with CSS modules)
|
||||||
|
@ -3,7 +3,7 @@ import { useEffect } from 'react'
|
|||||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||||
|
|
||||||
import AccountStats from 'components/Account/AccountStats'
|
import AccountStats from 'components/Account/AccountStats'
|
||||||
import { Button } from 'components/Button'
|
import Button from 'components/Button'
|
||||||
import Card from 'components/Card'
|
import Card from 'components/Card'
|
||||||
import { ArrowCircledTopRight, ArrowDownLine, ArrowUpLine, TrashBin } from 'components/Icons'
|
import { ArrowCircledTopRight, ArrowDownLine, ArrowUpLine, TrashBin } from 'components/Icons'
|
||||||
import Radio from 'components/Radio'
|
import Radio from 'components/Radio'
|
||||||
|
@ -5,7 +5,7 @@ import { useNavigate, useParams } from 'react-router-dom'
|
|||||||
import AccountList from 'components/Account/AccountList'
|
import AccountList from 'components/Account/AccountList'
|
||||||
import CreateAccount from 'components/Account/CreateAccount'
|
import CreateAccount from 'components/Account/CreateAccount'
|
||||||
import FundAccount from 'components/Account/FundAccount'
|
import FundAccount from 'components/Account/FundAccount'
|
||||||
import { Button } from 'components/Button'
|
import Button from 'components/Button'
|
||||||
import { CircularProgress } from 'components/CircularProgress'
|
import { CircularProgress } from 'components/CircularProgress'
|
||||||
import { Account, Plus, PlusCircled } from 'components/Icons'
|
import { Account, Plus, PlusCircled } from 'components/Icons'
|
||||||
import Overlay from 'components/Overlay'
|
import Overlay from 'components/Overlay'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Button } from 'components/Button'
|
import Button from 'components/Button'
|
||||||
import { ArrowRight } from 'components/Icons'
|
import { ArrowRight } from 'components/Icons'
|
||||||
import Text from 'components/Text'
|
import Text from 'components/Text'
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import BigNumber from 'bignumber.js'
|
|||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
|
|
||||||
import { Button } from 'components/Button'
|
import Button from 'components/Button'
|
||||||
import { ArrowRight, Cross } from 'components/Icons'
|
import { ArrowRight, Cross } from 'components/Icons'
|
||||||
import SwitchWithLabel from 'components/SwitchWithLabel'
|
import SwitchWithLabel from 'components/SwitchWithLabel'
|
||||||
import Text from 'components/Text'
|
import Text from 'components/Text'
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Row } from '@tanstack/react-table'
|
import { Row } from '@tanstack/react-table'
|
||||||
|
|
||||||
import { Button } from 'components/Button'
|
import Button from 'components/Button'
|
||||||
import useStore from 'store'
|
import useStore from 'store'
|
||||||
import { getMarketAssets } from 'utils/assets'
|
import { getMarketAssets } from 'utils/assets'
|
||||||
|
|
||||||
|
@ -1,205 +0,0 @@
|
|||||||
import classNames from 'classnames'
|
|
||||||
import React, { LegacyRef, ReactElement, ReactNode } from 'react'
|
|
||||||
|
|
||||||
import { CircularProgress } from 'components/CircularProgress'
|
|
||||||
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<HTMLButtonElement>) => void
|
|
||||||
leftIcon?: ReactElement
|
|
||||||
rightIcon?: ReactElement
|
|
||||||
iconClassName?: string
|
|
||||||
hasSubmenu?: boolean
|
|
||||||
hasFocus?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ctive:text-white',
|
|
||||||
}
|
|
||||||
|
|
||||||
const focusClasses = {
|
|
||||||
primary: 'bg-white/20',
|
|
||||||
secondary: 'bg-white/20',
|
|
||||||
tertiary: 'bg-white/20',
|
|
||||||
quaternary: 'text-white',
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonBorderClasses =
|
|
||||||
'before:content-[" "] before:absolute before:inset-0 before:rounded-sm before:p-[1px] before:border-glas before:-z-1'
|
|
||||||
|
|
||||||
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',
|
|
||||||
]
|
|
||||||
|
|
||||||
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',
|
|
||||||
}
|
|
||||||
|
|
||||||
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',
|
|
||||||
}
|
|
||||||
|
|
||||||
function glowElement(enableAnimations: boolean) {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
className={classNames(
|
|
||||||
enableAnimations && 'group-hover:animate-glow group-focus:animate-glow',
|
|
||||||
'glow-container isolate opacity-0',
|
|
||||||
'pointer-events-none absolute inset-0 h-full w-full',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<rect
|
|
||||||
pathLength='100'
|
|
||||||
strokeLinecap='round'
|
|
||||||
width='100%'
|
|
||||||
height='100%'
|
|
||||||
rx='4'
|
|
||||||
className='absolute glow-line group-hover:glow-hover group-focus:glow-hover'
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export 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 buttonClasses = []
|
|
||||||
const enableAnimations = useStore((s) => s.enableAnimations)
|
|
||||||
const isDisabled = disabled || showProgressIndicator
|
|
||||||
|
|
||||||
switch (variant) {
|
|
||||||
case 'round':
|
|
||||||
buttonClasses.push(
|
|
||||||
buttonSizeClasses[size],
|
|
||||||
buttonRoundSizeClasses[size],
|
|
||||||
buttonPaddingClasses[size],
|
|
||||||
buttonColorClasses[color],
|
|
||||||
)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'transparent':
|
|
||||||
buttonClasses.push(buttonSizeClasses[size], buttonTransparentColorClasses[color])
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'solid':
|
|
||||||
buttonClasses.push(
|
|
||||||
buttonSizeClasses[size],
|
|
||||||
buttonPaddingClasses[size],
|
|
||||||
buttonColorClasses[color],
|
|
||||||
)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={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,
|
|
||||||
)}
|
|
||||||
id={id}
|
|
||||||
ref={ref as LegacyRef<HTMLButtonElement>}
|
|
||||||
onClick={isDisabled ? () => {} : onClick}
|
|
||||||
>
|
|
||||||
{leftIcon && !showProgressIndicator && (
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
'flex items-center justify-center',
|
|
||||||
(text || children) && 'mr-2',
|
|
||||||
iconClassName ?? 'h-4 w-4',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{leftIcon}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{text && !children && !showProgressIndicator && <span>{text}</span>}
|
|
||||||
{children && !showProgressIndicator && children}
|
|
||||||
{rightIcon && !showProgressIndicator && (
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
'flex items-center justify-center',
|
|
||||||
(text || children) && 'ml-2',
|
|
||||||
iconClassName ?? 'h-4 w-4',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{rightIcon}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{hasSubmenu && !showProgressIndicator && (
|
|
||||||
<span className='ml-2 inline-block w-2.5'>
|
|
||||||
<ChevronDown />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{variant === 'solid' && !isDisabled && glowElement(enableAnimations)}
|
|
||||||
{showProgressIndicator && (
|
|
||||||
<CircularProgress size={size === 'small' ? 10 : size === 'medium' ? 12 : 18} />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})
|
|
55
src/components/Button/constants.ts
Normal file
55
src/components/Button/constants.ts
Normal file
@ -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',
|
||||||
|
}
|
127
src/components/Button/index.tsx
Normal file
127
src/components/Button/index.tsx
Normal file
@ -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<HTMLButtonElement>) => 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 (
|
||||||
|
<button
|
||||||
|
className={buttonClassNames}
|
||||||
|
id={id}
|
||||||
|
ref={ref as LegacyRef<HTMLButtonElement>}
|
||||||
|
onClick={isDisabled ? () => {} : onClick}
|
||||||
|
>
|
||||||
|
{showProgressIndicator ? (
|
||||||
|
<CircularProgress size={size === 'small' ? 10 : size === 'medium' ? 12 : 18} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{leftIcon && <span className={leftIconClassNames}>{leftIcon}</span>}
|
||||||
|
{shouldShowText && <span>{text}</span>}
|
||||||
|
{children && children}
|
||||||
|
{rightIcon && <span className={rightIconClassNames}>{rightIcon}</span>}
|
||||||
|
{hasSubmenu && (
|
||||||
|
<span data-testid='button-submenu-indicator' className='ml-2 inline-block w-2.5'>
|
||||||
|
<ChevronDown />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{shouldShowGlowElement && glowElement(enableAnimations)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Button
|
22
src/components/Button/utils.tsx
Normal file
22
src/components/Button/utils.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
export function glowElement(enableAnimations: boolean) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={classNames(
|
||||||
|
enableAnimations && 'group-hover:animate-glow group-focus:animate-glow',
|
||||||
|
'glow-container isolate opacity-0',
|
||||||
|
'pointer-events-none absolute inset-0 h-full w-full',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
pathLength='100'
|
||||||
|
strokeLinecap='round'
|
||||||
|
width='100%'
|
||||||
|
height='100%'
|
||||||
|
rx='4'
|
||||||
|
className='absolute glow-line group-hover:glow-hover group-focus:glow-hover'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
@ -31,7 +31,11 @@ export const CircularProgress = ({ color = '#FFFFFF', size = 20, className }: Pr
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={loaderClasses} style={{ width: `${size}px`, height: `${size}px` }}>
|
<div
|
||||||
|
data-testid='circular-progress-component'
|
||||||
|
className={loaderClasses}
|
||||||
|
style={{ width: `${size}px`, height: `${size}px` }}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={elementClasses}
|
className={elementClasses}
|
||||||
style={{
|
style={{
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Button } from 'components/Button'
|
import Button from 'components/Button'
|
||||||
import VaultLogo from 'components/Earn/vault/VaultLogo'
|
import VaultLogo from 'components/Earn/vault/VaultLogo'
|
||||||
import Text from 'components/Text'
|
import Text from 'components/Text'
|
||||||
import TitleAndSubCell from 'components/TitleAndSubCell'
|
import TitleAndSubCell from 'components/TitleAndSubCell'
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { ReactNode, useEffect, useRef } from 'react'
|
import { ReactNode, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
import { Button } from 'components/Button'
|
import Button from 'components/Button'
|
||||||
import Card from 'components/Card'
|
import Card from 'components/Card'
|
||||||
import { Cross } from 'components/Icons'
|
import { Cross } from 'components/Icons'
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import Image from 'next/image'
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import AccountSummary from 'components/Account/AccountSummary'
|
import AccountSummary from 'components/Account/AccountSummary'
|
||||||
import { Button } from 'components/Button'
|
import Button from 'components/Button'
|
||||||
import Card from 'components/Card'
|
import Card from 'components/Card'
|
||||||
import Divider from 'components/Divider'
|
import Divider from 'components/Divider'
|
||||||
import { ArrowRight } from 'components/Icons'
|
import { ArrowRight } from 'components/Icons'
|
||||||
|
@ -2,7 +2,7 @@ import BigNumber from 'bignumber.js'
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import AccountSummary from 'components/Account/AccountSummary'
|
import AccountSummary from 'components/Account/AccountSummary'
|
||||||
import { Button } from 'components/Button'
|
import Button from 'components/Button'
|
||||||
import Card from 'components/Card'
|
import Card from 'components/Card'
|
||||||
import Divider from 'components/Divider'
|
import Divider from 'components/Divider'
|
||||||
import { ArrowRight } from 'components/Icons'
|
import { ArrowRight } from 'components/Icons'
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import BigNumber from 'bignumber.js'
|
import BigNumber from 'bignumber.js'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { Button } from 'components/Button'
|
import Button from 'components/Button'
|
||||||
import Divider from 'components/Divider'
|
import Divider from 'components/Divider'
|
||||||
import { FormattedNumber } from 'components/FormattedNumber'
|
import { FormattedNumber } from 'components/FormattedNumber'
|
||||||
import { ArrowRight } from 'components/Icons'
|
import { ArrowRight } from 'components/Icons'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Button } from 'components/Button'
|
import Button from 'components/Button'
|
||||||
import { Gear } from 'components/Icons'
|
import { Gear } from 'components/Icons'
|
||||||
import Overlay from 'components/Overlay'
|
import Overlay from 'components/Overlay'
|
||||||
import Switch from 'components/Switch'
|
import Switch from 'components/Switch'
|
||||||
|
@ -3,7 +3,7 @@ import { toast as createToast, Slide, ToastContainer } from 'react-toastify'
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { mutate } from 'swr'
|
import { mutate } from 'swr'
|
||||||
|
|
||||||
import { Button } from 'components/Button'
|
import Button from 'components/Button'
|
||||||
import { CheckCircled, Cross, CrossCircled } from 'components/Icons'
|
import { CheckCircled, Cross, CrossCircled } from 'components/Icons'
|
||||||
import Text from 'components/Text'
|
import Text from 'components/Text'
|
||||||
import useStore from 'store'
|
import useStore from 'store'
|
||||||
|
@ -11,7 +11,7 @@ import { ASSETS } from 'constants/assets'
|
|||||||
import useStore from 'store'
|
import useStore from 'store'
|
||||||
import { BN } from 'utils/helpers'
|
import { BN } from 'utils/helpers'
|
||||||
import { FormattedNumber } from 'components/FormattedNumber'
|
import { FormattedNumber } from 'components/FormattedNumber'
|
||||||
import { Button } from 'components/Button'
|
import Button from 'components/Button'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
amount: BigNumber
|
amount: BigNumber
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useWalletManager, WalletConnectionStatus } from '@marsprotocol/wallet-connector'
|
import { useWalletManager, WalletConnectionStatus } from '@marsprotocol/wallet-connector'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
import { Button } from 'components/Button'
|
import Button from 'components/Button'
|
||||||
import { CircularProgress } from 'components/CircularProgress'
|
import { CircularProgress } from 'components/CircularProgress'
|
||||||
import { Wallet } from 'components/Icons'
|
import { Wallet } from 'components/Icons'
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import classNames from 'classnames'
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import useClipboard from 'react-use-clipboard'
|
import useClipboard from 'react-use-clipboard'
|
||||||
|
|
||||||
import { Button } from 'components/Button'
|
import Button from 'components/Button'
|
||||||
import { CircularProgress } from 'components/CircularProgress'
|
import { CircularProgress } from 'components/CircularProgress'
|
||||||
import { FormattedNumber } from 'components/FormattedNumber'
|
import { FormattedNumber } from 'components/FormattedNumber'
|
||||||
import { Check, Copy, ExternalLink, Osmo } from 'components/Icons'
|
import { Check, Copy, ExternalLink, Osmo } from 'components/Icons'
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ChainInfoID, WalletManagerProvider } from '@marsprotocol/wallet-connector'
|
import { ChainInfoID, WalletManagerProvider } from '@marsprotocol/wallet-connector'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
|
|
||||||
import { Button } from 'components/Button'
|
import Button from 'components/Button'
|
||||||
import { CircularProgress } from 'components/CircularProgress'
|
import { CircularProgress } from 'components/CircularProgress'
|
||||||
import { Cross } from 'components/Icons'
|
import { Cross } from 'components/Icons'
|
||||||
import { ENV } from 'constants/env'
|
import { ENV } from 'constants/env'
|
||||||
|
@ -268,10 +268,10 @@ module.exports = {
|
|||||||
'linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0) 100%), linear-gradient(0deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05))',
|
'linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0) 100%), linear-gradient(0deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05))',
|
||||||
},
|
},
|
||||||
'.gradient-primary-to-secondary': {
|
'.gradient-primary-to-secondary': {
|
||||||
background: 'linear-gradient(90deg, #FF625E 0%, #FB9562 100%)',
|
background: 'linear-gradient(180deg, #7F78E8 0%, #926AC8 100%)',
|
||||||
},
|
},
|
||||||
'.gradient-secondary-to-primary': {
|
'.gradient-secondary-to-primary': {
|
||||||
background: 'linear-gradient(90deg, #FB9562 0%, #FF625E 100%)',
|
background: 'linear-gradient(180deg, #926AC8 100%, #7F78E8 0%)',
|
||||||
},
|
},
|
||||||
'.gradient-tooltip': {
|
'.gradient-tooltip': {
|
||||||
background:
|
background:
|
||||||
|
Loading…
Reference in New Issue
Block a user