자신만의 확장 및 축소 콘텐츠 패널 만들기

게시 됨: 2022-03-10
요약 ↬ UI/UX에서 반복적으로 필요한 일반적인 패턴은 단순한 애니메이션 패널 또는 '서랍'입니다. 이것을 만들기 위해 라이브러리가 필요하지 않습니다. 기본적인 HTML/CSS 및 JavaScript를 사용하여 직접 수행하는 방법을 배웁니다.

지금까지는 '열기 및 닫기 패널'이라고 했지만 확장 패널, 더 간단히 확장 패널이라고도 합니다.

우리가 말하는 내용을 정확히 이해하려면 CodePen의 이 예제로 넘어가십시오.

CodePen에서 Ben Frain의 간편한 서랍 표시/숨기기(Multiples).

CodePen에서 Ben Frain의 간편한 서랍 표시/숨기기(Multiples).

이것이 이 짧은 튜토리얼에서 구축할 것입니다.

기능적 관점에서 우리가 찾고 있는 애니메이션 열기 및 닫기를 달성하는 몇 가지 방법이 있습니다. 각각의 접근 방식에는 고유한 이점과 장단점이 있습니다. 이 기사에서 '이동' 방법에 대한 세부 정보를 공유하겠습니다. 가능한 접근 방식을 먼저 고려해 보겠습니다.

구혼

이러한 기술에는 변형이 있지만 일반적으로 접근 방식은 세 가지 범주 중 하나로 분류됩니다.

  1. 콘텐츠의 height 또는 max-height 높이를 애니메이션/전환합니다.
  2. transform: translateY 를 사용하여 요소를 새 위치로 이동하여 패널이 닫히는 것처럼 보이게 한 다음 요소가 마무리 위치에 있는 변환이 완료되면 DOM을 다시 렌더링합니다.
  3. 1 또는 2의 일부 조합/변형을 수행하는 라이브러리를 사용하십시오!
점프 후 더! 아래에서 계속 읽기 ↓

각 접근 방식의 고려 사항

성능 관점에서 변환을 사용하는 것이 높이/최대 높이에 애니메이션을 적용하거나 전환하는 것보다 더 효과적입니다. 변환을 사용하면 움직이는 요소가 래스터화되고 GPU에 의해 이동됩니다. 이것은 GPU에 대한 저렴하고 쉬운 작업이므로 성능이 훨씬 더 좋아지는 경향이 있습니다.

변환 접근 방식을 사용할 때의 기본 단계는 다음과 같습니다.

  1. 축소할 콘텐츠의 높이를 가져옵니다.
  2. transform: translateY(Xpx) 를 사용하여 축소할 콘텐츠의 높이만큼 콘텐츠와 그 뒤의 모든 것을 이동합니다. 기분 좋은 시각 효과를 주기 위해 선택의 전환으로 변환을 작동합니다.
  3. JavaScript를 사용하여 transitionend 이벤트를 수신합니다. 실행되면 display: none 내용을 표시하고 변환을 제거하면 모든 것이 올바른 위치에 있어야 합니다.

너무 나쁘게 들리지 않죠?

그러나 이 기술에는 여러 가지 고려 사항이 있으므로 성능이 절대적으로 중요하지 않은 한 캐주얼한 구현에서는 사용하지 않는 경향이 있습니다.

예를 들어 transform: translateY 접근 방식을 사용하면 요소의 z-index 를 고려해야 합니다. 기본적으로 위로 변환되는 요소는 DOM의 트리거 요소 뒤에 있으므로 번역될 때 이전 항목의 맨 위에 나타납니다.

또한 DOM에서 축소하려는 콘텐츠 뒤에 얼마나 많은 항목이 표시되는지 고려해야 합니다. 레이아웃에 큰 구멍이 생기는 것을 원하지 않으면 JavaScript를 사용하여 컨테이너 요소에서 이동하려는 모든 것을 래핑하고 이동하는 것이 더 쉽다는 것을 알 수 있습니다. 관리할 수 있지만 더 복잡해졌습니다! 그러나 이것은 인/아웃에서 플레이어를 위아래 로 움직일 때 사용하는 접근 방식입니다. 여기에서 어떻게 했는지 볼 수 있습니다.

보다 캐주얼한 요구 사항을 위해 콘텐츠의 max-height 를 전환하는 경향이 있습니다. 이 접근 방식은 변환만큼 잘 수행되지 않습니다. 그 이유는 브라우저가 전환 전체에 걸쳐 축소 요소의 높이를 트위닝하고 있기 때문입니다. 이는 호스트 컴퓨터에 비해 저렴하지 않은 많은 레이아웃 계산을 유발합니다.

그러나 이 접근 방식은 단순성 관점에서 볼 때 승리합니다. 위에서 언급한 계산 적중으로 인한 결과는 DOM re-flow가 모든 것의 위치와 기하학을 처리한다는 것입니다. 작성해야 할 계산 방법이 거의 없으며 이를 잘 수행하는 데 필요한 JavaScript도 비교적 간단합니다.

방 안의 코끼리: 세부 사항 및 요약 요소

HTML 요소에 대한 깊은 지식을 가진 사람들은 detailssummary 요소의 형태로 이 문제에 대한 기본 HTML 솔루션이 있음을 알 것입니다. 다음은 몇 가지 마크업의 예입니다.

 <details> <summary>Click to open/close</summary> Here is the content that is revealed when clicking the summary... </details>

기본적으로 브라우저는 요약 요소 옆에 작은 펼침 삼각형을 제공합니다. 요약을 클릭하면 요약 아래의 내용이 표시됩니다.

좋아, 이봐? 세부 사항은 JavaScript의 toggle 이벤트도 지원하므로 이러한 종류의 작업을 수행하여 열려 있는지 닫혀 있는지에 따라 다른 작업을 수행할 수 있습니다(그런 종류의 JavaScript 표현식이 이상해 보이더라도 걱정하지 마십시오. 자세한 내용은 곧 자세히):

 details.addEventListener("toggle", () => { details.open ? thisCoolThing() : thisOtherThing(); })

좋아, 거기에서 당신의 흥분을 멈추게 해줄게. 세부 정보 및 요약 요소는 애니메이션되지 않습니다. 기본적으로 제공되지 않으며 현재 추가 CSS 및 JavaScript를 사용하여 애니메이션/전환을 열고 닫을 수 없습니다.

그렇지 않은 경우 잘못된 것으로 판명되고 싶습니다.

슬프게도, 여는 것과 닫는 미학이 필요하기 때문에 우리는 소매를 걷어붙이고 우리가 마음대로 사용할 수 있는 다른 도구를 사용하여 우리가 할 수 있는 최선과 가장 접근하기 쉬운 일을 해야 할 것입니다.

좋습니다. 우울한 소식은 접어두고 이 일이 일어나도록 합시다.

마크업 패턴

기본 마크업은 다음과 같습니다.

 <div class="container"> <button type="button" class="trigger">Show/Hide content</button> <div class="content"> All the content here </div> </div>

확장기를 래핑할 외부 컨테이너가 있고 첫 번째 요소는 작업의 트리거 역할을 하는 버튼입니다. 버튼의 type 속성이 보이시나요? 기본적으로 양식 내부의 버튼이 제출을 수행할 것이라는 점을 항상 포함합니다. 양식이 작동하지 않고 양식에 단추가 포함된 이유를 궁금해하는 데 몇 시간을 낭비하고 있는 경우 유형 속성을 확인하십시오!

버튼 뒤의 다음 요소는 콘텐츠 서랍 자체입니다. 숨기고 보여주고 싶은 모든 것.

사물에 생명을 불어넣기 위해 CSS 사용자 정의 속성, CSS 전환 및 약간의 JavaScript를 사용할 것입니다.

기본 논리

기본 논리는 다음과 같습니다.

  1. 페이지를 로드하고 콘텐츠의 높이를 측정합니다.
  2. CSS 사용자 정의 속성 값으로 컨테이너의 콘텐츠 높이를 설정합니다.
  3. aria-hidden: "true" 속성을 추가하여 콘텐츠를 즉시 숨깁니다. aria-hidden 을 사용하면 보조 기술이 콘텐츠도 숨겨져 있음을 알 수 있습니다.
  4. 콘텐츠 클래스의 max-height 가 사용자 정의 속성 값이 되도록 CSS를 연결합니다.
  5. 트리거 버튼을 누르면 aria-hidden 속성이 true에서 false로 전환되며, 이는 차례로 콘텐츠의 max-height0 과 사용자 정의 속성에 설정된 높이 사이에서 전환합니다. 해당 속성에 대한 전환은 시각적 감각을 제공합니다. 취향에 맞게 조정하십시오!

참고: 이제 max-height: auto 가 콘텐츠 높이와 같은 경우 클래스 또는 속성을 토글하는 간단한 경우입니다. 슬프게도 그렇지 않습니다. 여기 W3C에 가서 그것에 대해 외치세요.

그 접근 방식이 코드에서 어떻게 나타나는지 살펴보겠습니다. 번호가 매겨진 주석은 코드에서 위의 동등한 논리 단계를 보여줍니다.

자바스크립트는 다음과 같습니다.

 // Get the containing element const container = document.querySelector(".container"); // Get content const content = document.querySelector(".content"); // 1. Get height of content you want to show/hide const heightOfContent = content.getBoundingClientRect().height; // Get the trigger element const btn = document.querySelector(".trigger"); // 2. Set a CSS custom property with the height of content container.style.setProperty("--containerHeight", `${heightOfContent}px`); // Once height is read and set setTimeout(e => { document.documentElement.classList.add("height-is-set"); 3. content.setAttribute("aria-hidden", "true"); }, 0); btn.addEventListener("click", function(e) { container.setAttribute("data-drawer-showing", container.getAttribute("data-drawer-showing") === "true" ? "false" : "true"); // 5. Toggle aria-hidden content.setAttribute("aria-hidden", content.getAttribute("aria-hidden") === "true" ? "false" : "true"); })

CSS:

 .content { transition: max-height 0.2s; overflow: hidden; } .content[aria-hidden="true"] { max-height: 0; } // 4. Set height to value of custom property .content[aria-hidden="false"] { max-height: var(--containerHeight, 1000px); }

참고 사항

서랍이 여러 개라면?

한 페이지에 여러 개의 열고 숨김 서랍이 있는 경우 크기가 다를 수 있으므로 모두 반복해야 합니다.

이를 처리하려면 모든 컨테이너를 가져오기 위해 querySelectorAll 을 수행한 다음 forEach 내부의 각 콘텐츠에 대한 사용자 정의 변수 설정을 다시 실행해야 합니다.

그 setTimeout

컨테이너를 숨기도록 설정하기 전에 지속 시간이 0setTimeout 이 있습니다. 이것은 틀림없이 필요하지 않지만 콘텐츠의 높이를 읽을 수 있도록 페이지가 먼저 렌더링되도록 하기 위해 '벨트 및 중괄호' 접근 방식으로 사용합니다.

페이지가 준비되었을 때만 실행

다른 작업이 진행 중인 경우 페이지 로드 시 초기화되는 함수에서 서랍 코드를 래핑하도록 선택할 수 있습니다. 예를 들어, 서랍 함수가 initDrawers 라는 함수에 포함되어 있다고 가정해 보겠습니다.

 window.addEventListener("load", initDrawers);

사실, 우리는 그것을 곧 추가할 것입니다.

컨테이너의 추가 data-* 속성

토글되는 외부 컨테이너에도 데이터 속성이 있습니다. 서랍이 열리고 닫힐 때 트리거 또는 컨테이너로 변경해야 할 사항이 있는 경우에 추가됩니다. 예를 들어, 무언가의 색상을 변경하거나 아이콘을 표시하거나 토글하고 싶을 수 있습니다.

사용자 정의 속성의 기본값

CSS의 사용자 정의 속성에 기본값이 1000px 로 설정되어 있습니다. 값 내에서 쉼표 뒤의 비트입니다. var(--containerHeight, 1000px) . 이것은 --containerHeight 가 어떤 식으로든 엉망이 되어도 여전히 적절한 전환이 있어야 함을 의미합니다. 분명히 사용 사례에 적합한 것으로 설정할 수 있습니다.

100000px의 기본값을 사용하지 않는 이유는 무엇입니까?

max-height: auto 가 전환되지 않는다는 점을 감안할 때 필요한 것보다 더 큰 값의 설정 높이를 선택하지 않는 이유가 궁금할 것입니다. 예를 들어 10000000px?

이 접근 방식의 문제는 항상 해당 높이에서 전환된다는 것입니다. 전환 기간이 1초로 설정된 경우 전환은 1초에 10000000픽셀을 '이동'합니다. 콘텐츠 높이가 50px에 불과하면 매우 빠른 개폐 효과를 얻을 수 있습니다!

토글에 대한 삼항 연산자

속성을 토글하기 위해 삼항 연산자를 몇 번 사용했습니다. 어떤 사람들은 그들을 싫어하지만 나와 다른 사람들은 그들을 사랑합니다. 처음에는 약간 이상하고 '코드 골프'처럼 보일 수 있지만 구문에 익숙해지면 표준 if/else보다 더 직관적으로 읽을 수 있다고 생각합니다.

초보자의 경우 삼항 연산자는 if/else의 축약된 형식입니다. 확인해야 할 항목이 먼저 표시되고 그 다음이 ? 검사가 true인 경우 실행할 항목을 구분한 다음 : 검사가 false인 경우 실행할 항목을 구분합니다.

 isThisTrue ? doYesCode() : doNoCode();

속성 토글은 속성이 "true" 로 설정되어 있는지 확인하여 작동하고, 그렇다면 "false" 로 설정하고, 그렇지 않으면 "true" 로 설정합니다.

페이지 크기를 조정하면 어떻게 됩니까?

사용자가 브라우저 창의 크기를 조정하면 콘텐츠 높이가 변경될 가능성이 높습니다. 따라서 해당 시나리오에서 컨테이너의 높이 설정을 다시 실행할 수 있습니다. 이제 우리는 그러한 사태를 고려하고 있습니다. 조금 리팩토링하기에 좋은 시기인 것 같습니다.

높이를 설정하는 함수와 상호 작용을 처리하는 함수를 만들 수 있습니다. 그런 다음 창에 두 개의 리스너를 추가합니다. 하나는 위에서 언급한 것처럼 문서가 로드될 때를 위한 것이고 다른 하나는 크기 조정 이벤트를 수신하기 위한 것입니다.

조금 더 A11Y

aria-expanded , aria-controlsaria-labelledby 속성을 사용하여 접근성에 대한 약간의 추가 고려 사항을 추가할 수 있습니다. 이렇게 하면 서랍이 열리거나 확장되었을 때 보조 기술을 더 잘 알 수 있습니다. aria-controls="IDofcontent" aria-expanded="false" 를 버튼 마크업에 추가합니다. 여기서 IDofcontent 는 콘텐츠 컨테이너에 추가하는 id의 값입니다.

그런 다음 JavaScript에서 클릭 시 aria-expanded 속성을 토글하기 위해 다른 삼항 연산자를 사용합니다.

모두 함께

페이지 로드, 여러 서랍, 추가 A11Y 작업 및 크기 조정 이벤트 처리를 통해 JavaScript 코드는 다음과 같습니다.

 var containers; function initDrawers() { // Get the containing elements containers = document.querySelectorAll(".container"); setHeights(); wireUpTriggers(); window.addEventListener("resize", setHeights); } window.addEventListener("load", initDrawers); function setHeights() { containers.forEach(container => { // Get content let content = container.querySelector(".content"); content.removeAttribute("aria-hidden"); // Height of content to show/hide let heightOfContent = content.getBoundingClientRect().height; // Set a CSS custom property with the height of content container.style.setProperty("--containerHeight", `${heightOfContent}px`); // Once height is read and set setTimeout(e => { container.classList.add("height-is-set"); content.setAttribute("aria-hidden", "true"); }, 0); }); } function wireUpTriggers() { containers.forEach(container => { // Get each trigger element let btn = container.querySelector(".trigger"); // Get content let content = container.querySelector(".content"); btn.addEventListener("click", () => { btn.setAttribute("aria-expanded", btn.getAttribute("aria-expanded") === "false" ? "true" : "false"); container.setAttribute( "data-drawer-showing", container.getAttribute("data-drawer-showing") === "true" ? "false" : "true" ); content.setAttribute( "aria-hidden", content.getAttribute("aria-hidden") === "true" ? "false" : "true" ); }); }); }

여기 CodePen에서 게임을 할 수도 있습니다.

CodePen에서 Ben Frain의 간편한 서랍 표시/숨기기(Multiples).

CodePen에서 Ben Frain의 간편한 서랍 표시/숨기기(Multiples).

요약

더 많은 상황에 대해 더 다듬고 케이터링하는 것이 얼마 동안 계속될 수 있지만 콘텐츠를 위한 안정적인 개폐 서랍을 만드는 기본 메커니즘은 이제 손이 닿을 수 있는 범위 내에 있어야 합니다. 일부 위험 요소도 알고 있기를 바랍니다. details 요소는 애니메이션할 수 없습니다. max-height: auto 가 원하는 작업을 수행하지 않습니다. 대규모 max-height 값을 안정적으로 추가할 수 없으며 모든 콘텐츠 패널이 예상대로 열릴 것으로 예상할 수 없습니다.

여기서 우리의 접근 방식을 반복하려면: 컨테이너를 측정하고 컨테이너의 높이를 CSS 사용자 정의 속성으로 저장하고 콘텐츠를 숨긴 다음 간단한 토글을 사용하여 max-height 0과 사용자 정의 속성에 저장한 높이 사이를 전환합니다.

그것이 절대적으로 최고의 성능을 발휘하는 방법은 아닐 수도 있지만 대부분의 상황에서 완벽하게 적절하고 구현하기가 비교적 간단하다는 이점이 있습니다.