diff options
author | Ollie H <renahlee@outlook.com> | 2023-05-01 18:38:47 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-01 20:38:47 -0500 |
commit | 83959c595d52ceb7aa4e3f68441c5ac41c389ebc (patch) | |
tree | 3385d9a16e90fc8d5290ebdef104f922c17642a9 /src/view/com/composer | |
parent | c75c888de2407d3314cad07989174201313facaa (diff) | |
download | voidsky-83959c595d52ceb7aa4e3f68441c5ac41c389ebc.tar.zst |
React Native accessibility (#539)
* React Native accessibility * First round of changes * Latest update * Checkpoint * Wrap up * Lint * Remove unhelpful image hints * Fix navigation * Fix rebase and lint * Mitigate an known issue with the password entry in login * Fix composer dismiss * Remove focus on input elements for web * Remove i and npm * pls work * Remove stray declaration * Regenerate yarn.lock --------- Co-authored-by: Paul Frazee <pfrazee@gmail.com>
Diffstat (limited to 'src/view/com/composer')
-rw-r--r-- | src/view/com/composer/Composer.tsx | 274 | ||||
-rw-r--r-- | src/view/com/composer/ExternalEmbed.tsx | 8 | ||||
-rw-r--r-- | src/view/com/composer/Prompt.tsx | 5 | ||||
-rw-r--r-- | src/view/com/composer/photos/Gallery.tsx | 11 | ||||
-rw-r--r-- | src/view/com/composer/photos/OpenCameraBtn.tsx | 16 | ||||
-rw-r--r-- | src/view/com/composer/photos/SelectPhotoBtn.tsx | 16 | ||||
-rw-r--r-- | src/view/com/composer/text-input/TextInput.tsx | 41 | ||||
-rw-r--r-- | src/view/com/composer/text-input/TextInput.web.tsx | 4 | ||||
-rw-r--r-- | src/view/com/composer/text-input/mobile/Autocomplete.tsx | 4 |
9 files changed, 204 insertions, 175 deletions
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 5ccc229d6..45e67d7cb 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -7,7 +7,6 @@ import { ScrollView, StyleSheet, TouchableOpacity, - TouchableWithoutFeedback, View, } from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' @@ -19,6 +18,8 @@ import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete' import {ExternalEmbed} from './ExternalEmbed' import {Text} from '../util/text/Text' import * as Toast from '../util/Toast' +// TODO: Prevent naming components that coincide with RN primitives +// due to linting false positives import {TextInput, TextInputRef} from './text-input/TextInput' import {CharProgress} from './char-progress/CharProgress' import {UserAvatar} from '../util/UserAvatar' @@ -87,27 +88,6 @@ export const ComposePost = observer(function ComposePost({ autocompleteView.setup() }, [autocompleteView]) - useEffect(() => { - // HACK - // wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view - // -prf - let to: NodeJS.Timeout | undefined - if (textInput.current) { - to = setTimeout(() => { - textInput.current?.focus() - }, 250) - } - return () => { - if (to) { - clearTimeout(to) - } - } - }, []) - - const onPressContainer = useCallback(() => { - textInput.current?.focus() - }, [textInput]) - const onPressAddLinkCard = useCallback( (uri: string) => { setExtLink({uri, isLoading: true}) @@ -133,7 +113,7 @@ export const ComposePost = observer(function ComposePost({ if (rt.text.trim().length === 0 && gallery.isEmpty) { setError('Did you want to say anything?') - return false + return } setIsProcessing(true) @@ -203,133 +183,149 @@ export const ComposePost = observer(function ComposePost({ testID="composePostView" behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.outer}> - <TouchableWithoutFeedback onPressIn={onPressContainer}> - <View style={[s.flex1, viewStyles]}> - <View style={styles.topbar}> + <View style={[s.flex1, viewStyles]} aria-modal accessibilityViewIsModal> + <View style={styles.topbar}> + <TouchableOpacity + testID="composerCancelButton" + onPress={hackfixOnClose} + onAccessibilityEscape={hackfixOnClose} + accessibilityRole="button" + accessibilityLabel="Cancel" + accessibilityHint="Closes post composer"> + <Text style={[pal.link, s.f18]}>Cancel</Text> + </TouchableOpacity> + <View style={s.flex1} /> + {isProcessing ? ( + <View style={styles.postBtn}> + <ActivityIndicator /> + </View> + ) : canPost ? ( <TouchableOpacity - testID="composerCancelButton" - onPress={hackfixOnClose}> - <Text style={[pal.link, s.f18]}>Cancel</Text> + testID="composerPublishBtn" + onPress={() => { + onPressPublish(richtext) + }} + accessibilityRole="button" + accessibilityLabel={replyTo ? 'Publish reply' : 'Publish post'} + accessibilityHint={ + replyTo + ? 'Double tap to publish your reply' + : 'Double tap to publish your post' + }> + <LinearGradient + colors={[gradients.blueLight.start, gradients.blueLight.end]} + start={{x: 0, y: 0}} + end={{x: 1, y: 1}} + style={styles.postBtn}> + <Text style={[s.white, s.f16, s.bold]}> + {replyTo ? 'Reply' : 'Post'} + </Text> + </LinearGradient> </TouchableOpacity> - <View style={s.flex1} /> - {isProcessing ? ( - <View style={styles.postBtn}> - <ActivityIndicator /> - </View> - ) : canPost ? ( - <TouchableOpacity - testID="composerPublishBtn" - onPress={() => { - onPressPublish(richtext) - }}> - <LinearGradient - colors={[gradients.blueLight.start, gradients.blueLight.end]} - start={{x: 0, y: 0}} - end={{x: 1, y: 1}} - style={styles.postBtn}> - <Text style={[s.white, s.f16, s.bold]}> - {replyTo ? 'Reply' : 'Post'} - </Text> - </LinearGradient> - </TouchableOpacity> - ) : ( - <View style={[styles.postBtn, pal.btn]}> - <Text style={[pal.textLight, s.f16, s.bold]}>Post</Text> - </View> - )} + ) : ( + <View style={[styles.postBtn, pal.btn]}> + <Text style={[pal.textLight, s.f16, s.bold]}>Post</Text> + </View> + )} + </View> + {isProcessing ? ( + <View style={[pal.btn, styles.processingLine]}> + <Text style={pal.text}>{processingState}</Text> </View> - {isProcessing ? ( - <View style={[pal.btn, styles.processingLine]}> - <Text style={pal.text}>{processingState}</Text> + ) : undefined} + {error !== '' && ( + <View style={styles.errorLine}> + <View style={styles.errorIcon}> + <FontAwesomeIcon + icon="exclamation" + style={{color: colors.red4}} + size={10} + /> </View> - ) : undefined} - {error !== '' && ( - <View style={styles.errorLine}> - <View style={styles.errorIcon}> - <FontAwesomeIcon - icon="exclamation" - style={{color: colors.red4}} - size={10} - /> + <Text style={[s.red4, s.flex1]}>{error}</Text> + </View> + )} + <ScrollView + style={styles.scrollView} + keyboardShouldPersistTaps="always"> + {replyTo ? ( + <View style={[pal.border, styles.replyToLayout]}> + <UserAvatar avatar={replyTo.author.avatar} size={50} /> + <View style={styles.replyToPost}> + <Text type="xl-medium" style={[pal.text]}> + {sanitizeDisplayName( + replyTo.author.displayName || replyTo.author.handle, + )} + </Text> + <Text type="post-text" style={pal.text} numberOfLines={6}> + {replyTo.text} + </Text> </View> - <Text style={[s.red4, s.flex1]}>{error}</Text> </View> - )} - <ScrollView - style={styles.scrollView} - keyboardShouldPersistTaps="always"> - {replyTo ? ( - <View style={[pal.border, styles.replyToLayout]}> - <UserAvatar avatar={replyTo.author.avatar} size={50} /> - <View style={styles.replyToPost}> - <Text type="xl-medium" style={[pal.text]}> - {sanitizeDisplayName( - replyTo.author.displayName || replyTo.author.handle, - )} - </Text> - <Text type="post-text" style={pal.text} numberOfLines={6}> - {replyTo.text} - </Text> - </View> - </View> - ) : undefined} + ) : undefined} - <View style={[pal.border, styles.textInputLayout]}> - <UserAvatar avatar={store.me.avatar} size={50} /> - <TextInput - ref={textInput} - richtext={richtext} - placeholder={selectTextInputPlaceholder} - suggestedLinks={suggestedLinks} - autocompleteView={autocompleteView} - setRichText={setRichText} - onPhotoPasted={onPhotoPasted} - onPressPublish={onPressPublish} - onSuggestedLinksChanged={setSuggestedLinks} - onError={setError} - /> - </View> + <View style={[pal.border, styles.textInputLayout]}> + <UserAvatar avatar={store.me.avatar} size={50} /> + <TextInput + ref={textInput} + richtext={richtext} + placeholder={selectTextInputPlaceholder} + suggestedLinks={suggestedLinks} + autocompleteView={autocompleteView} + autoFocus={true} + setRichText={setRichText} + onPhotoPasted={onPhotoPasted} + onPressPublish={onPressPublish} + onSuggestedLinksChanged={setSuggestedLinks} + onError={setError} + accessible={true} + accessibilityLabel="Write post" + accessibilityHint="Compose posts up to 300 characters in length" + /> + </View> - <Gallery gallery={gallery} /> - {gallery.isEmpty && extLink && ( - <ExternalEmbed - link={extLink} - onRemove={() => setExtLink(undefined)} - /> - )} - {quote ? ( - <View style={s.mt5}> - <QuoteEmbed quote={quote} /> - </View> - ) : undefined} - </ScrollView> - {!extLink && suggestedLinks.size > 0 ? ( - <View style={s.mb5}> - {Array.from(suggestedLinks).map(url => ( - <TouchableOpacity - key={`suggested-${url}`} - testID="addLinkCardBtn" - style={[pal.borderDark, styles.addExtLinkBtn]} - onPress={() => onPressAddLinkCard(url)}> - <Text style={pal.text}> - Add link card: <Text style={pal.link}>{url}</Text> - </Text> - </TouchableOpacity> - ))} + <Gallery gallery={gallery} /> + {gallery.isEmpty && extLink && ( + <ExternalEmbed + link={extLink} + onRemove={() => setExtLink(undefined)} + /> + )} + {quote ? ( + <View style={s.mt5}> + <QuoteEmbed quote={quote} /> </View> - ) : null} - <View style={[pal.border, styles.bottomBar]}> - {canSelectImages ? ( - <> - <SelectPhotoBtn gallery={gallery} /> - <OpenCameraBtn gallery={gallery} /> - </> - ) : null} - <View style={s.flex1} /> - <CharProgress count={graphemeLength} /> + ) : undefined} + </ScrollView> + {!extLink && suggestedLinks.size > 0 ? ( + <View style={s.mb5}> + {Array.from(suggestedLinks).map(url => ( + <TouchableOpacity + key={`suggested-${url}`} + testID="addLinkCardBtn" + style={[pal.borderDark, styles.addExtLinkBtn]} + onPress={() => onPressAddLinkCard(url)} + accessibilityRole="button" + accessibilityLabel="Add link card" + accessibilityHint={`Creates a card with a thumbnail. The card links to ${url}`}> + <Text style={pal.text}> + Add link card: <Text style={pal.link}>{url}</Text> + </Text> + </TouchableOpacity> + ))} </View> + ) : null} + <View style={[pal.border, styles.bottomBar]}> + {canSelectImages ? ( + <> + <SelectPhotoBtn gallery={gallery} /> + <OpenCameraBtn gallery={gallery} /> + </> + ) : null} + <View style={s.flex1} /> + <CharProgress count={graphemeLength} /> </View> - </TouchableWithoutFeedback> + </View> </KeyboardAvoidingView> ) }) diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx index b6a45f6a3..a938562bd 100644 --- a/src/view/com/composer/ExternalEmbed.tsx +++ b/src/view/com/composer/ExternalEmbed.tsx @@ -60,7 +60,13 @@ export const ExternalEmbed = ({ </Text> )} </View> - <TouchableOpacity style={styles.removeBtn} onPress={onRemove}> + <TouchableOpacity + style={styles.removeBtn} + onPress={onRemove} + accessibilityRole="button" + accessibilityLabel="Remove image preview" + accessibilityHint={`Removes default thumbnail from ${link.uri}`} + onAccessibilityEscape={onRemove}> <FontAwesomeIcon size={18} icon="xmark" style={s.white} /> </TouchableOpacity> </View> diff --git a/src/view/com/composer/Prompt.tsx b/src/view/com/composer/Prompt.tsx index 301b90093..98a10b0f5 100644 --- a/src/view/com/composer/Prompt.tsx +++ b/src/view/com/composer/Prompt.tsx @@ -13,7 +13,10 @@ export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) { <TouchableOpacity testID="replyPromptBtn" style={[pal.view, pal.border, styles.prompt]} - onPress={() => onPressCompose()}> + onPress={() => onPressCompose()} + accessibilityRole="button" + accessibilityLabel="Compose reply" + accessibilityHint="Opens composer"> <UserAvatar avatar={store.me.avatar} size={38} /> <Text type="xl" diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index 98f0824fd..e2d95b2a4 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -107,6 +107,9 @@ export const Gallery = observer(function ({gallery}: Props) { <View key={`selected-image-${image.path}`} style={[imageStyle]}> <TouchableOpacity testID="altTextButton" + accessibilityRole="button" + accessibilityLabel="Add alt text" + accessibilityHint="Opens modal for inputting image alt text" onPress={() => { handleAddImageAltText(image) }} @@ -116,6 +119,9 @@ export const Gallery = observer(function ({gallery}: Props) { <View style={imageControlsSubgroupStyle}> <TouchableOpacity testID="cropPhotoButton" + accessibilityRole="button" + accessibilityLabel="Crop image" + accessibilityHint="Opens modal for cropping image" onPress={() => { handleEditPhoto(image) }} @@ -128,6 +134,9 @@ export const Gallery = observer(function ({gallery}: Props) { </TouchableOpacity> <TouchableOpacity testID="removePhotoButton" + accessibilityRole="button" + accessibilityLabel="Remove image" + accessibilityHint="" onPress={() => handleRemovePhoto(image)} style={styles.imageControl}> <FontAwesomeIcon @@ -144,6 +153,8 @@ export const Gallery = observer(function ({gallery}: Props) { source={{ uri: image.compressed.path, }} + accessible={true} + accessibilityIgnoresInvertColors /> </View> ) : null, diff --git a/src/view/com/composer/photos/OpenCameraBtn.tsx b/src/view/com/composer/photos/OpenCameraBtn.tsx index 809c41783..bfcfa6b78 100644 --- a/src/view/com/composer/photos/OpenCameraBtn.tsx +++ b/src/view/com/composer/photos/OpenCameraBtn.tsx @@ -1,5 +1,5 @@ import React, {useCallback} from 'react' -import {TouchableOpacity} from 'react-native' +import {TouchableOpacity, StyleSheet} from 'react-native' import { FontAwesomeIcon, FontAwesomeIconStyle, @@ -7,7 +7,6 @@ import { import {usePalette} from 'lib/hooks/usePalette' import {useAnalytics} from 'lib/analytics' import {useStores} from 'state/index' -import {s} from 'lib/styles' import {isDesktopWeb} from 'platform/detection' import {openCamera} from 'lib/media/picker' import {useCameraPermission} from 'lib/hooks/usePermissions' @@ -54,8 +53,11 @@ export function OpenCameraBtn({gallery}: Props) { <TouchableOpacity testID="openCameraButton" onPress={onPressTakePicture} - style={[s.pl5]} - hitSlop={HITSLOP}> + style={styles.button} + hitSlop={HITSLOP} + accessibilityRole="button" + accessibilityLabel="Camera" + accessibilityHint="Opens camera on device"> <FontAwesomeIcon icon="camera" style={pal.link as FontAwesomeIconStyle} @@ -64,3 +66,9 @@ export function OpenCameraBtn({gallery}: Props) { </TouchableOpacity> ) } + +const styles = StyleSheet.create({ + button: { + paddingHorizontal: 15, + }, +}) diff --git a/src/view/com/composer/photos/SelectPhotoBtn.tsx b/src/view/com/composer/photos/SelectPhotoBtn.tsx index 9569e08ad..0b8046a4b 100644 --- a/src/view/com/composer/photos/SelectPhotoBtn.tsx +++ b/src/view/com/composer/photos/SelectPhotoBtn.tsx @@ -1,12 +1,11 @@ import React, {useCallback} from 'react' -import {TouchableOpacity} from 'react-native' +import {TouchableOpacity, StyleSheet} from 'react-native' import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' import {usePalette} from 'lib/hooks/usePalette' import {useAnalytics} from 'lib/analytics' -import {s} from 'lib/styles' import {isDesktopWeb} from 'platform/detection' import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions' import {GalleryModel} from 'state/models/media/gallery' @@ -36,8 +35,11 @@ export function SelectPhotoBtn({gallery}: Props) { <TouchableOpacity testID="openGalleryBtn" onPress={onPressSelectPhotos} - style={[s.pl5, s.pr20]} - hitSlop={HITSLOP}> + style={styles.button} + hitSlop={HITSLOP} + accessibilityRole="button" + accessibilityLabel="Gallery" + accessibilityHint="Opens device photo gallery"> <FontAwesomeIcon icon={['far', 'image']} style={pal.link as FontAwesomeIconStyle} @@ -46,3 +48,9 @@ export function SelectPhotoBtn({gallery}: Props) { </TouchableOpacity> ) } + +const styles = StyleSheet.create({ + button: { + paddingHorizontal: 15, + }, +}) diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 10ac52b5d..7b09da93d 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -1,7 +1,14 @@ -import React, {forwardRef, useCallback, useEffect, useRef, useMemo} from 'react' +import React, { + forwardRef, + useCallback, + useRef, + useMemo, + ComponentProps, +} from 'react' import { NativeSyntheticEvent, StyleSheet, + TextInput as RNTextInput, TextInputSelectionChangeEventData, View, } from 'react-native' @@ -27,14 +34,14 @@ export interface TextInputRef { blur: () => void } -interface TextInputProps { +interface TextInputProps extends ComponentProps<typeof RNTextInput> { richtext: RichText placeholder: string suggestedLinks: Set<string> autocompleteView: UserAutocompleteModel - setRichText: (v: RichText) => void + setRichText: (v: RichText | ((v: RichText) => RichText)) => void onPhotoPasted: (uri: string) => void - onPressPublish: (richtext: RichText) => Promise<false | undefined> + onPressPublish: (richtext: RichText) => Promise<void> onSuggestedLinksChanged: (uris: Set<string>) => void onError: (err: string) => void } @@ -55,6 +62,7 @@ export const TextInput = forwardRef( onPhotoPasted, onSuggestedLinksChanged, onError, + ...props }: TextInputProps, ref, ) => { @@ -65,26 +73,11 @@ export const TextInput = forwardRef( React.useImperativeHandle(ref, () => ({ focus: () => textInput.current?.focus(), - blur: () => textInput.current?.blur(), + blur: () => { + textInput.current?.blur() + }, })) - useEffect(() => { - // HACK - // wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view - // -prf - let to: NodeJS.Timeout | undefined - if (textInput.current) { - to = setTimeout(() => { - textInput.current?.focus() - }, 250) - } - return () => { - if (to) { - clearTimeout(to) - } - } - }, []) - const onChangeText = useCallback( async (newText: string) => { const newRt = new RichText({text: newText}) @@ -206,8 +199,10 @@ export const TextInput = forwardRef( placeholder={placeholder} placeholderTextColor={pal.colors.textLight} keyboardAppearance={theme.colorScheme} + autoFocus={true} multiline - style={[pal.text, styles.textInput, styles.textInputFormatting]}> + style={[pal.text, styles.textInput, styles.textInputFormatting]} + {...props}> {textDecorated} </PasteInput> <Autocomplete diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 3f98a3595..4abedb3e2 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -25,9 +25,9 @@ interface TextInputProps { placeholder: string suggestedLinks: Set<string> autocompleteView: UserAutocompleteModel - setRichText: (v: RichText) => void + setRichText: (v: RichText | ((v: RichText) => RichText)) => void onPhotoPasted: (uri: string) => void - onPressPublish: (richtext: RichText) => Promise<false | undefined> + onPressPublish: (richtext: RichText) => Promise<void> onSuggestedLinksChanged: (uris: Set<string>) => void onError: (err: string) => void } diff --git a/src/view/com/composer/text-input/mobile/Autocomplete.tsx b/src/view/com/composer/text-input/mobile/Autocomplete.tsx index 879bac071..7806241f1 100644 --- a/src/view/com/composer/text-input/mobile/Autocomplete.tsx +++ b/src/view/com/composer/text-input/mobile/Autocomplete.tsx @@ -50,7 +50,9 @@ export const Autocomplete = observer( testID="autocompleteButton" key={item.handle} style={[pal.border, styles.item]} - onPress={() => onSelect(item.handle)}> + onPress={() => onSelect(item.handle)} + accessibilityLabel={`Select ${item.handle}`} + accessibilityHint={`Autocompletes to ${item.handle}`}> <Text type="md-medium" style={pal.text}> {item.displayName || item.handle} <Text type="sm" style={pal.textLight}> |