드롭다운은 프로젝트에서 자주 구현하게 되는 기능인만큼 만들다 보면 이것저것 기능이나 디자인을 넣고 싶어지는 데
이번에는 드롭다운 영역 외 클릭 시 드롭다운 메뉴가 닫히는 기능을 만들어 보았다
기능 구현 과정에서 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
'더 알아보기 > 기능' 카테고리의 다른 글
[Next.js] Drag&Drop 기능 구현하기 (0) | 2024.01.09 |
---|---|
[Next.js] Grouping Pagination 만들기 (라이브러리 X) (0) | 2023.12.11 |
[Next.js] 드래그 상태에서 css 조작하기 (2) | 2023.11.23 |
[React + Typescript] Canvas 태그와 useRef를 사용하여 그림판 만들기 (0) | 2023.10.20 |
[React + Typescript] 부모 자식 간에 useRef 사용하는 방법 (0) | 2023.10.19 |
댓글