@patternfly/pfe-core 5.0.2 → 5.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  var _FloatingDOMController_instances, _FloatingDOMController_open, _FloatingDOMController_opening, _FloatingDOMController_cleanup, _FloatingDOMController_anchor, _FloatingDOMController_alignment, _FloatingDOMController_styles, _FloatingDOMController_placement, _FloatingDOMController_options, _FloatingDOMController_invoker_get, _FloatingDOMController_content_get, _FloatingDOMController_arrow_get, _FloatingDOMController_update;
2
2
  import { __classPrivateFieldGet, __classPrivateFieldSet } from "tslib";
3
- import { autoUpdate, computePosition, offset as offsetMiddleware, shift as shiftMiddleware, flip as flipMiddleware, arrow as arrowMiddleware, } from '@floating-ui/dom';
3
+ import { isServer } from 'lit';
4
4
  /**
5
5
  * Controls floating DOM within a web component, e.g. tooltips and popovers
6
6
  */
@@ -106,18 +106,21 @@ _FloatingDOMController_open = new WeakMap(), _FloatingDOMController_opening = ne
106
106
  if (!invoker || !content) {
107
107
  return;
108
108
  }
109
- const { x, y, placement: _placement, middlewareData, } = await computePosition(invoker, content, {
109
+ const cache = new Map();
110
+ const { x, y, placement: _placement, arrow: arrowData, } = calculatePosition(invoker, content, {
110
111
  strategy: 'absolute',
111
112
  placement,
112
- middleware: [
113
- offsetMiddleware(offset),
114
- shift && shiftMiddleware({ padding }),
115
- arrow && arrowMiddleware({ element: arrow, padding: arrow.offsetHeight / 2 }),
116
- flip && flipMiddleware({ padding, fallbackPlacements }),
117
- ].filter(Boolean),
118
- });
113
+ offset,
114
+ enableShift: shift,
115
+ shiftPadding: padding,
116
+ enableFlip: flip,
117
+ flipPadding: padding,
118
+ fallbackPlacements,
119
+ arrow: arrow ?? undefined,
120
+ arrowPadding: arrow ? arrow.offsetHeight / 2 : undefined,
121
+ }, cache);
119
122
  if (arrow) {
120
- const { x: arrowX, y: arrowY } = middlewareData.arrow || {};
123
+ const { x: arrowX, y: arrowY } = arrowData || {};
121
124
  const staticSide = {
122
125
  top: 'bottom',
123
126
  right: 'left',
@@ -139,4 +142,1376 @@ _FloatingDOMController_open = new WeakMap(), _FloatingDOMController_opening = ne
139
142
  }, "f");
140
143
  this.host.requestUpdate();
141
144
  };
145
+ const absoluteOrFixed = new Set(['absolute', 'fixed']);
146
+ const noOffsets = createCoords(0);
147
+ const originSides = new Set(['left', 'top']);
148
+ const lastTraversableNodeNames = new Set(['html', 'body', '#document']);
149
+ const transformProperties = [
150
+ 'transform',
151
+ 'translate',
152
+ 'scale',
153
+ 'rotate',
154
+ 'perspective',
155
+ ];
156
+ const willChangeValues = [
157
+ 'transform',
158
+ 'translate',
159
+ 'scale',
160
+ 'rotate',
161
+ 'perspective',
162
+ 'filter',
163
+ ];
164
+ const containValues = ['paint', 'layout', 'strict', 'content'];
165
+ const invalidOverflowDisplayValues = new Set(['inline', 'contents']);
166
+ const tableElements = new Set(['table', 'td', 'th']);
167
+ const topLayerSelectors = [':popover-open', ':modal'];
168
+ const oppositeSideMap = {
169
+ left: 'right',
170
+ right: 'left',
171
+ bottom: 'top',
172
+ top: 'bottom',
173
+ };
174
+ const oppositeAlignmentMap = {
175
+ start: 'end',
176
+ end: 'start',
177
+ };
178
+ const yAxisSides = new Set(['top', 'bottom']);
179
+ // Utility functions
180
+ /**
181
+ * Creates a coordinate object with the same value for both x and y.
182
+ * @param v - The value to use for both coordinates
183
+ * @returns Coordinate object with x and y set to the same value
184
+ */
185
+ function createCoords(v) {
186
+ return { x: v, y: v };
187
+ }
188
+ ;
189
+ /**
190
+ * Clamps a value between a minimum and maximum range.
191
+ * @param start - The minimum value
192
+ * @param value - The value to clamp
193
+ * @param end - The maximum value
194
+ * @returns The clamped value
195
+ */
196
+ function clamp(start, value, end) {
197
+ return Math.max(start, Math.min(value, end));
198
+ }
199
+ /**
200
+ * Extracts the side from a placement string.
201
+ * @param placement - The placement string (e.g., 'top-start')
202
+ * @returns The side portion (e.g., 'top')
203
+ */
204
+ function getSide(placement) {
205
+ return placement.split('-')[0];
206
+ }
207
+ /**
208
+ * Extracts the alignment from a placement string.
209
+ * @param placement - The placement string (e.g., 'top-start')
210
+ * @returns The alignment portion (e.g., 'start') or undefined if no alignment
211
+ */
212
+ function getAlignment(placement) {
213
+ return placement.split('-')[1];
214
+ }
215
+ /**
216
+ * Gets the opposite axis (x ↔ y).
217
+ * @param axis - The axis to flip
218
+ * @returns The opposite axis
219
+ */
220
+ function getOppositeAxis(axis) {
221
+ return axis === 'x' ? 'y' : 'x';
222
+ }
223
+ /**
224
+ * Gets the length property name for an axis.
225
+ * @param axis - The axis ('x' or 'y')
226
+ * @returns 'width' for x-axis, 'height' for y-axis
227
+ */
228
+ function getAxisLength(axis) {
229
+ return axis === 'y' ? 'height' : 'width';
230
+ }
231
+ /**
232
+ * Gets the axis that runs along the side of a placement.
233
+ * @param placement - The placement to get the side axis for
234
+ * @returns 'y' for top/bottom sides, 'x' for left/right sides
235
+ */
236
+ function getSideAxis(placement) {
237
+ return yAxisSides.has(getSide(placement)) ? 'y' : 'x';
238
+ }
239
+ /**
240
+ * Gets the axis that runs along the alignment of a placement.
241
+ * @param placement - The placement to get the alignment axis for
242
+ * @returns The axis perpendicular to the side axis
243
+ */
244
+ function getAlignmentAxis(placement) {
245
+ return getOppositeAxis(getSideAxis(placement));
246
+ }
247
+ /**
248
+ * Flips the alignment portion of a placement (start ↔ end).
249
+ * @param placement - The placement string to flip alignment for
250
+ * @returns The placement with opposite alignment
251
+ */
252
+ function getOppositeAlignmentPlacement(placement) {
253
+ return placement.replace(/start|end/g, alignment => oppositeAlignmentMap[alignment]);
254
+ }
255
+ /**
256
+ * Gets the opposite placement by flipping the side.
257
+ * @param placement - The placement to flip
258
+ * @returns The placement with opposite side
259
+ */
260
+ function getOppositePlacement(placement) {
261
+ return placement.replace(/left|right|bottom|top/g, side => oppositeSideMap[side]);
262
+ }
263
+ /**
264
+ * Converts padding value to a complete side object.
265
+ * @param padding - The padding value (number or partial side object)
266
+ * @returns Complete side object with padding for all sides
267
+ */
268
+ function getPaddingObject(padding) {
269
+ return typeof padding !== 'number' ? {
270
+ top: 0,
271
+ right: 0,
272
+ bottom: 0,
273
+ left: 0,
274
+ ...padding,
275
+ } : {
276
+ top: padding,
277
+ right: padding,
278
+ bottom: padding,
279
+ left: padding,
280
+ };
281
+ }
282
+ /**
283
+ * Converts a basic rect to a client rect object with all sides.
284
+ * @param rect - The basic rect with x, y, width, height
285
+ * @returns Client rect object with top, left, right, bottom properties
286
+ */
287
+ function rectToClientRect(rect) {
288
+ const { x, y, width, height } = rect;
289
+ return {
290
+ width,
291
+ height,
292
+ top: y,
293
+ left: x,
294
+ right: x + width,
295
+ bottom: y + height,
296
+ x,
297
+ y,
298
+ };
299
+ }
300
+ // =============================================================================
301
+ // DOM UTILITY FUNCTIONS
302
+ // =============================================================================
303
+ /**
304
+ * Gets the node name of a given node or window object.
305
+ * @param {Node | Window} node - The node or window to get the name for
306
+ * @returns {string} The lowercase node name, or '#document' for non-Node objects
307
+ */
308
+ function getNodeName(node) {
309
+ if (isNode(node)) {
310
+ return (node.nodeName || '').toLowerCase();
311
+ }
312
+ // Mocked nodes in testing environments may not be instances of Node. By
313
+ // returning `#document` an infinite loop won't occur.
314
+ // https://github.com/floating-ui/floating-ui/issues/2317
315
+ return '#document';
316
+ }
317
+ /**
318
+ * Gets the document element (html element) for a given node or window.
319
+ * @param node - The node or window to get the document element for
320
+ * @returns The document element (html element)
321
+ */
322
+ function getDocumentElement(node) {
323
+ return ((isNode(node) ? node.ownerDocument : node.document) || window.document)?.documentElement;
324
+ }
325
+ /**
326
+ * Type guard to check if a value is a Node.
327
+ * @param value - The value to check
328
+ * @returns True if the value is a Node, false otherwise
329
+ */
330
+ function isNode(value) {
331
+ return !isServer && value instanceof Node;
332
+ }
333
+ /**
334
+ * Type guard to check if a value is an Element.
335
+ * @param value - The value to check
336
+ * @returns True if the value is an Element, false otherwise
337
+ */
338
+ function isElement(value) {
339
+ return !isServer && value instanceof Element;
340
+ }
341
+ /**
342
+ * Type guard to check if a value is an HTMLElement.
343
+ * @param value - The value to check
344
+ * @returns True if the value is an HTMLElement, false otherwise
345
+ */
346
+ function isHTMLElement(value) {
347
+ return !isServer && value instanceof HTMLElement;
348
+ }
349
+ /**
350
+ * Type guard to check if a value is a ShadowRoot.
351
+ * @param value - The value to check
352
+ * @returns True if the value is a ShadowRoot, false otherwise
353
+ */
354
+ function isShadowRoot(value) {
355
+ return !isServer && typeof ShadowRoot !== 'undefined' && value instanceof ShadowRoot;
356
+ }
357
+ /**
358
+ * Checks if an element has overflow properties that create a scrolling context.
359
+ * @param element - The element to check
360
+ * @returns True if the element has overflow properties that create a scrolling context
361
+ */
362
+ function isOverflowElement(element) {
363
+ const { overflow, overflowX, overflowY, display } = window.getComputedStyle(element);
364
+ return (/auto|scroll|overlay|hidden|clip/.test(overflow + overflowY + overflowX)
365
+ && !invalidOverflowDisplayValues.has(display));
366
+ }
367
+ /**
368
+ * Checks if an element is a table-related element (table, td, th).
369
+ * @param element - The element to check
370
+ * @returns True if the element is a table-related element
371
+ */
372
+ function isTableElement(element) {
373
+ return tableElements.has(getNodeName(element));
374
+ }
375
+ /**
376
+ * Checks if an element is in the top layer (popover or modal).
377
+ * @param element - The element to check
378
+ * @returns True if the element is in the top layer
379
+ */
380
+ function isTopLayer(element) {
381
+ return topLayerSelectors.some(selector => {
382
+ try {
383
+ return element.matches(selector);
384
+ }
385
+ catch {
386
+ return false;
387
+ }
388
+ });
389
+ }
390
+ /**
391
+ * Checks if an element or CSS style declaration creates a containing block.
392
+ * A containing block is the element relative to which positioned elements are positioned.
393
+ * @param elementOrCss - The element or CSS style declaration to check
394
+ * @returns True if the element creates a containing block
395
+ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
396
+ * @see https://drafts.csswg.org/css-transforms-2/#individual-transforms
397
+ */
398
+ function isContainingBlock(elementOrCss) {
399
+ const webkit = isWebKit();
400
+ const css = isElement(elementOrCss) ?
401
+ window.getComputedStyle(elementOrCss)
402
+ : elementOrCss;
403
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
404
+ // https://drafts.csswg.org/css-transforms-2/#individual-transforms
405
+ return (transformProperties.some(value => css[value] ?
406
+ css[value] !== 'none'
407
+ : false)
408
+ || (css.containerType ? css.containerType !== 'normal' : false)
409
+ || (!webkit && (css.backdropFilter ? css.backdropFilter !== 'none' : false))
410
+ || (!webkit && (css.filter ? css.filter !== 'none' : false))
411
+ || willChangeValues.some(value => (css.willChange || '').includes(value))
412
+ || containValues.some(value => (css.contain || '').includes(value)));
413
+ }
414
+ /**
415
+ * Gets the nearest containing block element for a given element.
416
+ * Traverses up the DOM tree to find the first element that creates a containing block.
417
+ * @param {Element} element - The element to find the containing block for
418
+ * @returns {HTMLElement | null} The containing block element, or null if none found
419
+ */
420
+ function getContainingBlock(element) {
421
+ let currentNode = getParentNode(element);
422
+ while (isHTMLElement(currentNode) && !isLastTraversableNode(currentNode)) {
423
+ if (isContainingBlock(currentNode)) {
424
+ return currentNode;
425
+ }
426
+ else if (isTopLayer(currentNode)) {
427
+ return null;
428
+ }
429
+ currentNode = getParentNode(currentNode);
430
+ }
431
+ return null;
432
+ }
433
+ /**
434
+ * Checks if the current browser is WebKit-based.
435
+ * @returns {boolean} True if the browser is WebKit-based, false otherwise
436
+ */
437
+ function isWebKit() {
438
+ if (typeof CSS === 'undefined' || !CSS.supports) {
439
+ return false;
440
+ }
441
+ return CSS.supports('-webkit-backdrop-filter', 'none');
442
+ }
443
+ /**
444
+ * Checks if a node is the last traversable node in the DOM tree.
445
+ * @param {Node} node - The node to check
446
+ * @returns {boolean} True if the node is the last traversable node (html, body, or #document)
447
+ */
448
+ function isLastTraversableNode(node) {
449
+ return lastTraversableNodeNames.has(getNodeName(node));
450
+ }
451
+ /**
452
+ * Gets the scroll position of an element or window.
453
+ * @param {Element | Window} element - The element or window to get scroll position for
454
+ * @returns {{scrollLeft: number, scrollTop: number}} Object containing scrollLeft and scrollTop values
455
+ */
456
+ function getNodeScroll(element) {
457
+ if (isElement(element)) {
458
+ return {
459
+ scrollLeft: element.scrollLeft,
460
+ scrollTop: element.scrollTop,
461
+ };
462
+ }
463
+ return {
464
+ scrollLeft: element.scrollX,
465
+ scrollTop: element.scrollY,
466
+ };
467
+ }
468
+ /**
469
+ * Gets the parent node of a given node, handling shadow DOM and slotted elements.
470
+ * @param {Node} node - The node to get the parent for
471
+ * @returns {Node} The parent node, handling shadow DOM boundaries and slotted elements
472
+ */
473
+ function getParentNode(node) {
474
+ if (getNodeName(node) === 'html') {
475
+ return node;
476
+ }
477
+ const result =
478
+ // Step into the shadow DOM of the parent of a slotted node.
479
+ node.assignedSlot
480
+ // DOM Element detected.
481
+ || node.parentNode
482
+ // ShadowRoot detected.
483
+ || (isShadowRoot(node) && node.host)
484
+ // Fallback.
485
+ || getDocumentElement(node);
486
+ return isShadowRoot(result) ? result.host : result;
487
+ }
488
+ /**
489
+ * Gets the nearest overflow ancestor element for a given node.
490
+ * Traverses up the DOM tree to find the first element that has overflow properties.
491
+ * @param {Node} node - The node to find the overflow ancestor for
492
+ * @returns {HTMLElement} The nearest overflow ancestor element
493
+ */
494
+ function getNearestOverflowAncestor(node) {
495
+ const parentNode = getParentNode(node);
496
+ if (isLastTraversableNode(parentNode)) {
497
+ return node.ownerDocument ?
498
+ node.ownerDocument.body
499
+ : node.body;
500
+ }
501
+ if (isHTMLElement(parentNode) && isOverflowElement(parentNode)) {
502
+ return parentNode;
503
+ }
504
+ return getNearestOverflowAncestor(parentNode);
505
+ }
506
+ /**
507
+ * Gets all overflow ancestors for a given node, including windows and visual viewports.
508
+ * @param node - The node to get overflow ancestors for
509
+ * @param list - Accumulator list for overflow ancestors
510
+ * @param traverseIframes - Whether to traverse iframe boundaries
511
+ * @returns Array of overflow ancestors including elements, windows, and visual viewports
512
+ */
513
+ function getOverflowAncestors(node, list = [], traverseIframes = true) {
514
+ const scrollableAncestor = getNearestOverflowAncestor(node);
515
+ const isBody = scrollableAncestor === node.ownerDocument?.body;
516
+ const win = window;
517
+ if (isBody) {
518
+ const frameElement = getFrameElement(win);
519
+ return list.concat(win, win.visualViewport || [], isOverflowElement(scrollableAncestor) ? scrollableAncestor : [], frameElement && traverseIframes ? getOverflowAncestors(frameElement) : []);
520
+ }
521
+ return list.concat(scrollableAncestor, getOverflowAncestors(scrollableAncestor, [], traverseIframes));
522
+ }
523
+ /**
524
+ * Gets the frame element for a window if it's within an iframe.
525
+ * @param {Window} win - The window to get the frame element for
526
+ * @returns {Element | null} The frame element if the window is within an iframe, null otherwise
527
+ */
528
+ function getFrameElement(win) {
529
+ return win.parent && Object.getPrototypeOf(win.parent) ?
530
+ win.frameElement
531
+ : null;
532
+ }
533
+ // =============================================================================
534
+ // CORE MIDDLEWARES
535
+ // =============================================================================
536
+ // Helper function implementations
537
+ function computeCoordsFromPlacement({ reference, floating }, placement, rtl) {
538
+ const sideAxis = getSideAxis(placement);
539
+ const alignmentAxis = getAlignmentAxis(placement);
540
+ const alignLength = getAxisLength(alignmentAxis);
541
+ const side = getSide(placement);
542
+ const isVertical = sideAxis === 'y';
543
+ const commonX = reference.x + reference.width / 2 - floating.width / 2;
544
+ const commonY = reference.y + reference.height / 2 - floating.height / 2;
545
+ const commonAlign = reference[alignLength] / 2 - floating[alignLength] / 2;
546
+ let coords;
547
+ switch (side) {
548
+ case 'top':
549
+ coords = {
550
+ x: commonX,
551
+ y: reference.y - floating.height,
552
+ };
553
+ break;
554
+ case 'bottom':
555
+ coords = {
556
+ x: commonX,
557
+ y: reference.y + reference.height,
558
+ };
559
+ break;
560
+ case 'right':
561
+ coords = {
562
+ x: reference.x + reference.width,
563
+ y: commonY,
564
+ };
565
+ break;
566
+ case 'left':
567
+ coords = {
568
+ x: reference.x - floating.width,
569
+ y: commonY,
570
+ };
571
+ break;
572
+ default:
573
+ coords = {
574
+ x: reference.x,
575
+ y: reference.y,
576
+ };
577
+ }
578
+ switch (getAlignment(placement)) {
579
+ case 'start':
580
+ coords[alignmentAxis] -= commonAlign * (rtl && isVertical ? -1 : 1);
581
+ break;
582
+ case 'end':
583
+ coords[alignmentAxis] += commonAlign * (rtl && isVertical ? -1 : 1);
584
+ break;
585
+ }
586
+ return coords;
587
+ }
588
+ /**
589
+ * Computes the `x` and `y` coordinates that will place the floating element
590
+ * next to a given reference element.
591
+ *
592
+ * @param reference - The reference element
593
+ * @param floating - The floating element
594
+ * @param config - Configuration options
595
+ * @param cache - Cache for clipping ancestor calculations
596
+ */
597
+ function calculatePosition(reference, floating, config, cache) {
598
+ const { placement: initialPlacement = 'bottom', strategy = 'absolute', offset = 0, enableShift = true, shiftPadding, enableFlip = true, flipPadding, fallbackPlacements, arrow, arrowPadding = 0, } = config;
599
+ const rtl = isRTL(floating);
600
+ const elements = { reference, floating };
601
+ let resetCount = 0;
602
+ let statefulPlacement = initialPlacement;
603
+ let arrowData;
604
+ let x = 0;
605
+ let y = 0;
606
+ // Main positioning loop (handles flip resets)
607
+ while (resetCount < 50) {
608
+ const rects = getElementRects({ reference, floating, strategy });
609
+ const coords = computeCoordsFromPlacement(rects, statefulPlacement, rtl);
610
+ ({ x, y } = coords);
611
+ // 1. Apply offset
612
+ if (offset) {
613
+ const offsetCoords = convertValueToCoords({ placement: statefulPlacement, elements, rects, x, y, strategy,
614
+ initialPlacement }, offset);
615
+ x += offsetCoords.x;
616
+ y += offsetCoords.y;
617
+ }
618
+ // 2. Apply shift (keep in viewport)
619
+ if (enableShift) {
620
+ const overflow = detectOverflow({ x, y, placement: statefulPlacement, strategy, rects, elements,
621
+ initialPlacement }, { padding: shiftPadding }, cache);
622
+ const side = getSideAxis(getSide(statefulPlacement));
623
+ const mainAxis = getOppositeAxis(side);
624
+ const minSide = mainAxis === 'y' ? 'top' : 'left';
625
+ const maxSide = mainAxis === 'y' ? 'bottom' : 'right';
626
+ const minCoord = (mainAxis === 'y' ? y : x) + overflow[minSide];
627
+ const maxCoord = (mainAxis === 'y' ? y : x) - overflow[maxSide];
628
+ if (mainAxis === 'y') {
629
+ y = clamp(minCoord, y, maxCoord);
630
+ }
631
+ else {
632
+ x = clamp(minCoord, x, maxCoord);
633
+ }
634
+ }
635
+ // 3. Calculate arrow position (if arrow element provided)
636
+ if (arrow) {
637
+ const axis = getAlignmentAxis(statefulPlacement);
638
+ const length = getAxisLength(axis);
639
+ const arrowDimensions = getDimensions(arrow);
640
+ const isYAxis = axis === 'y';
641
+ const clientProp = isYAxis ? 'clientHeight' : 'clientWidth';
642
+ const arrowOffsetParent = getOffsetParent(arrow);
643
+ let clientSize = arrowOffsetParent ? arrowOffsetParent[clientProp] : 0;
644
+ if (!clientSize || !isElement(arrowOffsetParent)) {
645
+ clientSize = floating[clientProp] || rects.floating[length];
646
+ }
647
+ const endDiff = rects.reference[length] + rects.reference[axis]
648
+ - (axis === 'y' ? y : x) - rects.floating[length];
649
+ const startDiff = (axis === 'y' ? y : x) - rects.reference[axis];
650
+ const centerToReference = endDiff / 2 - startDiff / 2;
651
+ const paddingObject = getPaddingObject(arrowPadding);
652
+ const minProp = isYAxis ? 'top' : 'left';
653
+ const maxProp = isYAxis ? 'bottom' : 'right';
654
+ const largestPossiblePadding = clientSize / 2 - arrowDimensions[length] / 2 - 1;
655
+ const minPadding = Math.min(paddingObject[minProp], largestPossiblePadding);
656
+ const maxPadding = Math.min(paddingObject[maxProp], largestPossiblePadding);
657
+ const center = clientSize / 2 - arrowDimensions[length] / 2 + centerToReference;
658
+ const arrowOffset = clamp(minPadding, center, clientSize - arrowDimensions[length] - maxPadding);
659
+ arrowData = {
660
+ [axis]: arrowOffset,
661
+ centerOffset: center - arrowOffset,
662
+ };
663
+ }
664
+ // 4. Check for flip
665
+ if (enableFlip) {
666
+ const overflow = detectOverflow({ x, y, placement: statefulPlacement, strategy, rects, elements,
667
+ initialPlacement }, { padding: flipPadding }, cache);
668
+ const side = getSide(statefulPlacement);
669
+ const isOverflowing = overflow[side] > 0;
670
+ if (isOverflowing) {
671
+ // Determine fallback placements
672
+ const isBasePlacement = getSide(initialPlacement) === initialPlacement;
673
+ const placements = fallbackPlacements || (isBasePlacement ?
674
+ [getOppositePlacement(initialPlacement)]
675
+ : (() => {
676
+ const oppositePlacement = getOppositePlacement(initialPlacement);
677
+ return [
678
+ getOppositeAlignmentPlacement(initialPlacement),
679
+ oppositePlacement,
680
+ getOppositeAlignmentPlacement(oppositePlacement),
681
+ ];
682
+ })());
683
+ const allPlacements = [initialPlacement, ...placements];
684
+ const nextIndex = resetCount + 1;
685
+ if (nextIndex < allPlacements.length) {
686
+ statefulPlacement = allPlacements[nextIndex];
687
+ resetCount++;
688
+ continue; // Restart loop with new placement
689
+ }
690
+ }
691
+ }
692
+ // No reset needed, we're done
693
+ break;
694
+ }
695
+ return {
696
+ x,
697
+ y,
698
+ placement: statefulPlacement,
699
+ strategy,
700
+ arrow: arrowData,
701
+ };
702
+ }
703
+ /**
704
+ * Resolves with an object of overflow side offsets that determine how much the
705
+ * element is overflowing a given clipping boundary on each side.
706
+ * - positive = overflowing the boundary by that number of pixels
707
+ * - negative = how many pixels left before it will overflow
708
+ * - 0 = lies flush with the boundary
709
+ * @see https://floating-ui.com/docs/detectOverflow
710
+ * @param state - The position state
711
+ * @param options - Detection options
712
+ * @param cache - Cache for clipping ancestor calculations
713
+ */
714
+ function detectOverflow(state, options = {}, cache) {
715
+ const { x, y, rects, elements, strategy, } = state;
716
+ const { boundary = 'clippingAncestors', rootBoundary = 'viewport', elementContext = 'floating', altBoundary = false, padding = 0, } = options;
717
+ const paddingObject = getPaddingObject(padding);
718
+ const altContext = elementContext === 'floating' ? 'reference' : 'floating';
719
+ const element = elements[altBoundary ? altContext : elementContext];
720
+ const clippingClientRect = rectToClientRect(getClippingRect({
721
+ element: isElement(element) ?
722
+ element
723
+ : (getDocumentElement(elements.floating) || elements.floating),
724
+ boundary,
725
+ rootBoundary,
726
+ strategy,
727
+ cache,
728
+ }));
729
+ const rect = elementContext === 'floating' ? {
730
+ x,
731
+ y,
732
+ width: rects.floating.width,
733
+ height: rects.floating.height,
734
+ } : rects.reference;
735
+ const offsetParent = getOffsetParent(elements.floating);
736
+ const offsetScale = (offsetParent && isElement(offsetParent)) ?
737
+ getScale(offsetParent) || { x: 1, y: 1 }
738
+ : { x: 1, y: 1 };
739
+ const elementClientRect = rectToClientRect(offsetParent ?
740
+ convertOffsetParentRelativeRectToViewportRelativeRect({
741
+ elements,
742
+ rect,
743
+ offsetParent,
744
+ strategy,
745
+ })
746
+ : rect);
747
+ return {
748
+ top: (clippingClientRect.top - elementClientRect.top + paddingObject.top)
749
+ / offsetScale.y,
750
+ bottom: (elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom)
751
+ / offsetScale.y,
752
+ left: (clippingClientRect.left - elementClientRect.left + paddingObject.left)
753
+ / offsetScale.x,
754
+ right: (elementClientRect.right - clippingClientRect.right + paddingObject.right)
755
+ / offsetScale.x,
756
+ };
757
+ }
758
+ function convertValueToCoords(state, options) {
759
+ const { placement, elements, } = state;
760
+ const rtl = isRTL(elements.floating);
761
+ const side = getSide(placement);
762
+ const alignment = getAlignment(placement);
763
+ const isVertical = getSideAxis(placement) === 'y';
764
+ const mainAxisMulti = originSides.has(side) ? -1 : 1;
765
+ const crossAxisMulti = rtl && isVertical ? -1 : 1;
766
+ const { mainAxis, crossAxis: initialCrossAxis, alignmentAxis, } = typeof options === 'number' ? {
767
+ mainAxis: options,
768
+ crossAxis: 0,
769
+ alignmentAxis: null,
770
+ } : {
771
+ mainAxis: options.mainAxis || 0,
772
+ crossAxis: options.crossAxis || 0,
773
+ alignmentAxis: options.alignmentAxis,
774
+ };
775
+ const crossAxis = alignment && typeof alignmentAxis === 'number' ?
776
+ alignment === 'end' ? alignmentAxis * -1 : alignmentAxis
777
+ : initialCrossAxis;
778
+ return isVertical ? {
779
+ x: crossAxis * crossAxisMulti,
780
+ y: mainAxis * mainAxisMulti,
781
+ } : {
782
+ x: mainAxis * mainAxisMulti,
783
+ y: crossAxis * crossAxisMulti,
784
+ };
785
+ }
786
+ /**
787
+ * Gets the CSS dimensions of an element, handling fallbacks for SVG elements.
788
+ * @param element - The element to get dimensions for
789
+ * @returns Object containing width, height, and fallback flag
790
+ */
791
+ function getCssDimensions(element) {
792
+ const css = window.getComputedStyle(element);
793
+ // In testing environments, the `width` and `height` properties are empty
794
+ // strings for SVG elements, returning NaN. Fallback to `0` in this case.
795
+ let width = parseFloat(css.width) || 0;
796
+ let height = parseFloat(css.height) || 0;
797
+ const hasOffset = isHTMLElement(element);
798
+ const offsetWidth = hasOffset ? element.offsetWidth : width;
799
+ const offsetHeight = hasOffset ? element.offsetHeight : height;
800
+ const shouldFallback = Math.round(width) !== offsetWidth || Math.round(height) !== offsetHeight;
801
+ if (shouldFallback) {
802
+ width = offsetWidth;
803
+ height = offsetHeight;
804
+ }
805
+ return {
806
+ width,
807
+ height,
808
+ $: shouldFallback,
809
+ };
810
+ }
811
+ /**
812
+ * Gets the scale factor of an element based on its bounding rect vs CSS dimensions.
813
+ * @param element - The element to get scale for
814
+ * @returns Coordinates object with x and y scale factors
815
+ */
816
+ function getScale(element) {
817
+ if (!isHTMLElement(element)) {
818
+ return createCoords(1);
819
+ }
820
+ const rect = element.getBoundingClientRect();
821
+ const { width, height, $, } = getCssDimensions(element);
822
+ let x = ($ ? Math.round(rect.width) : rect.width) / width;
823
+ let y = ($ ? Math.round(rect.height) : rect.height) / height;
824
+ // 0, NaN, or Infinity should always fallback to 1.
825
+ if (!x || !Number.isFinite(x)) {
826
+ x = 1;
827
+ }
828
+ if (!y || !Number.isFinite(y)) {
829
+ y = 1;
830
+ }
831
+ return {
832
+ x,
833
+ y,
834
+ };
835
+ }
836
+ /**
837
+ * Gets the visual viewport offsets for an element in WebKit browsers.
838
+ * @param element - The element to get visual offsets for
839
+ * @returns Coordinates object with x and y offsets
840
+ */
841
+ function getVisualOffsets() {
842
+ const win = window;
843
+ if (!isWebKit() || !win.visualViewport) {
844
+ return noOffsets;
845
+ }
846
+ return {
847
+ x: win.visualViewport.offsetLeft,
848
+ y: win.visualViewport.offsetTop,
849
+ };
850
+ }
851
+ /**
852
+ * Determines if visual offsets should be added for positioning calculations.
853
+ * @param element - The element to check
854
+ * @param isFixed - Whether the element uses fixed positioning
855
+ * @param floatingOffsetParent - The floating element's offset parent
856
+ * @returns True if visual offsets should be added
857
+ */
858
+ function shouldAddVisualOffsets(isFixed = false, floatingOffsetParent) {
859
+ if (!floatingOffsetParent || (isFixed && floatingOffsetParent !== window)) {
860
+ return false;
861
+ }
862
+ return isFixed;
863
+ }
864
+ /**
865
+ * Gets the bounding client rect of an element with optional scale and iframe handling.
866
+ * @param element - The element to get bounding rect for
867
+ * @param includeScale - Whether to include scale calculations
868
+ * @param isFixedStrategy - Whether the element uses fixed positioning strategy
869
+ * @param offsetParent - The offset parent for calculations
870
+ * @returns Client rect object with position and dimensions
871
+ */
872
+ function getBoundingClientRect(element, includeScale = false, isFixedStrategy = false, offsetParent) {
873
+ const clientRect = element.getBoundingClientRect();
874
+ let scale = createCoords(1);
875
+ if (includeScale) {
876
+ if (offsetParent) {
877
+ if (isElement(offsetParent)) {
878
+ scale = getScale(offsetParent);
879
+ }
880
+ }
881
+ else {
882
+ scale = getScale(element);
883
+ }
884
+ }
885
+ const visualOffsets = shouldAddVisualOffsets(isFixedStrategy, offsetParent) ?
886
+ getVisualOffsets()
887
+ : createCoords(0);
888
+ let x = (clientRect.left + visualOffsets.x) / scale.x;
889
+ let y = (clientRect.top + visualOffsets.y) / scale.y;
890
+ let width = clientRect.width / scale.x;
891
+ let height = clientRect.height / scale.y;
892
+ if (element) {
893
+ const win = window;
894
+ const offsetWin = offsetParent
895
+ && isElement(offsetParent) ? window : offsetParent;
896
+ let currentWin = win;
897
+ let currentIFrame = getFrameElement(currentWin);
898
+ while (currentIFrame && offsetParent && offsetWin !== currentWin) {
899
+ const iframeScale = getScale(currentIFrame);
900
+ const iframeRect = currentIFrame.getBoundingClientRect();
901
+ const css = window.getComputedStyle(currentIFrame);
902
+ const left = iframeRect.left
903
+ + (currentIFrame.clientLeft + parseFloat(css.paddingLeft)) * iframeScale.x;
904
+ const top = iframeRect.top
905
+ + (currentIFrame.clientTop + parseFloat(css.paddingTop)) * iframeScale.y;
906
+ x *= iframeScale.x;
907
+ y *= iframeScale.y;
908
+ width *= iframeScale.x;
909
+ height *= iframeScale.y;
910
+ x += left;
911
+ y += top;
912
+ currentWin = window;
913
+ currentIFrame = getFrameElement(currentWin);
914
+ }
915
+ }
916
+ return rectToClientRect({
917
+ width,
918
+ height,
919
+ x,
920
+ y,
921
+ });
922
+ }
923
+ /**
924
+ * Gets the X position of the window scrollbar.
925
+ * Note: If <html> has a CSS width greater than the viewport, this will be incorrect for RTL.
926
+ * @param element - The element to get scrollbar position for
927
+ * @param rect - Optional rect to use instead of calculating
928
+ * @returns The X position of the scrollbar
929
+ */
930
+ function getWindowScrollBarX(element, rect) {
931
+ const leftScroll = getNodeScroll(element).scrollLeft;
932
+ if (!rect) {
933
+ return getBoundingClientRect(getDocumentElement(element)).left + leftScroll;
934
+ }
935
+ return rect.left + leftScroll;
936
+ }
937
+ /**
938
+ * Gets the HTML offset for positioning calculations.
939
+ * @param documentElement - The document element
940
+ * @param scroll - The scroll position object
941
+ * @param scroll.scrollTop
942
+ * @param scroll.scrollLeft
943
+ * @param ignoreScrollbarX - Whether to ignore X scrollbar in calculations
944
+ * @returns Coordinates object with x and y offsets
945
+ */
946
+ function getHTMLOffset(documentElement, scroll, ignoreScrollbarX = false) {
947
+ const htmlRect = documentElement.getBoundingClientRect();
948
+ const x = htmlRect.left + scroll.scrollLeft - (ignoreScrollbarX ? 0
949
+ // RTL <body> scrollbar.
950
+ : getWindowScrollBarX(documentElement, htmlRect));
951
+ const y = htmlRect.top + scroll.scrollTop;
952
+ return {
953
+ x,
954
+ y,
955
+ };
956
+ }
957
+ /**
958
+ * Converts an offset parent relative rect to a viewport relative rect.
959
+ * @param args - Object containing elements, rect, offsetParent, and strategy
960
+ * @param args.strategy
961
+ * @param args.offsetParent
962
+ * @param args.rect
963
+ * @param args.elements
964
+ * @param args.elements.floating
965
+ * @returns Viewport-relative rect
966
+ */
967
+ function convertOffsetParentRelativeRectToViewportRelativeRect(args) {
968
+ const { elements, rect, offsetParent, strategy } = args;
969
+ const isFixed = strategy === 'fixed';
970
+ const documentElement = getDocumentElement(offsetParent);
971
+ const topLayer = elements ? isTopLayer(elements.floating) : false;
972
+ if (offsetParent === documentElement || (topLayer && isFixed)) {
973
+ return rect;
974
+ }
975
+ let scroll = {
976
+ scrollLeft: 0,
977
+ scrollTop: 0,
978
+ };
979
+ let scale = createCoords(1);
980
+ const offsets = createCoords(0);
981
+ const isOffsetParentAnElement = isHTMLElement(offsetParent);
982
+ if (isOffsetParentAnElement || (!isOffsetParentAnElement && !isFixed)) {
983
+ if (getNodeName(offsetParent) !== 'body' || isOverflowElement(documentElement)) {
984
+ scroll = getNodeScroll(offsetParent);
985
+ }
986
+ if (isHTMLElement(offsetParent)) {
987
+ const offsetRect = getBoundingClientRect(offsetParent);
988
+ scale = getScale(offsetParent);
989
+ offsets.x = offsetRect.x + offsetParent.clientLeft;
990
+ offsets.y = offsetRect.y + offsetParent.clientTop;
991
+ }
992
+ }
993
+ const htmlOffset = documentElement && !isOffsetParentAnElement && !isFixed ?
994
+ getHTMLOffset(documentElement, scroll, true)
995
+ : createCoords(0);
996
+ return {
997
+ width: rect.width * scale.x,
998
+ height: rect.height * scale.y,
999
+ x: rect.x * scale.x - scroll.scrollLeft * scale.x + offsets.x + htmlOffset.x,
1000
+ y: rect.y * scale.y - scroll.scrollTop * scale.y + offsets.y + htmlOffset.y,
1001
+ };
1002
+ }
1003
+ /**
1004
+ * Gets the entire size of the scrollable document area, even extending outside
1005
+ * of the `<html>` and `<body>` rect bounds if horizontally scrollable.
1006
+ * @param element - The element to get document rect for
1007
+ * @returns Rect object with document dimensions and position
1008
+ */
1009
+ function getDocumentRect(element) {
1010
+ const html = getDocumentElement(element);
1011
+ const scroll = getNodeScroll(element);
1012
+ const { body } = element.ownerDocument;
1013
+ const width = Math.max(html.scrollWidth, html.clientWidth, body.scrollWidth, body.clientWidth);
1014
+ const height = Math.max(html.scrollHeight, html.clientHeight, body.scrollHeight, body.clientHeight);
1015
+ let x = -scroll.scrollLeft + getWindowScrollBarX(element);
1016
+ const y = -scroll.scrollTop;
1017
+ if (window.getComputedStyle(body).direction === 'rtl') {
1018
+ x += Math.max(html.clientWidth, body.clientWidth) - width;
1019
+ }
1020
+ return {
1021
+ width,
1022
+ height,
1023
+ x,
1024
+ y,
1025
+ };
1026
+ }
1027
+ /**
1028
+ * Gets the viewport rect, accounting for visual viewport if available.
1029
+ * @param element - The element to get viewport rect for
1030
+ * @param strategy - The positioning strategy being used
1031
+ * @returns Rect object with viewport dimensions and position
1032
+ */
1033
+ function getViewportRect(element, strategy) {
1034
+ const win = window;
1035
+ const html = getDocumentElement(element);
1036
+ const { visualViewport } = win;
1037
+ const width = visualViewport ? visualViewport.width : html.clientWidth;
1038
+ const height = visualViewport ? visualViewport.height : html.clientHeight;
1039
+ let x = 0;
1040
+ let y = 0;
1041
+ if (visualViewport) {
1042
+ const visualViewportBased = isWebKit();
1043
+ if (!visualViewportBased || (visualViewportBased && strategy === 'fixed')) {
1044
+ x = visualViewport.offsetLeft;
1045
+ y = visualViewport.offsetTop;
1046
+ }
1047
+ }
1048
+ return {
1049
+ width,
1050
+ height,
1051
+ x,
1052
+ y,
1053
+ };
1054
+ }
1055
+ /**
1056
+ * Returns the inner client rect, subtracting scrollbars if present.
1057
+ * @param element - The element to get inner rect for
1058
+ * @param strategy - The positioning strategy being used
1059
+ * @returns Rect object with inner dimensions and position
1060
+ */
1061
+ function getInnerBoundingClientRect(element, strategy) {
1062
+ const clientRect = getBoundingClientRect(element, true, strategy === 'fixed');
1063
+ const top = clientRect.top + element.clientTop;
1064
+ const left = clientRect.left + element.clientLeft;
1065
+ const scale = isHTMLElement(element) ? getScale(element) : createCoords(1);
1066
+ const width = element.clientWidth * scale.x;
1067
+ const height = element.clientHeight * scale.y;
1068
+ const x = left * scale.x;
1069
+ const y = top * scale.y;
1070
+ return {
1071
+ width,
1072
+ height,
1073
+ x,
1074
+ y,
1075
+ };
1076
+ }
1077
+ /**
1078
+ * Gets the client rect from a clipping ancestor (viewport, document, or element).
1079
+ * @param element - The element being positioned
1080
+ * @param clippingAncestor - The clipping ancestor ('viewport', 'document', or element)
1081
+ * @param strategy - The positioning strategy being used
1082
+ * @returns Client rect object for the clipping boundary
1083
+ */
1084
+ function getClientRectFromClippingAncestor(element, clippingAncestor, strategy) {
1085
+ let rect;
1086
+ if (clippingAncestor === 'viewport') {
1087
+ rect = getViewportRect(element, strategy);
1088
+ }
1089
+ else if (clippingAncestor === 'document') {
1090
+ rect = getDocumentRect(getDocumentElement(element));
1091
+ }
1092
+ else if (isElement(clippingAncestor)) {
1093
+ rect = getInnerBoundingClientRect(clippingAncestor, strategy);
1094
+ }
1095
+ else {
1096
+ const visualOffsets = getVisualOffsets();
1097
+ rect = {
1098
+ x: clippingAncestor.x - visualOffsets.x,
1099
+ y: clippingAncestor.y - visualOffsets.y,
1100
+ width: clippingAncestor.width,
1101
+ height: clippingAncestor.height,
1102
+ };
1103
+ }
1104
+ return rectToClientRect(rect);
1105
+ }
1106
+ /**
1107
+ * Checks if an element has a fixed position ancestor up to a stop node.
1108
+ * @param element - The element to check
1109
+ * @param stopNode - The node to stop checking at
1110
+ * @returns True if a fixed position ancestor is found
1111
+ */
1112
+ function hasFixedPositionAncestor(element, stopNode) {
1113
+ const parentNode = getParentNode(element);
1114
+ if (parentNode === stopNode || !isElement(parentNode) || isLastTraversableNode(parentNode)) {
1115
+ return false;
1116
+ }
1117
+ return window.getComputedStyle(parentNode).position === 'fixed'
1118
+ || hasFixedPositionAncestor(parentNode, stopNode);
1119
+ }
1120
+ /**
1121
+ * A "clipping ancestor" is an `overflow` element with the characteristic of
1122
+ * clipping (or hiding) child elements. This returns all clipping ancestors
1123
+ * of the given element up the tree.
1124
+ * @param element - The element to find clipping ancestors for
1125
+ * @param cache - Cache map to store results
1126
+ * @returns Array of clipping ancestor elements
1127
+ */
1128
+ function getClippingElementAncestors(element, cache) {
1129
+ const cachedResult = cache.get(element);
1130
+ if (cachedResult) {
1131
+ return cachedResult;
1132
+ }
1133
+ let result = getOverflowAncestors(element, [], false).filter(el => isElement(el) && getNodeName(el) !== 'body');
1134
+ let currentContainingBlockComputedStyle = null;
1135
+ const elementIsFixed = window.getComputedStyle(element).position === 'fixed';
1136
+ let currentNode = elementIsFixed ? getParentNode(element) : element;
1137
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
1138
+ while (isElement(currentNode) && !isLastTraversableNode(currentNode)) {
1139
+ const computedStyle = window.getComputedStyle(currentNode);
1140
+ const currentNodeIsContaining = isContainingBlock(currentNode);
1141
+ if (!currentNodeIsContaining && computedStyle.position === 'fixed') {
1142
+ currentContainingBlockComputedStyle = null;
1143
+ }
1144
+ const shouldDropCurrentNode = elementIsFixed ?
1145
+ !currentNodeIsContaining && !currentContainingBlockComputedStyle
1146
+ : !currentNodeIsContaining && computedStyle.position === 'static'
1147
+ && !!currentContainingBlockComputedStyle
1148
+ && absoluteOrFixed.has(currentContainingBlockComputedStyle.position)
1149
+ || isOverflowElement(currentNode)
1150
+ && !currentNodeIsContaining
1151
+ && hasFixedPositionAncestor(element, currentNode);
1152
+ if (shouldDropCurrentNode) {
1153
+ // Drop non-containing blocks.
1154
+ result = result.filter(ancestor => ancestor !== currentNode);
1155
+ }
1156
+ else {
1157
+ // Record last containing block for next iteration.
1158
+ currentContainingBlockComputedStyle = computedStyle;
1159
+ }
1160
+ currentNode = getParentNode(currentNode);
1161
+ }
1162
+ cache.set(element, result);
1163
+ return result;
1164
+ }
1165
+ /**
1166
+ * Gets the maximum area that the element is visible in due to any number of
1167
+ * clipping ancestors.
1168
+ * @param args - Object containing element, boundary, rootBoundary, strategy, and cache
1169
+ * @returns Rect object representing the clipping area
1170
+ */
1171
+ function getClippingRect(args) {
1172
+ const { element, boundary, rootBoundary, strategy, cache } = args;
1173
+ const elementClippingAncestors = boundary === 'clippingAncestors' ?
1174
+ isTopLayer(element) ?
1175
+ []
1176
+ : getClippingElementAncestors(element, cache)
1177
+ : [boundary].flat();
1178
+ const clippingAncestors = [...elementClippingAncestors, rootBoundary];
1179
+ const [firstClippingAncestor] = clippingAncestors;
1180
+ const clippingRect = clippingAncestors.reduce((accRect, clippingAncestor) => {
1181
+ const rect = getClientRectFromClippingAncestor(element, clippingAncestor, strategy);
1182
+ accRect.top = Math.max(rect.top, accRect.top);
1183
+ accRect.right = Math.min(rect.right, accRect.right);
1184
+ accRect.bottom = Math.min(rect.bottom, accRect.bottom);
1185
+ accRect.left = Math.max(rect.left, accRect.left);
1186
+ return accRect;
1187
+ }, getClientRectFromClippingAncestor(element, firstClippingAncestor, strategy));
1188
+ return {
1189
+ width: clippingRect.right - clippingRect.left,
1190
+ height: clippingRect.bottom - clippingRect.top,
1191
+ x: clippingRect.left,
1192
+ y: clippingRect.top,
1193
+ };
1194
+ }
1195
+ /**
1196
+ * Gets the dimensions of an element.
1197
+ * @param element - The element to get dimensions for
1198
+ * @returns Dimensions object with width and height
1199
+ */
1200
+ function getDimensions(element) {
1201
+ const { width, height } = getCssDimensions(element);
1202
+ return {
1203
+ width,
1204
+ height,
1205
+ };
1206
+ }
1207
+ /**
1208
+ * Gets the rect of an element relative to its offset parent.
1209
+ * @param element - The element to get rect for
1210
+ * @param offsetParent - The offset parent element
1211
+ * @param strategy - The positioning strategy being used
1212
+ * @returns Rect object relative to the offset parent
1213
+ */
1214
+ function getRectRelativeToOffsetParent(element, offsetParent, strategy) {
1215
+ const isOffsetParentAnElement = isHTMLElement(offsetParent);
1216
+ const documentElement = getDocumentElement(offsetParent);
1217
+ const isFixed = strategy === 'fixed';
1218
+ const rect = getBoundingClientRect(element, true, isFixed, offsetParent);
1219
+ let scroll = {
1220
+ scrollLeft: 0,
1221
+ scrollTop: 0,
1222
+ };
1223
+ const offsets = createCoords(0);
1224
+ // If the <body> scrollbar appears on the left (e.g. RTL systems). Use
1225
+ // Firefox with layout.scrollbar.side = 3 in about:config to test this.
1226
+ function setLeftRTLScrollbarOffset() {
1227
+ offsets.x = getWindowScrollBarX(documentElement);
1228
+ }
1229
+ if (isOffsetParentAnElement || (!isOffsetParentAnElement && !isFixed)) {
1230
+ if (getNodeName(offsetParent) !== 'body' || isOverflowElement(documentElement)) {
1231
+ scroll = getNodeScroll(offsetParent);
1232
+ }
1233
+ if (isOffsetParentAnElement) {
1234
+ const offsetRect = getBoundingClientRect(offsetParent, true, isFixed, offsetParent);
1235
+ offsets.x = offsetRect.x + offsetParent.clientLeft;
1236
+ offsets.y = offsetRect.y + offsetParent.clientTop;
1237
+ }
1238
+ else if (documentElement) {
1239
+ setLeftRTLScrollbarOffset();
1240
+ }
1241
+ }
1242
+ if (isFixed && !isOffsetParentAnElement && documentElement) {
1243
+ setLeftRTLScrollbarOffset();
1244
+ }
1245
+ const htmlOffset = documentElement && !isOffsetParentAnElement && !isFixed ?
1246
+ getHTMLOffset(documentElement, scroll)
1247
+ : createCoords(0);
1248
+ const x = rect.left + scroll.scrollLeft - offsets.x - htmlOffset.x;
1249
+ const y = rect.top + scroll.scrollTop - offsets.y - htmlOffset.y;
1250
+ return {
1251
+ x,
1252
+ y,
1253
+ width: rect.width,
1254
+ height: rect.height,
1255
+ };
1256
+ }
1257
+ /**
1258
+ * Checks if an element has static positioning.
1259
+ * @param element - The element to check
1260
+ * @returns True if the element is statically positioned
1261
+ */
1262
+ function isStaticPositioned(element) {
1263
+ return window.getComputedStyle(element).position === 'static';
1264
+ }
1265
+ /**
1266
+ * Gets the true offset parent of an element, handling browser differences.
1267
+ * Firefox returns the <html> element as the offsetParent if it's non-static,
1268
+ * while Chrome and Safari return the <body> element. The <body> element must
1269
+ * be used to perform the correct calculations even if the <html> element is
1270
+ * non-static.
1271
+ * @param element - The element to get offset parent for
1272
+ * @param polyfill - Optional polyfill function for offset parent
1273
+ * @returns The true offset parent or null
1274
+ */
1275
+ function getTrueOffsetParent(element, polyfill) {
1276
+ if (!isHTMLElement(element) || window.getComputedStyle(element).position === 'fixed') {
1277
+ return null;
1278
+ }
1279
+ if (polyfill) {
1280
+ return polyfill(element);
1281
+ }
1282
+ let rawOffsetParent = element.offsetParent;
1283
+ if (getDocumentElement(element) === rawOffsetParent) {
1284
+ rawOffsetParent = rawOffsetParent.ownerDocument.body;
1285
+ }
1286
+ return rawOffsetParent;
1287
+ }
1288
+ /**
1289
+ * Gets the closest ancestor positioned element. Handles some edge cases,
1290
+ * such as table ancestors and cross browser bugs.
1291
+ * @param element - The element to get offset parent for
1292
+ * @param polyfill - Optional polyfill function for offset parent
1293
+ * @returns The offset parent element or window
1294
+ */
1295
+ function getOffsetParent(element, polyfill) {
1296
+ const win = window;
1297
+ if (isTopLayer(element)) {
1298
+ return win;
1299
+ }
1300
+ if (!isHTMLElement(element)) {
1301
+ let svgOffsetParent = getParentNode(element);
1302
+ while (svgOffsetParent && !isLastTraversableNode(svgOffsetParent)) {
1303
+ if (isElement(svgOffsetParent) && !isStaticPositioned(svgOffsetParent)) {
1304
+ return svgOffsetParent;
1305
+ }
1306
+ svgOffsetParent = getParentNode(svgOffsetParent);
1307
+ }
1308
+ return win;
1309
+ }
1310
+ let offsetParent = getTrueOffsetParent(element, polyfill);
1311
+ while (offsetParent && isTableElement(offsetParent) && isStaticPositioned(offsetParent)) {
1312
+ offsetParent = getTrueOffsetParent(offsetParent, polyfill);
1313
+ }
1314
+ if (offsetParent && isLastTraversableNode(offsetParent)
1315
+ && isStaticPositioned(offsetParent) && !isContainingBlock(offsetParent)) {
1316
+ return win;
1317
+ }
1318
+ return offsetParent || getContainingBlock(element) || win;
1319
+ }
1320
+ /**
1321
+ * Gets the element rects for reference and floating elements.
1322
+ * @param data - Object containing reference, floating, and strategy
1323
+ * @param data.reference
1324
+ * @param data.floating
1325
+ * @param data.strategy
1326
+ * @returns Element rects
1327
+ */
1328
+ function getElementRects(data) {
1329
+ const floatingDimensions = getDimensions(data.floating);
1330
+ return {
1331
+ reference: getRectRelativeToOffsetParent(data.reference, getOffsetParent(data.floating), data.strategy),
1332
+ floating: {
1333
+ x: 0,
1334
+ y: 0,
1335
+ width: floatingDimensions.width,
1336
+ height: floatingDimensions.height,
1337
+ },
1338
+ };
1339
+ }
1340
+ /**
1341
+ * Checks if an element is in a right-to-left (RTL) context.
1342
+ * @param element - The element to check
1343
+ * @returns True if the element is in RTL context
1344
+ */
1345
+ function isRTL(element) {
1346
+ return window.getComputedStyle(element).direction === 'rtl';
1347
+ }
1348
+ /**
1349
+ * Checks if two client rect objects are equal.
1350
+ * @param a - First rect object
1351
+ * @param b - Second rect object
1352
+ * @returns True if the rects are equal
1353
+ */
1354
+ function rectsAreEqual(a, b) {
1355
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
1356
+ }
1357
+ /**
1358
+ * Observes an element for movement and calls a callback when it moves.
1359
+ * Based on https://samthor.au/2021/observing-dom/
1360
+ * @param element - The element to observe
1361
+ * @param onMove - Callback function called when element moves
1362
+ * @returns Cleanup function to stop observing
1363
+ */
1364
+ function observeMove(element, onMove) {
1365
+ let io = null;
1366
+ let timeoutId;
1367
+ const root = getDocumentElement(element);
1368
+ function cleanup() {
1369
+ clearTimeout(timeoutId);
1370
+ io?.disconnect();
1371
+ io = null;
1372
+ }
1373
+ function refresh(skip = false, threshold = 1) {
1374
+ cleanup();
1375
+ const elementRectForRootMargin = element.getBoundingClientRect();
1376
+ const { left, top, width, height } = elementRectForRootMargin;
1377
+ if (!skip) {
1378
+ onMove();
1379
+ }
1380
+ if (!width || !height) {
1381
+ return;
1382
+ }
1383
+ const insetTop = Math.floor(top);
1384
+ const insetRight = Math.floor(root.clientWidth - (left + width));
1385
+ const insetBottom = Math.floor(root.clientHeight - (top + height));
1386
+ const insetLeft = Math.floor(left);
1387
+ const rootMargin = `${-insetTop}px ${-insetRight}px ${-insetBottom}px ${-insetLeft}px`;
1388
+ const options = {
1389
+ rootMargin,
1390
+ threshold: Math.max(0, Math.min(1, threshold)) || 1,
1391
+ };
1392
+ let isFirstUpdate = true;
1393
+ function handleObserve(entries) {
1394
+ const ratio = entries[0].intersectionRatio;
1395
+ if (ratio !== threshold) {
1396
+ if (!isFirstUpdate) {
1397
+ return refresh();
1398
+ }
1399
+ if (!ratio) {
1400
+ // If the reference is clipped, the ratio is 0. Throttle the refresh
1401
+ // to prevent an infinite loop of updates.
1402
+ timeoutId = setTimeout(() => {
1403
+ refresh(false, 1e-7);
1404
+ }, 1000);
1405
+ }
1406
+ else {
1407
+ refresh(false, ratio);
1408
+ }
1409
+ }
1410
+ if (ratio === 1
1411
+ && !rectsAreEqual(elementRectForRootMargin, element.getBoundingClientRect())) {
1412
+ // It's possible that even though the ratio is reported as 1, the
1413
+ // element is not actually fully within the IntersectionObserver's root
1414
+ // area anymore. This can happen under performance constraints. This may
1415
+ // be a bug in the browser's IntersectionObserver implementation. To
1416
+ // work around this, we compare the element's bounding rect now with
1417
+ // what it was at the time we created the IntersectionObserver. If they
1418
+ // are not equal then the element moved, so we refresh.
1419
+ refresh();
1420
+ }
1421
+ isFirstUpdate = false;
1422
+ }
1423
+ // Older browsers don't support a `document` as the root and will throw an
1424
+ // error.
1425
+ try {
1426
+ io = new IntersectionObserver(handleObserve, {
1427
+ ...options,
1428
+ // Handle <iframe>s
1429
+ root: root.ownerDocument,
1430
+ });
1431
+ }
1432
+ catch {
1433
+ io = new IntersectionObserver(handleObserve, options);
1434
+ }
1435
+ io.observe(element);
1436
+ }
1437
+ refresh(true);
1438
+ return cleanup;
1439
+ }
1440
+ /**
1441
+ * Automatically updates the position of the floating element when necessary.
1442
+ * Should only be called when the floating element is mounted on the DOM or
1443
+ * visible on the screen.
1444
+ * @param referenceEl
1445
+ * @param floating
1446
+ * @param update
1447
+ * @param options
1448
+ * @returns cleanup function that should be invoked when the floating element is
1449
+ * removed from the DOM or hidden from the screen.
1450
+ * @see https://floating-ui.com/docs/autoUpdate
1451
+ */
1452
+ function autoUpdate(referenceEl, floating, update, options = {}) {
1453
+ const { ancestorScroll = true, ancestorResize = true, elementResize = typeof ResizeObserver === 'function', layoutShift = typeof IntersectionObserver === 'function', animationFrame = false, } = options;
1454
+ const ancestors = ancestorScroll || ancestorResize ?
1455
+ [...(referenceEl ? getOverflowAncestors(referenceEl) : []), ...getOverflowAncestors(floating)]
1456
+ : [];
1457
+ ancestors.forEach(ancestor => {
1458
+ if (ancestorScroll) {
1459
+ ancestor.addEventListener('scroll', update, { passive: true });
1460
+ }
1461
+ if (ancestorResize) {
1462
+ ancestor.addEventListener('resize', update);
1463
+ }
1464
+ });
1465
+ const cleanupIo = referenceEl && layoutShift ? observeMove(referenceEl, update) : null;
1466
+ let reobserveFrame = -1;
1467
+ let resizeObserver = null;
1468
+ if (elementResize) {
1469
+ resizeObserver = new ResizeObserver(([firstEntry]) => {
1470
+ if (firstEntry && firstEntry.target === referenceEl && resizeObserver) {
1471
+ // Prevent update loops when using the `size` middleware.
1472
+ // https://github.com/floating-ui/floating-ui/issues/1740
1473
+ resizeObserver.unobserve(floating);
1474
+ cancelAnimationFrame(reobserveFrame);
1475
+ reobserveFrame = requestAnimationFrame(() => {
1476
+ resizeObserver?.observe(floating);
1477
+ });
1478
+ }
1479
+ update();
1480
+ });
1481
+ if (referenceEl && !animationFrame) {
1482
+ resizeObserver.observe(referenceEl);
1483
+ }
1484
+ resizeObserver.observe(floating);
1485
+ }
1486
+ let frameId;
1487
+ let prevRefRect = animationFrame ? getBoundingClientRect(referenceEl) : null;
1488
+ if (animationFrame) {
1489
+ frameLoop();
1490
+ }
1491
+ function frameLoop() {
1492
+ const nextRefRect = getBoundingClientRect(referenceEl);
1493
+ if (prevRefRect && !rectsAreEqual(prevRefRect, nextRefRect)) {
1494
+ update();
1495
+ }
1496
+ prevRefRect = nextRefRect;
1497
+ frameId = requestAnimationFrame(frameLoop);
1498
+ }
1499
+ update();
1500
+ return () => {
1501
+ ancestors.forEach(ancestor => {
1502
+ if (ancestorScroll) {
1503
+ ancestor.removeEventListener('scroll', update);
1504
+ }
1505
+ if (ancestorResize) {
1506
+ ancestor.removeEventListener('resize', update);
1507
+ }
1508
+ });
1509
+ cleanupIo?.();
1510
+ resizeObserver?.disconnect();
1511
+ resizeObserver = null;
1512
+ if (animationFrame) {
1513
+ cancelAnimationFrame(frameId);
1514
+ }
1515
+ };
1516
+ }
142
1517
  //# sourceMappingURL=floating-dom-controller.js.map