Как сделать компонент спойлера с помощью Web Components

Я наткнулся на интересное сообщение на WICG (официальное место, где никто не может предложить веб-функции) о необходимости встроенного элемента спойлера. На мой взгляд, это отличная возможность, поскольку она довольно распространена в Интернете, в частности, в приложениях типа доски объявлений. Например, вот два примера из Xenforo, популярной системы досок объявлений:

Toggle Spoiler:

Встроенный спойлер

Первый ведет себя очень похоже на элемент summary/details (но, по крайней мере, в Xenoforo это кнопка и div). Второй — это встроенный спойлер, который, по сути, скрывает текст и раскрывает его при нажатии (это именно то, о чем говорится в сообщении). Я также видел такие вещи, как черный текст с черным выделением, которое меняется при выделении текста.

В посте объясняются границы сводки/деталей, но в основном они сводятся к следующему:

  • Он свернут по какой-то причине, а не просто для экономии места.
  • Визуальная обработка отличается

Это кажется правдоподобным аргументом, но я думаю, что мы должны немного изучить этот вопрос.

Простые реализации HTML

Переключаемый спойлер — это каноническое резюме/детали:

<details>
  <summary>Citizen Kane Spoilers Ahead!</summary>
  Rosebud is the sled
</details>
Войти в полноэкранный режим Выход из полноэкранного режима

Встроенный спойлер может быть текстом со стилем :active:

<style>
.spoiler {
  background: black;
  display: inline-block;
}
.spoiler:active {
  background: transparent;
}
</style>
<p class="spoiler">Rosebud is the sled<p>
Войти в полноэкранный режим Выйти из полноэкранного режима

Без контекста выглядит не очень…

Обратите внимание, что для «выделенного» спойлера обычно достаточно фона того же цвета, но этот работает при нажатии. Однако он не переключается, для этого нам, вероятно, понадобится скрытый флажок и некоторые хакерские приемы с ярлыками, которые, я думаю, будут очень запутанными для читателей с экрана. Также возникает вопрос о доступности клавиатуры. В Summary/Details это встроено, так что вы можете сфокусироваться и активировать нормально. У встроенного спойлера этого нет. Чтобы добавить это, мы можем немного взломать его.

<style>
.spoiler {
  background: black;
  display: inline-block;
}
.spoiler:active,
.spoiler:focus {
  background: transparent;
}
</style>
<p tabindex="0" class="spoiler">Rosebud is the sled<p>
Войти в полноэкранный режим Выйти из полноэкранного режима

Придав ему tabindex, он становится фокусируемым, и мы используем состояние фокуса для его стилизации. Тем не менее, это не переключатель, и может оказаться, что вы не хотите показывать его только на вкладке, так как пользователь может просто перейти по этому пути. И снова я думаю, что это указывает на использование флажка.

Также возникает вопрос о том, как стилизовать это. Фон и передний план должны быть синхронизированы, чтобы он не был затемнен. А что если у нас будут эмодзи или другие разноцветные элементы? Это не сработает. Мы можем подойти к этому более творчески:

<style>
.spoiler {
  filter: blur(10px);
  display: inline-block;
}
.spoiler:active,
.spoiler:focus {
  filter: none;
}
</style>
<p tabindex="0" class="spoiler">Rosebud is the sled<p>
Войти в полноэкранный режим Выйти из полноэкранного режима

Используя фильтр размытия, можно также затемнить цветные элементы. Это классный эффект, который может быть полезен, но его может быть недостаточно. Возможно, мы действительно хотим получить толстый черный маркер. Мы можем добиться этого с помощью умного использования brightness:

.spoiler {
  filter: brightness(0%);
  background: white;
  display: inline-block;
}
.spoiler:active,
.spoiler:focus {
  filter: none;
  background: transparent;
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Таким образом, мы можем получить желаемый визуальный эффект с помощью CSS, пока нас не попросят переключить его. Давайте посмотрим, что для этого нужно:

<style>
.spoiler-4 {
  filter: brightness(0%);
  background: white;
}
.spoiler-4 input[type="checkbox"]{
  display: none;
}
.spoiler-4:has(input:checked){
  filter: none;
  background: white;
}
</style>
<label class="spoiler-4"><input type="checkbox" /> Rosebud is the sled</p>
Войти в полноэкранный режим Выход из полноэкранного режима

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

Или я так думал. В Safari (браузер, в котором сейчас действительно есть :has) есть ошибка, когда при переходе по вкладке к чекбоксу он не фокусируется. Нужно работать над основами, я думаю. Итак, давайте переформулируем это:

<style>
.spoiler-4 {
  filter: brightness(0%);
  background: white;
}
input[type="checkbox"]#spoiler-4{
  clip: rect(0 0 0 0);
  height: 0px;
  width: 0px;
  padding: 0px;
  margin: 0px;
}
input:checked + .spoiler-4{
  filter: none;
  background: white;
}
</style>
<input type="checkbox" id="spoiler-4" />
<label class="spoiler-4" tabindex="0" for="spoiler-4"> Rosebud is the sled </p>
Войти в полноэкранный режим Выход из полноэкранного режима

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

Экранные ридеры

Интересно также спросить, что делают программы для чтения с экрана. И практически для всех из них это немедленно испортит содержимое, потому что оно просто визуально скрыто, но все еще является частью тела (и группы tabindex). Четвертый вариант, как и ожидалось, не дает пользователю никакого представления о том, что происходит, поскольку это случайный флажок. Мы можем дать ему aria-label, чтобы он хотя бы говорил пользователю, для чего он предназначен, но в любом случае скрытое содержимое остается доступным для устройств чтения с экрана, независимо от того, установили они флажок или нет.

Пользовательский элемент

Итак, все это начинает становиться сложным. Думаю, следующим шагом будет создание пользовательского элемента, в котором будет заключено все это. Давайте начнем с шаблона:

class WcSpoiler extends HTMLElement {
    #isShown = false;
    bind(element) {
        this.render = this.render.bind(element);
        this.cacheDom = this.cacheDom.bind(element);
        this.attachEvents = this.attachEvents.bind(element);
    }

    connectedCallback() {
        this.render();
        this.cacheDom();
        this.attachEvents();
    }

    render() {
        this.attachShadow({ mode: "open" });
        this.shadowRoot.innerHTML = `
            <style>
            :host { position: relative; display: inline-block; }
            #label-button {
                width: 100%;
                height: 100%;
                position: absolute;
                top: 0px;
                left: 0px;
                background-color: black;
                border: none;
            }
            #button-text{
                clip: rect(0 0 0 0);
                height: 0px;
                width: 0px;
                padding: 0px;
                margin: 0px;
                overflow: hidden;
            }
            </style>
            <slot></slot>
            <button id="label-button"><div id="button-text">Toggle to read spoiler</div></button>
            `;
    }
    cacheDom() {
        this.dom = {
            labelButton: this.shadowRoot.querySelector("#label-button"),
            buttonText: this.shadowRoot.querySelector("#button-text"),
            slot: this.shadowRoot.querySelector("slot")
        };
    }
    attachEvents() {
        this.dom.labelButton.addEventListener("click", () => {
   //We'll be filling this in
        });
    }
}

customElements.define("wc-spoiler", WcSpoiler);
Войти в полноэкранный режим Выход из полноэкранного режима

Здесь не так много, всего несколько элементов: Слот для хранения внутреннего содержимого спойлера и кнопка для его отображения. Если бы у нас было только это, то программа чтения с экрана считывала бы внутренний HTML элемента wc-spoiler. Чтобы исправить это, мы можем дать слоту aria-hidden="true", а затем, когда мы нажмем на кнопку, она получит aria-hidden="false".

-<slot></slot>
+<slot aria-hidden="true"></slot>
Вход в полноэкранный режим Выйти из полноэкранного режима

Я также добавлю частное свойство для отслеживания состояния, поскольку оно будет использоваться в нескольких местах.

#isShown = false;
attachEvents() {
  this.dom.labelButton.addEventListener("click", () => {
    this.#isShown = !this.#isShown;
    this.dom.slot.ariaHidden = this.#isShown ? "false" : "true";
  });
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы также хотим визуально размыть содержимое. Хороший доступный способ сделать это — объединить состояние aria и CSS. В данном случае у нас есть кнопка переключения, которая представляет собой кнопку с нажатым состоянием.

-<button id="label-button"><div id="button-text">Toggle to read spoiler</div></button>
+<button id="label-button" aria-role="switch"><div id="button-text">Toggle to read spoiler</div></button>
Вход в полноэкранный режим Выход из полноэкранного режима

role="switch" сообщит устройству чтения с экрана, что это то, что мы хотели. Теперь мы можем обновить CSS:

#label-button[aria-pressed="true"] {
  background-color: transparent;
}
Вход в полноэкранный режим Выход из полноэкранного режима

При нажатии мы уберем отредактированную полоску. Чтобы нажать на нее, мы просто добавим это свойство при нажатии:

attachEvents() {
    this.dom.labelButton.addEventListener("click", () => {
        this.#isShown = !this.#isShown;
        this.dom.slot.ariaHidden = this.#isShown ? "false" : "true";
        this.dom.labelButton.ariaPressed = this.#isShown ? "true" : "false";
    });
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Это переключит оба состояния, чтобы программы чтения с экрана могли правильно сообщить об этом пользователю, а также покажет текст спойлера.

Однако при тестировании возникла проблема. Когда вы переключаете спойлер, вы не получаете никакой обратной связи. Он не считывает спойлер, что, по моему мнению, должно происходить. Я попробовал добавить aria-live к элементу и некоторым подэлементам, но это, похоже, не сработало, он не считывается при изменении, возможно, потому что изменение aria-hidden происходит в том же тике, я не уверен. Технология чтения с экрана очень привередлива. Поэтому я решил применить более жесткий подход.

Мы можем использовать определенные живые области, чтобы заставить программу чтения с экрана программно объявлять о происходящем. Сначала мы создадим живую область объявления div:

<div id="live-area" aria-live="polite"></div>
Вход в полноэкранный режим Выход из полноэкранного режима

polite — это хорошее значение по умолчанию, нам не нужно специально прерывать пользователя. Затем при переключении мы запишем textContent элемента спойлера в этот тег:

attachEvents() {
    this.dom.labelButton.addEventListener("click", () => {
        this.#isShown = !this.#isShown;
        this.dom.slot.ariaHidden = this.#isShown ? "false" : "true";
        this.dom.labelButton.ariaPressed = this.#isShown ? "true" : "false";
        this.dom.liveArea.textContent = this.#isShown ? this.textContent : "";
    });
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Это заставит программу чтения с экрана прочитать спойлер после того, как она закончит объявлять состояние кнопки.

Как только содержимое не будет скрыто с помощью aria-hidden="false", программа чтения с экрана будет читать его нормально. Единственное, в чем я не уверен, так это в том, что выбор вкладки действительно имеет смысл, поскольку встроенные спойлеры идут в потоке контента. Это может быть не очень интуитивно понятно. Но, по крайней мере, кажется, что это работает как визуально, так и для считывателей экрана.

Заключение

Если вы делаете свои собственные спойлеры <summary> и <detail> — это самый простой способ переключения спойлеров. Для инлайна это на самом деле довольно сложно. Я потратил много времени на тестирование и до сих пор не совсем уверен, что все работает правильно. Было бы неплохо, если бы платформа включала этот элемент.

Код можно найти здесь:

Оцените статью
Procodings.ru
Добавить комментарий