해당 프로젝트는
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 넣기