From 8d0e3e78e48e35066e1c1bc1d5485e5ead843d1c Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:52:16 -0400 Subject: [PATCH] chore: drop legacy floating panel helpers --- css/tailwind.source.css | 80 ------ docs/design-system.md | 45 +-- js/ui/utils/floatingPanelStyles.js | 172 ----------- js/ui/utils/positionFloatingPanel.js | 407 --------------------------- 4 files changed, 23 insertions(+), 681 deletions(-) delete mode 100644 js/ui/utils/floatingPanelStyles.js delete mode 100644 js/ui/utils/positionFloatingPanel.js diff --git a/css/tailwind.source.css b/css/tailwind.source.css index fb833b32..8eea856f 100644 --- a/css/tailwind.source.css +++ b/css/tailwind.source.css @@ -275,86 +275,6 @@ @apply font-mono; } - [data-floating-anchor="true"] { - anchor-name: --floating-panel; - } - - [data-floating-panel] { - --floating-transform-origin-inline: 50%; - --floating-transform-origin-block: 50%; - --floating-fallback-top: auto; - --floating-fallback-left: auto; - position: fixed; - inset: auto; - top: var(--floating-fallback-top); - left: var(--floating-fallback-left); - right: auto; - bottom: auto; - transform-origin: var(--floating-transform-origin-inline) - var(--floating-transform-origin-block); - } - - [data-floating-panel][data-floating-strategy="absolute"] { - position: absolute; - } - - [data-floating-panel][data-floating-mode="anchor"] { - position-anchor: --floating-panel; - } - - [data-floating-panel][data-floating-placement="top"] { - --floating-transform-origin-block: 100%; - } - - [data-floating-panel][data-floating-placement="bottom"] { - --floating-transform-origin-block: 0%; - } - - [data-floating-panel][data-floating-placement="left"] { - --floating-transform-origin-inline: 100%; - } - - [data-floating-panel][data-floating-placement="right"] { - --floating-transform-origin-inline: 0%; - } - - [data-floating-panel][data-floating-placement="top"][data-floating-alignment="start"], - [data-floating-panel][data-floating-placement="bottom"][data-floating-alignment="start"], - [data-floating-panel][data-floating-placement="top"][data-floating-alignment="end"], - [data-floating-panel][data-floating-placement="bottom"][data-floating-alignment="end"] { - --floating-transform-origin-inline: 50%; - } - - [data-floating-panel][data-floating-dir="ltr"][data-floating-placement="top"][data-floating-alignment="start"], - [data-floating-panel][data-floating-dir="ltr"][data-floating-placement="bottom"][data-floating-alignment="start"] { - --floating-transform-origin-inline: 0%; - } - - [data-floating-panel][data-floating-dir="ltr"][data-floating-placement="top"][data-floating-alignment="end"], - [data-floating-panel][data-floating-dir="ltr"][data-floating-placement="bottom"][data-floating-alignment="end"] { - --floating-transform-origin-inline: 100%; - } - - [data-floating-panel][data-floating-dir="rtl"][data-floating-placement="top"][data-floating-alignment="start"], - [data-floating-panel][data-floating-dir="rtl"][data-floating-placement="bottom"][data-floating-alignment="start"] { - --floating-transform-origin-inline: 100%; - } - - [data-floating-panel][data-floating-dir="rtl"][data-floating-placement="top"][data-floating-alignment="end"], - [data-floating-panel][data-floating-dir="rtl"][data-floating-placement="bottom"][data-floating-alignment="end"] { - --floating-transform-origin-inline: 0%; - } - - [data-floating-panel][data-floating-placement="left"][data-floating-alignment="start"], - [data-floating-panel][data-floating-placement="right"][data-floating-alignment="start"] { - --floating-transform-origin-block: 0%; - } - - [data-floating-panel][data-floating-placement="left"][data-floating-alignment="end"], - [data-floating-panel][data-floating-placement="right"][data-floating-alignment="end"] { - --floating-transform-origin-block: 100%; - } - .popover { @apply relative inline-flex; } diff --git a/docs/design-system.md b/docs/design-system.md index 295ede7e..36cde6b2 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -15,12 +15,11 @@ The design system is now the canonical styling path across bitvid—there is no Inline styles are blocked in production builds, so runtime components that need bespoke coordinates or measurements must route their overrides through `js/designSystem/dynamicStyles.js`. - `registerScope(id, selectors)` creates (or reuses) a constructable stylesheet when supported, allocates a unique identifier, and injects empty rules scoped to `[data-ds-style-id=""]`. Callers attach the attribute to any element that should inherit those rules. -- `setVariables(id, tokens)` updates CSS custom properties on the registered scope. Pass CSS variable names (`"--floating-fallback-top"`) and string or number values—`setVariables` handles serialization and removes entries when they resolve to empty strings. +- `setVariables(id, tokens)` updates CSS custom properties on the registered scope. Pass CSS variable names (for example, `"--ds-metric-probe-length"`) and string or number values—`setVariables` handles serialization and removes entries when they resolve to empty strings. - `releaseScope(id)` removes the generated rules and frees the identifier. Always invoke it from component teardown paths to avoid leaking rules between renders. -The floating panel utility and design-system metrics both lean on this helper: +Design-system metrics lean on this helper to keep runtime measurements inline-style free: -- `createFloatingPanelStyles` assigns each popover a unique scope and publishes fallback coordinates through CSS variables instead of mutating `.style`. Positioning logic reads and writes the same API, and teardown releases the scope before removing dataset attributes. - `readDesignTokenAsPixels` measures non-`px`/`rem` tokens with an offscreen probe that ships in core CSS (`.ds-metric-probe`). The probe is registered through the helper so dynamic lengths are fed into `--ds-metric-probe-length` without touching inline styles. When introducing a new runtime feature that requires dynamic styling: @@ -319,7 +318,7 @@ Logo glyphs now rely on dedicated selectors inside the SVG: apply `.bv-logo__acc ### Popovers -Use `.popover` as the relative anchor for floating content. Nest `.popover__panel` for the floating surface—it’s positioned with the shared helper, inherits the shared focus ring, and animates with opacity/scale tokens. Toggle `[data-state="open"]` on the panel to switch between the collapsed (`opacity-0 scale-95`) and expanded (`opacity-100 scale-100`) states. +Use `.popover` as the relative anchor for floating content. Nest `.popover__panel` for the floating surface—it’s positioned with the shared popover engine, inherits the shared focus ring, and animates with opacity/scale tokens. Toggle `[data-state="open"]` on the panel to switch between the collapsed (`opacity-0 scale-95`) and expanded (`opacity-100 scale-100`) states. ```html
@@ -330,36 +329,38 @@ Use `.popover` as the relative anchor for floating content. Nest `.popover__pane
``` -Use the shared positioning helper (`js/ui/utils/positionFloatingPanel.js`) instead of manual utility offsets. It calculates placement, flips when the surface collides with the viewport, and clamps to an optional padding gutter—all while listening for scroll/resize events and using `ResizeObserver` when available. Call it with the trigger and panel elements, then invoke `positioner.update()` once the panel is visible: +Use the shared popover engine (`js/ui/overlay/popoverEngine.js`) instead of manual offsets. The helper attaches panels to the overlay root, flips placement when they collide with the viewport, and toggles `aria` + `data-popover-state` attributes as visibility changes. Width and height clamps can be sourced from design tokens (`maxWidthToken`, `maxHeightToken`) so surfaces inherit theme-specific limits without inline constants. 【F:js/ui/overlay/popoverEngine.js†L252-L266】【F:js/ui/overlay/popoverEngine.js†L633-L706】【F:css/tailwind.source.css†L358-L399】 ```js -import positionFloatingPanel from "../utils/positionFloatingPanel.js"; +import createPopover from "../overlay/popoverEngine.js"; -const positioner = positionFloatingPanel(triggerEl, panelEl, { - placement: "bottom", // top | bottom | left | right - alignment: "end", // start | center | end (RTL aware) - offset: 8, - viewportPadding: 16, +const popover = createPopover(triggerEl, ({ container }) => { + const panel = document.createElement("div"); + panel.className = "popover__panel card"; + panel.dataset.state = "closed"; + panel.innerHTML = "

Filters

"; + container.appendChild(panel); + return panel; +}, { + placement: "bottom-start", + maxWidthToken: "--popover-inline-safe-max", }); -panelEl.hidden = false; -panelEl.dataset.state = "open"; -positioner.update(); +await popover.open(); +panel.dataset.state = "open"; // reveal animation hooks + +// Later, close the surface and restore focus. +panel.dataset.state = "closed"; +popover.close(); ``` -The helper tags the trigger and surface with floating metadata so CSS tokens can express transform-origins and fallback coordina -tes without bespoke inline styles. Panels receive `data-floating-panel="true"` plus placement, alignment, strategy, and directio -n hooks (`data-floating-placement`, `data-floating-alignment`, `data-floating-strategy`, `data-floating-dir`). The script updates -CSS custom properties `--floating-fallback-top`/`--floating-fallback-left` on each measurement so `css/tailwind.source.css` can -control layout purely through tokens. Browsers that support CSS anchor positioning flip `data-floating-mode` to `anchor`, but th -e same fallback properties keep older engines aligned. 【F:js/ui/utils/positionFloatingPanel.js†L238-L251】【F:css/tailwind.sourc -e.css†L163-L236】 +The render callback runs once the engine has created its portal container. Return the panel (and optional arrow) element so the helper can manage focus targets, `aria-controls`, and collision handling. Consumers remain responsible for their own visual state toggles (`data-state`) so Tailwind transitions continue to apply. Use `popover.update()` when the trigger or panel size changes, and call `popover.destroy()` during teardown to drop listeners and portals. 【F:js/ui/overlay/popoverEngine.js†L420-L583】【F:js/ui/overlay/popoverEngine.js†L888-L1014】 Recommended container pattern: - Wrap the trigger and panel in `.popover` so the markup stays discoverable and inherits spacing tokens. - Leave the wrapper’s overflow visible; the helper defaults to `position: fixed`, so floating surfaces escape scroll containers without getting clipped. Opt into `{ strategy: "absolute" }` if you intentionally want the panel constrained. -- When tearing down a component, call `positioner.destroy()` to drop scroll/resize listeners. +- When tearing down a component, call `popover.destroy()` to drop scroll/resize listeners and remove the portal container. The motion helpers defer to the global motion tokens so Task 5 can adjust timing globally without revisiting individual components. Animations respect `prefers-reduced-motion` by collapsing to opacity-only transitions. diff --git a/js/ui/utils/floatingPanelStyles.js b/js/ui/utils/floatingPanelStyles.js deleted file mode 100644 index cc77834b..00000000 --- a/js/ui/utils/floatingPanelStyles.js +++ /dev/null @@ -1,172 +0,0 @@ -import { - registerScope, - setVariables, - releaseScope, - getScopeAttributeName, -} from "../../designSystem/dynamicStyles.js"; - -const DEFAULT_STATE = { - placement: "bottom", - alignment: "start", - strategy: "fixed", - direction: "ltr", - mode: "fallback", - fallbackTop: null, - fallbackLeft: null, -}; - -const FALLBACK_TOP_VAR = "--floating-fallback-top"; -const FALLBACK_LEFT_VAR = "--floating-fallback-left"; -const SCOPE_BASE_ID = "floating-panel"; -const SCOPE_SELECTORS = [":scope"]; - -function toCssPixel(value) { - if (Number.isFinite(value)) { - return `${value}px`; - } - if (typeof value === "string" && value.trim() !== "") { - return value; - } - return "auto"; -} - -function normalizeDirection(value) { - return value === "rtl" ? "rtl" : "ltr"; -} - -function ensureDatasetValue(element, key, value) { - if (!element) { - return; - } - if (value === undefined || value === null || value === "") { - delete element.dataset[key]; - return; - } - element.dataset[key] = value; -} - -function applyFallbackVariables(scopeId, variables) { - if (!scopeId) { - return; - } - setVariables(scopeId, variables); -} - -export function createFloatingPanelStyles(panel) { - const safePanel = panel || null; - const state = { ...DEFAULT_STATE }; - - const documentRef = safePanel?.ownerDocument || null; - const scopeId = safePanel - ? registerScope(SCOPE_BASE_ID, SCOPE_SELECTORS, { documentRef }) - : null; - - if (safePanel) { - safePanel.dataset.floatingPanel = "true"; - if (scopeId) { - safePanel.setAttribute(getScopeAttributeName(), scopeId); - applyFallbackVariables(scopeId, { - [FALLBACK_TOP_VAR]: "auto", - [FALLBACK_LEFT_VAR]: "auto", - }); - } - } - - const api = { - element: safePanel, - scopeId, - setPlacement(value) { - state.placement = typeof value === "string" && value ? value : DEFAULT_STATE.placement; - ensureDatasetValue(safePanel, "floatingPlacement", state.placement); - return state.placement; - }, - getPlacement() { - return state.placement; - }, - setAlignment(value) { - state.alignment = typeof value === "string" && value ? value : DEFAULT_STATE.alignment; - ensureDatasetValue(safePanel, "floatingAlignment", state.alignment); - return state.alignment; - }, - getAlignment() { - return state.alignment; - }, - setStrategy(value) { - state.strategy = value === "absolute" ? "absolute" : DEFAULT_STATE.strategy; - ensureDatasetValue(safePanel, "floatingStrategy", state.strategy); - return state.strategy; - }, - getStrategy() { - return state.strategy; - }, - setDirection(value) { - state.direction = normalizeDirection(value); - ensureDatasetValue(safePanel, "floatingDir", state.direction); - return state.direction; - }, - getDirection() { - return state.direction; - }, - setMode(value) { - state.mode = value === "anchor" ? "anchor" : DEFAULT_STATE.mode; - ensureDatasetValue(safePanel, "floatingMode", state.mode); - return state.mode; - }, - getMode() { - return state.mode; - }, - setFallbackPosition({ top, left }) { - state.fallbackTop = Number.isFinite(top) ? Number(top) : null; - state.fallbackLeft = Number.isFinite(left) ? Number(left) : null; - if (scopeId) { - applyFallbackVariables(scopeId, { - [FALLBACK_TOP_VAR]: toCssPixel(state.fallbackTop), - [FALLBACK_LEFT_VAR]: toCssPixel(state.fallbackLeft), - }); - } - return { ...api.getFallbackPosition() }; - }, - getFallbackPosition() { - return { - top: state.fallbackTop, - left: state.fallbackLeft, - }; - }, - teardown() { - if (scopeId) { - applyFallbackVariables(scopeId, { - [FALLBACK_TOP_VAR]: "auto", - [FALLBACK_LEFT_VAR]: "auto", - }); - releaseScope(scopeId); - } - if (!safePanel) { - return; - } - safePanel.removeAttribute(getScopeAttributeName()); - delete safePanel.dataset.floatingPanel; - delete safePanel.dataset.floatingPlacement; - delete safePanel.dataset.floatingAlignment; - delete safePanel.dataset.floatingStrategy; - delete safePanel.dataset.floatingDir; - delete safePanel.dataset.floatingMode; - state.placement = DEFAULT_STATE.placement; - state.alignment = DEFAULT_STATE.alignment; - state.strategy = DEFAULT_STATE.strategy; - state.direction = DEFAULT_STATE.direction; - state.mode = DEFAULT_STATE.mode; - state.fallbackTop = DEFAULT_STATE.fallbackTop; - state.fallbackLeft = DEFAULT_STATE.fallbackLeft; - }, - }; - - api.setPlacement(state.placement); - api.setAlignment(state.alignment); - api.setStrategy(state.strategy); - api.setDirection(state.direction); - api.setMode(state.mode); - - return api; -} - -export default createFloatingPanelStyles; diff --git a/js/ui/utils/positionFloatingPanel.js b/js/ui/utils/positionFloatingPanel.js deleted file mode 100644 index d3dbefa6..00000000 --- a/js/ui/utils/positionFloatingPanel.js +++ /dev/null @@ -1,407 +0,0 @@ -import { createFloatingPanelStyles } from "./floatingPanelStyles.js"; - -const DEFAULT_OPTIONS = { - placement: "bottom", - alignment: "start", - offset: 8, - flip: true, - viewportPadding: 12, - strategy: "fixed", - rtl: null, - onUpdate: null, -}; - -const OPPOSITE_PLACEMENT = { - top: "bottom", - bottom: "top", - left: "right", - right: "left", -}; - -function normalizeNumber(value, fallback) { - const numeric = Number(value); - return Number.isFinite(numeric) ? numeric : fallback; -} - -function resolveDirection(anchor, documentRef, explicitRtl) { - if (typeof explicitRtl === "boolean") { - return explicitRtl; - } - const docDir = - documentRef?.documentElement?.dir || documentRef?.body?.dir || ""; - if (docDir) { - return docDir.toLowerCase() === "rtl"; - } - if (anchor && documentRef?.defaultView?.getComputedStyle) { - try { - const direction = documentRef.defaultView.getComputedStyle(anchor).direction; - return direction === "rtl"; - } catch (error) { - // Ignore getComputedStyle failures (e.g., detached elements in jsdom). - } - } - return false; -} - -function clamp(value, min, max) { - if (value < min) { - return min; - } - if (value > max) { - return max; - } - return value; -} - -function computeAlignmentX({ - alignment, - anchorRect, - panelWidth, - rtl, -}) { - switch (alignment) { - case "center": { - return anchorRect.left + anchorRect.width / 2 - panelWidth / 2; - } - case "end": { - return rtl - ? anchorRect.left - : anchorRect.right - panelWidth; - } - case "start": - default: { - return rtl - ? anchorRect.right - panelWidth - : anchorRect.left; - } - } -} - -function computeAlignmentY({ alignment, anchorRect, panelHeight }) { - switch (alignment) { - case "center": { - return anchorRect.top + anchorRect.height / 2 - panelHeight / 2; - } - case "end": { - return anchorRect.bottom - panelHeight; - } - case "start": - default: { - return anchorRect.top; - } - } -} - -function computePosition({ - placement, - alignment, - anchorRect, - panelSize, - offset, - rtl, -}) { - const [panelWidth, panelHeight] = panelSize; - switch (placement) { - case "top": { - return { - top: anchorRect.top - panelHeight - offset, - left: computeAlignmentX({ alignment, anchorRect, panelWidth, rtl }), - }; - } - case "left": { - return { - top: computeAlignmentY({ alignment, anchorRect, panelHeight }), - left: anchorRect.left - panelWidth - offset, - }; - } - case "right": { - return { - top: computeAlignmentY({ alignment, anchorRect, panelHeight }), - left: anchorRect.right + offset, - }; - } - case "bottom": - default: { - return { - top: anchorRect.bottom + offset, - left: computeAlignmentX({ alignment, anchorRect, panelWidth, rtl }), - }; - } - } -} - -function normalizePlacement(value) { - const normalized = typeof value === "string" ? value.toLowerCase() : ""; - if (normalized === "top" || normalized === "bottom") { - return normalized; - } - if (normalized === "left" || normalized === "right") { - return normalized; - } - return "bottom"; -} - -function normalizeAlignment(value) { - const normalized = typeof value === "string" ? value.toLowerCase() : ""; - if (normalized === "center" || normalized === "end") { - return normalized; - } - return "start"; -} - -function getViewportSize(windowRef, documentRef) { - const width = windowRef?.innerWidth || documentRef?.documentElement?.clientWidth || 0; - const height = windowRef?.innerHeight || documentRef?.documentElement?.clientHeight || 0; - return { width, height }; -} - -function resolvePanelSize(panel, previousSize) { - const rect = typeof panel.getBoundingClientRect === "function" - ? panel.getBoundingClientRect() - : { width: 0, height: 0 }; - let width = rect.width || panel.offsetWidth || previousSize?.width || 0; - let height = rect.height || panel.offsetHeight || previousSize?.height || 0; - - if (!width && previousSize?.width) { - width = previousSize.width; - } - if (!height && previousSize?.height) { - height = previousSize.height; - } - - return { width, height }; -} - -function detectAnchorSupport(windowRef) { - const css = windowRef?.CSS; - if (!css || typeof css.supports !== "function") { - return false; - } - try { - return ( - css.supports("anchor-name: --floating-panel") && - css.supports("position-anchor: --floating-panel") - ); - } catch (error) { - return false; - } -} - -export function positionFloatingPanel(anchor, panel, options = {}) { - const safeAnchor = anchor || null; - const safePanel = panel || null; - - if (!safeAnchor || !safePanel) { - return { - update() {}, - destroy() {}, - }; - } - - const documentRef = safeAnchor.ownerDocument || safePanel.ownerDocument || globalThis.document; - const windowRef = documentRef?.defaultView || globalThis; - - const { styles: providedStyles, ...optionOverrides } = options || {}; - const config = { - ...DEFAULT_OPTIONS, - ...optionOverrides, - }; - config.placement = normalizePlacement(config.placement); - config.alignment = normalizeAlignment(config.alignment); - config.offset = clamp(normalizeNumber(config.offset, DEFAULT_OPTIONS.offset), 0, Number.MAX_SAFE_INTEGER); - config.viewportPadding = clamp( - normalizeNumber(config.viewportPadding, DEFAULT_OPTIONS.viewportPadding), - 0, - Number.MAX_SAFE_INTEGER, - ); - config.strategy = config.strategy === "absolute" ? "absolute" : "fixed"; - const rtl = resolveDirection(safeAnchor, documentRef, config.rtl); - - const anchorSupported = detectAnchorSupport(windowRef); - - const state = { - lastSize: { width: 0, height: 0 }, - cleanup: [], - }; - - const styles = - providedStyles && providedStyles.element === safePanel - ? providedStyles - : createFloatingPanelStyles(safePanel); - - safeAnchor.dataset.floatingAnchor = "true"; - styles.setAlignment(config.alignment); - styles.setStrategy(config.strategy); - styles.setDirection(rtl ? "rtl" : "ltr"); - styles.setMode(anchorSupported ? "anchor" : "fallback"); - styles.setPlacement(config.placement); - - const applyPosition = ({ top, left, placement }) => { - styles.setPlacement(placement); - styles.setFallbackPosition({ top, left }); - if (typeof config.onUpdate === "function") { - config.onUpdate({ top, left, placement, alignment: config.alignment, rtl }); - } - }; - - const update = () => { - if (!safeAnchor.isConnected || !safePanel.isConnected) { - return; - } - - const anchorRect = safeAnchor.getBoundingClientRect(); - const { width: panelWidth, height: panelHeight } = resolvePanelSize( - safePanel, - state.lastSize, - ); - - if (!panelWidth && !panelHeight) { - return; - } - - state.lastSize = { width: panelWidth, height: panelHeight }; - - const viewport = getViewportSize(windowRef, documentRef); - - const placementsToTry = [config.placement]; - if (config.flip !== false) { - const opposite = OPPOSITE_PLACEMENT[config.placement]; - if (opposite) { - placementsToTry.push(opposite); - } - if (config.placement === "bottom" || config.placement === "top") { - placementsToTry.push("right", "left"); - } else { - placementsToTry.push("bottom", "top"); - } - } - - let chosenPlacement = config.placement; - let chosenPosition = computePosition({ - placement: chosenPlacement, - alignment: config.alignment, - anchorRect, - panelSize: [panelWidth, panelHeight], - offset: config.offset, - rtl, - }); - - const fitsViewport = ({ top, left }) => { - const withinVertical = - top >= config.viewportPadding && - top + panelHeight <= viewport.height - config.viewportPadding; - const withinHorizontal = - left >= config.viewportPadding && - left + panelWidth <= viewport.width - config.viewportPadding; - return withinVertical && withinHorizontal; - }; - - for (const placement of placementsToTry) { - const position = computePosition({ - placement, - alignment: config.alignment, - anchorRect, - panelSize: [panelWidth, panelHeight], - offset: config.offset, - rtl, - }); - - if (fitsViewport(position)) { - chosenPlacement = placement; - chosenPosition = position; - break; - } - } - - let { top, left } = chosenPosition; - - const maxTop = Math.max( - config.viewportPadding, - viewport.height - config.viewportPadding - panelHeight, - ); - const maxLeft = Math.max( - config.viewportPadding, - viewport.width - config.viewportPadding - panelWidth, - ); - - top = clamp(top, config.viewportPadding, maxTop); - left = clamp(left, config.viewportPadding, maxLeft); - - if (config.strategy === "absolute") { - const offsetParent = safePanel.offsetParent || safePanel.parentElement || documentRef.body; - const parentRect = offsetParent?.getBoundingClientRect - ? offsetParent.getBoundingClientRect() - : { top: 0, left: 0 }; - const scrollTop = offsetParent?.scrollTop || 0; - const scrollLeft = offsetParent?.scrollLeft || 0; - top = top - parentRect.top + scrollTop; - left = left - parentRect.left + scrollLeft; - } - - applyPosition({ top, left, placement: chosenPlacement }); - }; - - const handleScroll = () => update(); - const handleResize = () => update(); - - if (typeof windowRef?.addEventListener === "function") { - windowRef.addEventListener("scroll", handleScroll, true); - windowRef.addEventListener("resize", handleResize); - state.cleanup.push(() => { - windowRef.removeEventListener("scroll", handleScroll, true); - windowRef.removeEventListener("resize", handleResize); - }); - } - - if (typeof windowRef?.ResizeObserver === "function") { - const resizeObserver = new windowRef.ResizeObserver(() => update()); - try { - resizeObserver.observe(safeAnchor); - resizeObserver.observe(safePanel); - } catch (error) { - // Ignore observation errors for detached elements. - } - state.cleanup.push(() => { - resizeObserver.disconnect(); - }); - } - - // Schedule an initial measurement on the next frame to avoid layout thrash - // when the caller opens the panel synchronously. - if (typeof windowRef?.requestAnimationFrame === "function") { - windowRef.requestAnimationFrame(() => update()); - } else { - update(); - } - - return { - update, - destroy() { - while (state.cleanup.length) { - const fn = state.cleanup.pop(); - try { - fn(); - } catch (error) { - // Ignore cleanup errors to avoid breaking consumer teardown flows. - } - } - styles.teardown(); - delete safeAnchor.dataset.floatingAnchor; - }, - styles, - }; -} - -export function destroyFloatingPanel(positioner) { - if (positioner && typeof positioner.destroy === "function") { - positioner.destroy(); - } -} - -export function updateFloatingPanel(positioner) { - if (positioner && typeof positioner.update === "function") { - positioner.update(); - } -} - -export default positionFloatingPanel;