Получение ClientRect для inline контента

Inline-элементы (например, <span>, <a>, текстовые узлы) ведут себя иначе, чем блочные. Их геометрия может быть фрагментированной: один логический элемент способен занимать несколько строк, а значит — иметь несколько прямоугольников отображения.

Стандартный метод getBoundingClientRect() возвращает единый ограничивающий прямоугольник, который охватывает все строки inline-элемента. Это приводит к ряду проблем:

  • позиционирование подсказки (tooltip) оказывается неточным;
  • всплывающий элемент может «прилипать» к пустому пространству;
  • невозможно корректно определить конкретную строку текста, с которой происходит взаимодействие.

Для точного позиционирования Floating UI использует более гибкий подход — работу с ClientRect-коллекциями.


Метод getClientRects и его значение

Метод element.getClientRects() возвращает список прямоугольников (DOMRectList), каждый из которых соответствует отдельному фрагменту inline-контента:

const rects = element.getClientRects();

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

  • x, y — координаты относительно viewport;
  • width, height — размеры;
  • позицию конкретной строки или фрагмента.

Это особенно важно в случаях:

  • многострочного текста;
  • переносов строк;
  • сложной типографики (например, с line-height и letter-spacing);
  • RTL и смешанных направлений текста.

Как Floating UI использует ClientRect

Floating UI абстрагирует работу с DOM и позволяет передавать виртуальные элементы, реализующие метод getBoundingClientRect.

Для inline-контента создаётся объект, который вместо одного прямоугольника может использовать наиболее релевантный fragment.

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

const virtualElement = {
  getBoundingClientRect() {
    const rects = element.getClientRects();
    return rects[0]; // или другой выбранный rect
  }
};

Однако выбор rects[0] — не универсальное решение.


Выбор нужного прямоугольника

Floating UI предоставляет утилиту inline(), которая автоматически определяет, какой из ClientRect использовать.

Подключение middleware:

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

computePosition(referenceEl, floatingEl, {
  middleware: [inline()]
});

Принцип работы inline():

  • анализирует список ClientRect;
  • учитывает положение указателя мыши (если доступно);
  • выбирает ближайший прямоугольник к точке взаимодействия;
  • корректно обрабатывает многострочные элементы.

Привязка к позиции курсора

Для интерактивных сценариев (например, hover по тексту) важно учитывать координаты курсора.

Floating UI позволяет передать координаты события:

let mouseX = 0;
let mouseY = 0;

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

Затем inline() использует эти координаты для выбора нужного fragment.


Работа с Range API

Для более точного контроля над текстом можно использовать Range:

const range = document.createRange();
range.setStart(textNode, startOffset);
range.setEnd(textNode, endOffset);

const rects = range.getClientRects();

Это позволяет:

  • выделять конкретные символы или слова;
  • получать точные координаты подстроки;
  • строить позиционирование на уровне текста, а не элементов.

Floating UI может работать с таким диапазоном через виртуальный элемент:

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

Когда нужен getBoundingClientRect, а когда getClientRects

getBoundingClientRect() подходит:

  • для блочных элементов;
  • когда важна общая область;
  • для простых tooltip.

getClientRects() обязателен:

  • для inline-текста;
  • при многострочности;
  • при сложной типографике;
  • при точном позиционировании относительно курсора.

Ограничения и нюансы

Пустые прямоугольники

Некоторые rect’ы могут иметь нулевые размеры:

if (rect.width === 0 || rect.height === 0) {
  // игнорировать
}

Скролл и viewport

Координаты возвращаются относительно viewport, поэтому при вычислениях важно учитывать прокрутку:

const absoluteX = rect.left + window.scrollX;
const absoluteY = rect.top + window.scrollY;

Direction (RTL)

В правосторонних языках (direction: rtl) порядок rect’ов может отличаться. Необходимо учитывать это при ручной обработке.


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

Пример полной интеграции:

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

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

Здесь:

  • inline() отвечает за корректный выбор ClientRect;
  • остальные middleware обрабатывают позиционирование.

Практические сценарии

Tooltip для выделенного текста

  • используется Range;
  • берётся getClientRects;
  • отображается tooltip рядом с выделением.

Подсказки для ссылок в тексте

  • многострочные <a>;
  • inline() гарантирует привязку к нужной строке.

Редакторы текста

  • работа с курсором и selection;
  • точное позиционирование всплывающих меню.

Кастомная логика выбора rect

Иногда требуется собственный алгоритм:

function getClosestRect(rects, x, y) {
  let minDistance = Infinity;
  let closestRect = null;

  for (const rect of rects) {
    const dx = rect.left - x;
    const dy = rect.top - y;
    const distance = Math.sqrt(dx * dx + dy * dy);

    if (distance < minDistance) {
      minDistance = distance;
      closestRect = rect;
    }
  }

  return closestRect;
}

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

const virtualElement = {
  getBoundingClientRect() {
    const rects = element.getClientRects();
    return getClosestRect(rects, mouseX, mouseY);
  }
};

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

Работа с getClientRects() может быть дороже, чем с getBoundingClientRect(), особенно при:

  • большом количестве элементов;
  • частых событиях (mousemove).

Оптимизации:

  • кэширование rect’ов;
  • ограничение частоты обновлений (requestAnimationFrame);
  • использование ResizeObserver и IntersectionObserver.

Итоговая структура подхода

  1. Определение типа элемента (inline vs block)

  2. Получение ClientRect (если inline)

  3. Выбор нужного fragment:

    • через inline() middleware;
    • или кастомную функцию
  4. Передача виртуального элемента в Floating UI

  5. Применение middleware для позиционирования

Такой подход обеспечивает точное, предсказуемое и визуально корректное позиционирование floating-элементов даже в сложных текстовых интерфейсах.