Что такое виртуальный референс

В библиотеке Floating UI под референсом понимается объект, относительно которого вычисляется позиция плавающего элемента (tooltip, dropdown, popover и т.д.). Обычно это DOM-элемент, например кнопка или ссылка. Однако существуют ситуации, когда привязка к реальному элементу невозможна или нецелесообразна. Для таких случаев используется виртуальный референс (virtual element).

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


Когда используется виртуальный референс

Использование виртуального референса оправдано в следующих сценариях:

  • Контекстное меню по координатам курсора
  • Подсказки при выделении текста
  • Привязка к произвольной точке на экране
  • Работа с canvas или WebGL
  • Системы drag-and-drop
  • Редакторы (например, позиционирование панели форматирования)

В этих случаях отсутствует конкретный DOM-узел, но есть координаты, к которым нужно привязать интерфейсный элемент.


Интерфейс виртуального элемента

Виртуальный референс должен реализовывать метод:

getBoundingClientRect(): DOMRect

Этот метод возвращает объект, аналогичный Element.getBoundingClientRect(), содержащий:

  • x, y
  • top, left, right, bottom
  • width, height

Минимальный пример:

const virtualElement = {
  getBoundingClientRect() {
    return {
      x: 100,
      y: 200,
      top: 200,
      left: 100,
      right: 100,
      bottom: 200,
      width: 0,
      height: 0,
    };
  },
};

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

Функция computePosition принимает виртуальный элемент так же, как обычный DOM-элемент:

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

computePosition(virtualElement, floatingElement).then(({ x, y }) => {
  Object.assign(floatingElement.style, {
    left: `${x}px`,
    top: `${y}px`,
  });
});

Floating UI не делает различий между реальным и виртуальным референсом — важен только контракт метода getBoundingClientRect.


Пример: позиционирование по курсору

Один из самых распространённых кейсов — отображение tooltip рядом с курсором:

let x = 0;
let y = 0;

const virtualElement = {
  getBoundingClientRect() {
    return {
      x,
      y,
      top: y,
      left: x,
      right: x,
      bottom: y,
      width: 0,
      height: 0,
    };
  },
};

document.addEventListener('mousemove', (event) => {
  x = event.clientX;
  y = event.clientY;

  computePosition(virtualElement, tooltip).then(({ x, y }) => {
    Object.assign(tooltip.style, {
      left: `${x}px`,
      top: `${y}px`,
    });
  });
});

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


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

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

  • положение курсора
  • координаты выделенного текста
  • позиция в canvas
  • результат вычислений

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


Работа с выделением текста

При работе с текстом используется API Range:

const selection = window.getSelection();
const range = selection.getRangeAt(0);

const virtualElement = {
  getBoundingClientRect() {
    return range.getBoundingClientRect();
  },
};

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


Поддержка middleware

Виртуальные элементы полностью совместимы с middleware Floating UI:

  • offset
  • flip
  • shift
  • arrow
  • autoPlacement

Пример:

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

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

Ограничения виртуальных референсов

Несмотря на гибкость, есть ряд особенностей:

  • Отсутствие layout-контекста Floating UI не может учитывать родительские контейнеры, прокрутку и overflow так же точно, как с DOM-элементами.

  • Нет автоматического отслеживания изменений Нужно вручную вызывать перерасчёт позиции при изменении координат.

  • Отсутствие взаимодействия с ResizeObserver и IntersectionObserver


Интеграция с autoUpdate

Для автоматического обновления позиции используется autoUpdate, но с виртуальными элементами требуется явное управление:

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

const cleanup = autoUpdate(
  virtualElement,
  floatingElement,
  () => {
    computePosition(virtualElement, floatingElement).then(({ x, y }) => {
      Object.assign(floatingElement.style, {
        left: `${x}px`,
        top: `${y}px`,
      });
    });
  }
);

Однако обновления всё равно зависят от внешних событий (например, mousemove).


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

Привязка к элементу внутри canvas

const virtualElement = {
  getBoundingClientRect() {
    const { x, y } = getCanvasCoordinates();
    return {
      x,
      y,
      top: y,
      left: x,
      right: x,
      bottom: y,
      width: 0,
      height: 0,
    };
  },
};

Drag-and-drop интерфейсы

Во время перетаскивания можно использовать текущую позицию курсора или объекта:

let dragPosition = { x: 0, y: 0 };

const virtualElement = {
  getBoundingClientRect() {
    return {
      ...dragPosition,
      top: dragPosition.y,
      left: dragPosition.x,
      right: dragPosition.x,
      bottom: dragPosition.y,
      width: 0,
      height: 0,
    };
  },
};

Ключевые особенности

  • Не требует наличия DOM-элемента
  • Работает через геометрическое описание
  • Полностью совместим с API Floating UI
  • Позволяет реализовывать сложные интерактивные сценарии
  • Даёт полный контроль над позиционированием

Практическое значение

Виртуальный референс расширяет область применения Floating UI за пределы стандартных интерфейсов. Он позволяет работать с абстрактными точками, динамическими координатами и нестандартными источниками данных, что особенно важно для современных веб-приложений с высокой интерактивностью и кастомной графикой.