Accordion menu 구현

박상은·2022년 10월 4일
1

🌱 포트폴리오 🌱

목록 보기
2/10

해당 프로젝트는 Vanilla JSTypeScript만을 사용한 프로젝트입니다.

기본적으로는 닫아놨다가 누르면 아래로 열리고 추가 정보를 볼 수 있는 기능을 가진 Accodian menu를 만들어봤습니다.
예전에 HTML5를 공부하다가 보기만 하고 지나갔던 <details><summary>가 생각나서 관련해서 구글링해보면서 방법을 찾고 적용해봤습니다.

애니메이션 적용 전

아래 gif<details>/<summary>에 스타일링만 적용한 결과물입니다.
... 애니메이션 없는 gif 추가
transition을 적용한 것처럼 서서히 열리고 서서히 닫히는 기능을 원했지만 원하는대로 동작하지 않아서 애니메이션이 적용되는 방법을 찾아봤습니다.

🤔 ResizeObserver 사용

다른 방법을 찾다가 ResizeObserver의 존재를 알게 되었습니다.
ResizeObserver는 특정 엘리먼트의 크기 변화를 감지합니다.
해당 기능과 <details>를 클릭하면 height가 늘어나는 것을 이용해서 transition을 적용한 것처럼 효과를 줄 수 있습니다.

  • ResizeObserver 사용법
const RO = new ResizeObserver(callback)
RO.observe(element)

/*
 * "callback"의 인자는 "ResizeObserverEntry[]"값이 들어옵니다.
 * 관찰하는 모든 엘리먼트의 특정 정보를 담은 "ResizeObserverEntry"의 배열입니다.
*/
  • ResizeObserverEntry의 속성들 ( 자세한 정보는 mdn )
    1. target: 관찰 대상입니다.
    2. contentRect: 관찰 대상의 사각형 정보입니다.
    3. 나머지는 사용 안해봐서 생략...

🧐 실제 적용 예시

아래의 예시를 보면서 어떻게 적용했는지 설명하겠습니다. ( 참고한 포스트 )

export const setDetailsHeight = (wrapper: HTMLElement) => {
  // 특정 <details>의 닫힘과 열림의 "height"값을 미리 기록함 ( "--expanded"와 "--collapsed"에 기록 )
  // "width"를 "dataset"에 넣어두는 이유는 처음 실행인지 아닌지 즉, 닫힘과 열림의 크기를 미리 기록하는 것인지 판단을 위해서
  const setHeight = (detail: HTMLDetailsElement, open = false) => {
    detail.open = open;
    const { width, height } = detail.getBoundingClientRect();
    detail.dataset.width = width + "";
    detail.style.setProperty(
      open ? `--expanded` : `--collapsed`,
      `${height}px`
    );
  };

  // ResizeObserver 객체 생성
  const RO = new ResizeObserver((entries) =>
    entries.forEach((entry) => {
      const detail = entry.target as HTMLDetailsElement;
      const width = detail.dataset.width ? +detail.dataset.width : -1;

      // 처음 실행이라면 즉, 클릭에 의한 실행이 아니라면 실행
      // 단, 여기서 주의해야 할 점이 "entry.contentRect.width"값에는 padding이나 border를 포함하지 않은 값임
      // 따라서 항상 width값이 맞지 않아 if문 코드를 실행해서 <details>가 안열릴 수 있음
      // 그러므로 details에 직접적으로 padding이나 border를 안주는 것이 좋고 만약 값이 준다면 수치를 계산해서 직접 더해줘야 정상적으로 작동함
      if (width !== entry.contentRect.width) {
        detail.removeAttribute("style");
        // 닫힘 크기 기록
        setHeight(detail);
        // 열림 크기 기록
        setHeight(detail, true);
        // 원래 상태로 되돌림
        detail.open = false;
      }
    })
  );

  // wrapper 내부의 모든 <details>을 찾아서 관찰 대상으로 지정
  const details = wrapper.querySelectorAll("details");
  details.forEach((detail) => RO.observe(detail));
};

// setDetailsHeight()에 <details>들을 포함하는 가장 상위 태그를 인자로 넣어주면 됨
// 물론 document를 바로 넣어줘도 되지만 전체 탐색보다는 <details>를 사용하는 제일 상위태그를 넣어주는 게 더 효율적이라고 생각함
  • css 적용하기
details {
  /* 닫힌 "height" 값을 적용 */
  height: var(--collapsed);
  overflow: hidden;
  /* 닫힌 값과 열린 값이 다르기 때문에 "transition"이 적용됩니다. */
  transition: height 300ms cubic-bezier(0.4, 0.01, 0.165, 0.99);
}
details[open] {
  /* 열린 "height" 값을 적용 */
  height: var(--expanded);
}

😮 ResizeObserver 적용 결과물

... 결과 gif 넣기

0개의 댓글