Недостатки пользовательских элементов: Семантика и Доступность
📅
Недостатки пользовательских элементов: Семантика и Доступность
Проблема отсутствия наследования
Самый значительный недостаток автономных пользовательских элементов (Autonomous Custom Elements) — они не наследуют семантику, доступность и поведение стандартных HTML-элементов.
В чём заключается проблема? Создав элемент <taco-button>, вы не получаете автоматически все свойства native кнопки (<button>). Для браузеров, поисковых систем и вспомогательных технологий (скринридеров) ваш <taco-button> — просто непонятный блочный элемент. Он не фокусируется, не нажимается по клавише Enter или Пробелу и не объявляется как кнопка для незрячих пользователей.
Что нужно реализовывать вручную
Чтобы <taco-button> стал полноценной кнопкой, разработчик должен самостоятельно реализовать:
1. Доступность (A11y) с помощью ARIA
- Добавить роль
role="button"для скринридеров - Реализовать состояния типа
aria-disabled="true" - Управлять доступным именем через
aria-label
2. Фокусируемость
- Добавить
tabindex="0"для включения в навигацию - Удалять
tabindexпри отключении кнопки
3. Обработка клавиатурных событий
- Прослушивать
keydownдля Enter и Пробела - Преобразовывать их в события
click
4. Визуальные состояния
- Прописывать CSS для всех состояний
- Реализовывать стили для
taco-button[disabled]
5. Логика отключения
- Перехватывать событие
click - Отменять его распространение при
disabled
Пример сложной реализации
class TacoButton extends HTMLElement {
static observedAttributes = ["disabled"];
constructor() {
super();
this._internals = this.attachInternals();
this._internals.role = "button";
// Обработка клавиатурных событий
this.addEventListener("keydown", e => {
if (e.code === "Enter" || e.code === "Space") {
this.dispatchEvent(new PointerEvent("click", {
bubbles: true,
cancelable: true
}));
}
});
// Блокировка кликов при disabled
this.addEventListener("click", e => {
if (this.disabled) {
e.preventDefault();
e.stopImmediatePropagation();
}
});
// Отслеживание изменений текста
this._observer = new MutationObserver(() => {
this._internals.ariaLabel = this.textContent;
});
}
connectedCallback() {
this.setAttribute("tabindex", "0");
this._observer.observe(this, {
childList: true,
characterData: true,
subtree: true
});
}
disconnectedCallback() {
this._observer.disconnect();
}
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(flag) {
this.toggleAttribute("disabled", Boolean(flag));
}
attributeChangedCallback(name, oldValue, newValue) {
if (this.disabled) {
this.removeAttribute("tabindex");
this._internals.ariaDisabled = "true";
} else {
this.setAttribute("tabindex", "0");
this._internals.ariaDisabled = "false";
}
}
}
Проблемы качества разработки (DX)
Даже после сложной реализации элемент остается неудобным:
- "Магическое" поведение: Самопроизвольное изменение tabindex
- Жесткая логика: Фиксированное поведение фокусировки
- Сложность поддержки: Требуется глубокое понимание внутренней механики
Гораздо более простой и правильный путь — использование пользовательских встроенных элементов (Customized Built-in Elements), которые наследуются от стандартных HTML-элементов. Об этом и поговорим в следующем уроке.