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 {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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user