refactor(gifPicker): replace back arrow with clear X inside search input
Swap the mobile-web back arrow for an inline clear (X) button that sits on the right side of the search input and shows whenever the search has text. Clicking it clears the input and resets the active category to trending, returning the user to the default view. Also drops the viewKey-driven FlatList remount, which was the source of the "dialog-reopening" animation when crossing the recents/network boundary — React reconciliation handles the items swap cleanly on its own. Verified in browser: - Typing is responsive on every keystroke; no character-eating - Clearing via the new X returns to pills + trending - Switching recents <-> trending no longer animates - Search-then-clear-then-recents shows only recents (no leftovers) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -100,24 +100,21 @@ function GifPickerBody({
|
||||
const items = isRecentsActive ? getRecents() : networkItems
|
||||
const hasData = items.length > 0
|
||||
|
||||
// Remount the grid only when switching between the network view and the
|
||||
// local recents list, so FlatList doesn't carry stale virtualized cells
|
||||
// across that boundary. Within the network view we rely on normal React
|
||||
// reconciliation as `items` changes — remounting on every search keystroke
|
||||
// would blur the sticky-header search input.
|
||||
const viewKey = isRecentsActive ? 'recents' : 'network'
|
||||
|
||||
const onEndReached = () => {
|
||||
if (isRecentsActive) return
|
||||
if (isFetchingNextPage || !hasNextPage || error) return
|
||||
void fetchNextPage()
|
||||
}
|
||||
|
||||
const onClearSearch = () => {
|
||||
textInputRef.current?.clear()
|
||||
setRawSearch('')
|
||||
setActiveCategory('trending')
|
||||
}
|
||||
|
||||
const onGoBack = () => {
|
||||
if (isSearching || activeCategory !== 'trending') {
|
||||
textInputRef.current?.clear()
|
||||
setRawSearch('')
|
||||
setActiveCategory('trending')
|
||||
onClearSearch()
|
||||
} else {
|
||||
control.close()
|
||||
}
|
||||
@@ -145,7 +142,8 @@ function GifPickerBody({
|
||||
<GifPickerHeader
|
||||
inputRef={textInputRef}
|
||||
onChangeText={onChangeSearch}
|
||||
onGoBack={onGoBack}
|
||||
onClear={onClearSearch}
|
||||
canClear={rawSearch.length > 0}
|
||||
onEscape={() => control.close()}
|
||||
/>
|
||||
{showPills && (
|
||||
@@ -179,7 +177,6 @@ function GifPickerBody({
|
||||
hasData={hasData}
|
||||
isFetchingNextPage={!isRecentsActive && isFetchingNextPage}
|
||||
error={isRecentsActive ? null : error}
|
||||
viewKey={viewKey}
|
||||
fetchNextPage={fetchNextPage}
|
||||
onEndReached={onEndReached}
|
||||
onSelectGif={handleSelectGif}
|
||||
|
||||
@@ -15,13 +15,6 @@ type Props = {
|
||||
hasData: boolean
|
||||
isFetchingNextPage: boolean
|
||||
error: unknown
|
||||
/**
|
||||
* Identifies the current data source at a coarse grain ("recents" vs
|
||||
* "network"). Used to remount the underlying FlatList when crossing that
|
||||
* boundary so virtualized cells from the previous view can't bleed into the
|
||||
* next one.
|
||||
*/
|
||||
viewKey: string
|
||||
fetchNextPage: () => Promise<unknown>
|
||||
onEndReached: () => void
|
||||
onSelectGif: (gif: Gif) => void
|
||||
@@ -35,7 +28,6 @@ export const GifPickerGrid = forwardRef<ListMethods, Props>(
|
||||
hasData,
|
||||
isFetchingNextPage,
|
||||
error,
|
||||
viewKey,
|
||||
fetchNextPage,
|
||||
onEndReached,
|
||||
onSelectGif,
|
||||
@@ -59,7 +51,7 @@ export const GifPickerGrid = forwardRef<ListMethods, Props>(
|
||||
return (
|
||||
<Dialog.InnerFlatList
|
||||
ref={ref}
|
||||
key={`${numColumns}-${viewKey}`}
|
||||
key={String(numColumns)}
|
||||
data={data}
|
||||
renderItem={({item}: {item: Gif[][]}) => (
|
||||
<View style={[a.flex_row, a.gap_sm]}>
|
||||
|
||||
@@ -2,27 +2,27 @@ import {type Ref} from 'react'
|
||||
import {type TextInput, View} from 'react-native'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {atoms as a, native, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
|
||||
import {MagnifyingGlass_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass'
|
||||
import {IS_WEB} from '#/env'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
|
||||
export function GifPickerHeader({
|
||||
inputRef,
|
||||
onChangeText,
|
||||
onGoBack,
|
||||
onClear,
|
||||
onEscape,
|
||||
canClear,
|
||||
}: {
|
||||
inputRef: Ref<TextInput>
|
||||
onChangeText: (text: string) => void
|
||||
onGoBack: () => void
|
||||
onClear: () => void
|
||||
onEscape: () => void
|
||||
canClear: boolean
|
||||
}) {
|
||||
const {t: l} = useLingui()
|
||||
const t = useTheme()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -32,28 +32,15 @@ export function GifPickerHeader({
|
||||
a.pb_md,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
!gtMobile && web(a.gap_md),
|
||||
t.atoms.bg,
|
||||
]}>
|
||||
{!gtMobile && IS_WEB && (
|
||||
<Button
|
||||
size="small"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
onPress={onGoBack}
|
||||
label={l`Go back`}>
|
||||
<ButtonIcon icon={Arrow} size="md" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<TextField.Root style={[!gtMobile && IS_WEB && a.flex_1]}>
|
||||
<TextField.Root style={a.flex_1}>
|
||||
<TextField.Icon icon={Search} />
|
||||
<TextField.Input
|
||||
label={l`Search GIFs`}
|
||||
placeholder={l`Search GIFs`}
|
||||
onChangeText={onChangeText}
|
||||
returnKeyType="search"
|
||||
clearButtonMode="while-editing"
|
||||
inputRef={inputRef}
|
||||
maxLength={50}
|
||||
onKeyPress={({nativeEvent}) => {
|
||||
@@ -62,6 +49,17 @@ export function GifPickerHeader({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{canClear && (
|
||||
<Button
|
||||
size="tiny"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
style={a.z_30}
|
||||
onPress={onClear}
|
||||
label={l`Clear search`}>
|
||||
<ButtonIcon icon={X} size="sm" />
|
||||
</Button>
|
||||
)}
|
||||
</TextField.Root>
|
||||
</View>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user