packages/utils/src/index.ts
export type Alignment = 'start' | 'end';
export type Side = 'top' | 'right' | 'bottom' | 'left';
export type AlignedPlacement = `${Side}-${Alignment}`;
export type Placement = Side | AlignedPlacement;
export type Strategy = 'absolute' | 'fixed';
export type Axis = 'x' | 'y';
export type Coords = {[key in Axis]: number};
export type Length = 'width' | 'height';
export type Dimensions = {[key in Length]: number};
export type SideObject = {[key in Side]: number};
export type Rect = Coords & Dimensions;
export type Padding = number | Partial<SideObject>;
export type ClientRectObject = Rect & SideObject;
export interface ElementRects {
reference: Rect;
floating: Rect;
}
/**
* Custom positioning reference element.
* @see https://floating-ui.com/docs/virtual-elements
*/
export type VirtualElement = {
getBoundingClientRect(): ClientRectObject;
contextElement?: any;
};
export const sides: Side[] = ['top', 'right', 'bottom', 'left'];
export const alignments: Alignment[] = ['start', 'end'];
export const placements: Placement[] = sides.reduce(
(acc: Placement[], side) =>
acc.concat(side, `${side}-${alignments[0]}`, `${side}-${alignments[1]}`),
[],
);
export const min = Math.min;
export const max = Math.max;
export const round = Math.round;
export const floor = Math.floor;
export const createCoords = (v: number) => ({x: v, y: v});
const oppositeSideMap = {
left: 'right',
right: 'left',
bottom: 'top',
top: 'bottom',
};
const oppositeAlignmentMap = {
start: 'end',
end: 'start',
};
export function clamp(start: number, value: number, end: number): number {
return max(start, min(value, end));
}
export function evaluate<T, P>(value: T | ((param: P) => T), param: P): T {
return typeof value === 'function'
? (value as (param: P) => T)(param)
: value;
}
export function getSide(placement: Placement): Side {
return placement.split('-')[0] as Side;
}
export function getAlignment(placement: Placement): Alignment | undefined {
return placement.split('-')[1] as Alignment | undefined;
}
export function getOppositeAxis(axis: Axis): Axis {
return axis === 'x' ? 'y' : 'x';
}
export function getAxisLength(axis: Axis): Length {
return axis === 'y' ? 'height' : 'width';
}
export function getSideAxis(placement: Placement): Axis {
return ['top', 'bottom'].includes(getSide(placement)) ? 'y' : 'x';
}
export function getAlignmentAxis(placement: Placement): Axis {
return getOppositeAxis(getSideAxis(placement));
}
export function getAlignmentSides(
placement: Placement,
rects: ElementRects,
rtl = false,
): [Side, Side] {
const alignment = getAlignment(placement);
const alignmentAxis = getAlignmentAxis(placement);
const length = getAxisLength(alignmentAxis);
let mainAlignmentSide: Side =
alignmentAxis === 'x'
? alignment === (rtl ? 'end' : 'start')
? 'right'
: 'left'
: alignment === 'start'
? 'bottom'
: 'top';
if (rects.reference[length] > rects.floating[length]) {
mainAlignmentSide = getOppositePlacement(mainAlignmentSide);
}
return [mainAlignmentSide, getOppositePlacement(mainAlignmentSide)];
}
export function getExpandedPlacements(placement: Placement): Array<Placement> {
const oppositePlacement = getOppositePlacement(placement);
return [
getOppositeAlignmentPlacement(placement),
oppositePlacement,
getOppositeAlignmentPlacement(oppositePlacement),
];
}
export function getOppositeAlignmentPlacement<T extends string>(
placement: T,
): T {
return placement.replace(
/start|end/g,
(alignment) => oppositeAlignmentMap[alignment as Alignment],
) as T;
}
function getSideList(side: Side, isStart: boolean, rtl?: boolean): Placement[] {
const lr: Placement[] = ['left', 'right'];
const rl: Placement[] = ['right', 'left'];
const tb: Placement[] = ['top', 'bottom'];
const bt: Placement[] = ['bottom', 'top'];
switch (side) {
case 'top':
case 'bottom':
if (rtl) return isStart ? rl : lr;
return isStart ? lr : rl;
case 'left':
case 'right':
return isStart ? tb : bt;
default:
return [];
}
}
export function getOppositeAxisPlacements(
placement: Placement,
flipAlignment: boolean,
direction: 'none' | Alignment,
rtl?: boolean,
): Placement[] {
const alignment = getAlignment(placement);
let list = getSideList(getSide(placement), direction === 'start', rtl);
if (alignment) {
list = list.map((side) => `${side}-${alignment}` as Placement);
if (flipAlignment) {
list = list.concat(list.map(getOppositeAlignmentPlacement));
}
}
return list;
}
export function getOppositePlacement<T extends string>(placement: T): T {
return placement.replace(
/left|right|bottom|top/g,
(side) => oppositeSideMap[side as Side],
) as T;
}
export function expandPaddingObject(padding: Partial<SideObject>): SideObject {
return {top: 0, right: 0, bottom: 0, left: 0, ...padding};
}
export function getPaddingObject(padding: Padding): SideObject {
return typeof padding !== 'number'
? expandPaddingObject(padding)
: {top: padding, right: padding, bottom: padding, left: padding};
}
export function rectToClientRect(rect: Rect): ClientRectObject {
const {x, y, width, height} = rect;
return {
width,
height,
top: y,
left: x,
right: x + width,
bottom: y + height,
x,
y,
};
}