Поведение клавиши Tab в интерактивных веб-картах
напрямую влияет на доступность интерфейса и предсказуемость навигации. В
контексте Mapbox GL JS порядок фокусировки определяется сочетанием
DOM-структуры, атрибутов tabindex, встроенных контролов и
пользовательских компонентов, добавляемых поверх карты.
Карта в Mapbox фактически является сложным слоем DOM-элементов, где часть объектов управляется библиотекой, а часть создаётся разработчиком. Поэтому порядок перехода фокуса не является фиксированным и требует явного контроля.
Корневой контейнер карты создаётся как обычный div,
который по умолчанию не участвует в табуляции:
<div id="map"></div>
Чтобы карта стала доступной для клавиатурной навигации, контейнеру
задаётся tabindex:
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12'
});
map.getContainer().tabIndex = 0;
Значение tabindex = 0 означает:
Без этого шага фокус никогда не попадёт внутрь карты, даже если внутри есть интерактивные элементы.
Внутри Mapbox GL JS существует несколько категорий элементов, влияющих на tab order:
Элементы управления, такие как зум, компас и геолокация:
map.addControl(new mapboxgl.NavigationControl());
Каждая кнопка внутри контролов автоматически получает
tabindex="0" и добавляется в общий поток фокуса после
контейнера карты.
Порядок обычно выглядит так:
Этот порядок формируется DOM-порядком внутри control container.
Стандартные маркеры:
new mapboxgl.Marker()
.setLngLat([30, 50])
.addTo(map);
По умолчанию маркеры:
Чтобы включить их в обход клавишей Tab:
const el = document.createElement('div');
el.tabIndex = 0;
new mapboxgl.Marker(el)
.setLngLat([30, 50])
.addTo(map);
После этого маркер становится частью последовательности фокуса и упорядочивается согласно DOM-позиции слоя маркеров.
Popup-элементы создают временные контейнеры:
new mapboxgl.Popup()
.setLngLat([30, 50])
.setHTML('<button>Действие</button>')
.addTo(map);
Фокус внутри popup зависит от структуры HTML:
Tab order внутри карты подчиняется общим правилам браузера, но усложняется из-за вложенности слоёв:
Последовательность:
map container
controls
markers
popups
Фокус движется строго по DOM, а не по географическому положению объектов.
Несмотря на визуальное перекрытие слоёв:
z-index влияет только на отрисовкуЭто часто приводит к несоответствию визуального и логического порядка.
При добавлении новых маркеров или контролов:
map.on('load', () => {
addMarkers();
});
новые элементы вставляются в конец соответствующего контейнера, изменяя tab order без дополнительной сортировки.
При создании кастомных контролов используется интерфейс:
class CustomControl {
onAdd(map) {
this.container = document.createElement('div');
this.container.className = 'custom-control';
const button = document.createElement('button');
button.textContent = 'Action';
this.container.appendChild(button);
return this.container;
}
onRemove() {
this.container.parentNode.removeChild(this.container);
}
}
Чтобы контрол корректно участвовал в tab order:
this.container.tabIndex = 0;
или лучше:
button.tabIndex = 0;
Второй вариант предпочтительнее, поскольку:
При сложных интерфейсах может возникать необходимость контролировать переход фокуса:
map.getContainer().addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
console.log('Tab внутри карты');
}
});
Однако перехват Tab требует осторожности, поскольку:
Иногда требуется удерживать фокус внутри карты (modal-like behavior):
map.getContainer().addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
const focusable = map.getContainer().querySelectorAll(
'button, [href], input, [tabindex="0"]'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
}
if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
});
Такая логика создаёт циклический tab order внутри карты.
Визуальные слои карты (raster, vector, fill, line, symbol) не являются DOM-элементами. Поэтому:
Пример проблемы:
Решение — синхронизация слоя с DOM-маркером или скрытым контролом.
Хотя tab order и tabindex управляют последовательностью,
доступность дополняется ARIA-атрибутами:
<button aria-label="Zoom in map"></button>
или:
button.setAttribute('aria-label', 'Open marker details');
Это влияет не на порядок Tab, а на читаемость интерфейса скринридерами, которые следуют тому же DOM-порядку, что и клавиатурная навигация.
Добавление маркеров после загрузки может переместить их в конец tab order, что не соответствует логике карты.
Если tabindex задан только контейнеру карты,
пользователю приходится вручную входить внутрь интерфейса.
Перерендер popup может сбрасывать активный элемент и выводить фокус за пределы карты.
tabindex только интерактивным элементамtabindex > 0, чтобы не ломать естественный
порядокЕсли добавлено несколько контролов:
map.addControl(new mapboxgl.NavigationControl());
map.addControl(new mapboxgl.FullscreenControl());
tab order будет зависеть от порядка добавления:
Изменение порядка добавления — единственный надёжный способ управления последовательностью без переопределения DOM.
При наличии панели рядом с картой:
<div class="sidebar">
<button>Фильтр</button>
</div>
<div id="map"></div>
Tab order строится по DOM:
Это создаёт необходимость учитывать структуру страницы целиком, а не только карту.