Popover компонент

Popover — это плавающий элемент интерфейса, который появляется относительно другого элемента, называемого reference element. Библиотека Floating UI предоставляет инструменты для точного позиционирования таких элементов с учётом ограничений видимой области (viewport), коллизий с другими элементами и автоматического обновления позиции при изменении размера или прокрутки.

В основе Popover лежит функция computePosition, которая принимает два основных аргумента: элемент-источник и плавающий элемент, а также объект с опциями. Простейший пример использования:

import { computePosition, offset, flip, shift } from '@floating-ui/dom';

const reference = document.querySelector('#button');
const popover = document.querySelector('#popover');

computePosition(reference, popover, {
  placement: 'bottom-start',
  middleware: [offset(10), flip(), shift({ padding: 5 })],
}).then(({ x, y }) => {
  Object.assign(popover.style, {
    left: `${x}px`,
    top: `${y}px`,
    position: 'absolute',
  });
});

Здесь важно понимать ключевые моменты:

  • placement — направление относительно reference (top, bottom, left, right и их вариации, например, bottom-start).
  • middleware — цепочка функций, которые корректируют позицию для обработки отступов, коллизий и смещения.
  • offset — отступ между reference и popover.
  • flip — автоматически меняет сторону при недостатке места.
  • shift — корректирует позицию так, чтобы popover не выходил за границы видимой области.

Middleware и управление коллизиями

Middleware — это мощный инструмент для управления динамическим поведением Popover. Основные встроенные middleware:

  1. offset: задаёт расстояние между reference и Popover. Может принимать число или функцию, возвращающую значение динамически.
  2. flip: позволяет автоматически менять сторону, если выбранная изначально placement не помещается в viewport.
  3. shift: смещает Popover внутри видимой области, предотвращая обрезку.
  4. arrow: позиционирует стрелку относительно Popover и reference, учитывая размеры элемента и отступы.
  5. size: динамически изменяет размер Popover, чтобы он оставался в границах viewport.

Пример использования arrow middleware:

import { arrow } from '@floating-ui/dom';

const arrowElement = document.querySelector('#arrow');

computePosition(reference, popover, {
  placement: 'top',
  middleware: [offset(10), flip(), shift(), arrow({ element: arrowElement })],
}).then(({ x, y, middlewareData }) => {
  Object.assign(popover.style, {
    left: `${x}px`,
    top: `${y}px`,
  });

  const { x: arrowX, y: arrowY } = middlewareData.arrow;
  Object.assign(arrowElement.style, {
    left: arrowX != null ? `${arrowX}px` : '',
    top: arrowY != null ? `${arrowY}px` : '',
  });
});

Управление состоянием и динамическое обновление

Floating UI позволяет автоматически обновлять позицию Popover, если reference или viewport меняют размеры. Для этого используется функция autoUpdate:

import { autoUpdate } from '@floating-ui/dom';

const cleanup = autoUpdate(reference, popover, () => {
  computePosition(reference, popover, {
    placement: 'bottom',
    middleware: [offset(8), flip(), shift()],
  }).then(({ x, y }) => {
    Object.assign(popover.style, { left: `${x}px`, top: `${y}px` });
  });
});

// Вызов cleanup() прекратит автоматическое обновление

Это особенно важно для динамически изменяющихся интерфейсов, например, при анимации, скролле или изменении размеров окна.

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

Floating UI можно использовать не только с чистым DOM, но и с React, Vue и Svelte. В React есть отдельный пакет @floating-ui/react-dom с хуками, которые упрощают работу с состоянием:

import { useFloating, offset, flip, shift, arrow } from '@floating-ui/react-dom';

function Popover({ referenceRef, open }) {
  const { x, y, reference, floating, strategy, middlewareData } = useFloating({
    placement: 'bottom',
    middleware: [offset(10), flip(), shift(), arrow({ element: arrowRef })],
  });

  return open ? (
    <div ref={floating} style={{ position: strategy, left: x ?? 0, top: y ?? 0 }}>
      Контент Popover
      <div ref={arrowRef} className="arrow" />
    </div>
  ) : null;
}

Здесь хуки автоматически обновляют позицию при изменении размеров и скролле, избавляя от необходимости вручную вызывать computePosition.

Дополнительные рекомендации по Popover

  • Использовать минимальные offset и shift, чтобы Popover выглядел естественно и не «прилипал» к краям viewport.
  • Добавлять анимацию появления, но позицию рассчитывать заранее, чтобы избежать рывков.
  • Обрабатывать закрытие при клике вне Popover — можно сочетать с слушателями событий mousedown и touchstart.
  • Учесть доступность (accessibility): Popover должен быть фокусируемым и управляемым клавишами, особенно при использовании в форме или меню.

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

  • Ограничивать количество middleware. Каждый middleware добавляет вычисления позиции.
  • Использовать autoUpdate только для видимых Popover.
  • При множественных Popover на странице применять shared observers для скролла и resize, чтобы снизить нагрузку на рендеринг.

Popover с Floating UI предоставляет гибкое и высокопроизводительное решение для любых всплывающих элементов, сочетая точное позиционирование, обработку коллизий, стрелки, динамическое обновление и интеграцию с современными фреймворками.