[React + Typescript + emotion] Dropdown 컴포넌트 제작

2024. 8. 13. 17:10

기본적으로 dropdown컴포넌트에 필요한 props로는 options와 선택한 키값(selectedKey)과 변경되는 handleChange 함수가 필요했다.

 

그리고 완성작엔 드롭다운 하나만 보여주지만, 실제론 여러 개를 사용 중이라

다른 드롭다운을 클릭하면 기존껀 꺼져야 하고, 뒷 배경을 클릭해도 꺼지게 만들고 싶었다.


// Dropdown.tsx
import { useEffect, useRef, useState } from "react";
import * as S from "./Dropdown.styled";
import { CheckIcon, DropdownArrowIcon } from "assets";

interface DropdownProps {
  options: readonly { label: string; key: string }[];
  handleChange: (key: string) => void;
  selectedKey?: string;
}
const Dropdown = ({ options, handleChange, selectedKey }: DropdownProps) => {
  const [isOpen, setIsOpen] = useState(false);
  const [selectedOption, setSelectedOption] = useState<string | null>(null);
  const dropMenuRef = useRef<HTMLDivElement | null>(null);

  // 다른 구역 선택 시, 기존 dropdown close
  useEffect(() => {
    const handleClickOutside = (e: Event) => {
      const target = e.target as HTMLElement;
      if (!dropMenuRef?.current) return;
      if (isOpen && !dropMenuRef?.current.contains(target)) {
        setIsOpen(false);
      }
    };
    document.addEventListener("click", handleClickOutside);
    return () => document.removeEventListener("click", handleClickOutside);
  }, [isOpen]);

  useEffect(() => {
    // 받아온 데이터 선택값
    const getOption = options.find((option) => option.key === selectedKey);
    if (getOption) {
      setSelectedOption(getOption.label);
    }
  }, [selectedKey, options]);

  const handleToggle = () => {
    setIsOpen(!isOpen);
  };

  const handleSelect = (label: string, key: string) => {
    setSelectedOption(label);
    handleChange && handleChange(key);
    setIsOpen(false);
  };

  return (
    <S.DropdownContainer ref={dropMenuRef}>
      <S.SelectContainer onClick={handleToggle}>
        <S.SelectedValue> {selectedOption || "선택하기"}</S.SelectedValue>
        <DropdownArrowIcon css={S.arrowIcon} />
        {isOpen && (
          <S.DropdownList>
            {options.map((option, index) => (
              <S.DropdownItem
                key={index}
                onClick={() => handleSelect(option.label, option.key)}
              >
                {option.label}
                <CheckIcon css={S.checkIcon} />
              </S.DropdownItem>
            ))}
          </S.DropdownList>
        )}
      </S.SelectContainer>
    </S.DropdownContainer>
  );
};

export default Dropdown;

 

css에서는 hover 될 때, 체크표시가 같이 되어야 하는데, hover가 아닐 땐 체크표시를 white로 해서, 안 보이는 척(ㅎㅎ) 만들었다.

배경색이 있다면 배경색과 동일하게 변경하면 될 것 같다.

// Dropdown.styled.ts
import { css } from "@emotion/react";
import styled from "@emotion/styled";

export const DropdownContainer = styled.div`
  ${({ theme }) => css`
    width: 320px;
    height: 48px;
    border: 1px solid ${theme.colors.gray600};
    border-radius: 6px;
    display: flex;
    position: relative;
    flex-direction: column;
  `}
`;

export const SelectContainer = styled.div`
  display: flex;
  width: 100%;
  height: 100%;
  justify-content: center;
  align-items: center;
  cursor: pointer;
`;

export const SelectedValue = styled.div`
  ${({ theme }) => css`
    ${theme.fonts.subTitle_regular_16};
    padding-left: 12px;
    flex: 1;
  `}
`;

export const arrowIcon = css`
  margin-right: 8px;
  cursor: pointer;
`;

export const DropdownList = styled.ul`
  ${({ theme }) => css`
    position: absolute;
    background-color: ${theme.colors.white};
    box-shadow: 0 0 30px ${theme.colors.gray400};
    width: 320px;
    top: 102%;
    left: 0;
    margin: 0;
    padding: 0;
    list-style: none;
    z-index: 1;
  `}
`;

export const DropdownItem = styled.li`
  ${({ theme }) => css`
    ${theme.fonts.subTitle_bold_16};
    height: 54.01px;
    padding-left: 20px;
    cursor: pointer;
    display: flex;
    justify-content: space-between;
    align-items: center;

    &:hover {
      background-color: ${theme.colors.pink100};
      color: ${theme.colors.pink900};

      svg {
        & path {
          fill: ${theme.colors.pink900};
        }
      }
    }

    svg {
      & path {
        fill: ${theme.colors.white};
      }
    }

    &:last-child {
      border-bottom: none;
    }
  `}
`;

export const checkIcon = css`
  margin-right: 20px;
`;

BELATED ARTICLES

more