Клавиатурный доступ и фокус — Примеры
Пример 1 — Кольцо фокуса, которое убрал дизайн
Обводку фокуса по умолчанию удалили, потому что она конфликтовала с дизайном, — и это же правило скрывает кольцо, на которое опирается пользователь. Скажите, что потеряно, и переоформите кольцо вместо того, чтобы его удалять.
.menu-trigger:focus { outline: none; }
.menu { display: none; }
.menu-trigger:hover + .menu { display: block; }
Решение
.menu-trigger:focus-visible {
outline: 3px solid teal;
outline-offset: 2px;
}
.menu { display: none; }
.menu-trigger:hover + .menu,
.menu-trigger:focus-visible + .menu { display: block; }
Кольцо фокуса — курсор пользователя с клавиатуры, поэтому убрать его всё равно что убрать указатель для всех остальных: они печатают в то, что не могут опознать. :focus-visible и есть компромисс — он показывает кольцо, когда элемент достигнут клавиатурой, и обычно скрывает его после клика мышью, — а оформление толстой обводкой со смещением держит его видимым на пёстром фоне. Не менее важно и второе правило. Меню, открывающееся только по наведению, никогда не открывается для пользователя с клавиатуры, поэтому это же содержимое нужно раскрывать по фокусу и закрывать по Escape.
Пример 2 — Диалог, который открывается, не забирая фокус
Диалог показывают, меняя его display, и клавиатура остаётся за ним: скажите, почему диалог только выглядит модальным, и используйте поведение платформы, которое заставляет его вести себя правильно.
function openDialog() {
dialog.style.display = "block"; // ничего не двигается; Tab идёт по странице за ним
}
Решение
function openDialog() {
dialog.showModal(); // переносит фокус, удерживает Tab, Escape закрывает
}
closeButton.addEventListener("click", () => {
dialog.close();
trigger.focus(); // возвращает фокус туда, откуда он пришёл
});
Открытие диалога без переноса фокуса оставляет пользователя табулировать по странице под ним: он не может ни добраться до диалога, ни понять, что тот существует, а Escape ничего не делает. Нативный метод showModal() переносит фокус в диалог, циклически проводит Tab внутри него и закрывает его по Escape — ровно то поведение, которое пришлось бы писать вручную для немодальной имитации; возврат же фокуса к кнопке-триггеру при закрытии — вторая половина: удаление строки, на которой был пользователь, или закрытие меню должны вернуть его туда, где он был, иначе следующий Tab начнёт с начала документа.