From 93c52dd1322efd2e7b04112e3057789266f37464 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 2 Jun 2026 16:12:11 +0300 Subject: [PATCH] fix(popover): gate visibility until positioned, tighten types --- src/shared/ui/Popover/Popover.svelte | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/shared/ui/Popover/Popover.svelte b/src/shared/ui/Popover/Popover.svelte index 6ebdb09..27e7efc 100644 --- a/src/shared/ui/Popover/Popover.svelte +++ b/src/shared/ui/Popover/Popover.svelte @@ -47,7 +47,7 @@ interface Props { * ARIA role for the content * @default 'dialog' */ - role?: string; + role?: 'dialog' | 'menu' | 'listbox'; /** * Trigger snippet — spread the provided props onto your trigger element */ @@ -74,8 +74,20 @@ const contentId = `popover-${uid}`; let triggerEl: HTMLElement | undefined = $state(); let contentEl: HTMLElement | undefined = $state(); +/** + * Side actually used after flip. Seeded from the `side` prop; the authoritative + * value is written by updatePosition() on every open, so the seed only matters + * for the closed state (hence the intentional state_referenced_locally warning). + */ let resolvedSide = $state(side); +/** + * True once updatePosition has applied coordinates for the current open. + * Gates visibility so the content never paints at its pre-positioned (0,0) + * top-layer default before the first measurement. + */ +let positioned = $state(false); + /** * Actual DOM open state, driven by the `toggle` event. Source of truth for * whether the browser currently shows the popover; `open` is the public binding. @@ -122,14 +134,18 @@ function updatePosition(): void { resolvedSide = result.side; contentEl.style.left = `${result.x}px`; contentEl.style.top = `${result.y}px`; + positioned = true; } /** * Mirror the `toggle` event into our state. */ -function onToggle(event: Event & { newState?: string }): void { +function onToggle(event: ToggleEvent): void { shown = event.newState === 'open'; open = shown; + if (!shown) { + positioned = false; + } } /** @@ -175,6 +191,11 @@ $effect(() => { {@render trigger(triggerProps)} +
{ data-side={resolvedSide} data-state={shown ? 'open' : 'closed'} ontoggle={onToggle} - style="position: fixed; inset: auto; margin: 0;" + style={`position: fixed; inset: auto; margin: 0;${positioned ? '' : ' visibility: hidden;'}`} class={cn( 'opacity-0 scale-95 transition-discrete transition-all duration-fast', 'starting:opacity-0 starting:scale-95',