Элемент карты в Mapbox GL JS представляет собой canvas, встроенный в DOM-контейнер, который по умолчанию не участвует в системах фокусировки браузера. Для корректной работы клавиатурной навигации и доступности необходимо явно управлять тем, какие элементы могут получать фокус и как он перемещается между слоями интерфейса.
Ключевым моментом является то, что контейнер карты должен быть доступен для фокуса:
mapboxgl.accessToken = 'YOUR_TOKEN';
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [30.3, 59.9],
zoom: 10
});
map.getCanvas().setAttribute('tabindex', '0');
Добавление tabindex="0" делает canvas фокусируемым, что
позволяет пользователю активировать карту с клавиатуры через Tab.
После получения фокуса карта начинает обрабатывать клавиатурные события. Mapbox GL JS предоставляет встроенные привязки:
+ / - — масштабированиеShift + стрелки — наклон и поворот (в зависимости от
настроек)Ctrl + Drag — альтернативное управление камеройДополнительно можно расширять поведение:
map.on('keydown', (e) => {
if (e.originalEvent.key === 'r') {
map.resetNorth();
}
});
Важно учитывать, что любые пользовательские обработчики не должны блокировать стандартные действия без необходимости, иначе нарушается ожидаемая модель навигации.
При кликах вне карты фокус может уходить на другие элементы интерфейса. В сложных интерфейсах требуется контролировать возврат фокуса обратно на карту.
const mapContainer = map.getContainer();
document.addEventListener('click', (e) => {
if (mapContainer.contains(e.target)) {
map.getCanvas().focus();
}
});
Такой подход обеспечивает предсказуемое поведение при повторной активации карты.
Пользовательские элементы управления реализуются через интерфейс
IControl. Каждый контрол добавляется в DOM и должен
самостоятельно обеспечивать доступность.
class ZoomButtonControl {
onAdd(map) {
this._map = map;
this._container = document.createElement('div');
this._container.className = 'mapboxgl-ctrl';
this._button = document.createElement('button');
this._button.textContent = '+';
this._button.addEventListener('click', () => {
map.zoomIn();
});
this._container.appendChild(this._button);
return this._container;
}
onRemove() {
this._container.parentNode.removeChild(this._container);
this._map = undefined;
}
}
Для корректной работы клавиатуры необходимо явно задавать
tabindex и обрабатывать keydown:
this._button.setAttribute('tabindex', '0');
this._button.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
map.zoomIn();
}
});
Маркер по умолчанию представляет собой DOM-элемент. Для обеспечения доступности он должен быть фокусируемым:
const el = document.createElement('div');
el.setAttribute('tabindex', '0');
el.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
popup.addTo(map);
}
});
new mapboxgl.Marker(el)
.setLngLat([30.3, 59.9])
.addTo(map);
Popup-элементы требуют отдельного контроля фокуса. После открытия логично переместить фокус внутрь всплывающего окна:
const popup = new mapboxgl.Popup()
.setHTML('<button id="close">Закрыть</button>')
.setLngLat([30.3, 59.9])
.addTo(map);
popup.getElement().querySelector('#close').focus();
При наличии модальных окон поверх карты необходимо предотвращать уход фокуса в подложку. Распространённый подход — блокировка взаимодействия с canvas через CSS:
.map-disabled {
pointer-events: none;
}
И синхронизация состояния:
map.getContainer().classList.add('map-disabled');
Более строгий вариант включает управление inert:
map.getContainer().setAttribute('inert', '');
При создании панелей инструментов поверх карты часто используется паттерн roving tabindex, при котором только один элемент в группе доступен для Tab.
const items = document.querySelectorAll('.toolbar button');
let current = 0;
items.forEach((item, index) => {
item.setAttribute('tabindex', index === 0 ? '0' : '-1');
item.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight') {
items[current].setAttribute('tabindex', '-1');
current = (current + 1) % items.length;
items[current].setAttribute('tabindex', '0');
items[current].focus();
}
});
});
Этот подход предотвращает перегрузку клавиатурной навигации и упрощает перемещение по контролам.
Фокус часто используется как индикатор активного состояния карты. Например, изменение стиля при активном canvas:
map.getCanvas().addEventListener('focus', () => {
map.getContainer().classList.add('focused');
});
map.getCanvas().addEventListener('blur', () => {
map.getContainer().classList.remove('focused');
});
Это позволяет визуально отделить активное состояние от неактивного, особенно при работе с несколькими интерактивными областями.
В сложных интерфейсах с панелями и всплывающими окнами важно предотвращать потерю контекста при взаимодействии с картой:
map.on('click', () => {
map.getCanvas().focus();
});
Однако избыточное принудительное возвращение фокуса может ломать пользовательские сценарии, поэтому подобные решения должны быть ограничены условиями состояния приложения.
При использовании React или Vue фокус управляется через жизненный цикл компонентов. Основная задача — избегать конфликтов между виртуальным DOM и реальным DOM карты.
useEffect(() => {
if (mapRef.current) {
mapRef.current.getCanvas().setAttribute('tabindex', '0');
}
}, []);
Также важно не пересоздавать DOM-узлы контролов без необходимости, иначе фокус будет теряться.
При вызове setStyle DOM карта может частично
пересоздаваться, что приводит к сбросу фокуса:
map.setStyle('mapbox://styles/mapbox/light-v11');
map.once('styledata', () => {
map.getCanvas().setAttribute('tabindex', '0');
map.getCanvas().focus();
});
Сохранение фокуса в таких случаях требует повторной инициализации атрибутов доступности.
При наличии боковых панелей, модальных окон и карты часто возникает конкуренция за фокус. Стратегия заключается в явном определении активного слоя:
let activeLayer = 'map';
function setActiveLayer(layer) {
activeLayer = layer;
if (layer === 'map') {
map.getCanvas().focus();
}
}
Такой подход обеспечивает централизованное управление состоянием фокуса и предотвращает его «утечку» между компонентами интерфейса.