Drawer

Example

Steps

Prerequisite

Install Package

npm install classnames

Define Types

types/index.d.ts
interface ReactProps {
  children?: ReactNode
}
 
interface DrawerProps {
  isOpen: boolean
  onClose: () => void
  position?: 'left' | 'top' | 'right' | 'bottom'
  className?: Argument
}

Add Animation in tailwind.config.js

tailwind.config.js
module.exports = {
  theme: {
    extend: {
      keyframes: {
        'fade-in-top': {
          from: {
            opacity: 0,
            transform: 'translateY(100%)'
          },
          '60%': { opacity: 1 },
          to: { transform: 'none' }
        },
        'fade-in-right': {
          from: {
            opacity: 0,
            transform: 'translateX(-100%)'
          },
          '60%': { opacity: 1 },
          to: { transform: 'none' }
        },
        'fade-in-bottom': {
          from: {
            opacity: 0,
            transform: 'translateY(-100%)'
          },
          '60%': { opacity: 1 },
          to: { transform: 'none' }
        },
        'fade-in-left': {
          from: {
            opacity: 0,
            transform: 'translateX(100%)'
          },
          '60%': { opacity: 1 },
          to: { transform: 'none' }
        },
        'fade-out-top': {
          from: { opacity: 1 },
          '60%': { opacity: 0 },
          to: { transform: '-translateY(-150%)' }
        },
        'fade-out-right': {
          from: { opacity: 1 },
          '60%': { opacity: 0 },
          to: { transform: 'translateX(150%)' }
        },
        'fade-out-bottom': {
          from: { opacity: 1 },
          '60%': { opacity: 0 },
          to: { transform: 'translateY(150%)' }
        },
        'fade-out-left': {
          from: { opacity: 1 },
          '60%': { opacity: 0 },
          to: { transform: '-translateX(150%)' }
        }
      },
      animation: {
        'fade-in-right': 'fade-in-right 0.2s linear',
        'fade-in-top': 'fade-in-top 0.2s linear',
        'fade-in-bottom': 'fade-in-bottom 0.2s linear',
        'fade-in-left': 'fade-in-left 0.2s linear',
        'fade-out-top': 'fade-out-top 0.2s linear',
        'fade-out-right': 'fade-out-right 0.2s linear',
        'fade-out-bottom': 'fade-out-bottom 0.2s linear',
        'fade-out-left': 'fade-out-left 0.2s linear'
      }
    }
  }
}

Copy Code

containers/Drawer/index.tsx
import { memo, useCallback, useEffect } from 'react'
import type { FC } from 'react'
import classnames from 'classnames'
import { useObjectState } from 'services'
import { createPortal } from 'react-dom'
 
export interface Props extends DrawerProps, ReactProps {}
interface State {
  isClosed: boolean
}
 
const Drawer: FC<Props> = ({
  isOpen,
  onClose,
  position = 'right',
  children,
  className
}) => {
  const [{ isClosed }, setState] = useObjectState<State>({ isClosed: false })
 
  const onEscape = useCallback((e: KeyboardEvent) => {
    if (e.code === 'Escape') {
      setState({ isClosed: true })
      window.removeEventListener('keydown', onEscape)
    }
  }, [])
 
  useEffect(() => {
    if (!isOpen) return
    window.addEventListener('keydown', onEscape)
    return () => {
      window.removeEventListener('keydown', onEscape)
    }
  }, [isOpen])
 
  useEffect(() => {
    if (isClosed) setTimeout(() => onClose(), 150)
  }, [isClosed])
  return createPortal(
    <div
      id="drawer"
      role="dialog"
      tabIndex={-1}
      aria-labelledby="drawer-title"
      aria-modal="true"
    >
      <div
        className="fixed inset-0 z-20 bg-black/50 duration-200"
        onClick={() => setState({ isClosed: true })}
      />
      <div
        className={classnames(
          'fixed z-30 w-5/6 overflow-auto overscroll-contain bg-white duration-200 dark:bg-neutral-800',
          {
            'left-0': position === 'left',
            'right-0': position === 'right',
            'top-0': position === 'top',
            'bottom-0': position === 'bottom',
            'animate-fade-in-left': position === 'right' && isOpen,
            'animate-fade-in-right': position === 'left' && isOpen,
            'animate-fade-in-bottom': position === 'top' && isOpen,
            'animate-fade-in-top': position === 'bottom' && isOpen,
            'animate-fade-out-top': position === 'top' && isClosed,
            'animate-fade-out-right': position === 'right' && isClosed,
            'animate-fade-out-bottom': position === 'bottom' && isClosed,
            'animate-fade-out-left': position === 'left' && isClosed,
            'md:w-[432px]':
              !className && (position === 'left' || position === 'right'),
            'md:h-96':
              !className && (position === 'top' || position === 'bottom'),
            'top-0 h-screen': position === 'left' || position === 'right',
            'left-0 w-screen': position === 'top' || position === 'bottom'
          },
          className
        )}
      >
        {children}
      </div>
    </div>,
    document.body
  )
}
 
export default memo(Drawer)

Usage

<button
  onClick={() => setState({ isOpen: true })}
>
  Open
</button>
 
<Drawer
  isOpen={isOpen}
  onClose={() => setState({ isOpen: fasle })}
  position='right'
>
  children
</Drawer>

Props

NameTypeDefault
isOpenboolean
onClose() => void
posiitontop right bottom leftright
classNamestring
childrenReactNode

© 2023 kidow. All rights reserved.
안녕하세요?