LCOV - code coverage report
Current view: top level - dashql-app/src/ui/foundations - anchored_position.ts Coverage Total Hit
Test: DashQL 915828b Lines: 100.0 % 294 294
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from 'react';
       2              : import { observeSize } from './size_observer.js';
       3              : import { isDesktopHost } from '../../platform/native_globals.js';
       4              : 
       5            1 : export enum AnchorAlignment {
       6            1 :     Start,
       7            1 :     Center,
       8            1 :     End
       9            1 : }
      10              : 
      11            1 : export enum AnchorSide {
      12            1 :     InsideTop = 0 << 1,
      13            1 :     InsideBottom = 1 << 1,
      14            1 :     InsideLeft = 2 << 1,
      15            1 :     InsideRight = 3 << 1,
      16              : 
      17            1 :     InsideCenter = 4 << 1,
      18              : 
      19            1 :     OutsideTop = (5 << 1) | 0b1,
      20            1 :     OutsideBottom = (6 << 1) | 0b1,
      21            1 :     OutsideLeft = (7 << 1) | 0b1,
      22            1 :     OutsideRight = (8 << 1) | 0b1,
      23            1 : }
      24              : 
      25            1 : function isInsideAnchorSide(side: AnchorSide) { return (side & 0b1) == 0; }
      26              : 
      27              : export interface PositionSettings {
      28              :     /// Sets the side of the anchor element that the floating element should be
      29              :     /// pinned to. This side is given by a string starting with either "inside" or
      30              :     /// "outside", followed by a hyphen, followed by either "top", "right", "bottom",
      31              :     /// or "left". Additionally, "inside-center" is an allowed value.
      32              :     ///
      33              :     /// The first part of this string, "inside" or "outside", determines whether the
      34              :     /// floating element should be attempted to be placed "inside" the anchor element
      35              :     /// or "outside" of it. Using "inside" is useful for making it appear that the
      36              :     /// anchor _contains_ the floating element, and it can be used for implementing a
      37              :     /// dialog that is centered on the screen. The "outside" value is more common and
      38              :     /// can be used for tooltips, popovers, menus, etc.
      39              :     ///
      40              :     /// The second part of this string determines the _edge_ on the anchor element that
      41              :     /// the floating element will be anchored to. If side is "inside-center", then
      42              :     /// the floating element will be centered in the X-direction (while align is used
      43              :     /// to position it in the Y-direction).
      44              :     /// Note: "outside-center" is _not_ a valid value for this property.
      45              :     side: AnchorSide;
      46              :     /// Determines how the floating element should align with the anchor element. If
      47              :     /// set to "start", the floating element's first edge (top or left) will align
      48              :     /// with the anchor element's first edge. If set to "center", the floating
      49              :     /// element will be centered along the axis of the anchor edge. If set to "end",
      50              :     /// the floating element's last edge will align with the anchor element's last edge.
      51              :     align: AnchorAlignment;
      52              :     ///  The number of pixels between the anchor edge and the floating element.
      53              :     ///
      54              :     ///  Positive values move the floating element farther from the anchor element
      55              :     ///  (for outside positioning) or further inside the anchor element (for inside
      56              :     ///  positioning). Negative values have the opposite effect.
      57              :     anchorOffset: number;
      58              :     /// An additional offset, in pixels, to move the floating element from
      59              :     /// the aligning edge.
      60              :     ///
      61              :     /// Positive values move the floating element in the direction of center-
      62              :     /// alignment. Negative values move the floating element away from center-
      63              :     /// alignment. When align is "center", positive offsets move the floating
      64              :     /// element right (top or bottom anchor side) or down (left or right
      65              :     /// anchor side).
      66              :     alignmentOffset: number;
      67              :     /// If false, when the above settings result in rendering the floating element
      68              :     /// wholly or partially outside of the bounds of the containing element, attempt
      69              :     /// to adjust the settings to prevent this. Only applies to "outside" positioning.
      70              :     ///
      71              :     /// First, attempt to flip to the opposite edge of the anchor if the floating
      72              :     /// element is getting clipped in that direction. If flipping results in a
      73              :     /// similar clipping, try moving to the adjacent sides.
      74              :     ///
      75              :     /// Once we find a side that does not clip the overlay in its own dimension,
      76              :     /// check the rest of the sides to see if we need to adjust the alignment offset
      77              :     /// to fit in other dimensions.
      78              :     ///
      79              :     /// If we try all four sides and get clipped each time, settle for overflowing
      80              :     /// and use the "bottom" side, since the ability to scroll is most likely in
      81              :     /// this direction.
      82              :     allowOutOfBounds: boolean;
      83              : }
      84              : 
      85              : /// For each outside anchor position, list the order of alternate positions to try in
      86              : /// the event that the original position overflows. See comment on `allowOutOfBounds`
      87              : /// for a more detailed description.
      88            1 : const alternateOrders: Partial<Record<AnchorSide, [AnchorSide, AnchorSide, AnchorSide, AnchorSide]>> = {
      89            1 :     [AnchorSide.OutsideTop]: [AnchorSide.OutsideBottom, AnchorSide.OutsideRight, AnchorSide.OutsideLeft, AnchorSide.OutsideBottom],
      90            1 :     [AnchorSide.OutsideBottom]: [AnchorSide.OutsideTop, AnchorSide.OutsideRight, AnchorSide.OutsideLeft, AnchorSide.OutsideBottom],
      91            1 :     [AnchorSide.OutsideLeft]: [AnchorSide.OutsideRight, AnchorSide.OutsideBottom, AnchorSide.OutsideTop, AnchorSide.OutsideBottom],
      92            1 :     [AnchorSide.OutsideRight]: [AnchorSide.OutsideLeft, AnchorSide.OutsideBottom, AnchorSide.OutsideTop, AnchorSide.OutsideBottom],
      93            1 : }
      94              : 
      95              : /// For each alignment, list the order of alternate alignments to try in
      96              : /// the event that the original position overflows.
      97              : /// Prefer start or end over center.
      98            1 : const alternateAlignments: Partial<Record<AnchorAlignment, [AnchorAlignment, AnchorAlignment]>> = {
      99            1 :     [AnchorAlignment.Start]: [AnchorAlignment.End, AnchorAlignment.Center],
     100            1 :     [AnchorAlignment.End]: [AnchorAlignment.Start, AnchorAlignment.Start],
     101            1 :     [AnchorAlignment.Center]: [AnchorAlignment.End, AnchorAlignment.Start],
     102            1 : }
     103              : 
     104              : interface Size {
     105              :     width: number
     106              :     height: number
     107              : }
     108              : 
     109              : interface Position {
     110              :     top: number
     111              :     left: number
     112              : }
     113              : 
     114              : export interface AnchorPosition {
     115              :     top: number
     116              :     left: number
     117              :     anchorSide: AnchorSide
     118              :     anchorAlign: AnchorAlignment
     119              : }
     120              : 
     121              : interface BoxPosition extends Size, Position { }
     122              : 
     123              : /// Given a floating element and an anchor element, return coordinates for the top-left
     124              : /// of the floating element in order to absolutely position it such that it appears
     125              : /// near the anchor element.
     126            1 : export function getAnchoredPosition(
     127            1 :     floatingElement: Element,
     128            1 :     anchorElement: Element | DOMRect,
     129            1 :     settings: Partial<PositionSettings> = {},
     130            1 : ): AnchorPosition {
     131            1 :     const parentElement = getPositionedParent(floatingElement)
     132            1 :     const clippingRect = getClippingRect(parentElement)
     133              : 
     134            1 :     const parentElementStyle = getComputedStyle(parentElement)
     135            1 :     const parentElementRect = parentElement.getBoundingClientRect()
     136            1 :     const [borderTop, borderLeft] = [parentElementStyle.borderTopWidth, parentElementStyle.borderLeftWidth].map(
     137            1 :         v => parseInt(v, 10) || 0,
     138            1 :     )
     139              :     const relativeRect = {
     140            1 :         top: parentElementRect.top + borderTop,
     141            1 :         left: parentElementRect.left + borderLeft,
     142              :     }
     143              : 
     144            1 :     return pureCalculateAnchoredPosition(
     145            1 :         clippingRect,
     146            1 :         relativeRect,
     147            1 :         floatingElement.getBoundingClientRect(),
     148            1 :         anchorElement instanceof Element ? anchorElement.getBoundingClientRect() : anchorElement,
     149            1 :         getDefaultSettings(settings),
     150            1 :     )
     151            1 : }
     152              : 
     153              : /// Returns the nearest proper HTMLElement parent of `element` whose
     154              : /// position is not "static", or document.body, whichever is closer
     155            1 : function getPositionedParent(element: Element) {
     156            1 :     if (isOnTopLayer(element)) return document.body
     157            1 :     let parentNode = element.parentNode
     158            1 :     while (parentNode !== null) {
     159            1 :         if (parentNode instanceof HTMLElement && getComputedStyle(parentNode).position !== 'static') {
     160            1 :             return parentNode
     161              :         }
     162            1 :         parentNode = parentNode.parentNode
     163            1 :     }
     164            1 :     return document.body
     165            1 : }
     166              : 
     167              : /// Returns true if the element is likely to be on the `top-layer`.
     168            1 : function isOnTopLayer(element: Element) {
     169            1 :     if (element.tagName === 'DIALOG') {
     170            1 :         return true
     171              :     }
     172            1 :     try {
     173              :         // eslint-disable-next-line @typescript-eslint/no-explicit-any
     174            1 :         if (element.matches(':popover-open') && /native code/.test((document.body as any).showPopover?.toString())) {
     175            1 :             return true
     176              :         }
     177            1 :     } catch {
     178            1 :         return false
     179            1 :     }
     180            1 :     return false
     181            1 : }
     182              : 
     183              : /// Returns the rectangle (relative to the window) that will clip the given element
     184              : /// if it is rendered outside of its bounds.
     185            1 : function getClippingRect(element: Element): BoxPosition {
     186            1 :     let parentNode: typeof element.parentNode = element
     187            1 :     while (parentNode !== null) {
     188            1 :         if (!(parentNode instanceof Element)) {
     189            1 :             break
     190              :         }
     191            1 :         const parentNodeStyle = getComputedStyle(parentNode as Element)
     192            1 :         if (parentNodeStyle.overflow !== 'visible') {
     193            1 :             break
     194              :         }
     195            1 :         parentNode = parentNode.parentNode
     196            1 :     }
     197            1 :     const clippingNode = parentNode === document.body || !(parentNode instanceof HTMLElement) ? document.body : parentNode
     198              : 
     199            1 :     const elemRect = clippingNode.getBoundingClientRect()
     200            1 :     const elemStyle = getComputedStyle(clippingNode)
     201              : 
     202            1 :     const [borderTop, borderLeft, borderRight, borderBottom] = [
     203            1 :         elemStyle.borderTopWidth,
     204            1 :         elemStyle.borderLeftWidth,
     205            1 :         elemStyle.borderRightWidth,
     206            1 :         elemStyle.borderBottomWidth,
     207            1 :     ].map(v => parseInt(v, 10) || 0)
     208              : 
     209            1 :     return {
     210            1 :         top: elemRect.top + borderTop,
     211            1 :         left: elemRect.left + borderLeft,
     212            1 :         width: elemRect.width - borderRight - borderLeft,
     213              : 
     214              :         // If the clipping node is document.body, it can expand to the full height of the window
     215            1 :         height: Math.max(
     216            1 :             elemRect.height - borderTop - borderBottom,
     217            1 :             clippingNode === document.body ? window.innerHeight : -Infinity,
     218            1 :         ),
     219            1 :     }
     220            1 : }
     221              : 
     222              : // Pixels of breathing room kept between an out-of-bounds overlay and the
     223              : // window edges. Matches the max-width/max-height inset in overlay.module.css.
     224            1 : const VIEWPORT_INSET = 16;
     225              : 
     226              : // On macOS we run with a frameless window (titleBarStyle: "Overlay"), so the
     227              : // webview spans the full window including the traffic-light region. Reserve
     228              : // extra space at the top so anchored overlays don't underflow the controls.
     229            1 : const MACOS_TITLE_BAR_INSET = 36;
     230            1 : function getTopViewportInset(): number {
     231            1 :     return isDesktopHost() ? MACOS_TITLE_BAR_INSET : VIEWPORT_INSET;
     232            1 : }
     233              : 
     234              : // Default settings to position a floating element
     235            1 : const positionDefaults: PositionSettings = {
     236            1 :     side: AnchorSide.OutsideBottom,
     237            1 :     align: AnchorAlignment.Start,
     238              : 
     239              :     // note: the following default is not applied if side === "inside-center"
     240            1 :     anchorOffset: 4,
     241              : 
     242              :     // note: the following default is only applied if side starts with "inside"
     243              :     // and align is not center
     244            1 :     alignmentOffset: 4,
     245              : 
     246            1 :     allowOutOfBounds: false,
     247            1 : }
     248              : 
     249              : /// Compute a full PositionSettings object from the given partial PositionSettings object
     250              : /// by filling in with defaults where applicable.
     251            1 : function getDefaultSettings(settings: Partial<PositionSettings> = {}): PositionSettings {
     252            1 :     const side = settings.side ?? positionDefaults.side
     253            1 :     const align = settings.align ?? positionDefaults.align
     254            1 :     return {
     255            1 :         side,
     256            1 :         align,
     257              :         // offsets always default to 0 if their respective side/alignment is centered
     258            1 :         anchorOffset: settings.anchorOffset ?? (side === AnchorSide.InsideCenter ? 0 : positionDefaults.anchorOffset),
     259            1 :         alignmentOffset:
     260            1 :             settings.alignmentOffset ??
     261            1 :             (align !== AnchorAlignment.Center && isInsideAnchorSide(side) ? positionDefaults.alignmentOffset : 0),
     262            1 :         allowOutOfBounds: settings.allowOutOfBounds ?? positionDefaults.allowOutOfBounds,
     263            1 :     }
     264            1 : }
     265              : 
     266              : /// Note: This is a pure function with no dependency on DOM APIs.
     267            1 : function pureCalculateAnchoredPosition(
     268            1 :     viewportRect: BoxPosition,
     269            1 :     relativePosition: Position,
     270            1 :     floatingRect: Size,
     271            1 :     anchorRect: BoxPosition,
     272            1 :     { side, align, allowOutOfBounds, anchorOffset, alignmentOffset }: PositionSettings,
     273            1 : ): AnchorPosition {
     274              :     // Compute the relative viewport rect, to bring it into the same coordinate space as `pos`
     275            1 :     const relativeViewportRect: BoxPosition = {
     276            1 :         top: viewportRect.top - relativePosition.top,
     277            1 :         left: viewportRect.left - relativePosition.left,
     278            1 :         width: viewportRect.width,
     279            1 :         height: viewportRect.height,
     280            1 :     }
     281              : 
     282            1 :     let pos = calculatePosition(floatingRect, anchorRect, side, align, anchorOffset, alignmentOffset)
     283            1 :     let anchorSide = side
     284            1 :     let anchorAlign = align
     285            1 :     pos.top -= relativePosition.top
     286            1 :     pos.left -= relativePosition.left
     287              : 
     288              :     // Handle screen overflow
     289            1 :     if (!allowOutOfBounds) {
     290            1 :         const alternateOrder = alternateOrders[side]
     291              : 
     292            1 :         let positionAttempt = 0
     293            1 :         if (alternateOrder) {
     294            1 :             let prevSide = side
     295              : 
     296              :             // Try all the alternate sides until one does not overflow
     297            1 :             while (
     298            1 :                 positionAttempt < alternateOrder.length &&
     299            1 :                 shouldRecalculatePosition(prevSide, pos, relativeViewportRect, floatingRect)
     300            1 :             ) {
     301            1 :                 const nextSide = alternateOrder[positionAttempt++]
     302            1 :                 prevSide = nextSide
     303              : 
     304              :                 // If we have cut off in the same dimension as the "side" option, try flipping to the opposite side.
     305            1 :                 pos = calculatePosition(floatingRect, anchorRect, nextSide, align, anchorOffset, alignmentOffset)
     306            1 :                 pos.top -= relativePosition.top
     307            1 :                 pos.left -= relativePosition.left
     308            1 :                 anchorSide = nextSide
     309            1 :             }
     310            1 :         }
     311              : 
     312              :         // If using alternate anchor side does not stop overflow,
     313              :         // try using an alternate alignment
     314            1 :         const alternateAlignment = alternateAlignments[align]
     315              : 
     316            1 :         let alignmentAttempt = 0
     317            1 :         if (alternateAlignment) {
     318            1 :             let prevAlign = align
     319              : 
     320              :             // Try all the alternate alignments until one does not overflow
     321            1 :             while (
     322            1 :                 alignmentAttempt < alternateAlignment.length &&
     323            1 :                 shouldRecalculateAlignment(prevAlign, pos, relativeViewportRect, floatingRect)
     324            1 :             ) {
     325            1 :                 const nextAlign = alternateAlignment[alignmentAttempt++]
     326            1 :                 prevAlign = nextAlign
     327              : 
     328            1 :                 pos = calculatePosition(floatingRect, anchorRect, anchorSide, nextAlign, anchorOffset, alignmentOffset)
     329            1 :                 pos.top -= relativePosition.top
     330            1 :                 pos.left -= relativePosition.left
     331            1 :                 anchorAlign = nextAlign
     332            1 :             }
     333            1 :         }
     334              : 
     335              :         // At this point we've flipped the position if applicable. Now just nudge until it's on-screen,
     336              :         // keeping VIEWPORT_INSET pixels of breathing room from each window edge. The top inset is
     337              :         // larger on native macOS to clear the frameless-window title bar / traffic lights.
     338              :         // Clamp bottom *first* so the subsequent top clamp wins on overlays taller than the viewport
     339              :         // (we'd rather show the top of the content than the bottom).
     340            1 :         const topInset = getTopViewportInset();
     341            1 :         if (pos.top + floatingRect.height > viewportRect.height + relativeViewportRect.top - VIEWPORT_INSET) {
     342            1 :             pos.top = Math.max(viewportRect.height + relativeViewportRect.top - floatingRect.height - VIEWPORT_INSET, relativeViewportRect.top + topInset)
     343              :         }
     344            1 :         if (pos.top < relativeViewportRect.top + topInset) {
     345            1 :             pos.top = relativeViewportRect.top + topInset
     346              :         }
     347            1 :         if (pos.left < relativeViewportRect.left + VIEWPORT_INSET) {
     348            1 :             pos.left = relativeViewportRect.left + VIEWPORT_INSET
     349              :         }
     350            1 :         if (pos.left + floatingRect.width > viewportRect.width + relativeViewportRect.left - VIEWPORT_INSET) {
     351            1 :             pos.left = viewportRect.width + relativeViewportRect.left - floatingRect.width - VIEWPORT_INSET
     352              :         }
     353            1 :     }
     354              : 
     355            1 :     return { ...pos, anchorSide, anchorAlign }
     356            1 : }
     357              : 
     358              : /// Given a floating element and an anchor element, return coordinates for the
     359              : /// top-left of the floating element in order to absolutely position it such
     360              : /// that it appears near the anchor element.
     361            1 : function calculatePosition(
     362            1 :     elementDimensions: Size,
     363            1 :     anchorPosition: BoxPosition,
     364            1 :     side: AnchorSide,
     365            1 :     align: AnchorAlignment,
     366            1 :     anchorOffset: number,
     367            1 :     alignmentOffset: number,
     368            1 : ) {
     369            1 :     const anchorRight = anchorPosition.left + anchorPosition.width
     370            1 :     const anchorBottom = anchorPosition.top + anchorPosition.height
     371            1 :     let top = -1
     372            1 :     let left = -1
     373            1 :     if (side === AnchorSide.OutsideTop) {
     374            1 :         top = anchorPosition.top - anchorOffset - elementDimensions.height
     375            1 :     } else if (side === AnchorSide.OutsideBottom) {
     376            1 :         top = anchorBottom + anchorOffset
     377            1 :     } else if (side === AnchorSide.OutsideLeft) {
     378            1 :         left = anchorPosition.left - anchorOffset - elementDimensions.width
     379            1 :     } else if (side === AnchorSide.OutsideRight) {
     380            1 :         left = anchorRight + anchorOffset
     381              :     }
     382              : 
     383            1 :     if (side === AnchorSide.OutsideTop || side === AnchorSide.OutsideBottom) {
     384            1 :         if (align === AnchorAlignment.Start) {
     385            1 :             left = anchorPosition.left + alignmentOffset
     386            1 :         } else if (align === AnchorAlignment.Center) {
     387            1 :             left = anchorPosition.left - (elementDimensions.width - anchorPosition.width) / 2 + alignmentOffset
     388              :         } else {
     389              :             // end
     390            1 :             left = anchorRight - elementDimensions.width - alignmentOffset
     391              :         }
     392            1 :     }
     393              : 
     394            1 :     if (side === AnchorSide.OutsideLeft || side === AnchorSide.OutsideRight) {
     395            1 :         if (align === AnchorAlignment.Start) {
     396            1 :             top = anchorPosition.top + alignmentOffset
     397            1 :         } else if (align === AnchorAlignment.Center) {
     398            1 :             top = anchorPosition.top - (elementDimensions.height - anchorPosition.height) / 2 + alignmentOffset
     399              :         } else {
     400              :             // end
     401            1 :             top = anchorBottom - elementDimensions.height - alignmentOffset
     402              :         }
     403            1 :     }
     404              : 
     405            1 :     if (side === AnchorSide.InsideTop) {
     406            1 :         top = anchorPosition.top + anchorOffset
     407            1 :     } else if (side === AnchorSide.InsideBottom) {
     408            1 :         top = anchorBottom - anchorOffset - elementDimensions.height
     409            1 :     } else if (side === AnchorSide.InsideLeft) {
     410            1 :         left = anchorPosition.left + anchorOffset
     411            1 :     } else if (side === AnchorSide.InsideRight) {
     412            1 :         left = anchorRight - anchorOffset - elementDimensions.width
     413            1 :     } else if (side === AnchorSide.InsideCenter) {
     414            1 :         left = (anchorRight + anchorPosition.left) / 2 - elementDimensions.width / 2 + anchorOffset
     415              :     }
     416              : 
     417            1 :     if (side === AnchorSide.InsideTop || side === AnchorSide.InsideBottom) {
     418            1 :         if (align === AnchorAlignment.Start) {
     419            1 :             left = anchorPosition.left + alignmentOffset
     420            1 :         } else if (align === AnchorAlignment.Center) {
     421            1 :             left = anchorPosition.left - (elementDimensions.width - anchorPosition.width) / 2 + alignmentOffset
     422              :         } else {
     423              :             // end
     424            1 :             left = anchorRight - elementDimensions.width - alignmentOffset
     425              :         }
     426            1 :     } else if (side === AnchorSide.InsideLeft || side === AnchorSide.InsideRight || side === AnchorSide.InsideCenter) {
     427            1 :         if (align === AnchorAlignment.Start) {
     428            1 :             top = anchorPosition.top + alignmentOffset
     429            1 :         } else if (align === AnchorAlignment.Center) {
     430            1 :             top = anchorPosition.top - (elementDimensions.height - anchorPosition.height) / 2 + alignmentOffset
     431              :         } else {
     432              :             // end
     433            1 :             top = anchorBottom - elementDimensions.height - alignmentOffset
     434              :         }
     435            1 :     }
     436              : 
     437            1 :     return { top, left }
     438            1 : }
     439              : 
     440              : /// Determines if there is an overflow
     441            1 : function shouldRecalculatePosition(
     442            1 :     side: AnchorSide,
     443            1 :     currentPos: Position,
     444            1 :     containerDimensions: BoxPosition,
     445            1 :     elementDimensions: Size,
     446            1 : ) {
     447            1 :     if (side === AnchorSide.OutsideTop || side === AnchorSide.OutsideBottom) {
     448            1 :         return (
     449            1 :             currentPos.top < containerDimensions.top ||
     450            1 :             currentPos.top + elementDimensions.height > containerDimensions.height + containerDimensions.top
     451              :         )
     452              :     } else {
     453            1 :         return (
     454            1 :             currentPos.left < containerDimensions.left ||
     455            1 :             currentPos.left + elementDimensions.width > containerDimensions.width + containerDimensions.left
     456              :         )
     457              :     }
     458            1 : }
     459              : 
     460              : /// Determines if there is an overflow
     461            1 : function shouldRecalculateAlignment(
     462            1 :     align: AnchorAlignment,
     463            1 :     currentPos: Position,
     464            1 :     containerDimensions: BoxPosition,
     465            1 :     elementDimensions: Size,
     466            1 : ) {
     467            1 :     if (align === AnchorAlignment.End) {
     468            1 :         return currentPos.left < containerDimensions.left
     469            1 :     } else if (align === AnchorAlignment.Start || align === AnchorAlignment.Center) {
     470            1 :         return (
     471              :             // right edge
     472            1 :             currentPos.left + elementDimensions.width > containerDimensions.left + containerDimensions.width ||
     473              :             // left edge
     474            1 :             currentPos.left < containerDimensions.left
     475              :         )
     476              :     }
     477            1 : }
     478              : 
     479              : export interface AnchoredPositionHookArgs extends Partial<PositionSettings> {
     480              :     floatingElementRef?: React.RefObject<HTMLElement | null>
     481              :     anchorElementRef?: React.RefObject<Element | null>
     482              :     /// Observe window resizes? Callers can disable this while the floating element is hidden. @default true
     483              :     observeWindow?: boolean
     484              : }
     485              : 
     486            1 : export function useAnchoredPosition(args?: AnchoredPositionHookArgs, dependencies: React.DependencyList = []): {
     487              :     floatingElementRef: React.RefObject<Element | null>
     488              :     anchorElementRef: React.RefObject<Element | null>
     489              :     position: AnchorPosition | undefined
     490            1 : } {
     491            1 :     const altFloatingElementRef = React.useRef<HTMLElement | null>(null);
     492            1 :     const altAnchorElementRef = React.useRef<Element | null>(null);
     493              : 
     494            1 :     const floatingElementRef = args?.floatingElementRef ?? altFloatingElementRef;
     495            1 :     const anchorElementRef = args?.anchorElementRef ?? altAnchorElementRef;
     496            1 :     const [position, setPosition] = React.useState<AnchorPosition | undefined>(undefined)
     497              : 
     498            1 :     const windowElem = React.useRef(document.documentElement);
     499            1 :     const windowSize = observeSize(windowElem, args?.observeWindow ?? true);
     500              : 
     501              :     // Update the anchored position whenever the window resizes
     502            1 :     React.useLayoutEffect(() => {
     503            1 :         if (floatingElementRef.current instanceof Element && anchorElementRef.current instanceof Element) {
     504            1 :             setPosition(getAnchoredPosition(floatingElementRef.current, anchorElementRef.current, args))
     505              :         } else {
     506            1 :             setPosition(undefined)
     507              :         }
     508            1 :     }, [windowSize, ...dependencies]);
     509              : 
     510            1 :     return {
     511            1 :         floatingElementRef,
     512            1 :         anchorElementRef,
     513            1 :         position,
     514            1 :     }
     515            1 : }
        

Generated by: LCOV version 2.0-1