Fix search typeahead performance with absolute overlay (#11103)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-07-09 12:36:37 +03:00
committed by GitHub
parent 40b802ba15
commit 83da55c32b
2 changed files with 121 additions and 68 deletions
+55 -24
View File
@@ -13,6 +13,11 @@ import {
View, View,
type ViewStyle, type ViewStyle,
} from 'react-native' } from 'react-native'
import Animated, {
Easing,
FadeInDown,
FadeOutDown,
} from 'react-native-reanimated'
import {setStringAsync} from 'expo-clipboard' import {setStringAsync} from 'expo-clipboard'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useFocusEffect, useNavigation, useRoute} from '@react-navigation/native' import {useFocusEffect, useNavigation, useRoute} from '@react-navigation/native'
@@ -41,7 +46,15 @@ import {
withoutFilterParams, withoutFilterParams,
} from '#/screens/Search/searchParams' } from '#/screens/Search/searchParams'
import {makeSearchQuery} from '#/screens/Search/utils' import {makeSearchQuery} from '#/screens/Search/utils'
import {atoms as a, tokens, useBreakpoints, useTheme, web} from '#/alf' import {
atoms as a,
native,
platform,
tokens,
useBreakpoints,
useTheme,
web,
} from '#/alf'
import {useAutocomplete} from '#/components/Autocomplete' import {useAutocomplete} from '#/components/Autocomplete'
import {Button, ButtonIcon} from '#/components/Button' import {Button, ButtonIcon} from '#/components/Button'
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow' import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow'
@@ -387,13 +400,14 @@ export function SearchScreenShell({
const handleProfileClick = useCallback( const handleProfileClick = useCallback(
(profile: bsky.profile.AnyProfileView) => { (profile: bsky.profile.AnyProfileView) => {
updateSearchText('')
unstableCacheProfileView(queryClient, profile) unstableCacheProfileView(queryClient, profile)
// Slight delay to avoid updating during push nav animation. // Slight delay to avoid updating during push nav animation.
setTimeout(() => { setTimeout(() => {
updateProfileHistory(profile) updateProfileHistory(profile)
}, 400) }, 400)
}, },
[updateProfileHistory, queryClient], [updateProfileHistory, queryClient, updateSearchText],
) )
/** /**
@@ -468,6 +482,17 @@ export function SearchScreenShell({
} }
}, [setShowAutocomplete]) }, [setShowAutocomplete])
const onSearchInputBlur = useCallback(() => {
/*
* Bind autocomplete visibility to focus state on native. On web this
* doesn't work because of focus management, which would render the
* autocomplete results uninteractable.
*/
if (IS_NATIVE) {
setShowAutocomplete(false)
}
}, [])
const focusSearchInput = useCallback( const focusSearchInput = useCallback(
(tab?: TabParam) => { (tab?: TabParam) => {
textInput.current?.focus() textInput.current?.focus()
@@ -597,6 +622,7 @@ export function SearchScreenShell({
ref={textInput} ref={textInput}
value={searchText} value={searchText}
onFocus={onSearchInputFocus} onFocus={onSearchInputFocus}
onBlur={onSearchInputBlur}
onChangeText={onChangeText} onChangeText={onChangeText}
onClearText={onPressClearQuery} onClearText={onPressClearQuery}
onSubmitEditing={onSubmit('typed')} onSubmitEditing={onSubmit('typed')}
@@ -643,11 +669,31 @@ export function SearchScreenShell({
</Layout.Center> </Layout.Center>
</View> </View>
<View <View style={[a.flex_1, a.relative]}>
style={{ <View style={[a.flex_1, web(showAutocomplete && a.hidden)]}>
display: showAutocomplete && !fixedParams ? 'flex' : 'none', <SearchScreenInner
flex: 1, key={filters.lang ?? ''}
}}> activeTab={activeTab}
setActiveTab={setActiveTab}
query={query}
queryWithParams={queryWithParams}
filters={filters}
hasFilters={hasFilters}
headerHeight={headerHeight}
focusSearchInput={focusSearchInput}
/>
</View>
{showAutocomplete && !fixedParams && (
<Animated.View
entering={native(FadeInDown.easing(Easing.out(Easing.cubic)))}
exiting={native(FadeOutDown.easing(Easing.out(Easing.cubic)))}
style={platform({
web: [a.flex_1],
native: [t.atoms.bg, a.absolute, a.inset_0],
})}
accessibilityViewIsModal
accessibilityRole="list">
{searchText.length > 0 && IS_NATIVE ? ( {searchText.length > 0 && IS_NATIVE ? (
<AutocompleteResults <AutocompleteResults
items={autocompleteItems} items={autocompleteItems}
@@ -671,23 +717,8 @@ export function SearchScreenShell({
onRemoveProfileClick={deleteProfileHistoryItem} onRemoveProfileClick={deleteProfileHistoryItem}
/> />
)} )}
</View> </Animated.View>
<View )}
style={{
display: showAutocomplete ? 'none' : 'flex',
flex: 1,
}}>
<SearchScreenInner
key={filters.lang ?? ''}
activeTab={activeTab}
setActiveTab={setActiveTab}
query={query}
queryWithParams={queryWithParams}
filters={filters}
hasFilters={hasFilters}
headerHeight={headerHeight}
focusSearchInput={focusSearchInput}
/>
</View> </View>
</Layout.Screen> </Layout.Screen>
) )
+36 -14
View File
@@ -1,8 +1,8 @@
import {Pressable, ScrollView, View} from 'react-native' import {ScrollView, View} from 'react-native'
import {moderateProfile, type ModerationOpts} from '@atproto/api' import {moderateProfile, type ModerationOpts} from '@atproto/api'
import {Plural, Trans, useLingui} from '@lingui/react/macro' import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {createHitslop, HITSLOP_10} from '#/lib/constants' import {createHitslop} from '#/lib/constants'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
@@ -89,7 +89,7 @@ export function SearchHistory({
)} )}
{searchHistory.length > 0 && ( {searchHistory.length > 0 && (
<View style={[a.px_lg, a.pt_sm]}> <View style={[a.pt_sm]}>
{searchHistory.slice(0, 5).map((historyItem, index) => { {searchHistory.slice(0, 5).map((historyItem, index) => {
const {q, filters} = parseHistoryEntry(historyItem) const {q, filters} = parseHistoryEntry(historyItem)
const filterCount = countActiveFilters(filters) const filterCount = countActiveFilters(filters)
@@ -132,13 +132,24 @@ function SearchHistoryItem({
const {t: l} = useLingui() const {t: l} = useLingui()
return ( return (
<View style={[a.flex_row, a.gap_sm, a.align_center]}> <View style={[a.flex_row, a.align_center]}>
<Pressable <Button label={l`Search for ${q}`} onPress={onPress} style={[a.flex_1]}>
accessibilityRole="button" {({hovered, focused, pressed}) => (
onPress={onPress} <View
hitSlop={HITSLOP_10} style={[
style={[a.flex_1, a.py_sm, a.flex_row, a.align_center, a.gap_sm]}> a.flex_1,
<Text style={[a.text_md, a.flex_shrink]} numberOfLines={1}> a.flex_row,
a.align_center,
a.gap_sm,
a.px_lg,
a.py_md,
a.pr_5xl,
(hovered || focused || pressed) && t.atoms.bg_contrast_25,
]}>
<Text
emoji
style={[a.text_md, a.leading_snug, a.flex_shrink]}
numberOfLines={1}>
{q} {q}
</Text> </Text>
{filterCount > 0 ? ( {filterCount > 0 ? (
@@ -151,19 +162,30 @@ function SearchHistoryItem({
t.atoms.bg_contrast_25, t.atoms.bg_contrast_25,
]}> ]}>
<Text <Text
style={[a.text_xs, a.font_medium, t.atoms.text_contrast_medium]}> style={[
<Plural value={filterCount} one="+# filter" other="+# filters" /> a.text_xs,
a.font_medium,
t.atoms.text_contrast_medium,
]}>
<Plural
value={filterCount}
one="+# filter"
other="+# filters"
/>
</Text> </Text>
</View> </View>
) : null} ) : null}
</Pressable> </View>
)}
</Button>
<Button <Button
label={l`Remove ${q}`} label={l`Remove ${q}`}
onPress={onRemove} onPress={onRemove}
size="small" size="small"
variant="ghost" variant="ghost"
color="secondary" color="secondary"
shape="round"> shape="round"
style={[a.absolute, {right: 16}, a.bg_transparent]}>
<ButtonIcon icon={XIcon} /> <ButtonIcon icon={XIcon} />
</Button> </Button>
</View> </View>