fix(app): prevent desktop combobox initial 0,0 flash

This commit is contained in:
Mohamed Boudra
2026-02-16 18:37:04 +07:00
parent f590e60617
commit 427e3daca9
2 changed files with 183 additions and 4 deletions
+53 -4
View File
@@ -47,10 +47,29 @@ export interface ComboboxProps {
open?: boolean;
onOpenChange?: (open: boolean) => void;
desktopPlacement?: "top-start" | "bottom-start";
/**
* Prevents an initial frame at 0,0 by hiding desktop content until floating
* coordinates resolve. This intentionally disables fade enter/exit animation
* for that combobox instance to avoid animation overriding hidden opacity.
*/
desktopPreventInitialFlash?: boolean;
anchorRef: React.RefObject<View | null>;
children?: ReactNode;
}
function toNumericStyleValue(value: unknown): number | null {
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
if (typeof value === "string") {
const parsed = Number.parseFloat(value);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return null;
}
function ComboboxSheetBackground({ style }: BottomSheetBackgroundProps) {
return (
<Animated.View
@@ -171,6 +190,7 @@ export function Combobox({
open,
onOpenChange,
desktopPlacement = "top-start",
desktopPreventInitialFlash = true,
anchorRef,
children,
}: ComboboxProps): ReactElement {
@@ -180,6 +200,7 @@ export function Combobox({
const snapPoints = useMemo(() => ["60%", "90%"], []);
const [availableSize, setAvailableSize] = useState<{ width?: number; height?: number } | null>(null);
const [referenceWidth, setReferenceWidth] = useState<number | null>(null);
const [referenceAtOrigin, setReferenceAtOrigin] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [activeIndex, setActiveIndex] = useState<number>(-1);
@@ -263,10 +284,38 @@ export function Combobox({
setReferenceWidth(null);
return;
}
const raf = requestAnimationFrame(() => update());
const raf = requestAnimationFrame(() => void update());
return () => cancelAnimationFrame(raf);
}, [desktopPlacement, isMobile, update, isOpen]);
useEffect(() => {
if (!isOpen || isMobile) {
setReferenceAtOrigin(false);
return;
}
const referenceEl = anchorRef.current;
if (!referenceEl) {
setReferenceAtOrigin(false);
return;
}
referenceEl.measureInWindow((x, y) => {
setReferenceAtOrigin(Math.abs(x) <= 1 && Math.abs(y) <= 1);
});
}, [anchorRef, isMobile, isOpen]);
const floatingTop = toNumericStyleValue(floatingStyles.top);
const floatingLeft = toNumericStyleValue(floatingStyles.left);
const hasResolvedDesktopPosition =
referenceWidth !== null &&
floatingTop !== null &&
floatingLeft !== null &&
(floatingTop !== 0 || floatingLeft !== 0 || referenceAtOrigin);
const shouldHideDesktopContent =
desktopPreventInitialFlash && !hasResolvedDesktopPosition;
const shouldUseDesktopFade = !desktopPreventInitialFlash;
useEffect(() => {
if (!isMobile) return;
if (isOpen) {
@@ -514,8 +563,8 @@ export function Combobox({
<View ref={refs.setOffsetParent} collapsable={false} style={styles.desktopOverlay}>
<Pressable style={styles.desktopBackdrop} onPress={handleClose} />
<Animated.View
entering={FadeIn.duration(100)}
exiting={FadeOut.duration(100)}
entering={shouldUseDesktopFade ? FadeIn.duration(100) : undefined}
exiting={shouldUseDesktopFade ? FadeOut.duration(100) : undefined}
style={[
styles.desktopContainer,
{
@@ -524,7 +573,7 @@ export function Combobox({
maxWidth: 400,
},
floatingStyles,
referenceWidth === null ? { opacity: 0 } : null,
shouldHideDesktopContent ? { opacity: 0 } : null,
typeof availableSize?.height === "number" ? { maxHeight: Math.min(availableSize.height, 400) } : null,
]}
ref={refs.setFloating}