Кастомные платформы с типами

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

Кастомная платформа становится необходимой в следующих случаях:

  • работа вне браузера (например, Canvas, WebGL, React Native);
  • нестандартная система координат;
  • виртуальные элементы (не имеющие прямого DOM-представления);
  • оптимизация вычислений и отказ от стандартных DOM API.

Платформа представляет собой объект с набором строго определённых функций, которые используются внутренним механизмом позиционирования.


Базовый интерфейс платформы

Минимальная платформа должна реализовывать следующие методы:

interface Platform {
  getElementRects: (args: {
    reference: any;
    floating: any;
    strategy: 'absolute' | 'fixed';
  }) => Promise<{
    reference: Rect;
    floating: Rect;
  }>;

  getClippingRect: (args: {
    element: any;
    boundary: any;
    rootBoundary: any;
    strategy: 'absolute' | 'fixed';
  }) => Promise<Rect>;

  getDimensions: (element: any) => Promise<Dimensions>;

  getOffsetParent?: (element: any) => Promise<any>;

  getDocumentElement?: (element: any) => any;

  getClientRects?: (element: any) => DOMRect[];

  isElement?: (value: any) => boolean;

  isRTL?: (element: any) => boolean;
}

Типы Rect и Dimensions имеют следующий вид:

type Rect = {
  x: number;
  y: number;
  width: number;
  height: number;
};

type Dimensions = {
  width: number;
  height: number;
};

Типизация кастомной платформы

При работе с TypeScript важно точно описывать типы элементов, используемых платформой. Floating UI допускает произвольный тип Element, поэтому рекомендуется вводить собственные обобщения.

Пример:

type CustomElement = {
  x: number;
  y: number;
  width: number;
  height: number;
};

type CustomPlatform = Platform<CustomElement>;

Однако в текущей реализации Floating UI чаще применяется структурная типизация без явного дженерика, поэтому практическая реализация выглядит так:

const platform: Platform = {
  async getElementRects({ reference, floating }) {
    return {
      reference: reference.getRect(),
      floating: floating.getRect(),
    };
  },

  async getDimensions(element) {
    return {
      width: element.width,
      height: element.height,
    };
  },

  async getClippingRect() {
    return {
      x: 0,
      y: 0,
      width: 1000,
      height: 1000,
    };
  },
};

Реализация getElementRects

Метод getElementRects является ключевым. Он возвращает прямоугольники reference и floating элементов в одной системе координат.

Особенности:

  • координаты должны быть согласованы;
  • важно учитывать стратегию (absolute или fixed);
  • возвращаемые значения могут быть асинхронными.

Пример для Canvas:

async function getElementRects({ reference, floating }) {
  return {
    reference: {
      x: reference.x,
      y: reference.y,
      width: reference.width,
      height: reference.height,
    },
    floating: {
      x: floating.x,
      y: floating.y,
      width: floating.width,
      height: floating.height,
    },
  };
}

Реализация getClippingRect

Этот метод определяет область, в пределах которой элемент считается видимым.

В DOM это viewport или контейнер с overflow, но в кастомной среде логика может быть иной:

async function getClippingRect() {
  return {
    x: 0,
    y: 0,
    width: canvasWidth,
    height: canvasHeight,
  };
}

В сложных случаях можно учитывать:

  • масштабирование;
  • вложенные контейнеры;
  • пользовательские границы.

Работа с getDimensions

Метод возвращает размеры элемента без учёта трансформаций:

async function getDimensions(element) {
  return {
    width: element.width,
    height: element.height,
  };
}

В виртуальных системах размеры могут храниться в модели данных, а не вычисляться.


Поддержка offsetParent

Метод getOffsetParent используется для определения контекста позиционирования.

В нестандартной платформе:

async function getOffsetParent(element) {
  return element.parent || null;
}

Если концепция offsetParent отсутствует, можно возвращать null, но это влияет на расчёт смещений.


Проверка типа элемента

Метод isElement позволяет определить, является ли объект элементом платформы:

function isElement(value: any): boolean {
  return value && typeof value === 'object' && 'x' in value;
}

Это важно при смешивании различных типов (например, виртуальные элементы + реальные).


Поддержка RTL

Метод isRTL определяет направление текста:

function isRTL() {
  return false;
}

В кастомных системах часто игнорируется, но при поддержке интернационализации может быть критичен.


Полная кастомная платформа

Пример целостной реализации:

const customPlatform: Platform = {
  async getElementRects({ reference, floating }) {
    return {
      reference: reference.rect,
      floating: floating.rect,
    };
  },

  async getDimensions(element) {
    return {
      width: element.rect.width,
      height: element.rect.height,
    };
  },

  async getClippingRect() {
    return {
      x: 0,
      y: 0,
      width: 1920,
      height: 1080,
    };
  },

  async getOffsetParent(element) {
    return element.parent ?? null;
  },

  isElement(value) {
    return value && value.rect !== undefined;
  },

  isRTL() {
    return false;
  },
};

Использование кастомной платформы

Платформа передаётся в computePosition:

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

computePosition(reference, floating, {
  platform: customPlatform,
}).then(({ x, y }) => {
  floating.x = x;
  floating.y = y;
});

Виртуальные элементы

Floating UI поддерживает виртуальные reference-элементы. Это особенно полезно для:

  • курсора мыши;
  • координат в Canvas;
  • произвольных точек.

Пример:

const virtualReference = {
  getBoundingClientRect() {
    return {
      x: mouseX,
      y: mouseY,
      width: 0,
      height: 0,
    };
  },
};

В кастомной платформе аналог реализуется через собственные структуры:

const virtualReference = {
  rect: {
    x: 100,
    y: 200,
    width: 0,
    height: 0,
  },
};

Расширение платформы

Дополнительные методы позволяют оптимизировать и адаптировать поведение:

  • getClientRects — для поддержки inline-элементов;
  • getDocumentElement — если есть корневой контейнер;
  • scale (в некоторых реализациях) — для поддержки zoom.

Типовые ошибки при реализации

Несогласованные координаты

Разные системы координат между reference и floating приводят к неправильному позиционированию.

Игнорирование стратегии

absolute и fixed могут требовать разных вычислений.

Неверные размеры

Если getDimensions возвращает неточные значения, ломаются middleware (например, flip, shift).

Отсутствие clipping

Без корректного getClippingRect не работают ограничения видимости.


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

Кастомная платформа полностью совместима со всеми middleware:

  • offset
  • flip
  • shift
  • arrow
  • size

Однако корректность их работы зависит от точности платформенных методов.

Пример:

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

computePosition(reference, floating, {
  platform: customPlatform,
  middleware: [
    offset(10),
    flip(),
    shift(),
  ],
});

Практические сценарии применения

Canvas UI

Позиционирование tooltip внутри Canvas без DOM.

Игровые интерфейсы

Отображение всплывающих элементов в WebGL.

React Native

Использование Floating UI без браузерных API.

Графические редакторы

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


Вывод структуры платформы

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