melt
Version:
The next generation of Melt UI. Built for Svelte 5.
105 lines (104 loc) • 4.23 kB
JavaScript
import { arrow, autoUpdate, computePosition, flip, offset, shift, size, } from "@floating-ui/dom";
import { extract } from "./extract";
import { isFunction, isHtmlElement } from "./is";
import { deepMerge } from "./merge";
const ARROW_TRANSFORM = {
bottom: "rotate(45deg)",
left: "rotate(135deg)",
top: "rotate(225deg)",
right: "rotate(315deg)",
};
export function useFloating(args) {
const nodeEl = $derived(extract(args.node));
const floatingEl = $derived(extract(args.floating));
const config = $derived(extract(args.config, {}));
let data = $state();
const compute = () => {
const arrowEl = floatingEl.querySelector("[data-arrow]");
const arrowOffset = isHtmlElement(arrowEl) ? arrowEl.offsetHeight / 2 : 0;
const arrowMiddleware = isHtmlElement(arrowEl)
? arrow({ element: arrowEl, ...config.arrow })
: undefined;
const baseOptions = {
middleware: [
shift(config.shift),
flip(config.flip),
arrowMiddleware,
offset(typeof config.offset === "number"
? config.offset
: { mainAxis: 8 + arrowOffset, ...config.offset }),
config.sameWidth
? size({
apply({ rects, elements }) {
Object.assign(elements.floating?.style ?? {}, {
width: `${rects.reference.width}px`,
minWidth: `${rects.reference.width}px`,
});
},
})
: undefined,
],
};
computePosition(nodeEl, floatingEl, deepMerge(baseOptions, config.computePosition ?? {})).then((returned) => {
data = returned;
const { x, y, placement, middlewareData, strategy } = returned;
const floatingApply = (el = floatingEl) => {
const transformOriginMap = {
top: "bottom center",
"top-start": "bottom left",
"top-end": "bottom right",
bottom: "top center",
"bottom-start": "top left",
"bottom-end": "top right",
left: "center center",
"left-start": "top left",
"left-end": "bottom left",
right: "center center",
"right-start": "top right",
"right-end": "bottom right",
};
Object.assign(el.style, {
position: strategy,
left: `${x}px`,
top: `${y}px`,
});
const [side, align = "center"] = placement.split("-");
el.style.transformOrigin = transformOriginMap[placement];
el.dataset.side = side;
el.dataset.align = align;
};
const arrowApply = (el = undefined) => {
const actualEl = el ?? arrowEl;
if (!isHtmlElement(actualEl) || !middlewareData.arrow)
return;
const { x, y } = middlewareData.arrow;
const dir = placement.split("-")[0];
Object.assign(actualEl.style, {
position: "absolute",
left: x ? `${x}px` : "",
top: y ? `${y}px` : "",
[dir]: `calc(100% - ${arrowOffset}px)`,
transform: ARROW_TRANSFORM[dir],
backgroundColor: "inherit",
zIndex: "inherit",
});
actualEl.dataset.side = dir;
};
if (isFunction(config.onCompute)) {
config.onCompute({ ...returned, arrowApply, floatingApply });
}
else if (config.onCompute === undefined) {
floatingApply();
arrowApply();
}
});
};
$effect(() => {
return autoUpdate(nodeEl, floatingEl, compute);
});
return {
get data() {
return data;
},
};
}