Виртуальная клавиатура

Tom Select в мобильной среде ведёт себя принципиально иначе, чем в десктопных браузерах, прежде всего из-за работы виртуальной клавиатуры и динамического изменения viewport. В момент фокусировки на поле ввода браузер изменяет высоту видимой области, пересчитывает геометрию страницы и часто инициирует автоматический скролл к активному элементу. Для компонентов с выпадающими списками это приводит к смещению координат, обрезанию dropdown-меню и потере синхронизации между инпутом и позиционированием панели выбора.

На iOS и Android открытие клавиатуры не является простым overlay-слоем. Система изменяет размеры viewport, что приводит к:

  • уменьшению высоты visible area;
  • пересчёту window.innerHeight;
  • срабатыванию событий resize (не всегда стабильно);
  • изменению visualViewport.height (более надёжный источник);
  • автоматическому скроллу к активному input.

В контексте Tom Select это критично, поскольку dropdown позиционируется относительно control элемента. При изменении viewport без пересчёта координат список может оказаться:

  • перекрытым клавиатурой;
  • смещённым вверх за пределы экрана;
  • обрезанным контейнером с overflow: hidden;
  • визуально «оторванным» от input.

Использование VisualViewport API

Современный подход к обработке виртуальной клавиатуры основан на window.visualViewport. Он позволяет отслеживать реальные изменения видимой области:

  • visualViewport.height — текущая высота области просмотра;
  • visualViewport.offsetTop — смещение viewport;
  • событие resize — изменение размеров при открытии клавиатуры;
  • событие scroll — изменение позиции при фиксации input.

В интеграции с Tom Select логика пересчёта позиции dropdown должна привязываться именно к этим событиям, а не только к window.resize.

const vv = window.visualViewport;

vv.addEventListener('resize', () => {
  tomSelectInstance.refreshOptions(false);
  tomSelectInstance.positionDropdown();
});

Такой подход минимизирует рассинхронизацию между реальным положением input и визуальным размещением dropdown.

Поведение dropdown при фокусе input

При активации поля ввода внутри Tom Select происходит цепочка событий:

  1. focus на hidden input или control input;
  2. открытие dropdown (open() или onFocus);
  3. вычисление позиции через bounding rect;
  4. установка абсолютного позиционирования списка.

В момент появления виртуальной клавиатуры getBoundingClientRect() начинает возвращать устаревшие координаты, если не выполнить повторный расчёт.

Ключевой механизм стабилизации — принудительный reflow позиционирования:

onFocus() {
  this.isFocused = true;

  requestAnimationFrame(() => {
    this.positionDropdown();
  });
}

Проблема автоскролла и «прыгающего» интерфейса

Мобильные браузеры стремятся автоматически прокрутить страницу так, чтобы активный input оказался в центре экрана. Это приводит к конфликту с позиционированием dropdown:

  • страница уезжает вверх;
  • dropdown остаётся на старой координате;
  • возникает эффект «разрыва» UI.

Особенно это заметно в длинных формах, где Tom Select используется внутри scroll-контейнеров.

Решение заключается в комбинировании:

  • фиксации scroll контейнера;
  • отключения нежелательного scrollIntoView;
  • ручного управления фокусом.
controlInput.addEventListener('focus', (e) => {
  e.preventDefault();
});

В некоторых сценариях требуется также временно блокировать scroll страницы:

body.keyboard-open {
  overflow: hidden;
}

IME и composition events

Виртуальная клавиатура на мобильных устройствах почти всегда использует Input Method Editor (IME), особенно для языков с сложной письменностью. Это влияет на обработку ввода:

  • keydown не всегда содержит финальный символ;
  • ввод происходит через compositionstart, compositionupdate, compositionend;
  • значение input обновляется асинхронно.

Tom Select использует внутренний input для поиска и фильтрации опций, поэтому игнорирование composition событий приводит к:

  • преждевременной фильтрации;
  • пропущенным символам;
  • некорректному search query.

Корректная схема обработки:

let isComposing = false;

input.addEventListener('compositionstart', () => {
  isComposing = true;
});

input.addEventListener('compositionend', (e) => {
  isComposing = false;
  instance.setTextboxValue(e.target.value);
  instance.refreshOptions(false);
});

Фильтрация должна выполняться только после завершения composition.

Реакция на изменение высоты viewport

Изменение высоты окна при открытии клавиатуры требует повторного позиционирования dropdown. В противном случае список остаётся на месте, визуально «уезжая» за пределы экрана.

Практическая стратегия:

  • отслеживание visualViewport.resize;
  • проверка разницы высоты;
  • условный пересчёт позиции.
let lastHeight = window.visualViewport.height;

window.visualViewport.addEventListener('resize', () => {
  const currentHeight = window.visualViewport.height;

  if (Math.abs(currentHeight - lastHeight) > 50) {
    instance.positionDropdown();
  }

  lastHeight = currentHeight;
});

Пороговое значение позволяет игнорировать микролаги интерфейса.

Контроль scroll внутри контейнеров

Если Tom Select находится внутри блока с overflow: auto, открытие клавиатуры может привести к тому, что контейнер автоматически прокрутится, скрывая dropdown.

Решение включает:

  • фиксацию scroll позиции контейнера;
  • временное отключение scroll chaining;
  • использование overscroll-behavior.
.scroll-container {
  overscroll-behavior: contain;
}

Также может применяться сохранение позиции:

const scrollTop = container.scrollTop;

setTimeout(() => {
  container.scrollTop = scrollTop;
}, 0);

Поведение blur при закрытии клавиатуры

Закрытие виртуальной клавиатуры не всегда инициирует blur события. Это приводит к ситуации, когда dropdown остаётся открытым, хотя пользователь уже не взаимодействует с input.

В таких случаях используется комбинация:

  • focusout;
  • visibilitychange;
  • проверка document.activeElement.
document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    instance.close();
  }
});

Проблема масштабирования на iOS

iOS Safari автоматически масштабирует страницу при фокусе на input с font-size меньше 16px. Это вызывает:

  • скачок интерфейса;
  • изменение координат dropdown;
  • потерю контекста позиционирования.

Для стабильной работы Tom Select необходимо:

  • устанавливать font-size: 16px или выше;
  • избегать transform-scale на родителях;
  • контролировать viewport meta:
<meta name="viewport" content="width=device-width, initial-scale=1">

Синхронизация состояния dropdown с клавиатурой

Стабильная интеграция требует связывания жизненного цикла dropdown с состоянием клавиатуры:

  • клавиатура открыта → dropdown пересчитывается;
  • клавиатура закрыта → восстановление позиции;
  • изменение ориентации → полный recalculation.

Рекомендуемая модель состояния:

  • keyboardOpen
  • viewportHeight
  • dropdownOpen

Любое изменение одного из параметров должно триггерить перерасчёт позиции.

Производительные ограничения

Частые вызовы positionDropdown() могут вызывать layout thrashing. Особенно при resize событий виртуальной клавиатуры, которые могут происходить сериями.

Оптимизация включает:

  • throttling через requestAnimationFrame;
  • отказ от лишних DOM read/write в одном цикле;
  • кеширование bounding rect.
let ticking = false;

function update() {
  if (!ticking) {
    requestAnimationFrame(() => {
      instance.positionDropdown();
      ticking = false;
    });
    ticking = true;
  }
}

window.visualViewport.addEventListener('resize', update);

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