import { scrollbarSize } from "../layout/scrollbarSize"; import { BELOW_CENTER_ANCHOR } from "./constants"; import { createHorizontalPosition } from "./createHorizontalPosition"; import { createVerticalPosition } from "./createVerticalPosition"; import { findSizingContainer } from "./findSizingContainer"; import { getElementRect } from "./getElementRect"; import { getTransformOrigin } from "./getTransformOrigin"; import type { FixedPosition, FixedPositionOptions } from "./types"; /** * This is used when there is no `container` element so that some styles can * still be created. The main use-case for this is context menus and when the * `initialX` and `initialY` options have been provided. * * @internal * @remarks \@since 5.0.0 */ const FALLBACK_DOM_RECT: DOMRect = { x: 0, y: 0, height: 0, width: 0, left: 0, right: 0, top: 0, bottom: 0, toJSON() { // do nothing }, }; /** * One of the most complicated functions in this project that will attempt to * position an element relative to another container element while still being * visible within the viewport. Below is the logical flow for attempting to fix * the element to the container: * * No Container: If there is no container element, return the provided x and y * positions and no styles since there's nothing we can use to calculate the * position. * * No Element: If the container was provided but the element to position does * not exist, return an style object containing the `left` and `top` values for * the container and apply as many of the positioning options as possible so * that the styles are "as close as possible" before the fixed element is added * to the DOM. This will also return the provided x and y positions since * nothing could be swapped around yet. * * Container and Element: If both the container and fixed element were provided, * apply all the positioning options to the `left` and `top` values of the * container based on the sizes of both elements. * * Now that the `left` and `top` values were applied, check to see if the * element is fully visible within the viewport with the provided positioning * options. If it is fully visible, do nothing else. If it isn't... follow the * next flow: * * First, check the horizontal sizes and make sure that the element is still * within the viewport with the provided view width margin. If it isn't, first * try to swap only to a `right` style instead of left to see if that fixes it, * otherwise keep both the `left` and `right` styles. */ export function getFixedPosition(options: FixedPositionOptions): FixedPosition { const { element, anchor = BELOW_CENTER_ANCHOR, initialX, vwMargin = 16, vhMargin = 16, xMargin = 0, yMargin = 0, width: widthType = "auto", preventOverlap = false, transformOrigin = false, disableSwapping = false, disableVHBounds = false, } = options; let { initialY } = options; const container = findSizingContainer(options.container); if (process.env.NODE_ENV !== "production") { if (preventOverlap && anchor.y !== "above" && anchor.y !== "below") { throw new Error( 'Unable to prevent overlap when the vertical anchor is not `"above"` or `"below"`' ); } } if (!element) { return { actualX: anchor.x, actualY: anchor.y, style: { left: initialX, top: initialY, position: disableVHBounds ? "absolute" : "fixed", transformOrigin: transformOrigin ? getTransformOrigin({ x: anchor.x, y: anchor.y }) : undefined, }, }; } const containerRect = container?.getBoundingClientRect() ?? FALLBACK_DOM_RECT; const vh = window.innerHeight; const vw = window.innerWidth; const { minWidth: elMinWidth } = element.style; // Note: This makes it "min-content" or "min-container-width" if (widthType === "min") { element.style.overflow = "visible"; element.style.minWidth = ""; } const elementRect = getElementRect(element); const { height } = elementRect; let elWidth = elementRect.width; if (widthType === "min") { elWidth += scrollbarSize(); element.style.overflow = ""; element.style.minWidth = elMinWidth; } if (disableVHBounds) { const dialog = element.closest("[role='dialog']"); if (!dialog) { initialY = (initialY ?? 0) + window.scrollY; } } const { left, right, width, minWidth, actualX } = createHorizontalPosition({ x: anchor.x, vw, vwMargin, xMargin, width: widthType, elWidth, initialX, containerRect, disableSwapping, }); const { top, bottom, actualY, transformOriginY } = createVerticalPosition({ y: anchor.y, vh, vhMargin, yMargin, initialY, elHeight: height, containerRect, disableSwapping, preventOverlap, disableVHBounds, }); return { actualX, actualY, style: { left, top, right, bottom, width, minWidth, position: disableVHBounds ? "absolute" : "fixed", transformOrigin: transformOrigin ? getTransformOrigin({ x: actualX, y: actualY, transformOriginY, }) : undefined, }, }; }