import { ReactiveController, ReactiveControllerHost } from 'lit'; import { Placement, PlacementOptions, VirtualTrigger } from './types.js'; /** * **PlacementController** — Lit reactive controller that positions a floating * element relative to a trigger using [Floating UI](https://floating-ui.com/) * (`computePosition` + `autoUpdate`). * * Hyphenated placements are accepted on the public API. Logical sides * (`start`, `end`) resolve to physical sides against the trigger's computed * writing direction (`start` is the left in LTR, the right in RTL), so the * panel lands on the correct side. Logical alignment suffixes (`bottom-start`) * are left for Floating UI's own RTL handling; the consumer's CSS still owns * direction-aware styling such as tip orientation. A `dir` change on the * document root or the trigger's nearest `[dir]` ancestor recomputes * placement, so runtime LTR/RTL flips re-resolve correctly. * * ### Available-space custom properties * * The `size` middleware does **not** write `max-width` / `max-height` on the * floating element — an inline max-size would override the consuming * component's intended CSS max-size. Instead it exposes the space available to * the trigger as two custom properties on the floating element, refreshed on * every compute. The caller removes them after any exit transition completes * (clearing them in `stop()` would snap the floating element's size * mid-animation): * * - `--swc-placement-available-width` — usable inline space, in px. * - `--swc-placement-available-height` — usable block space, in px (floored to * a minimum so a cramped trigger still yields a usable panel). * * Only components positioned by this controller receive these properties. * Such a component opts into viewport-bounded sizing by combining them with * its own intended size via `min()`, with a fallback for when the controller * is not active: * * ```css * .floating { * box-sizing: border-box; * max-inline-size: min(var(--intended-width), var(--swc-placement-available-width, 100vi)); * max-block-size: min(var(--intended-height), var(--swc-placement-available-height, 90vb)); * overflow: auto; * } * ``` * * The fallbacks use logical viewport units (`vi` / `vb`); `90vb` leaves a small * buffer so the trigger isn't scrolled out of view. `box-sizing: border-box` is * required so the available-space values account for the element's own padding * and border rather than overflowing past them. Pair `max-block-size` with * `overflow: auto` so the panel scrolls when the block space is constrained * (read `isConstrained` to detect that state). * * @example * ```typescript * this.placement.start(this.trigger, this.dialog, { * placement: 'bottom-start', * offset: 0, * onPlacementChange: (p) => { * this.actualPlacement = p; * }, * }); * ``` */ export declare class PlacementController implements ReactiveController { private cleanup?; private session; /** * The computed placement after `flip` reorients (hyphenated). `null` when * `stop` has been called. * * Set synchronously to the requested `PlacementOptions.placement` * (or `DEFAULT_PLACEMENT`) when `start` is called, then refreshed on every * successful `computePlacement` pass. `PlacementOptions.onPlacementChange` * fires when this value changes (and once after the first compute), so * consumers can mirror it in a single callback without also reading it * synchronously after `start()`. */ actualPlacement: Placement | null; /** * Whether the floating content would overflow the available space * below/above the trigger on the last compute. `true` when the content is * taller than the available height, so a consumer applying * `max-block-size: min(…, var(--swc-placement-available-height))` will be * clamping (and should enable scrolling). Informational only — the * controller no longer writes `max-height` itself. */ isConstrained: boolean; /** * Natural floating-element height from the first un-constrained compute, * used as the baseline that determines whether subsequent computes are * clamping content. Cleared on `stop()`. */ private initialHeight?; /** * Last placement handed to `onPlacementChange`. `actualPlacement` is refreshed * on every compute, but the callback fires only when the computed placement * actually changes — `autoUpdate` ticks that recompute the same placement do * not re-notify consumers. `undefined` until the first compute (and after * `stop`), so the first compute of a session always notifies. */ private lastEmittedPlacement?; /** * Registers this controller on `host` via `addController`. * * @param host - Reactive element that owns the floating surface lifecycle. */ constructor(host: ReactiveControllerHost); /** * Begin positioning `floating` relative to `trigger`. * * Tears down any prior session, stores the new options, and subscribes to * Floating UI `autoUpdate` so placement stays correct on scroll and resize. * * @param trigger - Anchor element or `VirtualTrigger`. * @param floating - Element to position (`position: fixed` or top-layer). * @param options - `PlacementOptions`; omitted properties use defaults. */ start(trigger: HTMLElement | VirtualTrigger, floating: HTMLElement, options?: PlacementOptions): void; /** * Stop positioning: tear down `autoUpdate`, clear `actualPlacement` and * `isConstrained`. Inline style cleanup — including `translate`, `top`, * `left`, and `--swc-placement-available-*` — is left to the caller so * exit transitions can complete before those properties are removed. * Safe to call multiple times. */ stop(): void; /** * Force one recomputation outside the `autoUpdate` callback. * * Use when floating content reflows internally or when a * `VirtualTrigger` moves without a DOM mutation. No-op if * `start` has not been called. */ recompute(): void; /** * Lit lifecycle hook — tears down positioning when the host disconnects. * * @internal */ hostDisconnected(): void; /** * Run Floating UI `computePosition`, apply the result to the floating element, * and update `actualPlacement` when `flip` reorients. * * Waits for web fonts and a WebKit animation frame before measuring. Skips * when the floating element has zero dimensions. Aborts if `stop` or a * new `start` call replaces the active session while awaiting async work. */ private computePlacement; } export type { Placement, PlacementOptions, VirtualTrigger } from './types.js'; export { ALL_PLACEMENTS } from './types.js'; export { fromFloatingPlacement, toFloatingPlacement, } from './placement-conversion.js';