diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/lib/constants.ts | 2 | ||||
-rw-r--r-- | src/view/com/composer/AltTextCounterWrapper.tsx | 33 | ||||
-rw-r--r-- | src/view/com/composer/GifAltText.tsx | 137 | ||||
-rw-r--r-- | src/view/com/composer/char-progress/CharProgress.tsx | 50 | ||||
-rw-r--r-- | src/view/com/composer/photos/ImageAltTextDialog.tsx | 127 |
5 files changed, 231 insertions, 118 deletions
diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 9bf1fb35e..0edc9f2ad 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -50,7 +50,7 @@ export const MAX_DM_GRAPHEME_LENGTH = 1000 // Recommended is 100 per: https://www.w3.org/WAI/GL/WCAG20/tests/test3.html // but increasing limit per user feedback -export const MAX_ALT_TEXT = 1000 +export const MAX_ALT_TEXT = 2000 export function IS_TEST_USER(handle?: string) { return handle && handle?.endsWith('.test') diff --git a/src/view/com/composer/AltTextCounterWrapper.tsx b/src/view/com/composer/AltTextCounterWrapper.tsx new file mode 100644 index 000000000..d69252f4b --- /dev/null +++ b/src/view/com/composer/AltTextCounterWrapper.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import {View} from 'react-native' + +import {MAX_ALT_TEXT} from '#/lib/constants' +import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' +import {atoms as a, useTheme} from '#/alf' + +export function AltTextCounterWrapper({ + altText, + children, +}: { + altText?: string + children: React.ReactNode +}) { + const t = useTheme() + return ( + <View style={[a.flex_row]}> + <CharProgress + style={[ + a.flex_col_reverse, + a.align_center, + a.mr_xs, + {minWidth: 50, gap: 1}, + ]} + textStyle={[{marginRight: 0}, a.text_sm, t.atoms.text_contrast_medium]} + size={26} + count={altText?.length || 0} + max={MAX_ALT_TEXT} + /> + {children} + </View> + ) +} diff --git a/src/view/com/composer/GifAltText.tsx b/src/view/com/composer/GifAltText.tsx index 3479fb973..90d20d94f 100644 --- a/src/view/com/composer/GifAltText.tsx +++ b/src/view/com/composer/GifAltText.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useState} from 'react' +import React, {useState} from 'react' import {TouchableOpacity, View} from 'react-native' import {AppBskyEmbedExternal} from '@atproto/api' import {msg, Trans} from '@lingui/macro' @@ -11,14 +11,16 @@ import { EmbedPlayerParams, parseEmbedPlayerFromUrl, } from '#/lib/strings/embed-player' -import {enforceLen} from '#/lib/strings/helpers' import {isAndroid} from '#/platform/detection' import {Gif} from '#/state/queries/tenor' +import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper' import {atoms as a, native, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' +import {DialogControlProps} from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' +import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {PortalComponent} from '#/components/Portal' import {Text} from '#/components/Typography' @@ -52,18 +54,11 @@ export function GifAltText({ } }, [linkProp]) - const onPressSubmit = useCallback( - (alt: string) => { - control.close(() => { - onSubmit(alt) - }) - }, - [onSubmit, control], - ) + const parsedAlt = parseAltFromGIFDescription(link.description) + const [altText, setAltText] = useState(parsedAlt.alt) if (!gif || !params) return null - const parsedAlt = parseAltFromGIFDescription(link.description) return ( <> <TouchableOpacity @@ -99,13 +94,19 @@ export function GifAltText({ <AltTextReminder /> - <Dialog.Outer control={control} Portal={Portal}> + <Dialog.Outer + control={control} + onClose={() => { + onSubmit(altText) + }} + Portal={Portal}> <Dialog.Handle /> <AltTextInner - onSubmit={onPressSubmit} + altText={altText} + setAltText={setAltText} + control={control} link={link} params={params} - initialValue={parsedAlt.isPreferred ? parsedAlt.alt : ''} key={link.uri} /> </Dialog.Outer> @@ -114,61 +115,83 @@ export function GifAltText({ } function AltTextInner({ - onSubmit, + altText, + setAltText, + control, link, params, - initialValue: initalValue, }: { - onSubmit: (text: string) => void + altText: string + setAltText: (text: string) => void + control: DialogControlProps link: AppBskyEmbedExternal.ViewExternal params: EmbedPlayerParams - initialValue: string }) { - const {_} = useLingui() - const [altText, setAltText] = useState(initalValue) - const control = Dialog.useDialogContext() - - const onPressSubmit = useCallback(() => { - onSubmit(altText) - }, [onSubmit, altText]) + const t = useTheme() + const {_, i18n} = useLingui() return ( <Dialog.ScrollableInner label={_(msg`Add alt text`)}> <View style={a.flex_col_reverse}> <View style={[a.mt_md, a.gap_md]}> - <View> - <TextField.LabelText> - <Trans>Descriptive alt text</Trans> - </TextField.LabelText> - <TextField.Root> - <Dialog.Input - label={_(msg`Alt text`)} - placeholder={link.title} - onChangeText={text => - setAltText(enforceLen(text, MAX_ALT_TEXT)) - } - value={altText} - multiline - numberOfLines={3} - autoFocus - onKeyPress={({nativeEvent}) => { - if (nativeEvent.key === 'Escape') { - control.close() - } - }} - /> - </TextField.Root> + <View style={[a.gap_sm]}> + <View style={[a.relative]}> + <TextField.LabelText> + <Trans>Descriptive alt text</Trans> + </TextField.LabelText> + <TextField.Root> + <Dialog.Input + label={_(msg`Alt text`)} + placeholder={link.title} + onChangeText={text => { + setAltText(text) + }} + defaultValue={altText} + multiline + numberOfLines={3} + autoFocus + onKeyPress={({nativeEvent}) => { + if (nativeEvent.key === 'Escape') { + control.close() + } + }} + /> + </TextField.Root> + </View> + + {altText.length > MAX_ALT_TEXT && ( + <View style={[a.pb_sm, a.flex_row, a.gap_xs]}> + <CircleInfo fill={t.palette.negative_500} /> + <Text + style={[ + a.italic, + a.leading_snug, + t.atoms.text_contrast_medium, + ]}> + <Trans> + Alt text will be truncated. Limit:{' '} + {i18n.number(MAX_ALT_TEXT)} characters. + </Trans> + </Text> + </View> + )} </View> - <Button - label={_(msg`Save`)} - size="large" - color="primary" - variant="solid" - onPress={onPressSubmit}> - <ButtonText> - <Trans>Save</Trans> - </ButtonText> - </Button> + + <AltTextCounterWrapper altText={altText}> + <Button + label={_(msg`Save`)} + size="large" + color="primary" + variant="solid" + onPress={() => { + control.close() + }} + style={[a.flex_grow]}> + <ButtonText> + <Trans>Save</Trans> + </ButtonText> + </Button> + </AltTextCounterWrapper> </View> {/* below the text input to force tab order */} <View> diff --git a/src/view/com/composer/char-progress/CharProgress.tsx b/src/view/com/composer/char-progress/CharProgress.tsx index a205fe096..c61f753f2 100644 --- a/src/view/com/composer/char-progress/CharProgress.tsx +++ b/src/view/com/composer/char-progress/CharProgress.tsx @@ -1,48 +1,56 @@ import React from 'react' -import {View} from 'react-native' +import {StyleProp, TextStyle, View, ViewStyle} from 'react-native' // @ts-ignore no type definition -prf import ProgressCircle from 'react-native-progress/Circle' // @ts-ignore no type definition -prf import ProgressPie from 'react-native-progress/Pie' -import {MAX_GRAPHEME_LENGTH} from 'lib/constants' -import {usePalette} from 'lib/hooks/usePalette' -import {s} from 'lib/styles' +import {MAX_GRAPHEME_LENGTH} from '#/lib/constants' +import {usePalette} from '#/lib/hooks/usePalette' +import {s} from '#/lib/styles' import {Text} from '../../util/text/Text' -const DANGER_LENGTH = MAX_GRAPHEME_LENGTH - -export function CharProgress({count}: {count: number}) { +export function CharProgress({ + count, + max, + style, + textStyle, + size, +}: { + count: number + max?: number + style?: StyleProp<ViewStyle> + textStyle?: StyleProp<TextStyle> + size?: number +}) { + const maxLength = max || MAX_GRAPHEME_LENGTH const pal = usePalette('default') - const textColor = count > DANGER_LENGTH ? '#e60000' : pal.colors.text - const circleColor = count > DANGER_LENGTH ? '#e60000' : pal.colors.link + const textColor = count > maxLength ? '#e60000' : pal.colors.text + const circleColor = count > maxLength ? '#e60000' : pal.colors.link return ( - <> - <Text style={[s.mr10, s.tabularNum, {color: textColor}]}> - {MAX_GRAPHEME_LENGTH - count} + <View style={style}> + <Text style={[s.mr10, s.tabularNum, {color: textColor}, textStyle]}> + {maxLength - count} </Text> <View> - {count > DANGER_LENGTH ? ( + {count > maxLength ? ( <ProgressPie - size={30} + size={size ?? 30} borderWidth={4} borderColor={circleColor} color={circleColor} - progress={Math.min( - (count - MAX_GRAPHEME_LENGTH) / MAX_GRAPHEME_LENGTH, - 1, - )} + progress={Math.min((count - maxLength) / maxLength, 1)} /> ) : ( <ProgressCircle - size={30} + size={size ?? 30} borderWidth={1} borderColor={pal.colors.border} color={circleColor} - progress={count / MAX_GRAPHEME_LENGTH} + progress={count / maxLength} /> )} </View> - </> + </View> ) } diff --git a/src/view/com/composer/photos/ImageAltTextDialog.tsx b/src/view/com/composer/photos/ImageAltTextDialog.tsx index 16ce4351a..e9e8d4222 100644 --- a/src/view/com/composer/photos/ImageAltTextDialog.tsx +++ b/src/view/com/composer/photos/ImageAltTextDialog.tsx @@ -5,12 +5,16 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {MAX_ALT_TEXT} from '#/lib/constants' +import {enforceLen} from '#/lib/strings/helpers' import {isAndroid, isWeb} from '#/platform/detection' import {ComposerImage} from '#/state/gallery' +import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' +import {DialogControlProps} from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' +import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {PortalComponent} from '#/components/Portal' import {Text} from '#/components/Typography' @@ -21,32 +25,50 @@ type Props = { Portal: PortalComponent } -export const ImageAltTextDialog = (props: Props): React.ReactNode => { +export const ImageAltTextDialog = ({ + control, + image, + onChange, + Portal, +}: Props): React.ReactNode => { + const [altText, setAltText] = React.useState(image.alt) + return ( - <Dialog.Outer control={props.control} Portal={props.Portal}> + <Dialog.Outer + control={control} + onClose={() => { + onChange({ + ...image, + alt: enforceLen(altText, MAX_ALT_TEXT, true), + }) + }} + Portal={Portal}> <Dialog.Handle /> - <ImageAltTextInner {...props} /> + <ImageAltTextInner + control={control} + image={image} + altText={altText} + setAltText={setAltText} + /> </Dialog.Outer> ) } const ImageAltTextInner = ({ + altText, + setAltText, control, image, - onChange, -}: Props): React.ReactNode => { - const {_} = useLingui() +}: { + altText: string + setAltText: (text: string) => void + control: DialogControlProps + image: Props['image'] +}): React.ReactNode => { + const {_, i18n} = useLingui() const t = useTheme() - const windim = useWindowDimensions() - const [altText, setAltText] = React.useState(image.alt) - - const onPressSubmit = React.useCallback(() => { - control.close() - onChange({...image, alt: altText.trim()}) - }, [control, image, altText, onChange]) - const imageStyle = React.useMemo<ImageStyle>(() => { const maxWidth = isWeb ? 450 : windim.width const source = image.transformed ?? image.source @@ -90,32 +112,59 @@ const ImageAltTextInner = ({ </View> <View style={[a.mt_md, a.gap_md]}> - <View> - <TextField.LabelText> - <Trans>Descriptive alt text</Trans> - </TextField.LabelText> - <TextField.Root> - <Dialog.Input - label={_(msg`Alt text`)} - onChangeText={text => setAltText(text)} - value={altText} - multiline - numberOfLines={3} - autoFocus - /> - </TextField.Root> + <View style={[a.gap_sm]}> + <View style={[a.relative, {width: '100%'}]}> + <TextField.LabelText> + <Trans>Descriptive alt text</Trans> + </TextField.LabelText> + <TextField.Root> + <Dialog.Input + label={_(msg`Alt text`)} + onChangeText={text => { + setAltText(text) + }} + defaultValue={altText} + multiline + numberOfLines={3} + autoFocus + /> + </TextField.Root> + </View> + + {altText.length > MAX_ALT_TEXT && ( + <View style={[a.pb_sm, a.flex_row, a.gap_xs]}> + <CircleInfo fill={t.palette.negative_500} /> + <Text + style={[ + a.italic, + a.leading_snug, + t.atoms.text_contrast_medium, + ]}> + <Trans> + Alt text will be truncated. Limit: {i18n.number(MAX_ALT_TEXT)}{' '} + characters. + </Trans> + </Text> + </View> + )} </View> - <Button - label={_(msg`Save`)} - disabled={altText.length > MAX_ALT_TEXT || altText === image.alt} - size="large" - color="primary" - variant="solid" - onPress={onPressSubmit}> - <ButtonText> - <Trans>Save</Trans> - </ButtonText> - </Button> + + <AltTextCounterWrapper altText={altText}> + <Button + label={_(msg`Save`)} + disabled={altText === image.alt} + size="large" + color="primary" + variant="solid" + onPress={() => { + control.close() + }} + style={[a.flex_grow]}> + <ButtonText> + <Trans>Save</Trans> + </ButtonText> + </Button> + </AltTextCounterWrapper> </View> {/* Maybe fix this later -h */} {isAndroid ? <View style={{height: 300}} /> : null} |