refactor usedialogcontrol and update a11y translation comments

This commit is contained in:
vineyardbovines
2026-05-05 10:59:58 -04:00
parent a04a944f46
commit 1456e738c8
7 changed files with 170 additions and 42 deletions
+6 -12
View File
@@ -1,9 +1,9 @@
import {useEffect, useImperativeHandle, useRef, useState} from 'react' import {useEffect, useRef, useState} from 'react'
import {type TextInput} from 'react-native' import {type TextInput} from 'react-native'
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
import {type ListMethods} from '#/view/com/util/List' import {type ListMethods} from '#/view/com/util/List'
import {ios, useBreakpoints} from '#/alf' import {ios} from '#/alf'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {useThrottledValue} from '#/components/hooks/useThrottledValue' import {useThrottledValue} from '#/components/hooks/useThrottledValue'
import { import {
@@ -20,20 +20,14 @@ import {useRecentGifs} from '#/features/gifPicker/hooks/useRecentGifs'
import {type Gif} from '#/features/gifPicker/types' import {type Gif} from '#/features/gifPicker/types'
export function GifPickerDialog({ export function GifPickerDialog({
controlRef, control,
onClose, onClose,
onSelectGif: onSelectGifProp, onSelectGif: onSelectGifProp,
}: { }: {
controlRef: React.RefObject<{open: () => void} | null> control: Dialog.DialogControlProps
onClose?: () => void onClose?: () => void
onSelectGif: (gif: Gif) => void onSelectGif: (gif: Gif) => void
}) { }) {
const control = Dialog.useDialogControl()
useImperativeHandle(controlRef, () => ({
open: () => control.open(),
}))
const onSelectGif = (gif: Gif) => { const onSelectGif = (gif: Gif) => {
control.close(() => onSelectGifProp(gif)) control.close(() => onSelectGifProp(gif))
} }
@@ -66,7 +60,6 @@ function GifPickerBody({
control: Dialog.DialogControlProps control: Dialog.DialogControlProps
onSelectGif: (gif: Gif) => void onSelectGif: (gif: Gif) => void
}) { }) {
const {gtMobile} = useBreakpoints()
const textInputRef = useRef<TextInput>(null) const textInputRef = useRef<TextInput>(null)
const listRef = useRef<ListMethods>(null) const listRef = useRef<ListMethods>(null)
const [rawSearch, setRawSearch] = useState('') const [rawSearch, setRawSearch] = useState('')
@@ -125,6 +118,7 @@ function GifPickerBody({
textInputRef.current?.clear() textInputRef.current?.clear()
setRawSearch('') setRawSearch('')
setActiveCategory('trending') setActiveCategory('trending')
textInputRef.current?.focus()
} }
const onGoBack = () => { const onGoBack = () => {
@@ -182,7 +176,7 @@ function GifPickerBody({
return ( return (
<> <>
{gtMobile && <Dialog.Close />} <Dialog.Close />
<GifPickerGrid <GifPickerGrid
ref={listRef} ref={listRef}
items={items} items={items}
@@ -23,19 +23,82 @@ export type GifCategory = {
searchterm: string | null // null = trending/recents (handled by consumer) searchterm: string | null // null = trending/recents (handled by consumer)
} }
/*
* Category pill labels are icon-only buttons in the UI; the `label` field is
* what screen readers announce. Each is phrased "[topic] GIFs" so the
* announcement makes sense in isolation rather than just "Love" or "Happy".
*/
export const GIF_CATEGORIES: readonly GifCategory[] = [ export const GIF_CATEGORIES: readonly GifCategory[] = [
{id: 'recents', icon: Clock, label: msg`Recents`, searchterm: null}, {
{id: 'trending', icon: Trending, label: msg`Trending`, searchterm: null}, id: 'recents',
{id: 'love', icon: Heart, label: msg`Love`, searchterm: 'love'}, icon: Clock,
{id: 'happy', icon: EmojiSmile, label: msg`Happy`, searchterm: 'happy'}, label: msg({
{id: 'sad', icon: EmojiSad, label: msg`Sad`, searchterm: 'cry'}, message: 'Recent GIFs',
comment:
'Accessibility label for the icon-only pill that shows previously selected GIFs in the GIF picker.',
}),
searchterm: null,
},
{
id: 'trending',
icon: Trending,
label: msg({
message: 'Trending GIFs',
comment:
'Accessibility label for the icon-only pill that shows currently trending/featured GIFs in the GIF picker.',
}),
searchterm: null,
},
{
id: 'love',
icon: Heart,
label: msg({
message: 'Love GIFs',
comment:
'Accessibility label for the icon-only pill that filters the GIF picker to GIFs about love/affection.',
}),
searchterm: 'love',
},
{
id: 'happy',
icon: EmojiSmile,
label: msg({
message: 'Happy GIFs',
comment:
'Accessibility label for the icon-only pill that filters the GIF picker to happy/joyful GIFs.',
}),
searchterm: 'happy',
},
{
id: 'sad',
icon: EmojiSad,
label: msg({
message: 'Sad GIFs',
comment:
'Accessibility label for the icon-only pill that filters the GIF picker to sad/crying GIFs.',
}),
searchterm: 'cry',
},
{ {
id: 'party', id: 'party',
icon: Celebrate, icon: Celebrate,
label: msg`Party`, label: msg({
message: 'Party GIFs',
comment:
'Accessibility label for the icon-only pill that filters the GIF picker to celebration/party GIFs.',
}),
searchterm: 'congratulations', searchterm: 'congratulations',
}, },
{id: 'yes', icon: Shaka, label: msg`Yes`, searchterm: 'yes'}, {
id: 'yes',
icon: Shaka,
label: msg({
message: 'Yes GIFs',
comment:
'Accessibility label for the icon-only pill that filters the GIF picker to affirmation/agreement GIFs.',
}),
searchterm: 'yes',
},
] as const ] as const
export function GifCategoryPills({ export function GifCategoryPills({
@@ -10,20 +10,41 @@ export function GifPickerErrorBoundary({details}: {details?: string}) {
const control = Dialog.useDialogContext() const control = Dialog.useDialogContext()
return ( return (
<Dialog.ScrollableInner style={a.gap_md} label={l`An error has occurred`}> <Dialog.ScrollableInner
style={a.gap_md}
label={l({
message: 'An error has occurred',
comment:
'Accessibility label for the dialog shown when the GIF picker hits an unexpected runtime error and falls back to its error boundary.',
})}>
<Dialog.Close /> <Dialog.Close />
<ErrorScreen <ErrorScreen
title={l`Oh no!`} title={l({
message={l`There was an unexpected issue in the application. Please let us know if this happened to you!`} message: 'Oh no!',
comment:
'Title of the error screen shown when the GIF picker crashes unexpectedly.',
})}
message={l({
message:
'There was an unexpected issue in the application. Please let us know if this happened to you!',
comment:
'Body of the error screen shown when the GIF picker crashes unexpectedly. Encourages the user to report the issue.',
})}
details={details} details={details}
/> />
<Button <Button
label={l`Close dialog`} label={l({
message: 'Close dialog',
comment:
'Accessibility label for the button that dismisses the GIF picker error dialog.',
})}
onPress={() => control.close()} onPress={() => control.close()}
color="primary" color="primary"
size="large"> size="large">
<ButtonText> <ButtonText>
<Trans>Close</Trans> <Trans comment="Visible label of the button that dismisses the GIF picker error dialog.">
Close
</Trans>
</ButtonText> </ButtonText>
</Button> </Button>
</Dialog.ScrollableInner> </Dialog.ScrollableInner>
@@ -37,8 +37,16 @@ export function GifPickerHeader({
<TextField.Root style={a.flex_1}> <TextField.Root style={a.flex_1}>
<TextField.Icon icon={Search} /> <TextField.Icon icon={Search} />
<TextField.Input <TextField.Input
label={l`Search GIFs`} label={l({
placeholder={l`Search KLIPY`} message: 'Search GIFs',
comment:
'Accessibility label for the GIF search input inside the GIF picker dialog.',
})}
placeholder={l({
message: 'Search KLIPY',
comment:
'Placeholder text inside the GIF search input. KLIPY is the third-party GIF provider; keep the brand name as-is.',
})}
onChangeText={onChangeText} onChangeText={onChangeText}
returnKeyType="search" returnKeyType="search"
inputRef={inputRef} inputRef={inputRef}
@@ -56,7 +64,11 @@ export function GifPickerHeader({
shape="round" shape="round"
style={a.z_30} style={a.z_30}
onPress={onClear} onPress={onClear}
label={l`Clear search`}> label={l({
message: 'Clear GIF search',
comment:
'Accessibility label for the X button inside the search input that clears the typed query and returns to the trending feed.',
})}>
<ButtonIcon icon={X} size="sm" /> <ButtonIcon icon={X} size="sm" />
</Button> </Button>
)} )}
@@ -28,7 +28,11 @@ export function GifPickerItem({
return ( return (
<Button <Button
label={l`Select GIF "${gif.title}"`} label={l({
message: `Select GIF "${gif.title}"`,
comment:
'Accessibility label for an individual GIF tile in the picker grid. The placeholder is the GIF’s title from the provider.',
})}
onPress={onPress} onPress={onPress}
style={a.w_full}> style={a.w_full}>
{({pressed}) => ( {({pressed}) => (
@@ -22,22 +22,43 @@ export function GifPickerPlaceholder({
const {t: l} = useLingui() const {t: l} = useLingui()
const emptyMessage = isSearching const emptyMessage = isSearching
? l`No GIFs found for "${query}".` ? l({
message: `No GIFs found for "${query}".`,
comment:
'Empty-state message shown in the GIF picker when a search returns zero results. Placeholder is the user’s search query.',
})
: isRecentsEmpty : isRecentsEmpty
? l`No recent GIFs yet. Pick one to see it here.` ? l({
: l`No GIFs to show right now. Try again in a moment.` message: 'No recent GIFs yet. Pick one to see it here.',
comment:
'Empty-state message shown in the GIF picker’s Recents tab before the user has selected any GIFs.',
})
: l({
message: 'No GIFs to show right now. Try again in a moment.',
comment:
'Empty-state message shown when the trending/featured GIF feed returns no results (rare, usually a transient provider issue).',
})
return ( return (
<ListMaybePlaceholder <ListMaybePlaceholder
isLoading={isLoading} isLoading={isLoading}
isError={isError} isError={isError}
onRetry={onRetry} onRetry={isError ? onRetry : undefined}
onGoBack={onGoBack} onGoBack={onGoBack}
emptyType="results" emptyType="results"
sideBorders={false} sideBorders={false}
topBorder={false} topBorder={false}
errorTitle={l`Couldn't load GIFs`} errorTitle={l({
errorMessage={l`There was a problem loading GIFs. Check your connection and try again.`} message: 'Couldn’t load GIFs',
comment:
'Title of the error screen shown when the GIF provider request fails.',
})}
errorMessage={l({
message:
'There was a problem loading GIFs. Check your connection and try again.',
comment:
'Body message of the error screen shown when the GIF provider request fails. Encourages the user to retry.',
})}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
/> />
) )
+21 -8
View File
@@ -1,10 +1,11 @@
import {useCallback, useRef} from 'react' import {useCallback} from 'react'
import {Keyboard} from 'react-native' import {Keyboard} from 'react-native'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button' import {Button} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {GifSquare_Stroke2_Corner0_Rounded as GifIcon} from '#/components/icons/Gif' import {GifSquare_Stroke2_Corner0_Rounded as GifIcon} from '#/components/icons/Gif'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {GifPickerDialog} from '#/features/gifPicker/GifPickerDialog' import {GifPickerDialog} from '#/features/gifPicker/GifPickerDialog'
@@ -19,22 +20,34 @@ type Props = {
export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) { export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) {
const ax = useAnalytics() const ax = useAnalytics()
const {_} = useLingui() const {_} = useLingui()
const ref = useRef<{open: () => void}>(null) const control = Dialog.useDialogControl()
const t = useTheme() const t = useTheme()
const onPressSelectGif = useCallback(async () => { const onPressSelectGif = useCallback(() => {
ax.metric('composer:gif:open', {}) ax.metric('composer:gif:open', {})
Keyboard.dismiss() Keyboard.dismiss()
ref.current?.open() control.open()
}, [ax]) }, [ax, control])
return ( return (
<> <>
<Button <Button
testID="openGifBtn" testID="openGifBtn"
onPress={onPressSelectGif} onPress={onPressSelectGif}
label={_(msg`Select GIF`)} label={_(
accessibilityHint={_(msg`Opens GIF select dialog`)} msg({
message: 'Select GIF',
comment:
'Accessibility label for the button in the post composer that opens the GIF picker dialog.',
}),
)}
accessibilityHint={_(
msg({
message: 'Opens the GIF picker dialog',
comment:
'Accessibility hint announced after the GIF picker button label, describing what activating it will do.',
}),
)}
style={a.p_sm} style={a.p_sm}
variant="ghost" variant="ghost"
shape="round" shape="round"
@@ -44,7 +57,7 @@ export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) {
</Button> </Button>
<GifPickerDialog <GifPickerDialog
controlRef={ref} control={control}
onClose={onClose} onClose={onClose}
onSelectGif={onSelectGif} onSelectGif={onSelectGif}
/> />