Focus management

Фокус в интерфейсах веб-карт играет ключевую роль в доступности и предсказуемости взаимодействия. В MapLibre GL JS карта рендерится в <canvas>, который по умолчанию не обладает семантической структурой DOM, поэтому управление фокусом требует явного контроля через API и корректную настройку контейнеров. Основная задача заключается в том, чтобы обеспечить логичный переход фокуса между картой, элементами управления, попапами и внешними компонентами интерфейса.

Карта в MapLibre GL JS инициализируется внутри DOM-элемента, который содержит canvas. Для того чтобы карта могла получать фокус, необходимо учитывать свойства контейнера и самого canvas:

  • контейнер карты должен быть доступен для фокуса через tabindex
  • canvas должен корректно реагировать на события клавиатуры
  • фокус должен программно возвращаться при необходимости после взаимодействий

Ключевой метод управления:

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 и поддерживает клавиатурные события.

Контроль tab order внутри карты

В сложных интерфейсах карта часто соседствует с панелями фильтров, списками объектов и модальными окнами. В таких случаях важно управлять порядком табуляции.

Практика:

  • при открытии панели отключать фокусировку карты
  • при закрытии возвращать фокус обратно
  • избегать попадания внутренних элементов карты в основной tab flow страницы

Отключение взаимодействия:

map.scrollZoom.disable();
map.boxZoom.disable();
map.dragPan.disable();

Хотя это не напрямую связано с фокусом, это влияет на поведение клавиатурной навигации.

Работа с aria-атрибутами и доступностью

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();
  }
});

Это предотвращает потерю управления картой при программных перемещениях.

Предотвращение конфликтов фокуса при слоях интерфейса

При наличии сайдбаров, фильтров и всплывающих панелей важно учитывать, что карта не должна перехватывать фокус, если пользователь работает с формой.

Типичный подход:

  • при фокусе на input — карта временно игнорирует keyboard events
  • при выходе из input — управление возвращается карте
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;
}

Это помогает пользователю понять, что карта активна и готова к управлению с клавиатуры.

Фокус и события pointer/keyboard

В MapLibre GL JS часто возникает ситуация, когда pointer-события и keyboard-события конкурируют за управление состоянием.

Рекомендуемая логика:

  • pointermove и click не должны сбрасывать фокус
  • keyboard events активируются только при наличии фокуса на canvas
map.getCanvas().addEventListener('pointerdown', () => {
  map.getCanvas().focus();
});

Это обеспечивает предсказуемое переключение между мышью и клавиатурой.

Интеграция с внешними фреймворками

При использовании React или Vue управление фокусом часто становится частью жизненного цикла компонентов. Важно избегать пересоздания карты без сохранения состояния фокуса.

Подход:

  • хранить ref на контейнер карты
  • восстанавливать фокус после re-render
  • не пересоздавать map instance без необходимости
useEffect(() => {
  return () => {
    if (map) map.remove();
  };
}, []);

Фокус при этом должен управляться отдельно от жизненного цикла карты, а не зависеть от него напрямую.