상세 컨텐츠

본문 제목

[Javascript] 26. Stripe Follow Along Nav

Experience/[Javascript] JS 30

by winCow 2021. 5. 9. 15:23

본문

1. 개요

네비게이션로 마우스 포인터를 옮기면 메뉴가 펼쳐지도록 한다.

 

 

2. HTML

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Follow Along Nav</title>
</head>
<body>
  <h2>Cool</h2>
  <nav class="top">
    <div class="dropdownBackground">
      <span class="arrow"></span>
    </div>

    <ul class="cool">
      <li>
        <a href="#">About Me</a>
        <div class="dropdown dropdown1">
          <div class="bio">
            <img src="https://logo.clearbit.com/wesbos.com">
            <p>Wes Bos sure does love web development. He teaches things like JavaScript, CSS and BBQ. Wait. BBQ isn't part of web development. It should be though!</p>
          </div>
        </div>
      </li>
      <li>
        <a href="#">Courses</a>
        <ul class="dropdown courses">
          <li>
            <span class="code">RFB</span>
            <a href="https://ReactForBeginners.com">React For Beginners</a>
          </li>
          <li>
            <span class="code">ES6</span>
            <a href="https://ES6.io">ES6 For Everyone</a>
          </li>
          <li>
            <span class="code">NODE</span>
            <a href="https://LearnNode.com">Learn Node</a>
          </li>
          <li>
            <span class="code">STPU</span>
            <a href="https://SublimeTextBook.com">Sublime Text Power User</a>
          </li>
          <li>
            <span class="code">WTF</span>
            <a href="http://Flexbox.io">What The Flexbox?!</a>
          </li>
          <li>
            <span class="code">GRID</span>
            <a href="https://CSSGrid.io">CSS Grid</a>
          </li>
          <li>
            <span class="code">LRX</span>
            <a href="http://LearnRedux.com">Learn Redux</a>
          </li>
          <li>
            <span class="code">CLPU</span>
            <a href="http://CommandLinePowerUser.com">Command Line Power User</a>
          </li>
          <li>
            <span class="code">MMD</span>
            <a href="http://MasteringMarkdown.com">Mastering Markdown</a>
          </li>
        </ul>
      </li>
      <li>
        <a href="#">Other Links</a>
        <ul class="dropdown dropdown3">
          <li><a class="button" href="http://twitter.com/wesbos">Twitter</a></li>
          <li><a class="button" href="http://facebook.com/wesbos.developer">Facebook</a></li>
          <li><a class="button" href="http://wesbos.com">Blog</a></li>
          <li><a class="button" href="http://wesbos.com/courses">Course Catalog</a></li>
        </ul>
      </li>
    </ul>
  </nav>

</body>
</html>

nav의 리스트들(.cool > li)은 각각 타이틀이 담긴 a태그와 dropdown시에 드러날 div 혹은 ul 태그로 이루어져 있다.

흰색 배경을 추가하기 위한 dropdownBackground는 div 태그로 주어지며, 모든 드롭다운에 대해 크기와 위치를 바뀌게 하여 해당 태그 하나로만 표현을 할 것이다.

 

 

3. CSS

  html {
    box-sizing: border-box;
    font-family: "Arial Rounded MT Bold", "Helvetica Rounded", Arial, sans-serif;
  }
  
  *, *:before, *:after {
    box-sizing: inherit;
  }
  
  body {
    margin: 0;
    min-height: 100vh;
    background:
      linear-gradient(45deg, hsla(340, 100%, 55%, 1) 0%, hsla(340, 100%, 55%, 0) 70%),
      linear-gradient(135deg, hsla(225, 95%, 50%, 1) 10%, hsla(225, 95%, 50%, 0) 80%),
      linear-gradient(225deg, hsla(140, 90%, 50%, 1) 10%, hsla(140, 90%, 50%, 0) 80%),
      linear-gradient(315deg, hsla(35, 95%, 55%, 1) 100%, hsla(35, 95%, 55%, 0) 70%);
  }

  h2 {
    margin-top: 0;
    padding-top: .8em;
  }

  nav {
    position: relative;
    perspective: 600px;
  }

  .cool > li > a {
    color: yellow;
    text-decoration: none;
    font-size: 20px;
    background: rgba(0,0,0,0.2);
    padding: 10px 20px;
    display: inline-block;
    margin: 20px;
    border-radius: 5px;
  }

  nav ul {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    justify-content: center;
  }

  .cool > li {
    position: relative;
    display: flex;
    justify-content: center;
  }

  .dropdown {
    opacity: 0;
    position: absolute;
    overflow: hidden;
    padding: 20px;
    top: -20px;
    border-radius: 2px;
    transition: all 0.5s;
    transform: translateY(100px);
    will-change: opacity;
    display: none;
  }

  .trigger-enter .dropdown {
    display: block;
  }

  .trigger-enter-active .dropdown {
    opacity: 1;
  }
  
  .dropdownBackground {
    width: 100px;
    height: 100px;
    position: absolute;
    background: #fff;
    border-radius: 4px;
    box-shadow: 0 50px 100px rgba(50,50,93,.1), 0 15px 35px rgba(50,50,93,.15), 0 5px 15px rgba(0,0,0,.1);
    transition: all 0.3s, opacity 0.1s, transform 0.2s;
    transform-origin: 50% 0;
    display: flex;
    justify-content: center;
    opacity:0;
  }

  .dropdownBackground.open {
    opacity: 1;
  }

  .arrow {
    position: absolute;
    width: 20px;
    height: 20px;
    display: block;
    background: white;
    transform: translateY(-50%) rotate(45deg);
  }

  .bio {
    min-width: 500px;
    display: flex;
    justify-content: center;
    align-items: center;
    line-height: 1.7;
  }

  .bio img {
    float: left;
    margin-right: 20px;
  }

  .courses {
    min-width: 300px;
  }

  .courses li {
    padding: 10px 0;
    display: block;
    border-bottom: 1px solid rgba(0,0,0,0.2);
  }

  .dropdown a {
    text-decoration: none;
    color: #ffc600;
  }

  a.button {
    background: black;
    display: block;
    padding: 10px;
    color: white;
    margin-bottom: 10px;
  }

  /* Matches Twitter, TWITTER, twitter, tWitter, TWiTTeR... */
  .button[href*=twitter] { background: #019FE9; }
  .button[href*=facebook] { background: #3B5998; }
  .button[href*=courses] { background: #ffc600; }

 

 

4. Javascrip

  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')), 150;
    background.classList.add('open');

    const dropdown = this.querySelector('.dropdown');
    const dropdownCoords = dropdown.getBoundingClientRect();
    const navCoords = nav.getBoundingClientRect();
    const coords = {
      height: dropdownCoords.height,
      width: dropdownCoords.width,
      top: dropdownCoords.top - navCoords.top,
      left: dropdownCoords.left - navCoords.left
    };
    background.style.setProperty('width', `${coords.width}px`);
    background.style.setProperty('height', `${coords.height}px`);
    background.style.setProperty('transform', `translate(${coords.left}px, ${coords.top}px)`);
  }
  
  function handleLeave() {
    this.classList.remove('trigger-enter', 'trigger-enter-active');
    background.classList.remove('open');
  }

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

먼저 HTML 요소들을 선택한다. triggers는 nav의 리스트를, background는 dropdownBackground 클래스의 배경용 div 태그를, nav에 nav 태그 전체를 선택하여 할당하였다.

각각의 트리거에 mouseenter, mouseleave 이벤트를 할당하여 마우스가 들어가고 나가는 이벤트가 전달될 때 각각 handleEnter, handelLeave가 시행되도록 했다.

handelEnter는 mouseenter 이벤트가 전달되면, 이벤트가 전달되는 대상인 li 태그를 선택하여 trigger-enter 클래스를 추가한다. trigger-enter 클래스는 CSS에서 none 상태인 display를 block로 나타내도록 한다. 그러나 아직까지 화면에 내용이 표시되지는 않는데, 이는 해당 드롭다운 내용의 opacity 값이 0으로 투명한 상태를 유지하고 있기 때문이다. 따라서, setTimeout 매소드를 이용해 0.15초 뒤에 추가적으로 컨텐츠가 보이도록 한다. classList.contains는 해당 클래스가 존재하는지를 확인하는데, 이를 통해 trigger-enter가 추가되었는지를 체크하고, trigger-enter-active 클래스를 하나 더 추가한다. 두 가지 기능이 모두 참이어야 하므로, 이는 trigger-enter가 추가되었을 때만 trigger-enter-active가 추가되는 것이라고 볼 수 있다. 이러한 조건을 추가하는 이유는, trigger-enter 클래스와 trigger-enter-active 클래스의 실행 순서를 보다 명확하게 하기 위함이다. 이는 handleLeave와 관련되어 있는데, 일단 다른 내용을 먼저 작성한다.

투명한 상태의 background에 open 클래스를 추가하여 화면에 보이도록 한다. 또, background의 크기와 위치를 설정하기 위해 드롭다운 리스트들을 전부 dropdown으로 선택하고, getBoundingClientRect를 이용하여 드롭다운의 크기 정보와 nav 태그 전체의 크기 정보를 얻는다. coords에는 드롭다운의 높이, 넓이를 설정하여 크기를 맞추고, top과 left를 설정하고 transform: translate 속성에 입력하여 위치를 맞춘다. nav가 문서의 최상단에 있는 경우에는 상관없지만, 이번 코드와 같이 최상단에 nav 외에 다른 요소가 있다면 그만큼 background의 위치를 옮겨줘야 할 필요가 있다. 그러므로 nav의 top과 left를 빼 준다.

handleLeave 함수는 handleEnter 함수에서 추가한 세 개의 클래스를 제거해 주면 된다.

그런데, setTimeout 매소드에서 'trigger-enter가 추가되었을 때'라는 조건이 없다면 trigger-enter-active가 시행되는 데는 0.15초가 소요되기 때문에, 빠르게 마우스를 넣었다 빼기를 반복할 때, trigger-enter-active가 시행되기 전에 handleLeave가 시행되어 버리는 경우가 발생한다. 즉, mouseenter 이벤트가 발생하여 handleEnter 함수를 시행되었는데, display가 block으로 바뀌고 background가 open된 후, 0.15초가 지나기 전에 mouseleave 이벤트가 발생하여 모든 클래스가 제거되어번 뒤 trigger-enter-active가 실행되어 드롭다운의 내용이 표시되어 버리는 것이다. 이를 방지하기 위해, trigger-enter-active는 trigger-enter가 실행되는 것을 조건으로 하여 실행되도록 해야 할 필요가 있다.

 

 setTimeout(() => {
   if(this.classList.contains('trigger-enter')) {
     this.classList.add('trigger-enter-active')
   }
 }, 150);

즉, 위와 같은 코드가 필요하며, 먼저 작성한 코드는 이를 더 간략하게 나타낸 것이다.

 

 

 

관련글 더보기

댓글 영역