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 {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
import {type ListMethods} from '#/view/com/util/List'
import {ios, useBreakpoints} from '#/alf'
import {ios} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {useThrottledValue} from '#/components/hooks/useThrottledValue'
import {
@@ -20,20 +20,14 @@ import {useRecentGifs} from '#/features/gifPicker/hooks/useRecentGifs'
import {type Gif} from '#/features/gifPicker/types'
export function GifPickerDialog({
controlRef,
control,
onClose,
onSelectGif: onSelectGifProp,
}: {
controlRef: React.RefObject<{open: () => void} | null>
control: Dialog.DialogControlProps
onClose?: () => void
onSelectGif: (gif: Gif) => void
}) {
const control = Dialog.useDialogControl()
useImperativeHandle(controlRef, () => ({
open: () => control.open(),
}))
const onSelectGif = (gif: Gif) => {
control.close(() => onSelectGifProp(gif))
}
@@ -66,7 +60,6 @@ function GifPickerBody({
control: Dialog.DialogControlProps
onSelectGif: (gif: Gif) => void
}) {
const {gtMobile} = useBreakpoints()
const textInputRef = useRef<TextInput>(null)
const listRef = useRef<ListMethods>(null)
const [rawSearch, setRawSearch] = useState('')
@@ -125,6 +118,7 @@ function GifPickerBody({
textInputRef.current?.clear()
setRawSearch('')
setActiveCategory('trending')
textInputRef.current?.focus()
}
const onGoBack = () => {
@@ -182,7 +176,7 @@ function GifPickerBody({
return (
<>
{gtMobile && <Dialog.Close />}
<Dialog.Close />
<GifPickerGrid
ref={listRef}
items={items}
@@ -23,19 +23,82 @@ export type GifCategory = {
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[] = [
{id: 'recents', icon: Clock, label: msg`Recents`, searchterm: null},
{id: 'trending', icon: Trending, label: msg`Trending`, searchterm: null},
{id: 'love', icon: Heart, label: msg`Love`, searchterm: 'love'},
{id: 'happy', icon: EmojiSmile, label: msg`Happy`, searchterm: 'happy'},
{id: 'sad', icon: EmojiSad, label: msg`Sad`, searchterm: 'cry'},
{
id: 'recents',
icon: Clock,
label: msg({
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',
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',
},
{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
export function GifCategoryPills({
@@ -10,20 +10,41 @@ export function GifPickerErrorBoundary({details}: {details?: string}) {
const control = Dialog.useDialogContext()
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 />
<ErrorScreen
title={l`Oh no!`}
message={l`There was an unexpected issue in the application. Please let us know if this happened to you!`}
title={l({
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}
/>
<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()}
color="primary"
size="large">
<ButtonText>
<Trans>Close</Trans>
<Trans comment="Visible label of the button that dismisses the GIF picker error dialog.">
Close
</Trans>
</ButtonText>
</Button>
</Dialog.ScrollableInner>
@@ -37,8 +37,16 @@ export function GifPickerHeader({
<TextField.Root style={a.flex_1}>
<TextField.Icon icon={Search} />
<TextField.Input
label={l`Search GIFs`}
placeholder={l`Search KLIPY`}
label={l({
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}
returnKeyType="search"
inputRef={inputRef}
@@ -56,7 +64,11 @@ export function GifPickerHeader({
shape="round"
style={a.z_30}
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" />
</Button>
)}
@@ -28,7 +28,11 @@ export function GifPickerItem({
return (
<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 GIFs title from the provider.',
})}
onPress={onPress}
style={a.w_full}>
{({pressed}) => (
@@ -22,22 +22,43 @@ export function GifPickerPlaceholder({
const {t: l} = useLingui()
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 users search query.',
})
: isRecentsEmpty
? l`No recent GIFs yet. Pick one to see it here.`
: l`No GIFs to show right now. Try again in a moment.`
? l({
message: 'No recent GIFs yet. Pick one to see it here.',
comment:
'Empty-state message shown in the GIF pickers 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 (
<ListMaybePlaceholder
isLoading={isLoading}
isError={isError}
onRetry={onRetry}
onRetry={isError ? onRetry : undefined}
onGoBack={onGoBack}
emptyType="results"
sideBorders={false}
topBorder={false}
errorTitle={l`Couldn't load GIFs`}
errorMessage={l`There was a problem loading GIFs. Check your connection and try again.`}
errorTitle={l({
message: 'Couldnt 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}
/>
)
+21 -8
View File
@@ -1,10 +1,11 @@
import {useCallback, useRef} from 'react'
import {useCallback} from 'react'
import {Keyboard} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {GifSquare_Stroke2_Corner0_Rounded as GifIcon} from '#/components/icons/Gif'
import {useAnalytics} from '#/analytics'
import {GifPickerDialog} from '#/features/gifPicker/GifPickerDialog'
@@ -19,22 +20,34 @@ type Props = {
export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) {
const ax = useAnalytics()
const {_} = useLingui()
const ref = useRef<{open: () => void}>(null)
const control = Dialog.useDialogControl()
const t = useTheme()
const onPressSelectGif = useCallback(async () => {
const onPressSelectGif = useCallback(() => {
ax.metric('composer:gif:open', {})
Keyboard.dismiss()
ref.current?.open()
}, [ax])
control.open()
}, [ax, control])
return (
<>
<Button
testID="openGifBtn"
onPress={onPressSelectGif}
label={_(msg`Select GIF`)}
accessibilityHint={_(msg`Opens GIF select dialog`)}
label={_(
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}
variant="ghost"
shape="round"
@@ -44,7 +57,7 @@ export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) {
</Button>
<GifPickerDialog
controlRef={ref}
control={control}
onClose={onClose}
onSelectGif={onSelectGif}
/>