Wesbos - 동적 드롭다운 메뉴


완성본 예시

Sticky Nav와 같이 웹 사이트를 제작할 때 자주 사용할 수 있을 것 같은 기능이다.
마우스 hover → 서브메뉴들의 크기에 따라 동적으로 드롭다운 된다.


로직

  1. const를 통한 선언
  2. 함수 만들기 + 이벤트리스너 (mouseenter, mouseleave)
  3. 마우스 이벤트에 반응 할 CSS 작성
  4. classlist.addremove
  5. getBoundingClientRect()를 활용한 엘리먼트 위치 및 크기 값 활용

const 선언

-불편한 바보 드롭다운이 되지 않기 위해선 a가 아닌, li태그에 이벤트를 걸어줘야 한다.

-dropdownBackground가 따라다녀야 함

-nav태그의 위치값이 추후에 필요하므로 nav태그 또한 선언 필요

1
2
3
const triggers = document.querySelectorAll('.cool > li');
const background = document.querySelector('.dropdownBackground');
const nav = document.querySelector('.top');

함수 & eventListener

💡 마우스 hover와 같은 기능이자 해당 이벤트가 엘리먼트에만 적용되는 mouseenter, mouseleave

1
2
3
4
5
6
7
8
9
10
function handleEnter() {
console.log('Entered!'); // 잘 작동하는지 찍어보기
}

function handleLeave() {
console.log('Leaved!');
}

triggers.forEach((li) => li.addEventListener('mouseenter', handleEnter));
triggers.forEach((li) => li.addEventListener('mouseleave', handleLeave));

CSS 작성하기

1
2
3
4
5
6
7
8
9
10
11
.trigger-enter .dropdown {
display: block;
} /* trigger-enter 그리고 .dropdown 모두 */

.trigger-enter-active .dropdown {
opacity: 1;
}

.dropdownBackground.open {
opacity: 1;
}

classList.add & remove

1
2
3
4
5
6
7
8
9
10
function handleEnter() {
this.classList.add('trigger-enter');
this.classList.add('trigger-enter-active');
background.classList.add('open');
}

function handleLeave() {
this.classList.remove('trigger-enter', 'trigger-enter-active');
background.classList.remove('open');
}

‼️ 하지만… 이렇게 코딩할 경우, transition 효과가 적용되지 않는다.

💡 display : block에는 transition이 먹히지 않기 때문에, block이 된 후 opacity에 변화를 줘야 ⇒ transition이 적절하게 적용된다.

1
2
3
4
5
6
7
8
9
10
11
function handleEnter(){
this.classList.add('trigger-enter')
// && 앞이 조건문, 뒤에는 True일 경우의 결과
setTimeout(()=> this.classList.add('trigger-enter') && this.classList.add('trigger-enter-active'), 150)'))
background.classList.add('open')
};

function handleLeave(){
this.classList.remove('trigger-enter', 'trigger-enter-active')
background.classList.remove('open')
};

getBoundingClientRect()의 활용

💡 getBoundingClientRect() = 엘리먼트의 넓이, 뷰포트를 기준으로 한 위치값(x,y) 등을 알 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function handleEnter() {
this.classList.add('trigger-enter');
setTimeout(
() =>
this.classList.contains('trigger-enter') &&
this.classList.add('trigger-enter-active'),
100
);
background.classList.add('open');

const dropdown = this.querySelector('.dropdown');
const dropdownCoords = dropdown.getBoundingClientRect();
const navCoords = nav.getBoundingClientRect();

background.style.setProperty('width', `${dropdownCoords.width}px`);
background.style.setProperty('height', `${dropdownCoords.height}px`);
// nav의 위치가 변경되면 위치가 뒤틀릴 수 있기 때문에 애초에 nav의 top과 left값을 빼준다.
background.style.setProperty(
'transform',
`translate(${dropdownCoords.left - navCoords.left}px, ${
dropdownCoords.top - navCoords.top
}px)`
);
}

function handleLeave() {
this.classList.remove('trigger-enter', 'trigger-enter-active');
background.classList.remove('open');
}

최종 완성 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const triggers = document.querySelectorAll('.cool > li');
const background = document.querySelector('.dropdownBackground');
const nav = document.querySelector('.top');

function handleEnter() {
this.classList.add('trigger-enter');
setTimeout(
() =>
this.classList.contains('trigger-enter') &&
this.classList.add('trigger-enter-active'),
100
);
background.classList.add('open');

const dropdown = this.querySelector('.dropdown');
const dropdownCoords = dropdown.getBoundingClientRect();
const navCoords = nav.getBoundingClientRect();

background.style.setProperty('width', `${dropdownCoords.width}px`);
background.style.setProperty('height', `${dropdownCoords.height}px`);
background.style.setProperty(
'transform',
`translate(${dropdownCoords.left - navCoords.left}px, ${
dropdownCoords.top - navCoords.top
}px)`
);
}

function handleLeave() {
this.classList.remove('trigger-enter', 'trigger-enter-active');
background.classList.remove('open');
}

triggers.forEach((li) => li.addEventListener('mouseenter', handleEnter));
triggers.forEach((li) => li.addEventListener('mouseleave', handleLeave));

Author

Hoonjoo

Posted on

2022-01-05

Updated on

2022-02-07

Licensed under

Comments