Wire autocomplete into GifPickerHeader

Adds `autocomplete` prop (GifAutocompleteState) to GifPickerHeader,
restructures layout so suggestions render below the search bar, and
wires up keyboard navigation + ARIA attributes for the combobox pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
vineyardbovines
2026-04-14 14:19:23 -04:00
parent a5766e8426
commit aa576a1b28
@@ -9,65 +9,87 @@ import * as TextField from '#/components/forms/TextField'
import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
import {MagnifyingGlass_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass' import {MagnifyingGlass_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass'
import {IS_WEB} from '#/env' import {IS_WEB} from '#/env'
import {
GIF_AUTOCOMPLETE_LISTBOX_ID,
GifAutocompleteSuggestions,
suggestionItemId,
} from '#/features/gifPicker/components/GifAutocompleteSuggestions'
import {type GifAutocompleteState} from '#/features/gifPicker/hooks/useGifAutocomplete'
export function GifPickerHeader({ export function GifPickerHeader({
inputRef, inputRef,
onChangeText, onChangeText,
onClose, onClose,
onEscape, onEscape,
autocomplete,
}: { }: {
inputRef: Ref<TextInput> inputRef: Ref<TextInput>
onChangeText: (text: string) => void onChangeText: (text: string) => void
onClose: () => void onClose: () => void
onEscape: () => void onEscape: () => void
autocomplete: GifAutocompleteState
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
return ( return (
<View <View style={[native(a.pt_4xl), a.relative, a.mb_lg, a.pb_sm, t.atoms.bg]}>
style={[ <View style={[a.flex_row, a.align_center, !gtMobile && web(a.gap_md)]}>
native(a.pt_4xl), {!gtMobile && IS_WEB && (
a.relative, <Button
a.mb_lg, size="small"
a.flex_row, variant="ghost"
a.align_center, color="secondary"
!gtMobile && web(a.gap_md), shape="round"
a.pb_sm, onPress={onClose}
t.atoms.bg, label={_(msg`Close GIF dialog`)}>
]}> <ButtonIcon icon={Arrow} size="md" />
{!gtMobile && IS_WEB && ( </Button>
<Button )}
size="small"
variant="ghost"
color="secondary"
shape="round"
onPress={onClose}
label={_(msg`Close GIF dialog`)}>
<ButtonIcon icon={Arrow} size="md" />
</Button>
)}
<TextField.Root style={[!gtMobile && IS_WEB && a.flex_1]}> <TextField.Root style={[!gtMobile && IS_WEB && a.flex_1]}>
<TextField.Icon icon={Search} /> <TextField.Icon icon={Search} />
<TextField.Input <TextField.Input
label={_(msg`Search GIFs`)} label={_(msg`Search GIFs`)}
placeholder={_(msg`Search GIFs`)} placeholder={_(msg`Search GIFs`)}
onChangeText={onChangeText} onChangeText={onChangeText}
returnKeyType="search" returnKeyType="search"
clearButtonMode="while-editing" clearButtonMode="while-editing"
inputRef={inputRef} inputRef={inputRef}
maxLength={50} maxLength={50}
onKeyPress={({nativeEvent}) => { onKeyPress={({nativeEvent}) => {
if (nativeEvent.key === 'Escape') { if (nativeEvent.key === 'Escape') {
onEscape() if (!autocomplete.handleKeyDown('Escape')) {
onEscape()
}
} else {
autocomplete.handleKeyDown(nativeEvent.key)
}
}}
// @ts-ignore web-only ARIA props
role={autocomplete.isVisible ? 'combobox' : undefined}
aria-controls={
autocomplete.isVisible ? GIF_AUTOCOMPLETE_LISTBOX_ID : undefined
} }
}} aria-expanded={autocomplete.isVisible}
/> aria-autocomplete={autocomplete.isVisible ? 'list' : undefined}
</TextField.Root> aria-activedescendant={
autocomplete.isVisible && autocomplete.activeIndex >= 0
? suggestionItemId(autocomplete.activeIndex)
: undefined
}
/>
</TextField.Root>
</View>
{/* future: tabs (Trending / Recents / Categories) render here */} {autocomplete.isVisible && (
<GifAutocompleteSuggestions
suggestions={autocomplete.suggestions}
activeIndex={autocomplete.activeIndex}
onSelect={autocomplete.selectSuggestion}
/>
)}
</View> </View>
) )
} }