refactor usedialogcontrol and update a11y translation comments
This commit is contained in:
@@ -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 GIF’s 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 user’s 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 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 (
|
||||
<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: '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}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user