Фокус в интерфейсах веб-карт играет ключевую роль в доступности и
предсказуемости взаимодействия. В MapLibre GL JS карта рендерится в
<canvas>, который по умолчанию не обладает
семантической структурой DOM, поэтому управление фокусом требует явного
контроля через API и корректную настройку контейнеров. Основная задача
заключается в том, чтобы обеспечить логичный переход фокуса между
картой, элементами управления, попапами и внешними компонентами
интерфейса.
Карта в MapLibre GL JS инициализируется внутри DOM-элемента, который содержит canvas. Для того чтобы карта могла получать фокус, необходимо учитывать свойства контейнера и самого canvas:
tabindexКлючевой метод управления:
map.getCanvas().focus();
Этот вызов переводит фокус непосредственно на canvas, что важно при возвращении пользователя к карте после взаимодействия с модальными окнами или панелями.
Дополнительно контейнер можно настроить:
const mapContainer = document.getElementById('map');
mapContainer.setAttribute('tabindex', '0');
Такой подход позволяет включать карту в естественный порядок табуляции интерфейса.
MapLibre GL JS поддерживает базовые клавиатурные взаимодействия: перемещение карты стрелками, масштабирование, вращение (при включённой конфигурации). Однако управление фокусом определяет, когда эти действия активны.
События:
focus — карта активируетсяblur — карта теряет активностьПример обработки:
map.on('focus', () => {
map.getContainer().classList.add('map-focused');
});
map.on('blur', () => {
map.getContainer().classList.remove('map-focused');
});
Это позволяет визуально обозначать состояние активности и синхронизировать стили интерфейса.
Попапы являются одним из основных источников нарушения логики фокуса. При открытии попапа фокус должен переходить внутрь него, а при закрытии — возвращаться к исходному элементу.
Базовый сценарий:
const button = document.getElementById('marker-button');
button.addEventListener('click', () => {
const popup = new maplibregl.Popup()
.setLngLat([30, 50])
.setHTML('<button id="close-popup">Закрыть</button>')
.addTo(map);
const closeBtn = document.getElementById('close-popup');
closeBtn.focus();
closeBtn.addEventListener('click', () => {
popup.remove();
button.focus();
});
});
Такой подход предотвращает потерю контекста взаимодействия и обеспечивает предсказуемую навигацию.
Контролы MapLibre GL JS реализуются через интерфейс
IControl, где DOM-элементы создаются вручную. Для
обеспечения доступности каждый интерактивный элемент должен быть
фокусируемым.
Пример структуры:
class CustomControl {
onAdd(map) {
this._map = map;
this._container = document.createElement('div');
this._container.className = 'maplibre-ctrl';
this._button = document.createElement('button');
this._button.textContent = 'Сброс';
this._button.addEventListener('click', () => {
map.resetNorthPitch();
});
this._container.appendChild(this._button);
return this._container;
}
onRemove() {
this._container.parentNode.removeChild(this._container);
this._map = undefined;
}
}
Использование <button> вместо
<div> автоматически решает часть проблем с фокусом,
так как кнопка уже участвует в tab order и поддерживает клавиатурные
события.
В сложных интерфейсах карта часто соседствует с панелями фильтров, списками объектов и модальными окнами. В таких случаях важно управлять порядком табуляции.
Практика:
Отключение взаимодействия:
map.scrollZoom.disable();
map.boxZoom.disable();
map.dragPan.disable();
Хотя это не напрямую связано с фокусом, это влияет на поведение клавиатурной навигации.
Canvas не предоставляет семантики, поэтому необходимо компенсировать это через контейнер:
<div id="map" role="application" aria-label="Интерактивная карта"></div>
Роль application сообщает вспомогательным технологиям,
что внутри находится сложный интерактивный компонент, где стандартная
навигация может быть переопределена.
При наличии элементов управления:
<button aria-label="Увеличить масштаб">+</button>
Каждый элемент должен иметь явное текстовое или ARIA-описание.
Модальные окна часто нарушают цепочку фокуса, если не реализован механизм возврата.
Шаблон управления:
let lastFocusedElement = null;
function openModal() {
lastFocusedElement = document.activeElement;
const modal = document.getElementById('modal');
modal.style.display = 'block';
modal.querySelector('button').focus();
}
function closeModal() {
const modal = document.getElementById('modal');
modal.style.display = 'none';
if (lastFocusedElement) {
lastFocusedElement.focus();
}
}
В контексте MapLibre GL JS это особенно важно, если модальное окно открывается из маркера или всплывающего слоя.
Маркер в MapLibre GL JS может быть представлен как DOM-элемент, что позволяет управлять фокусом напрямую:
const el = document.createElement('button');
el.className = 'marker';
el.setAttribute('aria-label', 'Точка на карте');
el.addEventListener('click', () => {
map.flyTo({ center: [30, 50], zoom: 10 });
});
new maplibregl.Marker(el)
.setLngLat([30, 50])
.addTo(map);
Использование button вместо div делает
маркер доступным для клавиатурного взаимодействия без дополнительной
логики.
В некоторых интерфейсах требуется, чтобы фокус возвращался на карту после завершения анимаций или переходов:
map.on('moveend', () => {
if (document.activeElement.tagName !== 'INPUT') {
map.getCanvas().focus();
}
});
Это предотвращает потерю управления картой при программных перемещениях.
При наличии сайдбаров, фильтров и всплывающих панелей важно учитывать, что карта не должна перехватывать фокус, если пользователь работает с формой.
Типичный подход:
document.querySelectorAll('input, textarea').forEach(el => {
el.addEventListener('focus', () => map.keyboard.disable());
el.addEventListener('blur', () => map.keyboard.enable());
});
Поскольку canvas не имеет встроенного outline, визуальная индикация должна задаваться через контейнер:
.map-focused {
outline: 2px solid #4a90e2;
}
Это помогает пользователю понять, что карта активна и готова к управлению с клавиатуры.
В MapLibre GL JS часто возникает ситуация, когда pointer-события и keyboard-события конкурируют за управление состоянием.
Рекомендуемая логика:
map.getCanvas().addEventListener('pointerdown', () => {
map.getCanvas().focus();
});
Это обеспечивает предсказуемое переключение между мышью и клавиатурой.
При использовании React или Vue управление фокусом часто становится частью жизненного цикла компонентов. Важно избегать пересоздания карты без сохранения состояния фокуса.
Подход:
useEffect(() => {
return () => {
if (map) map.remove();
};
}, []);
Фокус при этом должен управляться отдельно от жизненного цикла карты, а не зависеть от него напрямую.