Создание собственного middleware

В библиотеке Floating UI middleware представляет собой функции, которые модифицируют поведение позиционирования всплывающих элементов. Каждая middleware-функция получает объект с текущими координатами (x, y) и размерами элементов (rects), а также дополнительные параметры конфигурации. Она возвращает объект с изменёнными координатами или дополнительными данными, которые могут быть использованы последующими middleware или самим Floating UI для расчета финального положения.

Middleware позволяет:

  • корректировать позицию всплывающего окна, чтобы оно не выходило за границы экрана;
  • автоматически менять направление (flip) или смещать элемент;
  • добавлять кастомные логики вроде ограничения по оси или динамического смещения в зависимости от содержимого.

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


Структура middleware

Middleware — это функция, которая принимает объект с параметрами и возвращает объект с функцией fn:

const myMiddleware = (options) => ({
  name: 'myMiddleware',
  options,
  fn({ x, y, rects, placement, strategy }) {
    // Логика изменения координат
    return { x: x + 10, y };
  }
});

Ключевые свойства и аргументы функции fn:

  • x, y — текущие координаты floating-элемента;
  • rects — объект с размерами reference и floating элементов;
  • placement — текущее расположение элемента (top, bottom, left, right);
  • strategy — стратегия позиционирования (absolute или fixed);
  • middlewareData — объект для передачи данных между различными middleware.

Возвращаемый объект может содержать:

  • x и y — новые координаты;
  • data — любые дополнительные данные, которые могут быть использованы другими middleware.

Создание собственного middleware

При создании middleware важно придерживаться следующих принципов:

  1. Именование Каждое middleware должно иметь уникальное свойство name, чтобы Floating UI мог различать их и использовать при необходимости повторно.

  2. Чистота функции Middleware не должен напрямую изменять внешние переменные. Все изменения координат должны возвращаться через объект из функции fn.

  3. Совместимость с другими middleware Любые дополнительные данные лучше помещать в data внутри объекта, чтобы последующие middleware могли их использовать:

const shiftRight = () => ({
  name: 'shiftRight',
  fn({ x, y }) {
    return {
      x: x + 20,
      data: { shifted: true }
    };
  }
});

Примеры использования кастомных middleware

1. Ограничение по экрану

Middleware для предотвращения выхода floating-элемента за границы окна:

const keepInViewport = () => ({
  name: 'keepInViewport',
  fn({ x, y, rects }) {
    const viewportWidth = window.innerWidth;
    const viewportHeight = window.innerHeight;

    let newX = x;
    let newY = y;

    if (x + rects.floating.width > viewportWidth) {
      newX = viewportWidth - rects.floating.width;
    }
    if (y + rects.floating.height > viewportHeight) {
      newY = viewportHeight - rects.floating.height;
    }

    return { x: newX, y: newY };
  }
});

2. Динамическое смещение в зависимости от scroll

Middleware для смещения элемента при скролле родителя:

const scrollOffset = ({ offset = 0 }) => ({
  name: 'scrollOffset',
  options: { offset },
  fn({ y }) {
    return { y: y + offset };
  }
});

3. Асинхронное вычисление размеров

Иногда требуется подождать, пока DOM полностью загрузится, чтобы правильно рассчитать координаты:

const asyncAdjust = () => ({
  name: 'asyncAdjust',
  async fn({ x, y }) {
    await new Promise(resolve => setTimeout(resolve, 50));
    return { x: x + 5, y: y + 5 };
  }
});

Использование собственного middleware с Floating UI

При вызове функции computePosition middleware подключаются через массив:

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

computePosition(referenceElement, floatingElement, {
  middleware: [
    keepInViewport(),
    shiftRight(),
    scrollOffset({ offset: 10 })
  ]
}).then(({ x, y }) => {
  Object.assign(floatingElement.style, {
    left: `${x}px`,
    top: `${y}px`
  });
});

Особенности работы с несколькими middleware:

  • Middleware вызываются последовательно в порядке массива;
  • Данные предыдущего middleware доступны через middlewareData;
  • Middleware могут как изменять координаты, так и добавлять метаданные для последующих обработчиков.

Продвинутая настройка middleware

Можно создавать композируемые middleware, которые сами вызывают другие middleware внутри себя. Это удобно для комплексных UI-компонентов с динамическими позициями:

const combinedMiddleware = () => ({
  name: 'combinedMiddleware',
  fn(args) {
    const shiftResult = shiftRight().fn(args);
    const viewportResult = keepInViewport().fn({ ...args, ...shiftResult });
    return { ...shiftResult, ...viewportResult };
  }
});

Такой подход позволяет создавать библиотеку собственных логик позиционирования, полностью интегрируемую с Floating UI.


Практические советы

  • Всегда проверять возвращаемые координаты, чтобы они не приводили к отрицательным значениям, если это не предусмотрено дизайном;
  • Middleware не должны вызывать layout thrashing — использовать минимальные DOM-операции;
  • Для асинхронных вычислений рекомендуется использовать await внутри fn только при реальной необходимости;
  • Можно комбинировать встроенные middleware Floating UI (flip, shift, offset) с собственными, чтобы достичь максимально гибкой системы позиционирования.

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