/** Minimal rect shape consumed by {@link computePosition}. */ export interface AnchorRect { top: number; left: number; right: number; bottom: number; width: number; height: number; } /** * Whether the floating element sits below or above the anchor. * @group Positioning */ export type LLSelectPlacement = 'below' | 'above'; /** * How the floating element decides its width. See `LLSelectBaseSettings` * (`popupWidthPolicy` field) for the user-facing contract. * @group Positioning */ export type LLSelectWidthPolicy = 'fit-content' | 'match-trigger'; /** Input to the pure positioning calculation. */ export interface PositionInput { anchorRect: AnchorRect; viewportWidth: number; viewportHeight: number; /** * Origin of the VISIBLE viewport in layout-viewport (client) coordinates. * `0` except under pinch zoom, where the visual viewport shrinks AND pans * (`visualViewport.offsetLeft/offsetTop`); anchor rects stay in client * coordinates, so clamping against `[0, viewportWidth]` alone would drag * the floating element toward the layout origin. Default `0`. */ viewportLeft?: number; /** See {@link PositionInput.viewportLeft}. Default `0`. */ viewportTop?: number; /** Measured height of the floating element. Pass 0 if unknown. */ floatingHeight: number; /** Width policy. Optional; default `'fit-content'`. */ widthPolicy?: LLSelectWidthPolicy; /** * Floating element's natural (max-content) width in px. Only consulted when * `widthPolicy === 'fit-content'`. Default `0`. */ floatingNaturalWidth?: number; /** * Writing direction of the anchor's context. Only consulted when * `widthPolicy === 'fit-content'`: `'rtl'` right-aligns the floating element * to the anchor and grows LEFTWARD (the mirror of ltr). Default `'ltr'`. * `'match-trigger'` is position-identical in both directions. */ direction?: 'ltr' | 'rtl'; /** * Placement currently in effect, for stickiness across repositions of one * open cycle. When set and the content still fits on that side, it is kept * even if the other side would also fit - so a transient content shrink * (e.g. a filter query matching nothing) does not flip the popup back and * forth. Omit / `undefined` (first placement) to pick fresh. */ currentPlacement?: LLSelectPlacement | undefined; } /** Result of {@link computePosition}: coordinates and chosen placement. */ export interface PositionResult { top: number; left: number; width: number; /** Maximum height the floating element may occupy. */ maxHeight: number; placement: LLSelectPlacement; } /** * Compute where to place the floating element relative to the anchor. * * Vertical: prefers placing below; flips above when it does not fit below and * either fits above or has more room above. When neither side fits, picks the * side with more space and clamps `maxHeight` accordingly. A * `currentPlacement` that still fits is kept (stickiness) - re-preferring * "below" on every content change would make the popup jump sides whenever * the list shrinks and regrows. * * Horizontal: `widthPolicy === 'fit-content'` (default) returns * `width = max(anchor.width, floatingNaturalWidth)`, clamps to * `viewport - 2 * VIEWPORT_PADDING`, and keeps the popup inside the viewport * margins. Growth direction follows `direction`: ltr aligns left edges and * grows rightward; rtl aligns RIGHT edges and grows leftward (the mirror). * * The "viewport" here is the VISIBLE window in client coordinates: * `[viewportLeft, viewportLeft + viewportWidth]` x * `[viewportTop, viewportTop + viewportHeight]`. The offsets are 0 except * under pinch zoom (see {@link PositionInput.viewportLeft}). * `widthPolicy === 'match-trigger'` returns `width = anchor.width` and * `left = anchor.left` (no collision handling - popup is the same width as * trigger; direction-independent). */ export declare function computePosition(input: PositionInput): PositionResult; /** * Whether `anchor` is currently hidden (scrolled out of the layout viewport or * clipped by a scrollable ancestor). Exposed so a caller can refuse to open a * popup against an off-screen trigger BEFORE building a positioner, rather than * opening and then hiding re-entrantly. */ export declare function isAnchorHidden(anchor: HTMLElement): boolean; /** Controls the lifecycle of an active positioner. */ export interface Positioner { /** Force a re-position now. Normally called automatically. */ reposition(): void; /** * Stop tracking and clear all inline styles + `data-placement` from the * floating element. Idempotent. Call once when the floating element is * dismissed. */ detach(): void; } /** Options passed to {@link createPositioner}. */ export interface PositionerOptions { /** * Called when the anchor becomes invisible (fully outside the layout * viewport, or fully clipped by a scrollable ancestor). Typical use: close the * floating element so it does not hang in space without a visible trigger. */ onHide?: () => void; /** * Width policy. Default `'fit-content'`, matching the `popupWidthPolicy` * setting default. */ widthPolicy?: LLSelectWidthPolicy; /** * The floating element's inner scroll container (the popup list). Under an * active `maxHeight` clamp the floating element's overflow is absorbed as * this element's internal scrolling, so `offsetHeight` alone under-reports * the natural height; its `scrollHeight - clientHeight` restores the * difference WITHOUT lifting the clamp to re-measure (a lift-and-restore * would clamp this element's scrollTop to 0 mid-frame - losing the * scrolled-to-chosen position - and caused a visible window-scroll jolt on * Firefox). Omit when the floating element has no inner scroller. * * INVARIANT: this element must have no author-set height cap of its own - * the positioner owns the popup's `maxHeight`. The reconstruction adds back * ALL of its overflow, so an independent `max-height` on the inner list * (theme or consumer CSS) is read as extra natural height and can pick a * side as if the popup were taller than it can render. Shipped themes honor * this; consumer themes must clamp the popup, not the inner list. */ innerScrollEl?: HTMLElement; } /** * Attach a positioner that keeps `floating` placed relative to `anchor`. * * Behavior: sets `floating` to `position: fixed`, listens to window scroll * (capture phase, so any ancestor scroll is caught), window resize, and * `ResizeObserver` on both elements. On every reposition: if the anchor is * outside the layout viewport or clipped by a scrollable ancestor and `onHide` is * provided, calls `onHide` and skips style updates. Otherwise applies the * coordinates from {@link computePosition} and sets `data-placement` on * `floating` for CSS hooks. * * Caller is responsible for calling `detach()` when the floating element is * dismissed; otherwise listeners leak. */ export declare function createPositioner(anchor: HTMLElement, floating: HTMLElement, options?: PositionerOptions): Positioner; //# sourceMappingURL=positioning.d.ts.map