더 알아보기/기능

[Next.js] 드롭다운 영역 외 클릭 시 메뉴 닫기

은돌1113 2023. 11. 27. 10:29

드롭다운은 프로젝트에서 자주 구현하게 되는 기능인만큼 만들다 보면 이것저것 기능이나 디자인을 넣고 싶어지는 데

이번에는 드롭다운 영역 외 클릭 시 드롭다운 메뉴가 닫히는 기능을 만들어 보았다

 

기능 구현 과정에서 useRef를 사용하였고 이 과정에서 ref.current.contains(e.target)이라는 개념도 새롭게 알게 되었다.

 

🤔 ref.current.contains(e.target)란?

  • ref : React의 'useRef' 훅을 통해 생성된 ref 객체 (ex. const ref = useRef(null)
  • ref.current : Ref 객체의 'current' 속성을 나타내며, 해당 Ref가 현재 참조하는 DOM 요소
  • ref.current.contains(e.target) : 현재 Ref가 참조하는 DOM 요소 안에서 클릭된 요소가 포함되어 있는지 여부를 확인합니다, 이 조건은 주로 클릭된 요소가 특정 DOM 요소 내에 있는 지를 검사하여 특정 동작을 수행하거나 무시하는 데 사용합니다.

 

🖥️ 실행 화면

 

🧑‍💻 코드 설명

(전체 코드는 접은 글에서 확인 가능합니다.)

 

  • useEffect()
    • handleOutside() : selectBoxRef에 참조 중인 DOM이 있으면서, 해당 DOM 내에 클릭된 요소가 없다면 setIsOpen(false)를 실행합니다.
    • document.addEventListener("mousedown", handleOutside) : document에서 mousedown 이벤트가 발생할 때마다 handleOutside()이 실행되면서 드롭다운 영역 외 부분이 클릭될 경우 메뉴를 닫아주도록 구현했습니다.
  • input
    • onClick={() => {setIsOpen((prev) =>! prev}} : input을 click 하면 드롭다운 메뉴가 열렸다, 닫혀야 하기 때문에 !prev를 사용하여 true인 경우 false로, false인 경우 true가 되도록 구현하였습니다.

 

 

 

(전체 코드)

더보기
import { ExpandMoreIcon } from "@/elements/common/Icon"
import { useEffect, useRef, useState } from "react"
import styled from "styled-components"

const Test = () => {
  const dropdownList = [
    { idx: 1, name: "A" },
    { idx: 2, name: "B" },
    { idx: 3, name: "C" },
    { idx: 4, name: "D" },
    { idx: 5, name: "E" },
  ]

  const [isOpen, setIsOpen] = useState(false)
  const [select, setSelect] = useState("등급 선택")

  const selectBoxRef = useRef(null)

  useEffect(() => {
    const handleOutside = (e) => {
      // current.contains(e.target) : 컴포넌트 특정 영역 외 클릭 감지를 위해 사용
      if (selectBoxRef.current && !selectBoxRef.current.contains(e.target)) {
        setIsOpen(false)
      }
    }

    document.addEventListener("mousedown", handleOutside)

    return () => {
      document.removeEventListener("mousedown", handleOutside)
    }
  }, [selectBoxRef])

  return (
    <SelectBoxCss ref={selectBoxRef}>
      <input
        id="dropdown"
        type="checkbox"
        checked={isOpen}
        onClick={() => {
          setIsOpen((prev) => !prev)
        }}
        readOnly
      />
      <label className="dropdownLabel" htmlFor="dropdown">
        <div>{select}</div>
        <ExpandMoreIcon className="caretIcon" style={isOpen ? { transform: "rotate(-180deg)" } : undefined} />
      </label>
      <div className="content">
        <ul className="contentUl">
          {isOpen &&
            dropdownList.map((item) => {
              return (
                <li
                  key={item.idx}
                  onClick={() => {
                    setSelect(item.name)
                    setIsOpen(false)
                  }}
                >
                  {item.name}
                </li>
              )
            })}
        </ul>
      </div>
    </SelectBoxCss>
  )
}

const SelectBoxCss = styled.ul`
  max-width: 100%;
  position: relative;
  margin-bottom: 0;

  #dropdown {
    width: 100%;
    height: 40px;
    left: 0;
    position: absolute;
    opacity: 0;
    transition: none;
    cursor: pointer;
  }

  .dropdownLabel {
    max-width: 100%;
    width: 170px;
    text-align: left;
    border: 1px solid var(--gray);
    border-radius: 500px;
    background: var(--white);
    font-size: 16px;
    font-weight: 500;
    color: #888;
    padding: 7px 10px 7px 15px;
    margin-left: 4px;

    display: flex;
    justify-content: space-between;
    align-items: center;
  }

  .content {
    display: none;
    position: absolute;
    width: 170px;
    top: 45px;
    left: 4px;
  }

  #dropdown:checked + label {
    border: 1.5px solid var(--brand-color);
  }

  #dropdown:checked + label + div {
    display: block;
    background: var(--white);
    border: 1px solid var(--gray);
    border-radius: 12px;
    z-index: 10;
    max-height: 450px;
    overflow-y: auto;

    font-size: 14.5px;
    letter-spacing: -0.03rem;
    color: #222;
    font-weight: 500;

    // 출처 : https://codingbroker.tistory.com/66
    &::-webkit-scrollbar {
      width: 15px;
      border-radius: 12px;
    }

    &::-webkit-scrollbar-thumb {
      border-radius: 12px;
      background: #eee;
      background-clip: padding-box;
      border: 3px solid transparent; // 스크롤 막대기 넓이 조절
      margin: 5px;
    }

    &::-webkit-scrollbar-track {
      border-radius: 0 12px 12px 0;
      background: var(--white);
    }
  }

  .caretIcon {
    float: right;
    transition: all 0.3s ease-in-out;
  }

  .content ul {
    list-style-type: none;
    margin: 0;
    border-radius: 12px;
  }

  .content ul li {
    padding: 10px 15px;
    cursor: pointer;
  }

  .content ul li:hover {
    background-color: #f7f7f7;
    color: var(--brand-color);
  }

  .content ul li:nth-child(1):hover {
    border-radius: 12px 0 0 0;
  }

  .content ul li:nth-last-child(1):hover {
    border-radius: 0 0 0 12px;
  }

  .expectedOpen {
    opacity: 0.8;
    color: var(--brand-color);
  }

  @media screen and (max-width: 1024px) {
    width: 100%;
    max-width: calc(100% - 30px);
    position: absolute;
    z-index: 11;
    top: 140px;
    left: 0;
    margin: 0 15px;

    .dropdownLabel {
      width: 100%;
      max-width: 100%;
      height: 50px;
      border-radius: 12px;
      padding: 12px;
      margin-left: 0;
      margin: -25px 0 0 0;
    }

    .content {
      background: red;
      width: 100%;
      left: 0;
      position: absolute;
      z-index: 11;
      margin-top: -15px !important;
    }
  }
`

export default Test