import type { Derivable, Middleware, Padding } from "@floating-ui/dom"; import { clamp, evaluate, getAlignment, getAlignmentAxis, getAxisLength, getPaddingObject, } from "@floating-ui/utils"; import { isElement } from "@floating-ui/utils/dom"; export interface ArrowOptions { /** * The arrow element to be positioned. * @default undefined */ element: | Element | (() => Element | null) | { current: Element | null } | null | undefined; /** * Padding between the arrow element and the floating element edges. * Useful when the floating element has rounded corners. * @default 0 */ padding?: Padding | undefined; /** * Which element to use as the offset parent. * @default 'real' */ offsetParent: "real" | "floating"; } function resolveArrowElement(element: ArrowOptions["element"]): Element | null { if (element == null) { return null; } if (typeof element === "function") { return element(); } if (typeof element === "object" && "current" in element) { return element.current; } return isElement(element) ? element : null; } /** * Fork of `@floating-ui/dom`'s `arrow` middleware that accepts an * `offsetParent` option ('real' vs 'floating'). * * The React source splits this into `baseArrow` + `arrow` with a `deps` * array so React can memoize the middleware. Solid doesn't need that * — consumers produce a new middleware each reactive run via a `createMemo` * or by referencing signals — so this port keeps only the base form. */ export const arrow = ( options: ArrowOptions | Derivable, ): Middleware => ({ name: "arrow", options, async fn(state) { const { x, y, placement, rects, platform, elements, middlewareData } = state; const { element, padding = 0, offsetParent = "real", } = evaluate(options, state) || {}; const arrowElement = resolveArrowElement(element); if (arrowElement == null) { return {}; } const paddingObject = getPaddingObject(padding); const coords = { x, y }; const axis = getAlignmentAxis(placement); const length = getAxisLength(axis); const arrowDimensions = await platform.getDimensions(arrowElement); const isYAxis = axis === "y"; const minProp = isYAxis ? "top" : "left"; const maxProp = isYAxis ? "bottom" : "right"; const clientProp = isYAxis ? "clientHeight" : "clientWidth"; const endDiff = rects.reference[length] + rects.reference[axis] - coords[axis] - rects.floating[length]; const startDiff = coords[axis] - rects.reference[axis]; const arrowOffsetParent = offsetParent === "real" ? await platform.getOffsetParent?.(arrowElement) : elements.floating; let clientSize = elements.floating[clientProp] || rects.floating[length]; // DOM platform can return `window` as the `offsetParent`. if (!clientSize || !(await platform.isElement?.(arrowOffsetParent))) { clientSize = elements.floating[clientProp] || rects.floating[length]; } const centerToReference = endDiff / 2 - startDiff / 2; // If padding is large enough that it causes the arrow to no longer be // centered, clamp the padding so the arrow stays centered. const largestPossiblePadding = clientSize / 2 - arrowDimensions[length] / 2 - 1; const minPadding = Math.min(paddingObject[minProp], largestPossiblePadding); const maxPadding = Math.min(paddingObject[maxProp], largestPossiblePadding); // Prevent the arrow from overflowing the floating element when the // center point is outside the floating element's bounds. const min = minPadding; const max = clientSize - arrowDimensions[length] - maxPadding; const center = clientSize / 2 - arrowDimensions[length] / 2 + centerToReference; const offset = clamp(min, center, max); // If the reference is small enough that the arrow's padding causes it // to point to nothing for an aligned placement, adjust the offset of // the floating element itself. A single reset keeps `shift()` active. const shouldAddOffset = !middlewareData.arrow && getAlignment(placement) != null && center !== offset && rects.reference[length] / 2 - (center < min ? minPadding : maxPadding) - arrowDimensions[length] / 2 < 0; let alignmentOffset = 0; if (shouldAddOffset) { alignmentOffset = center < min ? center - min : center - max; } return { [axis]: coords[axis] + alignmentOffset, data: { [axis]: offset, centerOffset: center - offset - alignmentOffset, ...(shouldAddOffset && { alignmentOffset }), }, reset: shouldAddOffset, }; }, });