Создайте свои собственные расширяющиеся и сужающиеся панели содержимого
Опубликовано: 2022-03-10До сих пор мы называли их «открывающейся и закрывающейся панелью», но их также называют панелями расширения или, проще говоря, расширяющимися панелями.
Чтобы прояснить, о чем именно мы говорим, перейдите к этому примеру на CodePen:
Это то, что мы будем строить в этом коротком уроке.
С точки зрения функциональности есть несколько способов добиться нужного нам анимированного открытия и закрытия. Каждый подход со своими преимуществами и компромиссами. В этой статье я подробно расскажу о своем методе «перехода». Сначала рассмотрим возможные подходы.
подходы
Существуют вариации этих методов, но в целом подходы можно отнести к одной из трех категорий:
- Анимация/переход по
height
илиmax-height
контента. - Используйте
transform: translateY
, чтобы переместить элементы в новое положение, создав иллюзию закрытия панели, а затем повторно визуализировать DOM после завершения преобразования с элементами в их конечной позиции. - Используйте библиотеку, которая делает некоторую комбинацию/вариацию 1 или 2!
Рассмотрение каждого подхода
С точки зрения производительности использование преобразования более эффективно, чем анимация или переход между высотой и максимальной высотой. При преобразовании движущиеся элементы растрируются и смещаются с помощью графического процессора. Это дешевая и простая операция для графического процессора, поэтому производительность, как правило, намного выше.
Основные шаги при использовании подхода преобразования:
- Получите высоту содержимого, которое нужно свернуть.
- Переместите содержимое и все после него на высоту содержимого, которое нужно свернуть, с помощью
transform: translateY(Xpx)
. Управляйте преобразованием с переходом по выбору, чтобы получить приятный визуальный эффект. - Используйте JavaScript для прослушивания события
transitionend
. Когда он сработает,display: none
содержимое и удалите преобразование, и все должно быть в нужном месте.
Звучит не так уж плохо, правда?
Тем не менее, есть ряд соображений, связанных с этим методом, поэтому я стараюсь избегать его для случайных реализаций, если только производительность не является абсолютно важной.
Например, при подходе transform: translateY
вам необходимо учитывать z-index
элементов. По умолчанию элементы, которые трансформируются вверх, располагаются после триггерного элемента в DOM и, следовательно, появляются поверх элементов, предшествующих им, при переводе вверх.
Вам также необходимо учитывать, сколько вещей появляется после содержимого, которое вы хотите свернуть в DOM. Если вам не нужна большая дыра в макете, вам может быть проще использовать JavaScript, чтобы обернуть все, что вы хотите переместить, в элемент-контейнер и просто переместить его. Управляемый, но мы только что добавили больше сложности! Однако именно такой подход я использовал при перемещении игроков вверх и вниз в In/Out. Как это было сделано, можно посмотреть здесь.
Для более случайных потребностей я обычно использую переход на max-height
содержимого. Этот подход не работает так же хорошо, как преобразование. Причина в том, что браузер анимирует высоту сворачивающегося элемента на протяжении всего перехода; это вызывает множество вычислений макета, которые не так дешевы для главного компьютера.
Однако этот подход выигрывает с точки зрения простоты. Расплатой за вышеупомянутый вычислительный удар является то, что повторный поток DOM заботится о положении и геометрии всего. У нас очень мало вычислений для написания, плюс JavaScript, необходимый для того, чтобы справиться с этим, сравнительно прост.
Слон в комнате: детали и основные элементы
Те, кто хорошо разбирается в элементах HTML, знают, что для этой проблемы существует встроенное HTML-решение в виде details
и summary
элементов. Вот пример разметки:
<details> <summary>Click to open/close</summary> Here is the content that is revealed when clicking the summary... </details>
По умолчанию браузеры предоставляют небольшой треугольник раскрытия рядом с элементом сводки; нажмите на сводку, и откроется содержимое под сводкой.
Отлично, эй? Детали даже поддерживают событие toggle
в JavaScript, так что вы можете делать подобные вещи для выполнения разных действий в зависимости от того, открыто оно или закрыто (не волнуйтесь, если такое выражение 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 в кнопке? Я всегда включаю это, так как по умолчанию кнопка внутри формы выполняет отправку. Если вы обнаружите, что тратите впустую пару часов, задаваясь вопросом, почему ваша форма не работает и в вашей форме задействованы кнопки; убедитесь, что вы проверили атрибут type!
Следующий элемент после кнопки — это сам лоток содержимого; все, что вы хотите скрыть и показать.
Чтобы воплотить вещи в жизнь, мы будем использовать пользовательские свойства CSS, переходы CSS и немного JavaScript.
Базовая логика
Основная логика такова:
- Дайте странице загрузиться, измерьте высоту контента.
- Задайте высоту содержимого в контейнере как значение пользовательского свойства CSS.
- Немедленно скройте содержимое, добавив к нему атрибут
aria-hidden: "true"
. Использованиеaria-hidden
гарантирует, что вспомогательные технологии знают, что содержимое также скрыто. - Подключите CSS так, чтобы
max-height
класса контента была значением пользовательского свойства. - Нажатие нашей триггерной кнопки переключает свойство aria-hidden с true на false, что, в свою очередь, переключает
max-height
содержимого между0
и высотой, установленной в пользовательском свойстве. Переход к этому свойству обеспечивает визуальное чутье — настраивайте по вкусу!
Примечание. Теперь это был бы простой случай переключения класса или атрибута, если max-height: auto
равняется высоте содержимого. К сожалению, это не так. Иди и кричи об этом W3C здесь.
Давайте посмотрим, как этот подход проявляется в коде. Пронумерованные комментарии показывают эквивалентные логические шаги, приведенные выше, в коде.
Вот JavaScript:
// 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
У меня есть setTimeout
с продолжительностью 0
, прежде чем скрыть контейнер. Это, возможно, не нужно, но я использую его как подход «пояса и скобок», чтобы убедиться, что страница отрисовывается первой, чтобы высота содержимого была доступна для чтения.
Активировать это только тогда, когда страница будет готова
Если у вас есть другие вещи, вы можете заключить код ящика в функцию, которая инициализируется при загрузке страницы. Например, предположим, что функция ящика была заключена в функцию с именем initDrawers
, мы могли бы сделать это:
window.addEventListener("load", initDrawers);
На самом деле, мы добавим это в ближайшее время.
Дополнительные атрибуты data-* в контейнере
Во внешнем контейнере есть атрибут данных, который также переключается. Это добавляется на случай, если что-то нужно изменить с помощью триггера или контейнера, когда ящик открывается/закрывается. Например, возможно, мы хотим изменить цвет чего-либо, показать или переключить значок.
Значение по умолчанию для пользовательского свойства
Для пользовательского свойства в CSS установлено значение по умолчанию 1000px
. Это бит после запятой внутри значения: var(--containerHeight, 1000px)
. Это означает, что если --containerHeight
каким-то образом облажается, у вас все равно должен быть достойный переход. Очевидно, вы можете установить это на все, что подходит для вашего варианта использования.
Почему бы просто не использовать значение по умолчанию 100000 пикселей?
Учитывая, что max-height: auto
не переходит, вы можете задаться вопросом, почему бы вам просто не выбрать для заданной высоты значение, большее, чем вам когда-либо понадобится. Например, 10000000 пикселей?
Проблема с этим подходом заключается в том, что он всегда будет переходить с этой высоты. Если продолжительность перехода установлена на 1 секунду, переход будет «перемещаться» на 10000000 пикселей за секунду. Если ваш контент имеет высоту всего 50 пикселей, вы получите довольно быстрый эффект открытия/закрытия!
Тернарный оператор для переключателей
Мы использовали тернарный оператор пару раз для переключения атрибутов. Некоторые люди ненавидят их, но я, и другие, люблю их. Поначалу они могут показаться немного странными и немного напоминающими «гольф кода», но как только вы привыкнете к синтаксису, я думаю, что они читаются более просто, чем стандартные операторы if/else.
Для непосвященных тернарный оператор — это сжатая форма if/else. Они написаны так, что сначала нужно проверить, а потом ?
разделяет, что выполнять, если проверка верна, а затем :
, чтобы различать, что должно выполняться, если проверка ложна.
isThisTrue ? doYesCode() : doNoCode();
Наши переключатели атрибутов работают, проверяя, установлено ли для атрибута значение "true"
, и если да, установите для него значение "false"
, в противном случае установите для него значение "true"
.
Что происходит при изменении размера страницы?
Если пользователь изменит размер окна браузера, высока вероятность того, что высота нашего контента изменится. Поэтому вы можете повторно запустить установку высоты для контейнеров в этом сценарии. Сейчас мы рассматриваем такие возможности, кажется, самое время немного реорганизовать вещи.
Мы можем сделать одну функцию для установки высоты и другую функцию для взаимодействия. Затем добавьте в окно двух слушателей; один для загрузки документа, как упоминалось выше, а затем другой для прослушивания события изменения размера.
Немного больше A11Y
Можно немного позаботиться о доступности, используя атрибуты aria-expanded
, aria-controls
и aria-labelledby
. Это даст лучшую индикацию вспомогательной технологии, когда ящики были открыты/расширены. Мы добавляем aria-expanded="false"
в разметку нашей кнопки вместе с aria-controls="IDofcontent"
, где IDofcontent
— это значение идентификатора, который мы добавляем в контейнер контента.
Затем мы используем другой тернарный оператор для переключения атрибута aria-expanded
при щелчке в JavaScript.
Все вместе
С загрузкой страницы, несколькими ящиками, дополнительной работой 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 здесь:
Резюме
Можно какое-то время продолжать дорабатывать и обслуживать все больше и больше ситуаций, но базовая механика создания надежного открывающегося и закрывающегося ящика для вашего контента теперь должна быть в пределах вашей досягаемости. Надеюсь, вы также знаете о некоторых опасностях. Элемент details
не может быть анимирован, max-height: auto
не делает то, на что вы надеялись, вы не можете надежно добавить массивное значение max-height и ожидать, что все панели содержимого откроются должным образом.
Чтобы повторить наш подход здесь: измерьте контейнер, сохраните его высоту как пользовательское свойство CSS, скройте содержимое, а затем используйте простой переключатель для переключения между max-height
, равной 0, и высотой, которую вы сохранили в пользовательском свойстве.
Это может быть не самый эффективный метод, но я обнаружил, что для большинства ситуаций он вполне подходит и выигрывает от того, что его сравнительно просто реализовать.