Граница теневого дерева меняет работу семантики и 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.