UNPKG

@melt-ui/svelte

Version:
126 lines (125 loc) 4.67 kB
// Modified from Grail UI v0.9.6 (2023-06-10) // Source: https://github.com/grail-ui/grail-ui // https://github.com/grail-ui/grail-ui/tree/master/packages/grail-ui/src/floating/placement.ts import { flip, offset, arrow, shift, size, computePosition, autoUpdate, } from '@floating-ui/dom'; import { isAttachedToDocument, isHTMLElement, isObject, noop, } from '../../helpers/index.js'; const defaultConfig = { strategy: 'absolute', placement: 'top', gutter: 5, flip: true, sameWidth: false, overflowPadding: 8, }; const ARROW_TRANSFORM = { bottom: 'rotate(45deg)', left: 'rotate(135deg)', top: 'rotate(225deg)', right: 'rotate(315deg)', }; const SIDE_OPTIONS = ['top', 'right', 'bottom', 'left']; const ALIGN_OPTIONS = ['start', 'center', 'end']; export function isVirtualElement(element) { return isObject(element) && 'getBoundingClientRect' in element; } export function useFloating(reference, floating, opts = {}) { if (!floating || !reference || opts === null) { return { destroy: noop, }; } const options = { ...defaultConfig, ...opts }; const arrowEl = floating.querySelector('[data-arrow=true]'); const middleware = []; if (options.flip) { middleware.push(flip({ boundary: options.boundary, padding: options.overflowPadding, ...(isObject(options.flip) && options.flip), })); } const arrowOffset = isHTMLElement(arrowEl) ? arrowEl.offsetHeight / 2 : 0; if (options.gutter || options.offset) { const data = options.gutter ? { mainAxis: options.gutter } : options.offset; if (data?.mainAxis != null) { data.mainAxis += arrowOffset; } middleware.push(offset(data)); } middleware.push(shift({ boundary: options.boundary, crossAxis: options.overlap, padding: options.overflowPadding, })); if (arrowEl) { middleware.push(arrow({ element: arrowEl, padding: 8 })); } middleware.push(size({ padding: options.overflowPadding, apply({ rects, availableHeight, availableWidth }) { if (options.sameWidth) { Object.assign(floating.style, { width: `${Math.round(rects.reference.width)}px`, minWidth: 'unset', }); } if (options.fitViewport) { Object.assign(floating.style, { maxWidth: `${availableWidth}px`, maxHeight: `${availableHeight}px`, }); } }, })); function compute() { if (!reference || !floating) return; if (!isVirtualElement(reference) && !isAttachedToDocument(reference)) return; const { placement, strategy } = options; computePosition(reference, floating, { placement, middleware, strategy, }).then((data) => { const x = Math.round(data.x); const y = Math.round(data.y); // get the chosen side and align from the placement to apply as attributes // to the floating element and arrow const [side, align] = getSideAndAlignFromPlacement(data.placement); floating.setAttribute('data-side', side); floating.setAttribute('data-align', align); Object.assign(floating.style, { position: options.strategy, top: `${y}px`, left: `${x}px`, }); if (isHTMLElement(arrowEl) && data.middlewareData.arrow) { const { x, y } = data.middlewareData.arrow; const dir = data.placement.split('-')[0]; arrowEl.setAttribute('data-side', dir); Object.assign(arrowEl.style, { position: 'absolute', left: x != null ? `${x}px` : '', top: y != null ? `${y}px` : '', [dir]: `calc(100% - ${arrowOffset}px)`, transform: ARROW_TRANSFORM[dir], backgroundColor: 'inherit', zIndex: 'inherit', }); } return data; }); } // Apply `position` to floating element prior to the computePosition() call. Object.assign(floating.style, { position: options.strategy, }); return { destroy: autoUpdate(reference, floating, compute), }; } function getSideAndAlignFromPlacement(placement) { const [side, align = 'center'] = placement.split('-'); return [side, align]; }