Fix autocomplete popover flicker

This commit is contained in:
Mohamed Boudra
2026-05-26 09:35:28 +07:00
parent 6e56ee9e32
commit 91b05b4111
4 changed files with 469 additions and 30 deletions
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState, type ReactElement } from "react";
import { Keyboard, Platform, StatusBar, View, type LayoutChangeEvent } from "react-native";
import { useEffect, useMemo, useState, type ReactElement } from "react";
import { Keyboard, Platform, StatusBar, View, useWindowDimensions } from "react-native";
import { Portal } from "@gorhom/portal";
import Animated, {
useAnimatedStyle,
@@ -54,7 +54,7 @@ export function AutocompletePopover({
emptyText,
}: AutocompletePopoverProps): ReactElement | null {
const [anchorRect, setAnchorRect] = useState<Rect | null>(null);
const [contentSize, setContentSize] = useState<{ width: number; height: number } | null>(null);
const { height: windowHeight } = useWindowDimensions();
const insets = useSafeAreaInsets();
const { height: rawKeyboardHeight } = useReanimatedKeyboardAnimation();
@@ -76,7 +76,6 @@ export function AutocompletePopover({
useEffect(() => {
if (!visible) {
setAnchorRect(null);
setContentSize(null);
return;
}
let cancelled = false;
@@ -106,34 +105,15 @@ export function AutocompletePopover({
};
}, [visible, anchorRef, openShift, shift]);
const handleLayout = useCallback((event: LayoutChangeEvent) => {
const { width, height } = event.nativeEvent.layout;
setContentSize({ width, height });
}, []);
const baseStyle = useMemo(() => {
if (!anchorRect) return null;
if (!contentSize) {
// Have the anchor, waiting on the popover's own height. Render with the
// final width so the inner Autocomplete lays out at its final size, but
// stay invisible — the first visible paint will already be at the correct
// top. Mirrors combobox.tsx `shouldHideDesktopContent`. See
// docs/floating-panels.md "the two-measurement flash".
return inlineUnistylesStyle({
position: "absolute" as const,
top: 0,
left: anchorRect.x,
width: anchorRect.width,
opacity: 0,
});
}
return inlineUnistylesStyle({
position: "absolute" as const,
top: anchorRect.y - contentSize.height - OFFSET_FROM_ANCHOR,
bottom: windowHeight - anchorRect.y + OFFSET_FROM_ANCHOR,
left: anchorRect.x,
width: anchorRect.width,
});
}, [anchorRect, contentSize]);
}, [anchorRect, windowHeight]);
const animatedTransformStyle = useAnimatedStyle(() => ({
transform: [{ translateY: openShift.value - shift.value }],
@@ -149,7 +129,7 @@ export function AutocompletePopover({
return (
<Portal>
<View style={styles.overlay} pointerEvents="box-none">
<Animated.View style={composedStyle} onLayout={handleLayout}>
<Animated.View testID="composer-autocomplete-popover" style={composedStyle}>
<Autocomplete
options={options}
selectedIndex={selectedIndex}
@@ -206,6 +206,7 @@ export function Autocomplete({
[ensureActiveItemVisible],
);
const selectedOption = options[selectedIndex];
const containerStyle = useMemo(() => [styles.container, { maxHeight }], [maxHeight]);
if (isLoading) {
@@ -238,8 +239,6 @@ export function Autocomplete({
);
}
const selectedOption = options[selectedIndex];
return (
<View style={styles.outerWrapper}>
{selectedOption?.kind === "command" && selectedOption.description ? (
@@ -168,7 +168,7 @@ function resolveAutocompleteIsLoading(args: {
optionsLength: number;
}): boolean {
if (args.mode === "command") {
return args.isCommandsLoading;
return args.isCommandsLoading && args.optionsLength === 0;
}
if (args.mode === "file") {
return (
@@ -251,7 +251,7 @@ export function useAgentAutocomplete(input: UseAgentAutocompleteInput): AgentAut
const isConnected = useHostRuntimeIsConnected(serverId);
const mode = resolveAutocompleteMode({ showFileAutocomplete, showCommandAutocomplete });
const isVisible = resolveAutocompleteIsVisible({
const canShowAutocomplete = resolveAutocompleteIsVisible({
mode,
canLoadCommands,
serverId,
@@ -270,6 +270,8 @@ export function useAgentAutocomplete(input: UseAgentAutocompleteInput): AgentAut
draftConfig: queryDraftConfig,
});
const isVisible = canShowAutocomplete && !(mode === "command" && isCommandsLoading);
const fileSuggestionsQuery = useQuery({
queryKey: [
"directorySuggestions",