해당 프로젝트는
Vanilla JS
에TypeScript
만을 사용한 프로젝트입니다.
기본적으로는 닫아놨다가 누르면 아래로 열리고 추가 정보를 볼 수 있는 기능을 가진 Accodian menu
를 만들어봤습니다.
예전에 HTML5
를 공부하다가 보기만 하고 지나갔던 <details>
와 <summary>
가 생각나서 관련해서 구글링해보면서 방법을 찾고 적용해봤습니다.
아래 gif
는 <details>
/<summary>
에 스타일링만 적용한 결과물입니다.
... 애니메이션 없는 gif 추가
transition
을 적용한 것처럼 서서히 열리고 서서히 닫히는 기능을 원했지만 원하는대로 동작하지 않아서 애니메이션이 적용되는 방법을 찾아봤습니다.
다른 방법을 찾다가 ResizeObserver
의 존재를 알게 되었습니다.
ResizeObserver
는 특정 엘리먼트의 크기 변화를 감지합니다.
해당 기능과 <details>
를 클릭하면 height
가 늘어나는 것을 이용해서 transition
을 적용한 것처럼 효과를 줄 수 있습니다.
const RO = new ResizeObserver(callback)
RO.observe(element)
/*
* "callback"의 인자는 "ResizeObserverEntry[]"값이 들어옵니다.
* 관찰하는 모든 엘리먼트의 특정 정보를 담은 "ResizeObserverEntry"의 배열입니다.
*/
ResizeObserverEntry
의 속성들 ( 자세한 정보는 mdn )target
: 관찰 대상입니다.contentRect
: 관찰 대상의 사각형 정보입니다.아래의 예시를 보면서 어떻게 적용했는지 설명하겠습니다. ( 참고한 포스트 )
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);
}
... 결과 gif 넣기