fix(popover): gate visibility until positioned, tighten types
This commit is contained in:
@@ -47,7 +47,7 @@ interface Props {
|
|||||||
* ARIA role for the content
|
* ARIA role for the content
|
||||||
* @default 'dialog'
|
* @default 'dialog'
|
||||||
*/
|
*/
|
||||||
role?: string;
|
role?: 'dialog' | 'menu' | 'listbox';
|
||||||
/**
|
/**
|
||||||
* Trigger snippet — spread the provided props onto your trigger element
|
* Trigger snippet — spread the provided props onto your trigger element
|
||||||
*/
|
*/
|
||||||
@@ -74,8 +74,20 @@ const contentId = `popover-${uid}`;
|
|||||||
|
|
||||||
let triggerEl: HTMLElement | undefined = $state();
|
let triggerEl: HTMLElement | undefined = $state();
|
||||||
let contentEl: 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);
|
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
|
* 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.
|
* whether the browser currently shows the popover; `open` is the public binding.
|
||||||
@@ -122,14 +134,18 @@ function updatePosition(): void {
|
|||||||
resolvedSide = result.side;
|
resolvedSide = result.side;
|
||||||
contentEl.style.left = `${result.x}px`;
|
contentEl.style.left = `${result.x}px`;
|
||||||
contentEl.style.top = `${result.y}px`;
|
contentEl.style.top = `${result.y}px`;
|
||||||
|
positioned = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mirror the `toggle` event into our state.
|
* Mirror the `toggle` event into our state.
|
||||||
*/
|
*/
|
||||||
function onToggle(event: Event & { newState?: string }): void {
|
function onToggle(event: ToggleEvent): void {
|
||||||
shown = event.newState === 'open';
|
shown = event.newState === 'open';
|
||||||
open = shown;
|
open = shown;
|
||||||
|
if (!shown) {
|
||||||
|
positioned = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -175,6 +191,11 @@ $effect(() => {
|
|||||||
|
|
||||||
{@render trigger(triggerProps)}
|
{@render trigger(triggerProps)}
|
||||||
|
|
||||||
|
<!--
|
||||||
|
inset:auto + margin:0 neutralize the UA popover stylesheet (which sets
|
||||||
|
inset:0; margin:auto to center it) so the JS-applied left/top win.
|
||||||
|
visibility is hidden until updatePosition runs (see `positioned`).
|
||||||
|
-->
|
||||||
<div
|
<div
|
||||||
bind:this={contentEl}
|
bind:this={contentEl}
|
||||||
id={contentId}
|
id={contentId}
|
||||||
@@ -183,7 +204,7 @@ $effect(() => {
|
|||||||
data-side={resolvedSide}
|
data-side={resolvedSide}
|
||||||
data-state={shown ? 'open' : 'closed'}
|
data-state={shown ? 'open' : 'closed'}
|
||||||
ontoggle={onToggle}
|
ontoggle={onToggle}
|
||||||
style="position: fixed; inset: auto; margin: 0;"
|
style={`position: fixed; inset: auto; margin: 0;${positioned ? '' : ' visibility: hidden;'}`}
|
||||||
class={cn(
|
class={cn(
|
||||||
'opacity-0 scale-95 transition-discrete transition-all duration-fast',
|
'opacity-0 scale-95 transition-discrete transition-all duration-fast',
|
||||||
'starting:opacity-0 starting:scale-95',
|
'starting:opacity-0 starting:scale-95',
|
||||||
|
|||||||
Reference in New Issue
Block a user