Динамические виртуальные элементы

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

В Floating UI виртуальный элемент описывается объектом, который реализует интерфейс VirtualElement. Основные свойства:

  • getBoundingClientRect(): функция, возвращающая объект DOMRect или аналогичный, содержащий координаты и размеры виртуального элемента.
  • contextElement (опционально): элемент DOM, используемый для расчёта относительных координат.

Пример базового виртуального элемента:

const virtualElement = {
  getBoundingClientRect: () => ({
    x: 100,
    y: 150,
    width: 50,
    height: 30,
    top: 150,
    left: 100,
    right: 150,
    bottom: 180,
  }),
};

Такой объект можно передавать в функции computePosition для вычисления позиции всплывающего окна:

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

const floating = document.querySelector('#tooltip');

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

Динамическое обновление координат

Одним из ключевых преимуществ виртуальных элементов является возможность динамически обновлять координаты. Для этого достаточно, чтобы функция getBoundingClientRect возвращала актуальные значения при каждом вызове computePosition. Например:

const dynamicVirtualElement = {
  getBoundingClientRect: () => {
    const mouseX = window.mouseX || 0;
    const mouseY = window.mouseY || 0;
    return {
      x: mouseX,
      y: mouseY,
      width: 0,
      height: 0,
      top: mouseY,
      left: mouseX,
      right: mouseX,
      bottom: mouseY,
    };
  },
};

window.addEventListener('mousemove', (event) => {
  window.mouseX = event.clientX;
  window.mouseY = event.clientY;

  computePosition(dynamicVirtualElement, floating, {
    placement: 'right',
    middleware: [offset(5), flip(), shift({ padding: 8 })],
  }).then(({ x, y }) => {
    Object.assign(floating.style, {
      left: `${x}px`,
      top: `${y}px`,
    });
  });
});

В этом примере всплывающий элемент следит за курсором мыши, при этом не существует в DOM как исходный элемент — позиция рассчитывается исключительно виртуально.

Интеграция с библиотечными компонентами

VirtualElement можно использовать не только для курсора, но и для сложных интерфейсных элементов:

  • Всплывающие меню с переменной длиной элементов: координаты подсчитываются на основе размеров динамически создаваемых пунктов меню.
  • Инструментальные подсказки на графиках: виртуальный элемент может позиционироваться относительно точек на канвасе или SVG, где прямой DOM-ссылки нет.
  • Drag & Drop индикаторы: элемент отображается на позиции курсора, но не связан с самим DOM-элементом.
function createTooltipVirtualElement(target) {
  return {
    getBoundingClientRect: () => target.getBoundingClientRect(),
  };
}

const tooltipFloating = document.querySelector('#tooltip');

targetElement.addEventListener('mouseenter', () => {
  const virtual = createTooltipVirtualElement(targetElement);
  computePosition(virtual, tooltipFloating, {
    placement: 'bottom',
    middleware: [offset(10), flip()],
  }).then(({ x, y }) => {
    Object.assign(tooltipFloating.style, { left: `${x}px`, top: `${y}px` });
  });
});

Сочетание виртуальных элементов с middleware

Floating UI предоставляет мощные middleware для коррекции положения, смещения и адаптации под границы окна. Виртуальные элементы полностью совместимы с этими инструментами:

  • offset(value) — добавляет смещение от виртуального элемента.
  • flip() — автоматически переворачивает положение при нехватке места.
  • shift({ padding }) — сдвигает элемент внутрь видимой области.
  • arrow({ element }) — позиционирует стрелку относительно виртуального элемента.

Комбинируя middleware с динамическими виртуальными элементами, можно создавать интерфейсы с адаптивным поведением, которые корректно реагируют на изменения DOM, размеры окна и взаимодействие пользователя.

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

Динамические виртуальные элементы могут часто пересчитывать позицию. Для улучшения производительности:

  • Использовать requestAnimationFrame вместо прямого вызова на каждый mousemove.
  • Кэшировать размеры виртуальных объектов, если они не изменяются каждое событие.
  • Минимизировать количество middleware при частых обновлениях координат.

Пример оптимизированного обновления:

let rafId;

window.addEventListener('mousemove', (event) => {
  window.mouseX = event.clientX;
  window.mouseY = event.clientY;

  if (rafId) cancelAnimationFrame(rafId);

  rafId = requestAnimationFrame(() => {
    computePosition(dynamicVirtualElement, floating, {
      placement: 'right',
      middleware: [offset(5), flip(), shift({ padding: 8 })],
    }).then(({ x, y }) => {
      Object.assign(floating.style, {
        left: `${x}px`,
        top: `${y}px`,
      });
    });
  });
});

Взаимодействие с порталом и Shadow DOM

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

  • getBoundingClientRect может учитывать трансформации родительских элементов или скролл.
  • Middleware корректно работает даже при изоляции Shadow DOM.

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