UNPKG

@sandlada/vue-mdc

Version:

![Vue MDC Logo](https://raw.githubusercontent.com/sandlada/vue-mdc/refs/heads/main/docs/vue-mdc-cover.png)

286 lines (285 loc) 11.3 kB
/** * @license * Copyright 2023 Google LLC * SPDX-License-Identifier: Apache-2.0 * * @link https://github.com/material-components/material-web/blob/main/menu/internal/controllers/surfacePositionController.ts * * [Modified by Sandlada & Kai Orion] * * @license * Copyright 2025 Sandlada & Kai Orion * SPDX-License-Identifier: MIT */ import { ref, computed, unref, watch, onMounted } from "vue"; import { isServer } from "../../utils/is-server.js"; const Corner = { EndStart: "end-start", StartStart: "start-start" }; const defaultProps = { disableBlockFlip: false, disableInlineFlip: false, positioning: "absolute", xOffset: 0, yOffset: 0, repositionStrategy: "move", onBeforeOpen: async () => { }, onOpen: () => { }, onBeforeClose: async () => { }, onClose: () => { } }; function useSurfacePosition(options) { const surfaceStyles = ref({ display: "none" }); const currentOptions = computed(() => { const rawOpts = unref(options); return { ...defaultProps, ...rawOpts, // Ensure required fields are present, even if null to satisfy type, consumer must provide them. anchorCorner: rawOpts.anchorCorner, surfaceCorner: rawOpts.surfaceCorner, anchorEl: rawOpts.anchorEl, surfaceEl: rawOpts.surfaceEl, isOpen: rawOpts.isOpen }; }); const calculateBlock = (config) => { const { surfaceRect, anchorRect, anchorBlock, surfaceBlock, yOffset, positioning, windowInnerHeight, blockScrollbarHeight } = config; const relativeToWindow = positioning === "fixed" || positioning === "document" ? 1 : 0; const relativeToDocument = positioning === "document" ? 1 : 0; const isSurfaceBlockStart = surfaceBlock === "start" ? 1 : 0; const isSurfaceBlockEnd = surfaceBlock === "end" ? 1 : 0; const isOneBlockEnd = anchorBlock !== surfaceBlock ? 1 : 0; const blockAnchorOffset = isOneBlockEnd * anchorRect.height + yOffset; const blockTopLayerOffset = isSurfaceBlockStart * anchorRect.top + isSurfaceBlockEnd * (windowInnerHeight - anchorRect.bottom - blockScrollbarHeight); const blockDocumentOffset = isSurfaceBlockStart * window.scrollY - isSurfaceBlockEnd * window.scrollY; const blockOutOfBoundsCorrection = Math.abs(Math.min(0, windowInnerHeight - blockTopLayerOffset - blockAnchorOffset - surfaceRect.height)); const blockInset = relativeToWindow * blockTopLayerOffset + relativeToDocument * blockDocumentOffset + blockAnchorOffset; const surfaceBlockProperty = surfaceBlock === "start" ? "inset-block-start" : "inset-block-end"; return { blockInset, blockOutOfBoundsCorrection, surfaceBlockProperty }; }; const calculateInline = (config) => { const { isLTR: isLTRBool, surfaceInline, anchorInline, anchorRect, surfaceRect, xOffset, positioning, windowInnerWidth, inlineScrollbarWidth } = config; const relativeToWindow = positioning === "fixed" || positioning === "document" ? 1 : 0; const relativeToDocument = positioning === "document" ? 1 : 0; const isLTR = isLTRBool ? 1 : 0; const isRTL = isLTRBool ? 0 : 1; const isSurfaceInlineStart = surfaceInline === "start" ? 1 : 0; const isSurfaceInlineEnd = surfaceInline === "end" ? 1 : 0; const isOneInlineEnd = anchorInline !== surfaceInline ? 1 : 0; const inlineAnchorOffset = isOneInlineEnd * anchorRect.width + xOffset; const inlineTopLayerOffsetLTR = isSurfaceInlineStart * anchorRect.left + isSurfaceInlineEnd * (windowInnerWidth - anchorRect.right - inlineScrollbarWidth); const inlineTopLayerOffsetRTL = isSurfaceInlineStart * (windowInnerWidth - anchorRect.right - inlineScrollbarWidth) + isSurfaceInlineEnd * anchorRect.left; const inlineTopLayerOffset = isLTR * inlineTopLayerOffsetLTR + isRTL * inlineTopLayerOffsetRTL; const inlineDocumentOffsetLTR = isSurfaceInlineStart * window.scrollX - isSurfaceInlineEnd * window.scrollX; const inlineDocumentOffsetRTL = isSurfaceInlineEnd * window.scrollX - isSurfaceInlineStart * window.scrollX; const inlineDocumentOffset = isLTR * inlineDocumentOffsetLTR + isRTL * inlineDocumentOffsetRTL; const inlineOutOfBoundsCorrection = Math.abs(Math.min(0, windowInnerWidth - inlineTopLayerOffset - inlineAnchorOffset - surfaceRect.width)); const inlineInset = relativeToWindow * inlineTopLayerOffset + inlineAnchorOffset + relativeToDocument * inlineDocumentOffset; let surfaceInlineProperty = surfaceInline === "start" ? "inset-inline-start" : "inset-inline-end"; if (positioning === "document" || positioning === "fixed") { if (surfaceInline === "start" && isLTRBool || surfaceInline === "end" && !isLTRBool) { surfaceInlineProperty = "left"; } else { surfaceInlineProperty = "right"; } } return { inlineInset, inlineOutOfBoundsCorrection, surfaceInlineProperty }; }; const openSurface = async () => { if (isServer()) { return; } const props = currentOptions.value; const { surfaceEl, anchorEl, anchorCorner: anchorCornerRaw, surfaceCorner: surfaceCornerRaw, positioning, xOffset, yOffset, disableBlockFlip, disableInlineFlip, repositionStrategy } = props; if (!surfaceEl || !anchorEl) { if (props.isOpen && surfaceStyles.value.display !== "none") { surfaceStyles.value = { display: "none" }; } return; } const anchorCorner = anchorCornerRaw.toLowerCase().trim(); const surfaceCorner = surfaceCornerRaw.toLowerCase().trim(); const windowInnerWidth = window.innerWidth; const windowInnerHeight = window.innerHeight; const div = document.createElement("div"); div.style.opacity = "0"; div.style.position = "fixed"; div.style.display = "block"; div.style.inset = "0"; document.body.appendChild(div); const scrollbarTestRect = div.getBoundingClientRect(); div.remove(); const blockScrollbarHeight = window.innerHeight - scrollbarTestRect.bottom; const inlineScrollbarWidth = window.innerWidth - scrollbarTestRect.right; if (surfaceEl?.popover && surfaceEl?.isConnected) { const openEvent = new Event("open", { cancelable: true }); const preventOpen = !surfaceEl?.dispatchEvent(openEvent); if (!preventOpen) { surfaceEl?.showPopover(); } } const surfaceRect = surfaceEl?.getSurfacePositionClientRect ? surfaceEl?.getSurfacePositionClientRect() : surfaceEl?.getBoundingClientRect(); const anchorRect = anchorEl?.getSurfacePositionClientRect ? anchorEl?.getSurfacePositionClientRect() : anchorEl?.getBoundingClientRect(); const [surfaceBlock, surfaceInline] = surfaceCorner.split("-"); const [anchorBlock, anchorInline] = anchorCorner.split("-"); const isLTR = getComputedStyle(surfaceEl).direction === "ltr"; let { blockInset, blockOutOfBoundsCorrection, surfaceBlockProperty } = calculateBlock({ surfaceRect, anchorRect, anchorBlock, surfaceBlock, yOffset, positioning, windowInnerHeight, blockScrollbarHeight }); if (blockOutOfBoundsCorrection && !disableBlockFlip) { const flippedSurfaceBlock = surfaceBlock === "start" ? "end" : "start"; const flippedAnchorBlock = anchorBlock === "start" ? "end" : "start"; const flippedBlock = calculateBlock({ surfaceRect, anchorRect, anchorBlock: flippedAnchorBlock, surfaceBlock: flippedSurfaceBlock, yOffset, positioning, windowInnerHeight, blockScrollbarHeight }); if (blockOutOfBoundsCorrection > flippedBlock.blockOutOfBoundsCorrection) { blockInset = flippedBlock.blockInset; blockOutOfBoundsCorrection = flippedBlock.blockOutOfBoundsCorrection; surfaceBlockProperty = flippedBlock.surfaceBlockProperty; } } let { inlineInset, inlineOutOfBoundsCorrection, surfaceInlineProperty } = calculateInline({ surfaceRect, anchorRect, anchorInline, surfaceInline, xOffset, positioning, isLTR, windowInnerWidth, inlineScrollbarWidth }); if (inlineOutOfBoundsCorrection && !disableInlineFlip) { const flippedSurfaceInline = surfaceInline === "start" ? "end" : "start"; const flippedAnchorInline = anchorInline === "start" ? "end" : "start"; const flippedInline = calculateInline({ surfaceRect, anchorRect, anchorInline: flippedAnchorInline, surfaceInline: flippedSurfaceInline, xOffset, positioning, isLTR, windowInnerWidth, inlineScrollbarWidth }); if (Math.abs(inlineOutOfBoundsCorrection) > Math.abs(flippedInline.inlineOutOfBoundsCorrection)) { inlineInset = flippedInline.inlineInset; inlineOutOfBoundsCorrection = flippedInline.inlineOutOfBoundsCorrection; surfaceInlineProperty = flippedInline.surfaceInlineProperty; } } if (repositionStrategy === "move") { blockInset = blockInset - blockOutOfBoundsCorrection; inlineInset = inlineInset - inlineOutOfBoundsCorrection; } const newStyles = { display: "block", opacity: "1", [surfaceBlockProperty]: `${blockInset}px`, [surfaceInlineProperty]: `${inlineInset}px` }; if (repositionStrategy === "resize") { if (blockOutOfBoundsCorrection) { newStyles["height"] = `${surfaceRect.height - blockOutOfBoundsCorrection}px`; } if (inlineOutOfBoundsCorrection) { newStyles["width"] = `${surfaceRect.width - inlineOutOfBoundsCorrection}px`; } } surfaceStyles.value = newStyles; }; const closeSurface = () => { if (isServer()) { return; } surfaceStyles.value = { display: "none" }; const { surfaceEl } = currentOptions.value; if (surfaceEl?.popover && surfaceEl?.isConnected) { const closeEvent = new Event("close", { cancelable: true }); const preventClose = !surfaceEl.dispatchEvent(closeEvent); if (!preventClose) { surfaceEl.hidePopover(); } } }; if (!isServer()) { watch( currentOptions, async (newProps, oldProps) => { const { isOpen, anchorEl, surfaceEl, onBeforeOpen, onOpen, onBeforeClose, onClose } = newProps; if (!anchorEl || !surfaceEl) { closeSurface(); return; } if (isOpen) { if (onBeforeOpen) await onBeforeOpen(); await openSurface(); if (onOpen) onOpen(); } else { if (onBeforeClose) await onBeforeClose(); closeSurface(); if (onClose) onClose(); } }, { immediate: true, deep: true } // immediate to run on mount, deep for nested changes in options (though options is a flat object here) ); } onMounted(() => { if (isServer()) { return; } const { surfaceEl } = currentOptions.value; if (!surfaceEl) { return; } if (surfaceEl.popover !== "manual") { surfaceEl.popover = "manual"; } }); return { surfaceStyles, closeSurface, openSurface }; } export { Corner, useSurfacePosition }; //# sourceMappingURL=use-surface-position.js.map