UNPKG

22.9 kBJavaScriptView Raw
1import { rectToClientRect, autoPlacement as autoPlacement$1, shift as shift$1, flip as flip$1, size as size$1, hide as hide$1, arrow as arrow$1, inline as inline$1, limitShift as limitShift$1, computePosition as computePosition$1 } from '@floating-ui/core';
2export { detectOverflow, offset } from '@floating-ui/core';
3import { round, createCoords, max, min, floor } from '@floating-ui/utils';
4import { getComputedStyle, isHTMLElement, isElement, getWindow, isWebKit, getDocumentElement, getNodeName, isOverflowElement, getNodeScroll, getOverflowAncestors, getParentNode, isLastTraversableNode, isContainingBlock, isTableElement, getContainingBlock } from '@floating-ui/utils/dom';
5export { getOverflowAncestors } from '@floating-ui/utils/dom';
6
7function getCssDimensions(element) {
8 const css = getComputedStyle(element);
9 // In testing environments, the `width` and `height` properties are empty
10 // strings for SVG elements, returning NaN. Fallback to `0` in this case.
11 let width = parseFloat(css.width) || 0;
12 let height = parseFloat(css.height) || 0;
13 const hasOffset = isHTMLElement(element);
14 const offsetWidth = hasOffset ? element.offsetWidth : width;
15 const offsetHeight = hasOffset ? element.offsetHeight : height;
16 const shouldFallback = round(width) !== offsetWidth || round(height) !== offsetHeight;
17 if (shouldFallback) {
18 width = offsetWidth;
19 height = offsetHeight;
20 }
21 return {
22 width,
23 height,
24 $: shouldFallback
25 };
26}
27
28function unwrapElement(element) {
29 return !isElement(element) ? element.contextElement : element;
30}
31
32function getScale(element) {
33 const domElement = unwrapElement(element);
34 if (!isHTMLElement(domElement)) {
35 return createCoords(1);
36 }
37 const rect = domElement.getBoundingClientRect();
38 const {
39 width,
40 height,
41 $
42 } = getCssDimensions(domElement);
43 let x = ($ ? round(rect.width) : rect.width) / width;
44 let y = ($ ? round(rect.height) : rect.height) / height;
45
46 // 0, NaN, or Infinity should always fallback to 1.
47
48 if (!x || !Number.isFinite(x)) {
49 x = 1;
50 }
51 if (!y || !Number.isFinite(y)) {
52 y = 1;
53 }
54 return {
55 x,
56 y
57 };
58}
59
60const noOffsets = /*#__PURE__*/createCoords(0);
61function getVisualOffsets(element) {
62 const win = getWindow(element);
63 if (!isWebKit() || !win.visualViewport) {
64 return noOffsets;
65 }
66 return {
67 x: win.visualViewport.offsetLeft,
68 y: win.visualViewport.offsetTop
69 };
70}
71function shouldAddVisualOffsets(element, isFixed, floatingOffsetParent) {
72 if (isFixed === void 0) {
73 isFixed = false;
74 }
75 if (!floatingOffsetParent || isFixed && floatingOffsetParent !== getWindow(element)) {
76 return false;
77 }
78 return isFixed;
79}
80
81function getBoundingClientRect(element, includeScale, isFixedStrategy, offsetParent) {
82 if (includeScale === void 0) {
83 includeScale = false;
84 }
85 if (isFixedStrategy === void 0) {
86 isFixedStrategy = false;
87 }
88 const clientRect = element.getBoundingClientRect();
89 const domElement = unwrapElement(element);
90 let scale = createCoords(1);
91 if (includeScale) {
92 if (offsetParent) {
93 if (isElement(offsetParent)) {
94 scale = getScale(offsetParent);
95 }
96 } else {
97 scale = getScale(element);
98 }
99 }
100 const visualOffsets = shouldAddVisualOffsets(domElement, isFixedStrategy, offsetParent) ? getVisualOffsets(domElement) : createCoords(0);
101 let x = (clientRect.left + visualOffsets.x) / scale.x;
102 let y = (clientRect.top + visualOffsets.y) / scale.y;
103 let width = clientRect.width / scale.x;
104 let height = clientRect.height / scale.y;
105 if (domElement) {
106 const win = getWindow(domElement);
107 const offsetWin = offsetParent && isElement(offsetParent) ? getWindow(offsetParent) : offsetParent;
108 let currentWin = win;
109 let currentIFrame = currentWin.frameElement;
110 while (currentIFrame && offsetParent && offsetWin !== currentWin) {
111 const iframeScale = getScale(currentIFrame);
112 const iframeRect = currentIFrame.getBoundingClientRect();
113 const css = getComputedStyle(currentIFrame);
114 const left = iframeRect.left + (currentIFrame.clientLeft + parseFloat(css.paddingLeft)) * iframeScale.x;
115 const top = iframeRect.top + (currentIFrame.clientTop + parseFloat(css.paddingTop)) * iframeScale.y;
116 x *= iframeScale.x;
117 y *= iframeScale.y;
118 width *= iframeScale.x;
119 height *= iframeScale.y;
120 x += left;
121 y += top;
122 currentWin = getWindow(currentIFrame);
123 currentIFrame = currentWin.frameElement;
124 }
125 }
126 return rectToClientRect({
127 width,
128 height,
129 x,
130 y
131 });
132}
133
134const topLayerSelectors = [':popover-open', ':modal'];
135function isTopLayer(floating) {
136 return topLayerSelectors.some(selector => {
137 try {
138 return floating.matches(selector);
139 } catch (e) {
140 return false;
141 }
142 });
143}
144
145function convertOffsetParentRelativeRectToViewportRelativeRect(_ref) {
146 let {
147 elements,
148 rect,
149 offsetParent,
150 strategy
151 } = _ref;
152 const isFixed = strategy === 'fixed';
153 const documentElement = getDocumentElement(offsetParent);
154 const topLayer = elements ? isTopLayer(elements.floating) : false;
155 if (offsetParent === documentElement || topLayer && isFixed) {
156 return rect;
157 }
158 let scroll = {
159 scrollLeft: 0,
160 scrollTop: 0
161 };
162 let scale = createCoords(1);
163 const offsets = createCoords(0);
164 const isOffsetParentAnElement = isHTMLElement(offsetParent);
165 if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {
166 if (getNodeName(offsetParent) !== 'body' || isOverflowElement(documentElement)) {
167 scroll = getNodeScroll(offsetParent);
168 }
169 if (isHTMLElement(offsetParent)) {
170 const offsetRect = getBoundingClientRect(offsetParent);
171 scale = getScale(offsetParent);
172 offsets.x = offsetRect.x + offsetParent.clientLeft;
173 offsets.y = offsetRect.y + offsetParent.clientTop;
174 }
175 }
176 return {
177 width: rect.width * scale.x,
178 height: rect.height * scale.y,
179 x: rect.x * scale.x - scroll.scrollLeft * scale.x + offsets.x,
180 y: rect.y * scale.y - scroll.scrollTop * scale.y + offsets.y
181 };
182}
183
184function getClientRects(element) {
185 return Array.from(element.getClientRects());
186}
187
188function getWindowScrollBarX(element) {
189 // If <html> has a CSS width greater than the viewport, then this will be
190 // incorrect for RTL.
191 return getBoundingClientRect(getDocumentElement(element)).left + getNodeScroll(element).scrollLeft;
192}
193
194// Gets the entire size of the scrollable document area, even extending outside
195// of the `<html>` and `<body>` rect bounds if horizontally scrollable.
196function getDocumentRect(element) {
197 const html = getDocumentElement(element);
198 const scroll = getNodeScroll(element);
199 const body = element.ownerDocument.body;
200 const width = max(html.scrollWidth, html.clientWidth, body.scrollWidth, body.clientWidth);
201 const height = max(html.scrollHeight, html.clientHeight, body.scrollHeight, body.clientHeight);
202 let x = -scroll.scrollLeft + getWindowScrollBarX(element);
203 const y = -scroll.scrollTop;
204 if (getComputedStyle(body).direction === 'rtl') {
205 x += max(html.clientWidth, body.clientWidth) - width;
206 }
207 return {
208 width,
209 height,
210 x,
211 y
212 };
213}
214
215function getViewportRect(element, strategy) {
216 const win = getWindow(element);
217 const html = getDocumentElement(element);
218 const visualViewport = win.visualViewport;
219 let width = html.clientWidth;
220 let height = html.clientHeight;
221 let x = 0;
222 let y = 0;
223 if (visualViewport) {
224 width = visualViewport.width;
225 height = visualViewport.height;
226 const visualViewportBased = isWebKit();
227 if (!visualViewportBased || visualViewportBased && strategy === 'fixed') {
228 x = visualViewport.offsetLeft;
229 y = visualViewport.offsetTop;
230 }
231 }
232 return {
233 width,
234 height,
235 x,
236 y
237 };
238}
239
240// Returns the inner client rect, subtracting scrollbars if present.
241function getInnerBoundingClientRect(element, strategy) {
242 const clientRect = getBoundingClientRect(element, true, strategy === 'fixed');
243 const top = clientRect.top + element.clientTop;
244 const left = clientRect.left + element.clientLeft;
245 const scale = isHTMLElement(element) ? getScale(element) : createCoords(1);
246 const width = element.clientWidth * scale.x;
247 const height = element.clientHeight * scale.y;
248 const x = left * scale.x;
249 const y = top * scale.y;
250 return {
251 width,
252 height,
253 x,
254 y
255 };
256}
257function getClientRectFromClippingAncestor(element, clippingAncestor, strategy) {
258 let rect;
259 if (clippingAncestor === 'viewport') {
260 rect = getViewportRect(element, strategy);
261 } else if (clippingAncestor === 'document') {
262 rect = getDocumentRect(getDocumentElement(element));
263 } else if (isElement(clippingAncestor)) {
264 rect = getInnerBoundingClientRect(clippingAncestor, strategy);
265 } else {
266 const visualOffsets = getVisualOffsets(element);
267 rect = {
268 ...clippingAncestor,
269 x: clippingAncestor.x - visualOffsets.x,
270 y: clippingAncestor.y - visualOffsets.y
271 };
272 }
273 return rectToClientRect(rect);
274}
275function hasFixedPositionAncestor(element, stopNode) {
276 const parentNode = getParentNode(element);
277 if (parentNode === stopNode || !isElement(parentNode) || isLastTraversableNode(parentNode)) {
278 return false;
279 }
280 return getComputedStyle(parentNode).position === 'fixed' || hasFixedPositionAncestor(parentNode, stopNode);
281}
282
283// A "clipping ancestor" is an `overflow` element with the characteristic of
284// clipping (or hiding) child elements. This returns all clipping ancestors
285// of the given element up the tree.
286function getClippingElementAncestors(element, cache) {
287 const cachedResult = cache.get(element);
288 if (cachedResult) {
289 return cachedResult;
290 }
291 let result = getOverflowAncestors(element, [], false).filter(el => isElement(el) && getNodeName(el) !== 'body');
292 let currentContainingBlockComputedStyle = null;
293 const elementIsFixed = getComputedStyle(element).position === 'fixed';
294 let currentNode = elementIsFixed ? getParentNode(element) : element;
295
296 // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
297 while (isElement(currentNode) && !isLastTraversableNode(currentNode)) {
298 const computedStyle = getComputedStyle(currentNode);
299 const currentNodeIsContaining = isContainingBlock(currentNode);
300 if (!currentNodeIsContaining && computedStyle.position === 'fixed') {
301 currentContainingBlockComputedStyle = null;
302 }
303 const shouldDropCurrentNode = elementIsFixed ? !currentNodeIsContaining && !currentContainingBlockComputedStyle : !currentNodeIsContaining && computedStyle.position === 'static' && !!currentContainingBlockComputedStyle && ['absolute', 'fixed'].includes(currentContainingBlockComputedStyle.position) || isOverflowElement(currentNode) && !currentNodeIsContaining && hasFixedPositionAncestor(element, currentNode);
304 if (shouldDropCurrentNode) {
305 // Drop non-containing blocks.
306 result = result.filter(ancestor => ancestor !== currentNode);
307 } else {
308 // Record last containing block for next iteration.
309 currentContainingBlockComputedStyle = computedStyle;
310 }
311 currentNode = getParentNode(currentNode);
312 }
313 cache.set(element, result);
314 return result;
315}
316
317// Gets the maximum area that the element is visible in due to any number of
318// clipping ancestors.
319function getClippingRect(_ref) {
320 let {
321 element,
322 boundary,
323 rootBoundary,
324 strategy
325 } = _ref;
326 const elementClippingAncestors = boundary === 'clippingAncestors' ? getClippingElementAncestors(element, this._c) : [].concat(boundary);
327 const clippingAncestors = [...elementClippingAncestors, rootBoundary];
328 const firstClippingAncestor = clippingAncestors[0];
329 const clippingRect = clippingAncestors.reduce((accRect, clippingAncestor) => {
330 const rect = getClientRectFromClippingAncestor(element, clippingAncestor, strategy);
331 accRect.top = max(rect.top, accRect.top);
332 accRect.right = min(rect.right, accRect.right);
333 accRect.bottom = min(rect.bottom, accRect.bottom);
334 accRect.left = max(rect.left, accRect.left);
335 return accRect;
336 }, getClientRectFromClippingAncestor(element, firstClippingAncestor, strategy));
337 return {
338 width: clippingRect.right - clippingRect.left,
339 height: clippingRect.bottom - clippingRect.top,
340 x: clippingRect.left,
341 y: clippingRect.top
342 };
343}
344
345function getDimensions(element) {
346 const {
347 width,
348 height
349 } = getCssDimensions(element);
350 return {
351 width,
352 height
353 };
354}
355
356function getRectRelativeToOffsetParent(element, offsetParent, strategy) {
357 const isOffsetParentAnElement = isHTMLElement(offsetParent);
358 const documentElement = getDocumentElement(offsetParent);
359 const isFixed = strategy === 'fixed';
360 const rect = getBoundingClientRect(element, true, isFixed, offsetParent);
361 let scroll = {
362 scrollLeft: 0,
363 scrollTop: 0
364 };
365 const offsets = createCoords(0);
366 if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {
367 if (getNodeName(offsetParent) !== 'body' || isOverflowElement(documentElement)) {
368 scroll = getNodeScroll(offsetParent);
369 }
370 if (isOffsetParentAnElement) {
371 const offsetRect = getBoundingClientRect(offsetParent, true, isFixed, offsetParent);
372 offsets.x = offsetRect.x + offsetParent.clientLeft;
373 offsets.y = offsetRect.y + offsetParent.clientTop;
374 } else if (documentElement) {
375 offsets.x = getWindowScrollBarX(documentElement);
376 }
377 }
378 const x = rect.left + scroll.scrollLeft - offsets.x;
379 const y = rect.top + scroll.scrollTop - offsets.y;
380 return {
381 x,
382 y,
383 width: rect.width,
384 height: rect.height
385 };
386}
387
388function getTrueOffsetParent(element, polyfill) {
389 if (!isHTMLElement(element) || getComputedStyle(element).position === 'fixed') {
390 return null;
391 }
392 if (polyfill) {
393 return polyfill(element);
394 }
395 return element.offsetParent;
396}
397
398// Gets the closest ancestor positioned element. Handles some edge cases,
399// such as table ancestors and cross browser bugs.
400function getOffsetParent(element, polyfill) {
401 const window = getWindow(element);
402 if (!isHTMLElement(element) || isTopLayer(element)) {
403 return window;
404 }
405 let offsetParent = getTrueOffsetParent(element, polyfill);
406 while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === 'static') {
407 offsetParent = getTrueOffsetParent(offsetParent, polyfill);
408 }
409 if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle(offsetParent).position === 'static' && !isContainingBlock(offsetParent))) {
410 return window;
411 }
412 return offsetParent || getContainingBlock(element) || window;
413}
414
415const getElementRects = async function (data) {
416 const getOffsetParentFn = this.getOffsetParent || getOffsetParent;
417 const getDimensionsFn = this.getDimensions;
418 return {
419 reference: getRectRelativeToOffsetParent(data.reference, await getOffsetParentFn(data.floating), data.strategy),
420 floating: {
421 x: 0,
422 y: 0,
423 ...(await getDimensionsFn(data.floating))
424 }
425 };
426};
427
428function isRTL(element) {
429 return getComputedStyle(element).direction === 'rtl';
430}
431
432const platform = {
433 convertOffsetParentRelativeRectToViewportRelativeRect,
434 getDocumentElement,
435 getClippingRect,
436 getOffsetParent,
437 getElementRects,
438 getClientRects,
439 getDimensions,
440 getScale,
441 isElement,
442 isRTL
443};
444
445// https://samthor.au/2021/observing-dom/
446function observeMove(element, onMove) {
447 let io = null;
448 let timeoutId;
449 const root = getDocumentElement(element);
450 function cleanup() {
451 var _io;
452 clearTimeout(timeoutId);
453 (_io = io) == null || _io.disconnect();
454 io = null;
455 }
456 function refresh(skip, threshold) {
457 if (skip === void 0) {
458 skip = false;
459 }
460 if (threshold === void 0) {
461 threshold = 1;
462 }
463 cleanup();
464 const {
465 left,
466 top,
467 width,
468 height
469 } = element.getBoundingClientRect();
470 if (!skip) {
471 onMove();
472 }
473 if (!width || !height) {
474 return;
475 }
476 const insetTop = floor(top);
477 const insetRight = floor(root.clientWidth - (left + width));
478 const insetBottom = floor(root.clientHeight - (top + height));
479 const insetLeft = floor(left);
480 const rootMargin = -insetTop + "px " + -insetRight + "px " + -insetBottom + "px " + -insetLeft + "px";
481 const options = {
482 rootMargin,
483 threshold: max(0, min(1, threshold)) || 1
484 };
485 let isFirstUpdate = true;
486 function handleObserve(entries) {
487 const ratio = entries[0].intersectionRatio;
488 if (ratio !== threshold) {
489 if (!isFirstUpdate) {
490 return refresh();
491 }
492 if (!ratio) {
493 timeoutId = setTimeout(() => {
494 refresh(false, 1e-7);
495 }, 100);
496 } else {
497 refresh(false, ratio);
498 }
499 }
500 isFirstUpdate = false;
501 }
502
503 // Older browsers don't support a `document` as the root and will throw an
504 // error.
505 try {
506 io = new IntersectionObserver(handleObserve, {
507 ...options,
508 // Handle <iframe>s
509 root: root.ownerDocument
510 });
511 } catch (e) {
512 io = new IntersectionObserver(handleObserve, options);
513 }
514 io.observe(element);
515 }
516 refresh(true);
517 return cleanup;
518}
519
520/**
521 * Automatically updates the position of the floating element when necessary.
522 * Should only be called when the floating element is mounted on the DOM or
523 * visible on the screen.
524 * @returns cleanup function that should be invoked when the floating element is
525 * removed from the DOM or hidden from the screen.
526 * @see https://floating-ui.com/docs/autoUpdate
527 */
528function autoUpdate(reference, floating, update, options) {
529 if (options === void 0) {
530 options = {};
531 }
532 const {
533 ancestorScroll = true,
534 ancestorResize = true,
535 elementResize = typeof ResizeObserver === 'function',
536 layoutShift = typeof IntersectionObserver === 'function',
537 animationFrame = false
538 } = options;
539 const referenceEl = unwrapElement(reference);
540 const ancestors = ancestorScroll || ancestorResize ? [...(referenceEl ? getOverflowAncestors(referenceEl) : []), ...getOverflowAncestors(floating)] : [];
541 ancestors.forEach(ancestor => {
542 ancestorScroll && ancestor.addEventListener('scroll', update, {
543 passive: true
544 });
545 ancestorResize && ancestor.addEventListener('resize', update);
546 });
547 const cleanupIo = referenceEl && layoutShift ? observeMove(referenceEl, update) : null;
548 let reobserveFrame = -1;
549 let resizeObserver = null;
550 if (elementResize) {
551 resizeObserver = new ResizeObserver(_ref => {
552 let [firstEntry] = _ref;
553 if (firstEntry && firstEntry.target === referenceEl && resizeObserver) {
554 // Prevent update loops when using the `size` middleware.
555 // https://github.com/floating-ui/floating-ui/issues/1740
556 resizeObserver.unobserve(floating);
557 cancelAnimationFrame(reobserveFrame);
558 reobserveFrame = requestAnimationFrame(() => {
559 var _resizeObserver;
560 (_resizeObserver = resizeObserver) == null || _resizeObserver.observe(floating);
561 });
562 }
563 update();
564 });
565 if (referenceEl && !animationFrame) {
566 resizeObserver.observe(referenceEl);
567 }
568 resizeObserver.observe(floating);
569 }
570 let frameId;
571 let prevRefRect = animationFrame ? getBoundingClientRect(reference) : null;
572 if (animationFrame) {
573 frameLoop();
574 }
575 function frameLoop() {
576 const nextRefRect = getBoundingClientRect(reference);
577 if (prevRefRect && (nextRefRect.x !== prevRefRect.x || nextRefRect.y !== prevRefRect.y || nextRefRect.width !== prevRefRect.width || nextRefRect.height !== prevRefRect.height)) {
578 update();
579 }
580 prevRefRect = nextRefRect;
581 frameId = requestAnimationFrame(frameLoop);
582 }
583 update();
584 return () => {
585 var _resizeObserver2;
586 ancestors.forEach(ancestor => {
587 ancestorScroll && ancestor.removeEventListener('scroll', update);
588 ancestorResize && ancestor.removeEventListener('resize', update);
589 });
590 cleanupIo == null || cleanupIo();
591 (_resizeObserver2 = resizeObserver) == null || _resizeObserver2.disconnect();
592 resizeObserver = null;
593 if (animationFrame) {
594 cancelAnimationFrame(frameId);
595 }
596 };
597}
598
599/**
600 * Optimizes the visibility of the floating element by choosing the placement
601 * that has the most space available automatically, without needing to specify a
602 * preferred placement. Alternative to `flip`.
603 * @see https://floating-ui.com/docs/autoPlacement
604 */
605const autoPlacement = autoPlacement$1;
606
607/**
608 * Optimizes the visibility of the floating element by shifting it in order to
609 * keep it in view when it will overflow the clipping boundary.
610 * @see https://floating-ui.com/docs/shift
611 */
612const shift = shift$1;
613
614/**
615 * Optimizes the visibility of the floating element by flipping the `placement`
616 * in order to keep it in view when the preferred placement(s) will overflow the
617 * clipping boundary. Alternative to `autoPlacement`.
618 * @see https://floating-ui.com/docs/flip
619 */
620const flip = flip$1;
621
622/**
623 * Provides data that allows you to change the size of the floating element —
624 * for instance, prevent it from overflowing the clipping boundary or match the
625 * width of the reference element.
626 * @see https://floating-ui.com/docs/size
627 */
628const size = size$1;
629
630/**
631 * Provides data to hide the floating element in applicable situations, such as
632 * when it is not in the same clipping context as the reference element.
633 * @see https://floating-ui.com/docs/hide
634 */
635const hide = hide$1;
636
637/**
638 * Provides data to position an inner element of the floating element so that it
639 * appears centered to the reference element.
640 * @see https://floating-ui.com/docs/arrow
641 */
642const arrow = arrow$1;
643
644/**
645 * Provides improved positioning for inline reference elements that can span
646 * over multiple lines, such as hyperlinks or range selections.
647 * @see https://floating-ui.com/docs/inline
648 */
649const inline = inline$1;
650
651/**
652 * Built-in `limiter` that will stop `shift()` at a certain point.
653 */
654const limitShift = limitShift$1;
655
656/**
657 * Computes the `x` and `y` coordinates that will place the floating element
658 * next to a given reference element.
659 */
660const computePosition = (reference, floating, options) => {
661 // This caches the expensive `getClippingElementAncestors` function so that
662 // multiple lifecycle resets re-use the same result. It only lives for a
663 // single call. If other functions become expensive, we can add them as well.
664 const cache = new Map();
665 const mergedOptions = {
666 platform,
667 ...options
668 };
669 const platformWithCache = {
670 ...mergedOptions.platform,
671 _c: cache
672 };
673 return computePosition$1(reference, floating, {
674 ...mergedOptions,
675 platform: platformWithCache
676 });
677};
678
679export { arrow, autoPlacement, autoUpdate, computePosition, flip, hide, inline, limitShift, platform, shift, size };