This commit is contained in:
eewing
2026-02-17 14:10:16 -06:00
parent 2bca5834c5
commit cf73cd3b4c
11246 changed files with 1690552 additions and 0 deletions
@@ -0,0 +1,7 @@
import type { MaybeGetter } from "svelte-toolbelt";
export declare function get<T>(valueOrGetValue: MaybeGetter<T>): T;
export declare function getDPR(element: Element): number;
export declare function roundByDPR(element: Element, value: number): number;
export declare function getFloatingContentCSSVars(name: string): {
[x: string]: string;
};
@@ -0,0 +1,24 @@
export function get(valueOrGetValue) {
return typeof valueOrGetValue === "function"
? valueOrGetValue()
: valueOrGetValue;
}
export function getDPR(element) {
if (typeof window === "undefined")
return 1;
const win = element.ownerDocument.defaultView || window;
return win.devicePixelRatio || 1;
}
export function roundByDPR(element, value) {
const dpr = getDPR(element);
return Math.round(value * dpr) / dpr;
}
export function getFloatingContentCSSVars(name) {
return {
[`--bits-${name}-content-transform-origin`]: `var(--bits-floating-transform-origin)`,
[`--bits-${name}-content-available-width`]: `var(--bits-floating-available-width)`,
[`--bits-${name}-content-available-height`]: `var(--bits-floating-available-height)`,
[`--bits-${name}-anchor-width`]: `var(--bits-floating-anchor-width)`,
[`--bits-${name}-anchor-height`]: `var(--bits-floating-anchor-height)`,
};
}
+97
View File
@@ -0,0 +1,97 @@
import type { FloatingElement, Middleware, MiddlewareData, Placement, ReferenceElement, Strategy } from "@floating-ui/dom";
import type { ReadableBox, WritableBox } from "svelte-toolbelt";
type ValueOrGetValue<T> = T | (() => T);
export type Measurable = {
getBoundingClientRect: () => DOMRect;
};
export type UseFloatingOptions = {
/**
* Represents the open/close state of the floating element.
* @default true
*/
open?: ValueOrGetValue<boolean | undefined>;
/**
* Where to place the floating element relative to its reference element.
* @default 'bottom'
*/
placement?: ValueOrGetValue<Placement | undefined>;
/**
* The type of CSS position property to use.
* @default 'absolute'
*/
strategy?: ValueOrGetValue<Strategy | undefined>;
/**
* These are plain objects that modify the positioning coordinates in some fashion,
* or provide useful data for the consumer to use.
* @default undefined
*/
middleware?: ValueOrGetValue<Middleware[] | undefined>;
/**
* Whether to use `transform` instead of `top` and `left` styles to
* position the floating element (`floatingStyles`).
* @default true
*/
transform?: ValueOrGetValue<boolean | undefined>;
/**
* Reference / Anchor element to position the floating element relative to
*/
reference: ReadableBox<Measurable | HTMLElement | null>;
/**
* Callback to handle mounting/unmounting of the elements.
* @default undefined
*/
whileElementsMounted?: (reference: ReferenceElement, floating: FloatingElement, update: () => void) => () => void;
/**
* The offset from the reference element along the side axis.
* Used to detect bad coordinates during transitions.
* @default undefined
*/
sideOffset?: ValueOrGetValue<number | undefined>;
/**
* The offset from the reference element along the alignment axis.
* Used to detect bad coordinates during transitions.
* @default undefined
*/
alignOffset?: ValueOrGetValue<number | undefined>;
};
export type UseFloatingReturn = {
/**
* The reference element to position the floating element relative to.
*/
reference: ReadableBox<Measurable | HTMLElement | null>;
/**
* The floating element to position.
*/
floating: WritableBox<HTMLElement | null>;
/**
* The stateful placement, which can be different from the initial `placement` passed as options.
*/
placement: Readonly<Placement>;
/**
* The type of CSS position property to use.
*/
strategy: Readonly<Strategy>;
/**
* Additional data from middleware.
*/
middlewareData: Readonly<MiddlewareData>;
/**
* The boolean that let you know if the floating element has been positioned.
*/
isPositioned: Readonly<boolean>;
/**
* CSS styles to apply to the floating element to position it.
*/
floatingStyles: Readonly<{
position: Strategy;
top: string;
left: string;
transform?: string;
willChange?: string;
}>;
/**
* The function to update floating position manually.
*/
update: () => void;
};
export {};
+1
View File
@@ -0,0 +1 @@
export {};
@@ -0,0 +1,2 @@
import type { UseFloatingOptions, UseFloatingReturn } from "./types.js";
export declare function useFloating(options: UseFloatingOptions): UseFloatingReturn;
@@ -0,0 +1,120 @@
import { computePosition } from "@floating-ui/dom";
import { simpleBox } from "svelte-toolbelt";
import { get, getDPR, roundByDPR } from "./floating-utils.svelte.js";
export function useFloating(options) {
/** Options */
const whileElementsMountedOption = options.whileElementsMounted;
const openOption = $derived(get(options.open) ?? true);
const middlewareOption = $derived(get(options.middleware));
const transformOption = $derived(get(options.transform) ?? true);
const placementOption = $derived(get(options.placement) ?? "bottom");
const strategyOption = $derived(get(options.strategy) ?? "absolute");
const sideOffsetOption = $derived(get(options.sideOffset) ?? 0);
const alignOffsetOption = $derived(get(options.alignOffset) ?? 0);
const reference = options.reference;
/** State */
let x = $state(0);
let y = $state(0);
const floating = simpleBox(null);
// svelte-ignore state_referenced_locally
let strategy = $state(strategyOption);
// svelte-ignore state_referenced_locally
let placement = $state(placementOption);
let middlewareData = $state({});
let isPositioned = $state(false);
const floatingStyles = $derived.by(() => {
// preserve last known position when floating ref is null (during transitions)
const xVal = floating.current ? roundByDPR(floating.current, x) : x;
const yVal = floating.current ? roundByDPR(floating.current, y) : y;
if (transformOption) {
return {
position: strategy,
left: "0",
top: "0",
transform: `translate(${xVal}px, ${yVal}px)`,
...(floating.current &&
getDPR(floating.current) >= 1.5 && {
willChange: "transform",
}),
};
}
return {
position: strategy,
left: `${xVal}px`,
top: `${yVal}px`,
};
});
/** Effects */
let whileElementsMountedCleanup;
function update() {
if (reference.current === null || floating.current === null)
return;
computePosition(reference.current, floating.current, {
middleware: middlewareOption,
placement: placementOption,
strategy: strategyOption,
}).then((position) => {
// ignore bad coordinates that cause jumping during close transitions
if (!openOption && x !== 0 && y !== 0) {
// if we had a good position and now getting coordinates near
// the expected offset bounds during close, ignore it
const maxExpectedOffset = Math.max(Math.abs(sideOffsetOption), Math.abs(alignOffsetOption), 15);
if (position.x <= maxExpectedOffset && position.y <= maxExpectedOffset)
return;
}
x = position.x;
y = position.y;
strategy = position.strategy;
placement = position.placement;
middlewareData = position.middlewareData;
isPositioned = true;
});
}
function cleanup() {
if (typeof whileElementsMountedCleanup === "function") {
whileElementsMountedCleanup();
whileElementsMountedCleanup = undefined;
}
}
function attach() {
cleanup();
if (whileElementsMountedOption === undefined) {
update();
return;
}
if (reference.current === null || floating.current === null)
return;
whileElementsMountedCleanup = whileElementsMountedOption(reference.current, floating.current, update);
}
function reset() {
if (!openOption) {
isPositioned = false;
}
}
$effect(update);
$effect(attach);
$effect(reset);
$effect(() => cleanup);
return {
floating,
reference,
get strategy() {
return strategy;
},
get placement() {
return placement;
},
get middlewareData() {
return middlewareData;
},
get isPositioned() {
return isPositioned;
},
get floatingStyles() {
return floatingStyles;
},
get update() {
return update;
},
};
}