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 {MagnifyingGlass_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass'
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({
inputRef,
onChangeText,
onClose,
onEscape,
autocomplete,
}: {
inputRef: Ref<TextInput>
onChangeText: (text: string) => void
onClose: () => void
onEscape: () => void
autocomplete: GifAutocompleteState
}) {
const {_} = useLingui()
const t = useTheme()
const {gtMobile} = useBreakpoints()
return (
<View
style={[
native(a.pt_4xl),
a.relative,
a.mb_lg,
a.flex_row,
a.align_center,
!gtMobile && web(a.gap_md),
a.pb_sm,
t.atoms.bg,
]}>
{!gtMobile && IS_WEB && (
<Button
size="small"
variant="ghost"
color="secondary"
shape="round"
onPress={onClose}
label={_(msg`Close GIF dialog`)}>
<ButtonIcon icon={Arrow} size="md" />
</Button>
)}
<View style={[native(a.pt_4xl), a.relative, a.mb_lg, a.pb_sm, t.atoms.bg]}>
<View style={[a.flex_row, a.align_center, !gtMobile && web(a.gap_md)]}>
{!gtMobile && IS_WEB && (
<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.Icon icon={Search} />
<TextField.Input
label={_(msg`Search GIFs`)}
placeholder={_(msg`Search GIFs`)}
onChangeText={onChangeText}
returnKeyType="search"
clearButtonMode="while-editing"
inputRef={inputRef}
maxLength={50}
onKeyPress={({nativeEvent}) => {
if (nativeEvent.key === 'Escape') {
onEscape()
<TextField.Root style={[!gtMobile && IS_WEB && a.flex_1]}>
<TextField.Icon icon={Search} />
<TextField.Input
label={_(msg`Search GIFs`)}
placeholder={_(msg`Search GIFs`)}
onChangeText={onChangeText}
returnKeyType="search"
clearButtonMode="while-editing"
inputRef={inputRef}
maxLength={50}
onKeyPress={({nativeEvent}) => {
if (nativeEvent.key === 'Escape') {
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
}
}}
/>
</TextField.Root>
aria-expanded={autocomplete.isVisible}
aria-autocomplete={autocomplete.isVisible ? 'list' : undefined}
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>
)
}