Система middleware в библиотеке Floating UI построена на цепочке
последовательных преобразований координат. Каждый middleware — это
функция, которая получает текущее состояние позиционирования
(x, y, placement,
rects, elements и др.) и возвращает
модифицированное состояние.
Базовый интерфейс middleware:
type Middleware = {
name: string;
options?: any;
fn: (state: MiddlewareState) => MiddlewareReturn;
};
Расширение типов middleware позволяет:
Floating UI предоставляет базовый тип MiddlewareState,
который можно расширять:
interface MiddlewareState {
x: number;
y: number;
placement: Placement;
strategy: Strategy;
middlewareData: Record<string, any>;
elements: {
reference: Element;
floating: HTMLElement;
};
rects: {
reference: DOMRect;
floating: DOMRect;
};
}
Для расширения:
interface ExtendedMiddlewareState extends MiddlewareState {
customData?: {
offsetCache: number;
};
}
Это позволяет добавлять внутренние данные, используемые несколькими middleware.
Каждый middleware может принимать объект options. Без
строгой типизации это приводит к ошибкам.
Пример базового middleware:
const customOffset = (options?: { offset: number }) => ({
name: 'customOffset',
options,
fn(state) {
return {
...state,
y: state.y + (options?.offset ?? 0),
};
},
});
Улучшенный вариант с типами:
type CustomOffsetOptions = {
offset: number;
};
const customOffset = (options: CustomOffsetOptions): Middleware => ({
name: 'customOffset',
options,
fn(state: MiddlewareState) {
return {
...state,
y: state.y + options.offset,
};
},
});
Для повышения гибкости используются дженерики:
type MiddlewareWithOptions<T> = {
name: string;
options: T;
fn: (state: MiddlewareState) => MiddlewareReturn;
};
Пример:
function createMiddleware<T>(name: string, fn: (state: MiddlewareState, options: T) => MiddlewareReturn) {
return (options: T): MiddlewareWithOptions<T> => ({
name,
options,
fn: (state) => fn(state, options),
});
}
Использование:
const shiftBy = createMiddleware<{ dx: number; dy: number }>(
'shiftBy',
(state, options) => ({
...state,
x: state.x + options.dx,
y: state.y + options.dy,
})
);
middlewareData используется для передачи данных между
middleware:
middlewareData: {
[name: string]: any;
}
Расширение типов:
interface CustomMiddlewareData {
customOffset?: {
applied: boolean;
value: number;
};
}
Объединение:
type ExtendedState = MiddlewareState & {
middlewareData: MiddlewareState['middlewareData'] & CustomMiddlewareData;
};
Пример middleware с данными:
const customOffset = (offset: number): Middleware => ({
name: 'customOffset',
fn(state: ExtendedState) {
return {
...state,
y: state.y + offset,
middlewareData: {
...state.middlewareData,
customOffset: {
applied: true,
value: offset,
},
},
};
},
});
При использовании массива middleware возникает проблема потери типов:
computePosition(reference, floating, {
middleware: [offset(10), flip(), shift()],
});
Для строгой типизации:
type TypedMiddlewareArray = Array<MiddlewareWithOptions<any>>;
Более строгий вариант с объединением:
type MiddlewareUnion =
| MiddlewareWithOptions<{ offset: number }>
| MiddlewareWithOptions<{ padding: number }>;
Создание комбинированных middleware:
function composeMiddleware(...middlewares: Middleware[]): Middleware {
return {
name: 'composed',
fn(state) {
return middlewares.reduce(
(acc, middleware) => middleware.fn(acc),
state
);
},
};
}
Типизированный вариант:
function composeMiddlewareTyped<T extends Middleware[]>(...middlewares: T): Middleware {
return {
name: 'composed',
fn(state) {
return middlewares.reduce((acc, m) => m.fn(acc), state);
},
};
}
Floating UI поддерживает асинхронные вычисления. Типы необходимо расширить:
type AsyncMiddleware = {
name: string;
fn: (state: MiddlewareState) => Promise<MiddlewareReturn>;
};
Пример:
const asyncMiddleware: AsyncMiddleware = {
name: 'asyncExample',
async fn(state) {
const data = await fetch('/api/position').then(r => r.json());
return {
...state,
x: state.x + data.offsetX,
};
},
};
Иногда требуется различное поведение в зависимости от опций:
type OffsetOptions =
| { type: 'static'; value: number }
| { type: 'dynamic'; compute: (state: MiddlewareState) => number };
Реализация:
const offsetMiddleware = (options: OffsetOptions): Middleware => ({
name: 'offset',
fn(state) {
const offset =
options.type === 'static'
? options.value
: options.compute(state);
return {
...state,
y: state.y + offset,
};
},
});
Floating UI использует типы:
type Placement = 'top' | 'bottom' | 'left' | 'right';
type Strategy = 'absolute' | 'fixed';
Расширение:
type ExtendedPlacement = Placement | 'center';
interface ExtendedState extends MiddlewareState {
placement: ExtendedPlacement;
}
Пример middleware:
const centerPlacement: Middleware = {
name: 'centerPlacement',
fn(state: ExtendedState) {
if (state.placement === 'center') {
return {
...state,
x: state.rects.reference.width / 2,
y: state.rects.reference.height / 2,
};
}
return state;
},
};
В сложных приложениях middleware может зависеть от бизнес-логики:
interface ThemeConfig {
spacing: number;
}
const themedOffset = (theme: ThemeConfig): Middleware => ({
name: 'themedOffset',
fn(state) {
return {
...state,
y: state.y + theme.spacing,
};
},
});
Для предотвращения ошибок:
function isMiddleware(obj: any): obj is Middleware {
return typeof obj?.name === 'string' && typeof obj?.fn === 'function';
}
В TypeScript можно расширить глобальные типы:
declare module '@floating-ui/core' {
interface MiddlewareData {
customOffset?: {
value: number;
};
}
}
Это позволяет автоматически получать типизированные данные:
state.middlewareData.customOffset?.value;
Создание фабрик повышает переиспользуемость:
function createOffsetMiddleware<T extends number>(offset: T) {
return {
name: 'offset',
options: offset,
fn(state: MiddlewareState) {
return {
...state,
y: state.y + offset,
};
},
};
}
Потеря типов в массиве
const middleware = [customOffset({ offset: 10 }), shift()];
TypeScript приводит к Middleware[], теряя конкретные
типы.
Решение:
as constКлючевые принципы:
anytype TooltipOptions = {
offset: number;
arrowSize: number;
};
const tooltipMiddleware = (options: TooltipOptions): Middleware => ({
name: 'tooltip',
options,
fn(state) {
const { offset, arrowSize } = options;
return {
...state,
y: state.y + offset,
middlewareData: {
...state.middlewareData,
tooltip: {
arrowOffset: arrowSize / 2,
},
},
};
},
});
Такой подход объединяет: