fix(e5): stop the filter dropdown flashing at the origin before it positions

The combi-input popover flashed in the top-left corner for a frame, then
jumped into place. Two causes, both confirmed with a per-frame capture in
the browser:

1. The panel painted before floating-ui computed a position — gated its
   visibility on `isPositioned` (hidden until positioned), same as
   PopoverSurface.
2. The bigger one: the `popIn` entrance animation animates `transform`
   (scale/translateY), and it sat on the SAME element floating-ui positions
   via `transform` — so during the 0.16s animation the keyframes OVERRODE
   the translate(x,y) and the panel rendered at the origin. Moved the glass
   styling + popIn onto an inner child, leaving the positioned element's
   transform untouched.

Frame capture after the fix: frame 0 hidden, first visible frame already at
translate(82,474) — never visible at the origin. Also gave the size
middleware a min width for a narrow rail.
This commit is contained in:
2026-08-21 04:00:54 +02:00
parent 29f2c9059e
commit caced8664c
+15 -10
View File
@@ -51,7 +51,7 @@ export default function MultiSelectChips({
const inputRef = useRef<HTMLInputElement>(null);
const fade = useScrollFade();
const { refs, floatingStyles, context } = useFloating({
const { refs, floatingStyles, context, isPositioned } = useFloating({
open,
onOpenChange: setOpen,
placement: "bottom-start",
@@ -62,12 +62,10 @@ export default function MultiSelectChips({
shift({ padding: 8 }),
size({
padding: 8,
apply({ rects, elements, availableHeight }) {
// Match the field width and cap to the space below (or 320px, whichever is smaller).
Object.assign(elements.floating.style, {
width: `${rects.reference.width}px`,
maxHeight: `${Math.min(availableHeight, 320)}px`,
});
apply({ rects, elements }) {
// Match the field width (with a sane minimum for a very narrow rail); the inner scroll box
// owns the height cap (max-h-64).
elements.floating.style.width = `${Math.max(rects.reference.width, 192)}px`;
},
}),
],
@@ -166,15 +164,22 @@ export default function MultiSelectChips({
<FloatingPortal>
<div
ref={refs.setFloating}
style={floatingStyles}
// Keep the panel hidden until floating-ui has computed a real position — otherwise the first
// frame paints it at the container's top-left before autoUpdate runs. (isPositioned guard,
// same as PopoverSurface.)
style={{ ...floatingStyles, visibility: isPositioned ? "visible" : "hidden" }}
{...getFloatingProps()}
aria-label={label}
className="glass-menu z-popover min-w-48 rounded-xl p-1.5 shadow-lg animate-[popIn_0.16s_ease]"
className="z-popover"
>
{/* The entrance animation + glass styling live ONE level in: popIn animates `transform`
(scale/translateY), which on the positioned element itself would OVERRIDE floating-ui's
transform-based position and flash the panel at the origin for the animation's duration.
Keeping it on a child leaves the outer element's position transform untouched. */}
<div
ref={fade.ref}
style={fade.style}
className="max-h-64 overflow-y-auto no-scrollbar"
className="glass-menu max-h-64 w-full overflow-y-auto rounded-xl p-1.5 no-scrollbar shadow-lg animate-[popIn_0.16s_ease]"
>
{filtered.length === 0 ? (
<p className="px-2 py-1.5 text-xs text-muted">