Множественные floating элементы

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

Контекст и родительские элементы

Каждый floating элемент должен быть привязан к reference элементу. В случае множественных всплывающих блоков важно правильно организовать иерархию привязок. Например, если один поповер вложен в другой, его reference элементом будет не DOM-элемент из основной страницы, а элемент внутреннего поповера. Это позволяет библиотеке точно рассчитывать позицию без пересечения с родительским элементом.

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

const parentButton = document.querySelector('#parent-button');
const childButton = document.querySelector('#child-button');
const parentPopover = document.querySelector('#parent-popover');
const childPopover = document.querySelector('#child-popover');

computePosition(parentButton, parentPopover, {
  middleware: [offset(10), flip(), shift({ padding: 5 })],
}).then(({ x, y }) => {
  Object.assign(parentPopover.style, {
    left: `${x}px`,
    top: `${y}px`,
  });
});

computePosition(childButton, childPopover, {
  middleware: [offset(5), flip(), shift({ padding: 5 })],
}).then(({ x, y }) => {
  Object.assign(childPopover.style, {
    left: `${x}px`,
    top: `${y}px`,
  });
});

Middleware для сложных сценариев

Middleware — это ключевой механизм управления поведением floating элементов. При работе с несколькими всплывающими блоками особое внимание стоит уделить следующим:

  • offset — задаёт расстояние между reference элементом и floating.
  • flip — позволяет элементу «переворачиваться» на противоположную сторону, если нет места.
  • shift — корректирует позицию при переполнении границ окна.
  • arrow — автоматически позиционирует стрелку для визуальной связи с reference элементом.

Для множественных элементов часто используется комбинация middleware с разной конфигурацией для каждого уровня вложенности. Это предотвращает наложение и обеспечивает читаемость интерфейса.

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

computePosition(button, tooltip, {
  middleware: [
    offset(8),
    flip(),
    shift({ padding: 8 }),
    arrow({ element: tooltipArrow })
  ],
});

Управление наложениями (z-index)

Когда на странице одновременно присутствуют несколько floating элементов, возникает необходимость контролировать их порядок наложения. Floating UI напрямую не управляет z-index, поэтому стоит внедрять логику динамического назначения слоя:

let zIndexCounter = 1000;

function bringToFront(element) {
  element.style.zIndex = ++zIndexCounter;
}

popover.addEventListener('mouseenter', () => bringToFront(popover));

Такой подход гарантирует, что активный элемент всегда отображается поверх остальных.

Работа с динамическим контентом

Множественные floating элементы часто содержат контент, который может изменяться по высоте или ширине. В таких случаях необходимо пересчитывать позицию при изменении размера:

const observer = new ResizeObserver(() => {
  computePosition(button, popover, {
    middleware: [offset(10), flip(), shift({ padding: 5 })],
  }).then(({ x, y }) => {
    Object.assign(popover.style, { left: `${x}px`, top: `${y}px` });
  });
});

observer.observe(popover);

Это обеспечивает корректное позиционирование даже при динамическом контенте, изменении размеров окна или контекста.

Сложные вложенные сценарии

В случае глубокой вложенности popover’ов рекомендуется:

  1. Использовать отдельные middleware для каждого уровня, чтобы избежать конфликтов при вычислении позиции.
  2. Применять события открытия и закрытия для синхронизации поведения вложенных элементов.
  3. Организовать централизованное управление состоянием, чтобы предотвратить одновременное перекрытие элементов и обеспечить корректную фокусировку.
const openPopovers = [];

function openPopover(popover) {
  popover.style.display = 'block';
  openPopovers.push(popover);
}

function closePopover(popover) {
  popover.style.display = 'none';
  const index = openPopovers.indexOf(popover);
  if (index > -1) openPopovers.splice(index, 1);
}

Производительность

При множественных floating элементах важно минимизировать количество вызовов computePosition, особенно при анимациях или скролле. Рекомендуется:

  • Использовать debounce или throttle для обработчиков событий scroll и resize.
  • Пересчитывать позиции только для видимых элементов.
  • Объединять middleware, чтобы не создавать лишние вычисления.
function throttle(fn, wait) {
  let timeout;
  return (...args) => {
    if (!timeout) {
      timeout = setTimeout(() => {
        fn(...args);
        timeout = null;
      }, wait);
    }
  };
}

window.addEventListener('scroll', throttle(updateAllPopovers, 16));

Практические рекомендации

  • Для интерфейсов с большим количеством всплывающих элементов лучше использовать централизованное управление состоянием, например через объект popoversManager, который хранит все открытые элементы и их reference.
  • Middleware можно переиспользовать, создавая заранее настроенные конфигурации для разных типов элементов (tooltips, dropdowns, modals).
  • Всегда учитывать контекстные ограничения — если элемент находится в scrollable контейнере, позиция должна учитывать его boundaries.

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