Поддержка клавиатурной навигации

Клавиатурная навигация — важный аспект доступности и удобства интерфейсов. При использовании библиотеки Locomotive Scroll управление прокруткой осуществляется через виртуальный скролл, что может нарушать стандартное поведение браузера: стрелки, Page Up/Down, Home/End и Tab могут работать некорректно или вовсе игнорироваться.

Корректная поддержка клавиатуры требует явной реализации, поскольку Locomotive Scroll перехватывает события прокрутки и заменяет их собственной логикой.


Проблемы стандартной клавиатурной навигации

При подключении Locomotive Scroll возникают следующие особенности:

  • Отключается нативный scroll контейнера

  • События scroll заменяются внутренними обработчиками

  • Фокус (focus) может не прокручивать страницу к активному элементу

  • Клавиши:

    • ArrowUp / ArrowDown
    • PageUp / PageDown
    • Home / End не приводят к ожидаемому поведению

Причина — использование трансформаций (transform: translate) вместо стандартного скролла.


Базовая настройка обработчиков клавиатуры

Для восстановления функциональности необходимо вручную обрабатывать события keydown и синхронизировать их с API Locomotive Scroll.

Пример инициализации

import LocomotiveScroll from 'locomotive-scroll';

const scroll = new LocomotiveScroll({
  el: document.querySelector('[data-scroll-container]'),
  smooth: true
});

Обработка клавиш прокрутки

Основные клавиши и их поведение

Клавиша Действие
ArrowDown Прокрутка вниз
ArrowUp Прокрутка вверх
PageDown Прокрутка на высоту окна
PageUp Прокрутка вверх на экран
Home В начало страницы
End В конец страницы

Реализация обработчика

window.addEventListener('keydown', (e) => {
  const scrollStep = 100;
  const pageStep = window.innerHeight;

  switch (e.key) {
    case 'ArrowDown':
      scroll.scrollTo(scroll.scroll.instance.scroll.y + scrollStep);
      break;

    case 'ArrowUp':
      scroll.scrollTo(scroll.scroll.instance.scroll.y - scrollStep);
      break;

    case 'PageDown':
      scroll.scrollTo(scroll.scroll.instance.scroll.y + pageStep);
      break;

    case 'PageUp':
      scroll.scrollTo(scroll.scroll.instance.scroll.y - pageStep);
      break;

    case 'Home':
      scroll.scrollTo(0);
      break;

    case 'End':
      scroll.scrollTo(document.body.scrollHeight);
      break;
  }
});

Управление плавностью и инерцией

Locomotive Scroll поддерживает анимацию прокрутки через параметры:

scroll.scrollTo(target, {
  duration: 800,
  easing: [0.25, 0.00, 0.35, 1.00],
  disableLerp: false
});

Ключевые параметры:

  • duration — длительность прокрутки (мс)
  • easing — кривая анимации
  • disableLerp — отключение сглаживания

Для клавиатурной навигации часто используется более короткая длительность:

duration: 400

Обработка фокуса (focus)

При переходе по Tab браузер должен прокручивать страницу к активному элементу. В Locomotive Scroll это не происходит автоматически.

Решение

document.addEventListener('focusin', (e) => {
  const target = e.target;

  if (target && typeof target.getBoundingClientRect === 'function') {
    scroll.scrollTo(target, {
      offset: -100,
      duration: 300
    });
  }
});

Особенности:

  • Используется событие focusin, так как оно всплывает
  • Добавляется отступ (offset) для лучшего позиционирования

Ограничение области обработки

Важно не перехватывать ввод, если пользователь работает с формами.

Проверка активного элемента

window.addEventListener('keydown', (e) => {
  const active = document.activeElement;

  const isInput =
    active.tagName === 'INPUT' ||
    active.tagName === 'TEXTAREA' ||
    active.isContentEditable;

  if (isInput) return;

  // логика прокрутки
});

Поддержка доступности (Accessibility)

Рекомендации:

  • Добавлять tabindex к интерактивным элементам
  • Использовать aria-label, если контент нестандартный
  • Следить за логическим порядком Tab-навигации
  • Обеспечивать визуальный фокус (outline)

Синхронизация с якорными ссылками

При переходе по якорям (#section) стандартный скролл не работает.

Перехват ссылок

document.querySelectorAll('a[href^="#"]').forEach(anchor => {
  anchor.addEventListener('click', function (e) {
    e.preventDefault();

    const target = document.querySelector(this.getAttribute('href'));

    if (target) {
      scroll.scrollTo(target);
    }
  });
});

Обработка вложенных scroll-контейнеров

Если внутри страницы есть элементы с собственной прокруткой (overflow: auto), клавиатурная навигация должна учитывать это.

Подход:

  • Проверять, находится ли фокус внутри scrollable элемента
  • При необходимости не перехватывать события
function isScrollable(el) {
  return el.scrollHeight > el.clientHeight;
}

Оптимизация производительности

Частое нажатие клавиш может вызывать множество анимаций.

Дебаунсинг

let isScrolling = false;

window.addEventListener('keydown', (e) => {
  if (isScrolling) return;

  isScrolling = true;

  scroll.scrollTo(...);

  setTimeout(() => {
    isScrolling = false;
  }, 300);
});

Расширенные сценарии

1. Управление скоростью через Shift

const multiplier = e.shiftKey ? 3 : 1;

2. Кастомные шаги прокрутки

const scrollStep = 80 * multiplier;

3. Интеграция с другими библиотеками

При использовании GSAP или других анимационных библиотек важно:

  • синхронизировать состояние скролла
  • избегать конфликтов с transform

Проверка поведения

Ключевые сценарии тестирования:

  • Навигация стрелками
  • Переход по Tab
  • Работа с формами
  • Якорные ссылки
  • Быстрое нажатие клавиш
  • Работа на разных устройствах

Типичные ошибки

  • Отсутствие проверки activeElement
  • Жестко заданные значения scroll
  • Игнорирование focus
  • Перехват всех клавиш без фильтрации
  • Конфликты с другими обработчиками событий

Архитектурные рекомендации

  • Выносить обработку клавиатуры в отдельный модуль
  • Использовать централизованный контроллер прокрутки
  • Избегать дублирования логики
  • Поддерживать единый API для всех типов навигации

Пример структурированной реализации

class KeyboardScrollController {
  constructor(scroll) {
    this.scroll = scroll;
    this.bindEvents();
  }

  bindEvents() {
    window.addEventListener('keydown', this.handleKey.bind(this));
  }

  handleKey(e) {
    if (this.isInputActive()) return;

    const y = this.scroll.scroll.instance.scroll.y;

    switch (e.key) {
      case 'ArrowDown':
        this.scroll.scrollTo(y + 100);
        break;
      case 'ArrowUp':
        this.scroll.scrollTo(y - 100);
        break;
    }
  }

  isInputActive() {
    const el = document.activeElement;
    return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA';
  }
}
const controller = new KeyboardScrollController(scroll);

Итоговая логика взаимодействия

  • Locomotive Scroll управляет визуальным скроллом
  • Клавиатура управляет API библиотеки
  • Фокус синхронизируется вручную
  • Доступность обеспечивается дополнительной логикой
  • Поведение браузера частично переопределяется, но не должно ломать пользовательский опыт