bits-ui
Version:
The headless components for Svelte.
193 lines (192 loc) • 6.82 kB
JavaScript
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);
let hasWhileMountedPosition = false;
let updateRequestId = 0;
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;
const referenceNode = reference.current;
const floatingNode = floating.current;
const requestId = ++updateRequestId;
computePosition(referenceNode, floatingNode, {
middleware: middlewareOption,
placement: placementOption,
strategy: strategyOption,
}).then((position) => {
// ignore stale async resolutions when newer updates were requested.
if (requestId !== updateRequestId)
return;
// ignore stale resolutions after ref replacement.
if (reference.current !== referenceNode || floating.current !== floatingNode)
return;
const referenceHidden = isReferenceHidden(referenceNode);
if (referenceHidden) {
// keep last good coordinates when the anchor disappears to avoid
// a transient jump to viewport origin before close propagates.
middlewareData = {
...middlewareData,
hide: {
// oxlint-disable-next-line no-explicit-any
...middlewareData.hide,
referenceHidden: true,
},
};
return;
}
// 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;
}
updateRequestId++;
}
function attach() {
cleanup();
if (whileElementsMountedOption === undefined) {
update();
return;
}
if (!openOption)
return;
if (reference.current === null || floating.current === null)
return;
whileElementsMountedCleanup = whileElementsMountedOption(reference.current, floating.current, update);
}
function reset() {
if (!openOption && floating.current === null) {
isPositioned = false;
}
}
function trackWhileMountedDeps() {
return [
middlewareOption,
placementOption,
strategyOption,
sideOffsetOption,
alignOffsetOption,
openOption,
];
}
$effect(() => {
if (whileElementsMountedOption !== undefined)
return;
if (!openOption)
return;
update();
});
$effect(attach);
$effect(() => {
if (whileElementsMountedOption === undefined)
return;
trackWhileMountedDeps();
if (!openOption) {
hasWhileMountedPosition = false;
return;
}
if (!isPositioned) {
hasWhileMountedPosition = false;
return;
}
// skip the first post-position run, since autoUpdate already computed it
if (!hasWhileMountedPosition) {
hasWhileMountedPosition = true;
return;
}
update();
});
$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;
},
};
}
function isReferenceHidden(node) {
if (!(node instanceof Element))
return false;
if (!node.isConnected)
return true;
if (node instanceof HTMLElement && node.hidden)
return true;
return node.getClientRects().length === 0;
}