Доступные веб-компоненты

Граница теневого дерева меняет работу семантики и ARIA. Преобладают две ошибки: отсутствие семантики внутри теневого дерева и ссылки ARIA, которые молча ломаются, поскольку не могут пересекать теневые корни.

Начните с семантической разметки: используйте в шаблоне настоящие элементы <button>, <nav>, <ul>, чтобы получить большую часть семантики, и заполните пробелы с помощью ElementInternals/ARIA.

Семантика по умолчанию через миксин ElementInternals ARIA

Задайте роль и состояние ARIA для пользовательского элемента в JS с помощью ElementInternals, а не записывайте атрибуты ARIA на хосте. Это позволяет сохранить семантику по умолчанию, которой владеет компонент, и в то же время позволяет потребителю переопределить ее с помощью атрибутов в Light DOM.

class ToggleSwitch extends HTMLElement {
  #internals;
  constructor() {
    super();
    this.#internals = this.attachInternals();
    this.#internals.role = 'switch';          // default role, no host attribute needed
    this.#internals.ariaChecked = 'false';    // exposed to the accessibility tree
    this.addEventListener('click', () => this.toggle());
    this.addEventListener('keydown', (e) => {
      if (e.key === ' ') { e.preventDefault(); this.toggle(); }
    });
  }
  connectedCallback() {
    // Put the host in the tab order on connect (setting tabIndex in the constructor throws on createElement).
    if (!this.hasAttribute('tabindex')) this.tabIndex = 0;
  }
  toggle() {
    const on = this.#internals.ariaChecked !== 'true';
    this.#internals.ariaChecked = String(on);
  }
}
customElements.define('toggle-switch', ToggleSwitch);

Это позволяет избежать загрязнения DOM атрибутами ARIA и предотвращает их случайное повреждение потребителями, в то время как дерево специальных возможностей по-прежнему видит правильную роль и состояние.

Управление фокусом и delegatesFocus

  • Если сам хост-элемент является интерактивным элементом управления и передает роль и состояние через ElementInternals (как ToggleSwitch выше), поместите хост в порядок табуляции внутри connectedCallback (if (!this.hasAttribute('tabindex')) this.tabIndex = 0 – никогда в конструкторе, который выдает ошибку в document.createElement), чтобы фокус попадал на хост, а технология специальных возможностей объявляла его роль и состояние. Не устанавливайте tabindex на хосте для статических неинтерактивных компонентов (например, выноски или карточки).
  • Прикрепите корневой элемент Shadow DOM с помощью delegatesFocus: true, если компонент содержит внутренний элемент управления, на который можно установить фокус и который определяет семантику (или в пользовательском элементе, связанном с формой, с помощью static formAssociated = true). При установке фокуса на главный элемент (и нажатии на связанный элемент <label> для элементов, связанных с формой) фокус будет перенаправлен на первый дочерний элемент, на который можно установить фокус, а :focus будет соответствовать главному элементу. Не используйте delegatesFocus: true вместе с ролью или состоянием хоста, иначе фокус будет перемещен на внутренний элемент, а состояние хоста будет пропущено.
  • Чтобы переместить фокус на новый элемент или область, задайте для целевого элемента атрибут tabindex="-1" и вызовите метод .focus(). Не полагайтесь на порядок перехода по клавише Tab, а явно указывайте элементы, на которые можно переместить фокус программно.

Отношения ARIA между теневыми корнями

  • ОБЯЗАТЕЛЬНО. Элемент id, на который ссылаются элементы aria-labelledby, aria-describedby, aria-controls и т. д., разрешается только в пределах собственного дерева ссылающегося элемента. Атрибут на основе id в Light DOM не может указывать на id внутри теневого дерева, и наоборот. Ссылка будет игнорироваться без уведомления.
  • Чтобы пересечь границу, используйте отраженные свойства элемента ARIA (например, ariaLabelledByElements, ariaDescribedByElements и т.д.), которые принимают прямые ссылки на элементы JS вместо идентификаторов. Они разрешаются наружу: элемент внутри дерева Shadow DOM может указывать на цель в Light DOM или любом дереве предков (включая контент в слотах), поэтому вы можете пометить внутренний элемент управления Shadow DOM из контента Light DOM – то, что ссылки id не могут выразить.
  • Они не разрешаются внутрь: элемент не может ссылаться на дочернее теневое дерево (например, хост не может быть помечен собственным теневым контентом). В этом направлении развивается механизм Reference Target, но он пока не получил широкого распространения.
  • Если связь нельзя выразить ни одним из способов, оставьте ее в одном дереве (отрисуйте ярлык в том же дереве) или используйте текст aria-label вместо ссылки id.

Резервные стратегии

Если примесь ARIA ElementInternals недоступна, задайте роль и состояние ARIA в качестве атрибутов хоста (this.setAttribute('role', 'switch')). Это работает везде, но в DOM добавляются атрибуты, которые пользователь может переопределить.

Если отраженные свойства элемента ARIA недоступны, используйте атрибут aria-* на основе id для связей в одном дереве. У ярлыка, пересекающего границы, нет эквивалента на основе id, поэтому используйте текст aria-label.