diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-03-31 14:25:12 +0200 | 
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-03-31 15:25:41 +0200 | 
| commit | d7d453f7333def28007b94b9c9d872f89224fc91 (patch) | |
| tree | 13d5f88b1d39d3eb46b7ba1c7003d11f827255e6 /src/components | |
| parent | 6640cdd35cab960237b3011d7badc5b9b2eaa5bd (diff) | |
chore: add a button component
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/atoms/buttons/button.stories.tsx | 120 | ||||
| -rw-r--r-- | src/components/atoms/buttons/button.test.tsx | 18 | ||||
| -rw-r--r-- | src/components/atoms/buttons/button.tsx | 53 | ||||
| -rw-r--r-- | src/components/atoms/buttons/buttons.module.scss | 154 | 
4 files changed, 345 insertions, 0 deletions
| diff --git a/src/components/atoms/buttons/button.stories.tsx b/src/components/atoms/buttons/button.stories.tsx new file mode 100644 index 0000000..5af61bd --- /dev/null +++ b/src/components/atoms/buttons/button.stories.tsx @@ -0,0 +1,120 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import ButtonComponent from './button'; + +export default { +  title: 'Atoms/Buttons', +  component: ButtonComponent, +  args: { +    disabled: false, +    kind: 'secondary', +    type: 'button', +  }, +  argTypes: { +    'aria-label': { +      control: { +        type: 'text', +      }, +      description: 'An accessible label.', +      table: { +        category: 'Accessibility', +      }, +      type: { +        name: 'string', +        required: false, +      }, +    }, +    children: { +      control: { +        type: 'text', +      }, +      description: 'The button body.', +      type: { +        name: 'string', +        required: true, +      }, +    }, +    disabled: { +      control: { +        type: 'boolean', +      }, +      description: 'Render button as disabled.', +      table: { +        category: 'Options', +        defaultValue: { summary: false }, +      }, +      type: { +        name: 'boolean', +        required: false, +      }, +    }, +    kind: { +      control: { +        type: 'select', +      }, +      description: 'Button kind.', +      options: ['primary', 'secondary', 'tertiary'], +      table: { +        category: 'Options', +        defaultValue: { summary: 'secondary' }, +      }, +      type: { +        name: 'string', +        required: false, +      }, +    }, +    onClick: { +      control: { +        type: null, +      }, +      description: 'A callback function to handle click.', +      table: { +        category: 'Events', +      }, +      type: { +        name: 'function', +        required: false, +      }, +    }, +    type: { +      control: { +        type: 'select', +      }, +      description: 'Button type attribute.', +      options: ['button', 'reset', 'submit'], +      table: { +        category: 'Options', +        defaultValue: { summary: 'button' }, +      }, +      type: { +        name: 'string', +        required: false, +      }, +    }, +  }, +} as ComponentMeta<typeof ButtonComponent>; + +const Template: ComponentStory<typeof ButtonComponent> = (args) => { +  const { children, type, ...props } = args; + +  const getBody = () => { +    if (children) return children; + +    switch (type) { +      case 'reset': +        return 'Reset'; +      case 'submit': +        return 'Submit'; +      case 'button': +      default: +        return 'Button'; +    } +  }; + +  return ( +    <ButtonComponent type={type} {...props}> +      {getBody()} +    </ButtonComponent> +  ); +}; + +export const Button = Template.bind({}); diff --git a/src/components/atoms/buttons/button.test.tsx b/src/components/atoms/buttons/button.test.tsx new file mode 100644 index 0000000..57c79c6 --- /dev/null +++ b/src/components/atoms/buttons/button.test.tsx @@ -0,0 +1,18 @@ +import { render, screen } from '@test-utils'; +import Button from './button'; + +describe('Button', () => { +  it('renders the Button component', () => { +    render(<Button onClick={() => null}>Button</Button>); +    expect(screen.getByRole('button')).toBeInTheDocument(); +  }); + +  it('renders the Button component with disabled state', () => { +    render( +      <Button onClick={() => null} disabled={true}> +        Disabled Button +      </Button> +    ); +    expect(screen.getByRole('button')).toBeDisabled(); +  }); +}); diff --git a/src/components/atoms/buttons/button.tsx b/src/components/atoms/buttons/button.tsx new file mode 100644 index 0000000..420ee74 --- /dev/null +++ b/src/components/atoms/buttons/button.tsx @@ -0,0 +1,53 @@ +import { FC, MouseEventHandler } from 'react'; +import styles from './buttons.module.scss'; + +export type ButtonProps = { +  /** +   * Button accessible label. +   */ +  'aria-label'?: string; +  /** +   * Button state. Default: false. +   */ +  disabled?: boolean; +  /** +   * Button kind. Default: secondary. +   */ +  kind?: 'primary' | 'secondary' | 'tertiary'; +  /** +   * A callback function to handle click. +   */ +  onClick?: MouseEventHandler<HTMLButtonElement>; +  /** +   * Button type attribute. Default: button. +   */ +  type?: 'button' | 'reset' | 'submit'; +}; + +/** + * Button component + * + * Use a button as call to action. + */ +const Button: FC<ButtonProps> = ({ +  children, +  disabled = false, +  kind = 'secondary', +  type = 'button', +  ...props +}) => { +  const kindClass = styles[`btn--${kind}`]; + +  return ( +    <button +      type={type} +      disabled={disabled} +      className={`${styles.btn} ${kindClass}`} +      {...props} +    > +      {children} +    </button> +  ); +}; + +export default Button; diff --git a/src/components/atoms/buttons/buttons.module.scss b/src/components/atoms/buttons/buttons.module.scss new file mode 100644 index 0000000..d6a488d --- /dev/null +++ b/src/components/atoms/buttons/buttons.module.scss @@ -0,0 +1,154 @@ +@use "@styles/abstracts/functions" as fun; + +.btn { +  display: block; +  max-width: max-content; +  padding: var(--spacing-2xs) var(--spacing-md); +  border: none; +  border-radius: fun.convert-px(5); +  font-size: var(--font-size-md); +  font-weight: 600; +  transition: all 0.3s ease-in-out 0s; + +  &:disabled { +    cursor: wait; +  } + +  &--primary { +    background: var(--color-primary); +    border: fun.convert-px(2) solid var(--color-bg); +    box-shadow: 0 0 0 fun.convert-px(2) var(--color-primary), +      0 0 0 fun.convert-px(3) var(--color-primary-darker), +      fun.convert-px(2) fun.convert-px(2) 0 fun.convert-px(3) +        var(--color-primary-dark); +    color: var(--color-fg-inverted); +    text-shadow: fun.convert-px(2) fun.convert-px(2) 0 var(--color-shadow); + +    &:disabled { +      background: var(--color-primary-darker); +    } + +    &:not(:disabled) { +      &:hover, +      &:focus { +        background: var(--color-primary-light); +        box-shadow: 0 0 0 fun.convert-px(2) var(--color-primary-light), +          0 0 0 fun.convert-px(3) var(--color-primary-darker), +          fun.convert-px(7) fun.convert-px(7) 0 fun.convert-px(2) +            var(--color-primary-dark); +        transform: translateX(#{fun.convert-px(-4)}) +          translateY(#{fun.convert-px(-4)}); +      } + +      &:focus { +        text-decoration: underline solid var(--color-fg-inverted) +          fun.convert-px(2); +      } + +      &:active { +        background: var(--color-primary-dark); +        box-shadow: 0 0 0 fun.convert-px(2) var(--color-primary), +          0 0 0 fun.convert-px(3) var(--color-primary-darker), +          0 0 0 0 var(--color-primary-dark); +        text-decoration: none; +        transform: translateX(#{fun.convert-px(4)}) +          translateY(#{fun.convert-px(4)}); +      } +    } +  } + +  &--secondary { +    background: var(--color-bg); +    border: fun.convert-px(3) solid var(--color-primary); +    box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) +        var(--color-shadow), +      fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2) +        var(--color-shadow), +      fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4) +        var(--color-shadow); +    color: var(--color-primary); +    text-decoration: underline transparent 0; + +    &:disabled { +      border-color: var(--color-border-dark); +      color: var(--color-fg-light); +    } + +    &:not(:disabled) { +      &:hover, +      &:focus { +        border-color: var(--color-primary-light); +        color: var(--color-primary-light); +        box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) +            var(--color-shadow-light), +          fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) +            fun.convert-px(-2) var(--color-shadow-light), +          fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) +            fun.convert-px(-4) var(--color-shadow-light), +          fun.convert-px(7) fun.convert-px(10) fun.convert-px(12) +            fun.convert-px(-3) var(--color-shadow-light); +        transform: scale(1.1); +      } + +      &:focus { +        text-decoration: underline var(--color-primary-light) fun.convert-px(3); +      } + +      &:active { +        border-color: var(--color-primary-dark); +        box-shadow: 0 0 0 0 var(--color-shadow); +        color: var(--color-primary-dark); +        text-decoration: underline transparent 0; +        transform: scale(0.94); +      } +    } +  } + +  &--tertiary { +    background: var(--color-bg); +    border: fun.convert-px(3) solid var(--color-primary); +    box-shadow: fun.convert-px(2) fun.convert-px(2) 0 0 var(--color-bg), +      fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-primary-dark), +      fun.convert-px(5) fun.convert-px(5) 0 0 var(--color-bg), +      fun.convert-px(6) fun.convert-px(6) 0 0 var(--color-primary-dark); +    color: var(--color-primary); + +    &:disabled { +      color: var(--color-fg-light); +      border-color: var(--color-border-dark); +      box-shadow: fun.convert-px(2) fun.convert-px(2) 0 0 var(--color-bg), +        fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-primary-darker), +        fun.convert-px(5) fun.convert-px(5) 0 0 var(--color-bg), +        fun.convert-px(6) fun.convert-px(6) 0 0 var(--color-primary-darker); +    } + +    &:not(:disabled) { +      &:hover, +      &:focus { +        border-color: var(--color-primary-light); +        box-shadow: fun.convert-px(2) fun.convert-px(3) 0 0 var(--color-bg), +          fun.convert-px(4) fun.convert-px(5) 0 0 var(--color-primary), +          fun.convert-px(6) fun.convert-px(8) 0 0 var(--color-bg), +          fun.convert-px(8) fun.convert-px(10) 0 0 var(--color-primary), +          fun.convert-px(10) fun.convert-px(12) fun.convert-px(1) 0 +            var(--color-shadow-light), +          fun.convert-px(10) fun.convert-px(12) fun.convert-px(5) +            fun.convert-px(1) var(--color-shadow-light); +        color: var(--color-primary-light); +        transform: translateX(#{fun.convert-px(-3)}) +          translateY(#{fun.convert-px(-5)}); +      } + +      &:focus { +        text-decoration: underline var(--color-primary) fun.convert-px(2); +      } + +      &:active { +        box-shadow: 0 0 0 0 var(--color-shadow); +        text-decoration: none; +        transform: translateX(#{fun.convert-px(5)}) +          translateY(#{fun.convert-px(6)}); +      } +    } +  } +} | 
