Создаем блоки раскрывающегося текста на HTML, CSS и JavaScript

В этой статье описывается, как без использования сторонних библиотек с помощью HTML, CSS и JavaScript создать раскрывающийся текст. Вот как этот элемент интерфейса выглядит в действии.

Подходы

Для создания подобных панелей расширения используется несколько подходов:

  1. На основе анимации и переходов, примененных к свойствам height или max-height контента.
  2. Использование transform: translateY для перемещения элементов на новую позицию, создания эффекта закрытия панели и повторной визуализации DOM.
  3. Применение сторонней библиотеки.

Какой из подходов лучше?

С точки зрения производительности использование transform более эффективно, чем анимация height и max-height. При применении CSS-свойства transform элементы растризуются и перемещаются графическим процессором. Это низко затратная и простая операция для графического процессора.

Для реализации данного подхода нужно выполнить следующие действия:

  1. Получить высоту контента, который будет располагаться на панели.
  2. Переместить контент выше на высоту содержимого, которое будет свернуто с помощью transform: translateY(Xpx). С помощью перехода реализовать эффект открытия и закрытия панели.
  3. С помощью JavaScript перехватить событие transitionend. После его наступления задаем display: none для контента и удаляем преобразование.

Но у данного метода есть множество недостатков. Например, при использовании transform: translateY необходимо учитывать z-index элемента.

Поэтому проще с помощью JavaScript обернуть все, что вы хотите переместить, в контейнер и переместить его. Вот пример использования данного подхода.

Применение переходов к max-height работает не так хорошо, как свойство transform. Так как браузер изменяет высоту сворачивающегося элемента на протяжении всего перехода. Эта операция потребляет много ресурсов памяти и графического процессора. Но зато данный подход проще в реализации.

Подход на основе элементов details и summary

В HTML существуют элементы details и summary, которые позволяют создать панель расширения:

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

Кроме этого элемент details поддерживает JavaScript-событие toggle. Поэтому можно изменять HTML в зависимости от того, скрыто или отображается содержимое панели.

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

Но элементы details и summary не анимируются и к ним нельзя применять переходы. Поэтому используем другие средства.

Шаблон разметки

Основная разметка будет выглядеть следующим образом:

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

У нас есть внешний контейнер, который оборачивает расширяемый блок. Первым элементом является кнопка. За ней идет блок содержимого, которое будет скрываться, и отображаться с помощью пользовательских свойств CSS, переходов и JavaScript.

Базовая логика

  1. После загрузки веб-страницы измеряем высоту содержимого.
  2. Устанавливаем высоту содержимого в контейнере в качестве значения пользовательского свойства CSS.
  3. Скрываем содержимое, добавив к нему атрибут aria-hidden: «true».
  4. Устанавливаем max-height в качестве значения пользовательского свойства.
  5. Нажатие кнопки изменяет значение свойства aria-hidden с true на false. А также max-height содержимого с 0 на высоту, заданную в пользовательском свойстве. Затем с помощью переходов реализуем визуальный эффект.

JavaScript-код

// Получаем контейнер
const container = document.querySelector(".container");
// Получаем контент:
const content = document.querySelector(".content");
// 1. Получаем высоту контента, который мы хотим показать/скрыть
const heightOfContent = content.getBoundingClientRect().height;
// Получаем кнопку
const btn = document.querySelector(".trigger");

// 2. Задаем пользовательские свойства CSS с высотой контента
container.style.setProperty("--containerHeight", `${heightOfContent}px`);

// Когда высота задана
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. Переключаем значение 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. Задаем для высоты значение пользовательского свойства
.content[aria-hidden="false"] {
  max-height: var(--containerHeight, 1000px);
}

Как насчет нескольких блоков?

Если на странице есть несколько раскрывающихся блоков, то нужно будет перебрать их все. Но только в том случае, если они разного размера. Для этого используйте querySelectorAll, чтобы получить все контейнеры и повторно задать пользовательские переменные через forEach.

setTimeout

Метод setTimeout с продолжительностью 0 до вывода контейнера используется для первоначального вывода веб-страницы. Это позволяет получить высоту контента.

Когда страница готова

Кроме этого можно обернуть код блока в функцию, которая инициализируется при загрузке страницы. Например:

window.addEventListener("load", initDrawers);

Мы добавим ее в ближайшее время.

Дополнительные атрибуты data для контейнера

Мы добавляем атрибут data тогда, нужно что-то нужно изменить, когда панель открывается / закрывается. Например, цвет какого-то элемента.

Значение по умолчанию для пользовательского свойства

По умолчанию для пользовательского свойства установлено значение 1000px. Оно указывается после запятой внутри значения: var(—containerHeight, 1000px). Вы можете установить другое значение.

Почему бы не использовать значение по умолчанию 10000000px?

Проблема заключается в том, что переход всегда будет выполняться от этой высоты. Если длительность перехода установлена ​​в 1 сек., переход будет выполняться со скоростью 10000000 пикселей в секунду. Если контент имеет высоту всего 50px, то вы получите довольно быстрый эффект открытия / закрытия.

Тернарный оператор для переключателей

Тернарный оператор является укороченной формой if / else. В нем сначала указывается условие, которое нужно проверить. Затем ? отделяет код для выполнения, если true. После : идет код, который будет выполняться, если проверка ложна.

isThisTrue ? doYesCode() : doNoCode();

Что происходит при изменении размера окна браузера?

При изменении размера окна браузера высота контента тоже может измениться. В подобном случае придется повторно установить высоту контейнеров. Для этого можно использовать две функции: одну – для установки высоты, другую – для взаимодействия. А также добавить для окна браузера два прослушивателя: для перехвата события загрузки страницы и события изменения размера.

Повышаем доступность (Accessibility)

Чтобы повысить доступность создаваемой панели, используйте атрибуты aria-expanded, aria-controls и aria-labelledby. Это даст вспомогательным технологиям лучшее представление о том, когда панели будут открыты / раскрыты. Мы добавляем aria-expanded=»false» к кнопке и aria-controls=»IDofcontent», где IDofcontent — это идентификатора контейнера с контентом.

Затем мы используем другой тернарный оператор для переключения в JavaScript атрибута aria-expanded по клику.

Все вместе

Полная версия JavaScript-кода примера:

var containers;
function initDrawers() {
    // Получаем контейнер с контентом
    containers = document.querySelectorAll(".container");
    setHeights();
    wireUpTriggers();
    window.addEventListener("resize", setHeights);
}

window.addEventListener("load", initDrawers);

function setHeights() {
    containers.forEach(container => {
        // Получаем контент
        let content = container.querySelector(".content");
        content.removeAttribute("aria-hidden");
        // Высота контента, который нужно скрыть/показать
        let heightOfContent = content.getBoundingClientRect().height;
        // Задаем пользовательские свойства CSS с высотой контента
        container.style.setProperty("--containerHeight", `${heightOfContent}px`);
        // Когда высота считана и задана
        setTimeout(e => {
            container.classList.add("height-is-set");
            content.setAttribute("aria-hidden", "true");
        }, 0);
    });
}

function wireUpTriggers() {
    containers.forEach(container => {
        // Получаем все элементы-триггеры
        let btn = container.querySelector(".trigger");
        // Получаем контент
        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

Заключение

Возможно, это не самый эффективный метод реализации панелей расширения. Но для большинства ситуаций он подходит.

Данная публикация является переводом статьи «Make Your Own Expanding And Contracting Content Panels» , подготовленная редакцией проекта.

Меню