Переход с modifiers на middleware

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

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


Сравнение структуры modifiers и middleware

Modifiers представляли собой массив объектов с обязательными свойствами name и options:

const modifiers = [
  { name: 'offset', options: { offset: [0, 10] } },
  { name: 'flip', options: { fallbackPlacements: ['top', 'bottom'] } },
];

Middleware использует функцию middleware, переданную в computePosition:

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

computePosition(reference, floating, {
  middleware: [
    offset(10),
    flip({ fallbackPlacements: ['top', 'bottom'] })
  ],
});

Ключевые различия:

  • Чистота интерфейса: middleware — функции, а не объекты с name и options.
  • Композиция: middleware легко комбинировать в цепочку.
  • Пошаговая обработка: каждый middleware получает текущее положение и может его изменить.

Основные middleware

  1. offset Управляет смещением floating-элемента относительно reference.

    offset(10) // смещает на 10px по основной оси
    offset({ mainAxis: 10, crossAxis: 5 }) // настройка по обеим осям

    Отличие от старого offset в modifiers: теперь можно точно задавать смещение по каждой оси и интегрировать его в цепочку middleware.

  2. flip Реализует автоматический выбор альтернативной позиции при выходе элемента за пределы видимой области.

    flip({ fallbackPlacements: ['top', 'left'] })

    Middleware flip возвращает объект с координатами и новой позицией, что упрощает обработку нестандартных сценариев.

  3. shift Подвинет floating-элемент, чтобы он не выходил за границы контейнера или viewport:

    shift({ limiter: limitShift() })

    Можно комбинировать с flip и offset для более точного контроля.

  4. arrow Позиционирует стрелку (tooltip arrow) относительно reference.

    arrow({ element: arrowElement })

    Middleware arrow интегрируется в цепочку после offset и flip, чтобы корректно учитывать смещения и изменение позиции.


Принципы создания кастомных middleware

Любая функция middleware должна возвращать объект с ключами:

  • x и y — координаты смещения, если нужно изменить позицию.
  • data — дополнительная информация для последующих middleware.
  • reset — объект с новыми параметрами placement для повторного вычисления позиции, если произошла коллизия.

Пример кастомного middleware:

function customShift() {
  return ({ x, y, placement }) => {
    const adjustedX = Math.max(0, x);
    const adjustedY = Math.max(0, y);
    return { x: adjustedX, y: adjustedY };
  };
}

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


Пошаговая миграция

  1. Анализ существующих modifiers Определить, какие modifiers использовались (offset, flip, preventOverflow и др.).

  2. Замена на соответствующие middleware Каждому modifier соответствует один или несколько middleware:

    • offsetoffset()
    • flipflip()
    • preventOverflowshift()
  3. Композиция цепочки Middleware нужно располагать в логическом порядке: сначала смещения, затем корректировка границ (shift), затем flip, затем arrow.

  4. Тестирование edge-cases Проверить поведение при ограниченном пространстве, динамическом изменении размеров и скролле, чтобы убедиться в корректной работе цепочки middleware.


Особенности нового подхода

  • Модульность — добавление или удаление middleware не влияет на остальные.
  • Прозрачность — каждая функция получает текущее положение и возвращает новые координаты, легко отлаживается.
  • Поддержка асинхронных вычислений — middleware может использовать асинхронные операции, например, подгружать размеры floating-элемента из DOM перед вычислением позиции.

Практический пример полной конфигурации

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

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

computePosition(referenceElement, floatingElement, {
  placement: 'bottom',
  middleware: [
    offset({ mainAxis: 8, crossAxis: 4 }),
    flip({ fallbackPlacements: ['top', 'right'] }),
    shift({ limiter: limitShift() }),
    arrow({ element: arrowElement })
  ],
}).then(({ x, y, placement, middlewareData }) => {
  Object.assign(floatingElement.style, {
    left: `${x}px`,
    top: `${y}px`,
  });

  const arrowX = middlewareData.arrow?.x ?? 0;
  const arrowY = middlewareData.arrow?.y ?? 0;

  Object.assign(arrowElement.style, {
    left: `${arrowX}px`,
    top: `${arrowY}px`,
  });
});

Этот пример демонстрирует полный переход от старого механизма modifiers к новой цепочке middleware, обеспечивая точное позиционирование, обработку коллизий и корректное расположение стрелки tooltip.