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/util | |
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/util')
23 files changed, 277 insertions, 296 deletions
diff --git a/src/view/com/util/BottomSheetCustomBackdrop.tsx b/src/view/com/util/BottomSheetCustomBackdrop.tsx index e175b33a5..91379f1c9 100644 --- a/src/view/com/util/BottomSheetCustomBackdrop.tsx +++ b/src/view/com/util/BottomSheetCustomBackdrop.tsx @@ -1,5 +1,5 @@ import React, {useMemo} from 'react' -import {GestureResponderEvent, TouchableWithoutFeedback} from 'react-native' +import {TouchableWithoutFeedback} from 'react-native' import {BottomSheetBackdropProps} from '@gorhom/bottom-sheet' import Animated, { Extrapolate, @@ -8,7 +8,7 @@ import Animated, { } from 'react-native-reanimated' export function createCustomBackdrop( - onClose?: ((event: GestureResponderEvent) => void) | undefined, + onClose?: (() => void) | undefined, ): React.FC<BottomSheetBackdropProps> { const CustomBackdrop = ({animatedIndex, style}: BottomSheetBackdropProps) => { // animated variables @@ -27,7 +27,15 @@ export function createCustomBackdrop( ) return ( - <TouchableWithoutFeedback onPress={onClose}> + <TouchableWithoutFeedback + onPress={onClose} + accessibilityLabel="Close bottom drawer" + accessibilityHint="" + onAccessibilityEscape={() => { + if (onClose !== undefined) { + onClose() + } + }}> <Animated.View style={containerStyle} /> </TouchableWithoutFeedback> ) diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 5110acf48..503e22084 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, {ComponentProps} from 'react' import {observer} from 'mobx-react-lite' import { Linking, @@ -29,6 +29,16 @@ type Event = | React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent +interface Props extends ComponentProps<typeof TouchableOpacity> { + testID?: string + style?: StyleProp<ViewStyle> + href?: string + title?: string + children?: React.ReactNode + noFeedback?: boolean + asAnchor?: boolean +} + export const Link = observer(function Link({ testID, style, @@ -37,15 +47,9 @@ export const Link = observer(function Link({ children, noFeedback, asAnchor, -}: { - testID?: string - style?: StyleProp<ViewStyle> - href?: string - title?: string - children?: React.ReactNode - noFeedback?: boolean - asAnchor?: boolean -}) { + accessible, + ...props +}: Props) { const store = useStores() const navigation = useNavigation<NavigationProp>() @@ -64,7 +68,10 @@ export const Link = observer(function Link({ testID={testID} onPress={onPress} // @ts-ignore web only -prf - href={asAnchor ? sanitizeUrl(href) : undefined}> + href={asAnchor ? sanitizeUrl(href) : undefined} + accessible={accessible} + accessibilityRole="link" + {...props}> <View style={style}> {children ? children : <Text>{title || 'link'}</Text>} </View> @@ -76,8 +83,11 @@ export const Link = observer(function Link({ testID={testID} style={style} onPress={onPress} + accessible={accessible} + accessibilityRole="link" // @ts-ignore web only -prf - href={asAnchor ? sanitizeUrl(href) : undefined}> + href={asAnchor ? sanitizeUrl(href) : undefined} + {...props}> {children ? children : <Text>{title || 'link'}</Text>} </TouchableOpacity> ) diff --git a/src/view/com/util/Picker.tsx b/src/view/com/util/Picker.tsx deleted file mode 100644 index 9007cb1f0..000000000 --- a/src/view/com/util/Picker.tsx +++ /dev/null @@ -1,157 +0,0 @@ -// TODO: replaceme with something in the design system - -import React, {useRef} from 'react' -import { - StyleProp, - StyleSheet, - TextStyle, - TouchableOpacity, - TouchableWithoutFeedback, - View, - ViewStyle, -} from 'react-native' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import RootSiblings from 'react-native-root-siblings' -import {Text} from './text/Text' -import {colors} from 'lib/styles' - -interface PickerItem { - value: string - label: string -} - -interface PickerOpts { - style?: StyleProp<ViewStyle> - labelStyle?: StyleProp<TextStyle> - iconStyle?: FontAwesomeIconStyle - items: PickerItem[] - value: string - onChange: (value: string) => void - enabled?: boolean -} - -const MENU_WIDTH = 200 - -export function Picker({ - style, - labelStyle, - iconStyle, - items, - value, - onChange, - enabled, -}: PickerOpts) { - const ref = useRef<View>(null) - const valueLabel = items.find(item => item.value === value)?.label || value - const onPress = () => { - if (!enabled) { - return - } - ref.current?.measure( - ( - _x: number, - _y: number, - width: number, - height: number, - pageX: number, - pageY: number, - ) => { - createDropdownMenu(pageX, pageY + height, MENU_WIDTH, items, onChange) - }, - ) - } - return ( - <TouchableWithoutFeedback onPress={onPress}> - <View style={[styles.outer, style]} ref={ref}> - <View style={styles.label}> - <Text style={labelStyle}>{valueLabel}</Text> - </View> - <FontAwesomeIcon icon="angle-down" style={[styles.icon, iconStyle]} /> - </View> - </TouchableWithoutFeedback> - ) -} - -function createDropdownMenu( - x: number, - y: number, - width: number, - items: PickerItem[], - onChange: (value: string) => void, -): RootSiblings { - const onPressItem = (index: number) => { - sibling.destroy() - onChange(items[index].value) - } - const onOuterPress = () => sibling.destroy() - const sibling = new RootSiblings( - ( - <> - <TouchableWithoutFeedback onPress={onOuterPress}> - <View style={styles.bg} /> - </TouchableWithoutFeedback> - <View style={[styles.menu, {left: x, top: y, width}]}> - {items.map((item, index) => ( - <TouchableOpacity - key={index} - style={[styles.menuItem, index !== 0 && styles.menuItemBorder]} - onPress={() => onPressItem(index)}> - <Text style={styles.menuItemLabel}>{item.label}</Text> - </TouchableOpacity> - ))} - </View> - </> - ), - ) - return sibling -} - -const styles = StyleSheet.create({ - outer: { - flexDirection: 'row', - alignItems: 'center', - }, - label: { - marginRight: 5, - }, - icon: {}, - bg: { - position: 'absolute', - top: 0, - right: 0, - bottom: 0, - left: 0, - backgroundColor: '#000', - opacity: 0.1, - }, - menu: { - position: 'absolute', - backgroundColor: '#fff', - borderRadius: 14, - opacity: 1, - paddingVertical: 6, - }, - menuItem: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 6, - paddingLeft: 15, - paddingRight: 30, - }, - menuItemBorder: { - borderTopWidth: 1, - borderTopColor: colors.gray2, - marginTop: 4, - paddingTop: 12, - }, - menuItemIcon: { - marginLeft: 6, - marginRight: 8, - }, - menuItemLabel: { - fontSize: 15, - }, -}) diff --git a/src/view/com/util/PostCtrls.tsx b/src/view/com/util/PostCtrls.tsx index 07a67fd8a..725f3bbbe 100644 --- a/src/view/com/util/PostCtrls.tsx +++ b/src/view/com/util/PostCtrls.tsx @@ -170,83 +170,94 @@ export function PostCtrls(opts: PostCtrlsOpts) { return ( <View style={[styles.ctrls, opts.style]}> - <View> - <TouchableOpacity - testID="replyBtn" - style={styles.ctrl} - hitSlop={HITSLOP} - onPress={opts.onPressReply}> - <CommentBottomArrow - style={[defaultCtrlColor, opts.big ? s.mt2 : styles.mt1]} - strokeWidth={3} - size={opts.big ? 20 : 15} - /> - {typeof opts.replyCount !== 'undefined' ? ( - <Text style={[defaultCtrlColor, s.ml5, s.f15]}> - {opts.replyCount} - </Text> - ) : undefined} - </TouchableOpacity> - </View> - <View> - <TouchableOpacity - testID="repostBtn" - hitSlop={HITSLOP} - onPress={onPressToggleRepostWrapper} - style={styles.ctrl}> - <RepostIcon + <TouchableOpacity + testID="replyBtn" + style={styles.ctrl} + hitSlop={HITSLOP} + onPress={opts.onPressReply} + accessibilityRole="button" + accessibilityLabel="Reply" + accessibilityHint="Opens reply composer"> + <CommentBottomArrow + style={[defaultCtrlColor, opts.big ? s.mt2 : styles.mt1]} + strokeWidth={3} + size={opts.big ? 20 : 15} + /> + {typeof opts.replyCount !== 'undefined' ? ( + <Text style={[defaultCtrlColor, s.ml5, s.f15]}> + {opts.replyCount} + </Text> + ) : undefined} + </TouchableOpacity> + <TouchableOpacity + testID="repostBtn" + hitSlop={HITSLOP} + onPress={onPressToggleRepostWrapper} + style={styles.ctrl} + accessibilityRole="button" + accessibilityLabel={opts.isReposted ? 'Undo repost' : 'Repost'} + accessibilityHint={ + opts.isReposted + ? `Remove your repost of ${opts.author}'s post` + : `Repost or quote post ${opts.author}'s post` + }> + <RepostIcon + style={ + opts.isReposted + ? (styles.ctrlIconReposted as StyleProp<ViewStyle>) + : defaultCtrlColor + } + strokeWidth={2.4} + size={opts.big ? 24 : 20} + /> + {typeof opts.repostCount !== 'undefined' ? ( + <Text + testID="repostCount" style={ opts.isReposted - ? (styles.ctrlIconReposted as StyleProp<ViewStyle>) - : defaultCtrlColor - } - strokeWidth={2.4} - size={opts.big ? 24 : 20} + ? [s.bold, s.green3, s.f15, s.ml5] + : [defaultCtrlColor, s.f15, s.ml5] + }> + {opts.repostCount} + </Text> + ) : undefined} + </TouchableOpacity> + <TouchableOpacity + testID="likeBtn" + style={styles.ctrl} + hitSlop={HITSLOP} + onPress={onPressToggleLikeWrapper} + accessibilityRole="button" + accessibilityLabel={opts.isLiked ? 'Unlike' : 'Like'} + accessibilityHint={ + opts.isReposted + ? `Removes like from ${opts.author}'s post` + : `Like ${opts.author}'s post` + }> + {opts.isLiked ? ( + <HeartIconSolid + style={styles.ctrlIconLiked as StyleProp<ViewStyle>} + size={opts.big ? 22 : 16} /> - {typeof opts.repostCount !== 'undefined' ? ( - <Text - testID="repostCount" - style={ - opts.isReposted - ? [s.bold, s.green3, s.f15, s.ml5] - : [defaultCtrlColor, s.f15, s.ml5] - }> - {opts.repostCount} - </Text> - ) : undefined} - </TouchableOpacity> - </View> - <View> - <TouchableOpacity - testID="likeBtn" - style={styles.ctrl} - hitSlop={HITSLOP} - onPress={onPressToggleLikeWrapper}> - {opts.isLiked ? ( - <HeartIconSolid - style={styles.ctrlIconLiked as StyleProp<ViewStyle>} - size={opts.big ? 22 : 16} - /> - ) : ( - <HeartIcon - style={[defaultCtrlColor, opts.big ? styles.mt1 : undefined]} - strokeWidth={3} - size={opts.big ? 20 : 16} - /> - )} - {typeof opts.likeCount !== 'undefined' ? ( - <Text - testID="likeCount" - style={ - opts.isLiked - ? [s.bold, s.red3, s.f15, s.ml5] - : [defaultCtrlColor, s.f15, s.ml5] - }> - {opts.likeCount} - </Text> - ) : undefined} - </TouchableOpacity> - </View> + ) : ( + <HeartIcon + style={[defaultCtrlColor, opts.big ? styles.mt1 : undefined]} + strokeWidth={3} + size={opts.big ? 20 : 16} + /> + )} + {typeof opts.likeCount !== 'undefined' ? ( + <Text + testID="likeCount" + style={ + opts.isLiked + ? [s.bold, s.red3, s.f15, s.ml5] + : [defaultCtrlColor, s.f15, s.ml5] + }> + {opts.likeCount} + </Text> + ) : undefined} + </TouchableOpacity> <View> {opts.big ? undefined : ( <PostDropdownBtn diff --git a/src/view/com/util/Selector.tsx b/src/view/com/util/Selector.tsx index 016ea77b8..223a069c8 100644 --- a/src/view/com/util/Selector.tsx +++ b/src/view/com/util/Selector.tsx @@ -85,6 +85,8 @@ export function Selector({ onSelect?.(index) } + const numItems = items.length + return ( <View style={[pal.view, styles.outer]} @@ -97,7 +99,9 @@ export function Selector({ <Pressable testID={`selector-${i}`} key={item} - onPress={() => onPressItem(i)}> + onPress={() => onPressItem(i)} + accessibilityLabel={`Select ${item}`} + accessibilityHint={`Select option ${i} of ${numItems}`}> <View style={styles.item} ref={itemRefs[i]}> <Text style={ diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 7f55bf773..a2e607e47 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -150,6 +150,7 @@ export function UserAvatar({ borderRadius: Math.floor(size / 2), }} source={{uri: avatar}} + accessibilityRole="image" /> ) : ( <DefaultAvatar size={size} /> @@ -167,7 +168,11 @@ export function UserAvatar({ <View style={{width: size, height: size}}> <HighPriorityImage testID="userAvatarImage" - style={{width: size, height: size, borderRadius: Math.floor(size / 2)}} + style={{ + width: size, + height: size, + borderRadius: Math.floor(size / 2), + }} contentFit="cover" source={{uri: avatar}} blurRadius={moderation?.blur ? BLUR_AMOUNT : 0} diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index 14459bf77..51cfbccbb 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -5,7 +5,6 @@ import {IconProp} from '@fortawesome/fontawesome-svg-core' import {Image} from 'expo-image' import {colors} from 'lib/styles' import {openCamera, openCropper, openPicker} from '../../../lib/media/picker' -import {Image as TImage} from 'lib/media/types' import {useStores} from 'state/index' import { usePhotoLibraryPermission, @@ -15,6 +14,7 @@ import {DropdownButton} from './forms/DropdownButton' import {usePalette} from 'lib/hooks/usePalette' import {AvatarModeration} from 'lib/labeling/types' import {isWeb, isAndroid} from 'platform/detection' +import {Image as RNImage} from 'react-native-image-crop-picker' export function UserBanner({ banner, @@ -23,7 +23,7 @@ export function UserBanner({ }: { banner?: string | null moderation?: AvatarModeration - onSelectNewBanner?: (img: TImage | null) => void + onSelectNewBanner?: (img: RNImage | null) => void }) { const store = useStores() const pal = usePalette('default') @@ -94,6 +94,8 @@ export function UserBanner({ testID="userBannerImage" style={styles.bannerImage} source={{uri: banner}} + accessible={true} + accessibilityIgnoresInvertColors /> ) : ( <View @@ -118,6 +120,8 @@ export function UserBanner({ resizeMode="cover" source={{uri: banner}} blurRadius={moderation?.blur ? 100 : 0} + accessible={true} + accessibilityIgnoresInvertColors /> ) : ( <View diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index 816c835cc..9c85cfa24 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -60,7 +60,14 @@ export const ViewHeader = observer(function ({ testID="viewHeaderDrawerBtn" onPress={canGoBack ? onPressBack : onPressMenu} hitSlop={BACK_HITSLOP} - style={canGoBack ? styles.backBtn : styles.backBtnWide}> + style={canGoBack ? styles.backBtn : styles.backBtnWide} + accessibilityRole="button" + accessibilityLabel={canGoBack ? 'Go back' : 'Go to menu'} + accessibilityHint={ + canGoBack + ? 'Navigates to the previous screen' + : 'Navigates to the menu' + }> {canGoBack ? ( <FontAwesomeIcon size={18} @@ -171,9 +178,9 @@ const styles = StyleSheet.create({ height: 30, }, backBtnWide: { - width: 40, + width: 30, height: 30, - marginLeft: 6, + paddingHorizontal: 6, }, backIcon: { marginTop: 6, diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx index 02717053d..f9ef0945d 100644 --- a/src/view/com/util/ViewSelector.tsx +++ b/src/view/com/util/ViewSelector.tsx @@ -132,7 +132,12 @@ export function Selector({ <Pressable testID={`selector-${i}`} key={item} - onPress={() => onPressItem(i)}> + onPress={() => onPressItem(i)} + accessibilityLabel={item} + accessibilityHint={`Selects ${item}`} + // TODO: Modify the component API such that lint fails + // at the invocation site as well + > <View style={[ styles.item, diff --git a/src/view/com/util/error/ErrorMessage.tsx b/src/view/com/util/error/ErrorMessage.tsx index cc0df1b59..370f10ae3 100644 --- a/src/view/com/util/error/ErrorMessage.tsx +++ b/src/view/com/util/error/ErrorMessage.tsx @@ -47,7 +47,10 @@ export function ErrorMessage({ <TouchableOpacity testID="errorMessageTryAgainButton" style={styles.btn} - onPress={onPressTryAgain}> + onPress={onPressTryAgain} + accessibilityRole="button" + accessibilityLabel="Retry" + accessibilityHint="Retries the last action, which errored out"> <FontAwesomeIcon icon="arrows-rotate" style={{color: theme.palette.error.icon}} diff --git a/src/view/com/util/error/ErrorScreen.tsx b/src/view/com/util/error/ErrorScreen.tsx index c849e37db..a5deeb18f 100644 --- a/src/view/com/util/error/ErrorScreen.tsx +++ b/src/view/com/util/error/ErrorScreen.tsx @@ -57,7 +57,9 @@ export function ErrorScreen({ testID="errorScreenTryAgainButton" type="default" style={[styles.btn]} - onPress={onPressTryAgain}> + onPress={onPressTryAgain} + accessibilityLabel="Retry" + accessibilityHint="Retries the last action, which errored out"> <FontAwesomeIcon icon="arrows-rotate" style={pal.link as FontAwesomeIconStyle} diff --git a/src/view/com/util/fab/FABInner.tsx b/src/view/com/util/fab/FABInner.tsx index 3d44c0dd4..5eb4a6588 100644 --- a/src/view/com/util/fab/FABInner.tsx +++ b/src/view/com/util/fab/FABInner.tsx @@ -1,25 +1,19 @@ -import React from 'react' +import React, {ComponentProps} from 'react' import {observer} from 'mobx-react-lite' -import { - Animated, - GestureResponderEvent, - StyleSheet, - TouchableWithoutFeedback, -} from 'react-native' +import {Animated, StyleSheet, TouchableWithoutFeedback} from 'react-native' import LinearGradient from 'react-native-linear-gradient' import {gradients} from 'lib/styles' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {useStores} from 'state/index' import {isMobileWeb} from 'platform/detection' -type OnPress = ((event: GestureResponderEvent) => void) | undefined -export interface FABProps { +export interface FABProps + extends ComponentProps<typeof TouchableWithoutFeedback> { testID?: string icon: JSX.Element - onPress: OnPress } -export const FABInner = observer(({testID, icon, onPress}: FABProps) => { +export const FABInner = observer(({testID, icon, ...props}: FABProps) => { const store = useStores() const interp = useAnimatedValue(0) React.useEffect(() => { @@ -34,7 +28,7 @@ export const FABInner = observer(({testID, icon, onPress}: FABProps) => { transform: [{translateY: Animated.multiply(interp, 60)}], } return ( - <TouchableWithoutFeedback testID={testID} onPress={onPress}> + <TouchableWithoutFeedback testID={testID} {...props}> <Animated.View style={[styles.outer, isMobileWeb && styles.mobileWebOuter, transform]}> <LinearGradient diff --git a/src/view/com/util/forms/Button.tsx b/src/view/com/util/forms/Button.tsx index 8548860d0..3b5b00284 100644 --- a/src/view/com/util/forms/Button.tsx +++ b/src/view/com/util/forms/Button.tsx @@ -26,6 +26,7 @@ export type ButtonType = | 'secondary-light' | 'default-light' +// TODO: Enforce that button always has a label export function Button({ type = 'primary', label, @@ -131,7 +132,8 @@ export function Button({ <Pressable style={[typeOuterStyle, styles.outer, style]} onPress={onPressWrapped} - testID={testID}> + testID={testID} + accessibilityRole="button"> {label ? ( <Text type="button" style={[typeLabelStyle, labelStyle]}> {label} diff --git a/src/view/com/util/forms/DropdownButton.tsx b/src/view/com/util/forms/DropdownButton.tsx index 725d45c1b..04346d91f 100644 --- a/src/view/com/util/forms/DropdownButton.tsx +++ b/src/view/com/util/forms/DropdownButton.tsx @@ -1,4 +1,4 @@ -import React, {useRef} from 'react' +import React, {PropsWithChildren, useMemo, useRef} from 'react' import { Dimensions, StyleProp, @@ -39,6 +39,19 @@ type MaybeDropdownItem = DropdownItem | false | undefined export type DropdownButtonType = ButtonType | 'bare' +interface DropdownButtonProps { + testID?: string + type?: DropdownButtonType + style?: StyleProp<ViewStyle> + items: MaybeDropdownItem[] + label?: string + menuWidth?: number + children?: React.ReactNode + openToRight?: boolean + rightOffset?: number + bottomOffset?: number +} + export function DropdownButton({ testID, type = 'bare', @@ -50,18 +63,7 @@ export function DropdownButton({ openToRight = false, rightOffset = 0, bottomOffset = 0, -}: { - testID?: string - type?: DropdownButtonType - style?: StyleProp<ViewStyle> - items: MaybeDropdownItem[] - label?: string - menuWidth?: number - children?: React.ReactNode - openToRight?: boolean - rightOffset?: number - bottomOffset?: number -}) { +}: PropsWithChildren<DropdownButtonProps>) { const ref1 = useRef<TouchableOpacity>(null) const ref2 = useRef<View>(null) @@ -105,6 +107,18 @@ export function DropdownButton({ ) } + const numItems = useMemo( + () => + items.filter(item => { + if (item === undefined || item === false) { + return false + } + + return isBtn(item) + }).length, + [items], + ) + if (type === 'bare') { return ( <TouchableOpacity @@ -112,7 +126,10 @@ export function DropdownButton({ style={style} onPress={onPress} hitSlop={HITSLOP} - ref={ref1}> + ref={ref1} + accessibilityRole="button" + accessibilityLabel={`Opens ${numItems} options`} + accessibilityHint={`Opens ${numItems} options`}> {children} </TouchableOpacity> ) @@ -283,9 +300,20 @@ const DropdownItems = ({ const separatorColor = theme.colorScheme === 'dark' ? pal.borderDark : pal.border + const numItems = items.filter(isBtn).length + return ( <> - <TouchableWithoutFeedback onPress={onOuterPress}> + <TouchableWithoutFeedback + onPress={onOuterPress} + // TODO: Refactor dropdown components to: + // - (On web, if not handled by React Native) use semantic <select /> + // and <option /> elements for keyboard navigation out of the box + // - (On mobile) be buttons by default, accept `label` and `nativeID` + // props, and always have an explicit label + accessibilityRole="button" + accessibilityLabel="Toggle dropdown" + accessibilityHint=""> <View style={[styles.bg]} /> </TouchableWithoutFeedback> <View @@ -301,7 +329,9 @@ const DropdownItems = ({ testID={item.testID} key={index} style={[styles.menuItem]} - onPress={() => onPressItem(index)}> + onPress={() => onPressItem(index)} + accessibilityLabel={item.label} + accessibilityHint={`Option ${index + 1} of ${numItems}`}> {item.icon && ( <FontAwesomeIcon style={styles.icon} diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx index 8c31f5614..e6aba46f3 100644 --- a/src/view/com/util/images/AutoSizedImage.tsx +++ b/src/view/com/util/images/AutoSizedImage.tsx @@ -62,12 +62,17 @@ export function AutoSizedImage({ onLongPress={onLongPress} onPressIn={onPressIn} delayPressIn={DELAY_PRESS_IN} - style={[styles.container, style]}> + style={[styles.container, style]} + accessible={true} + accessibilityLabel="Share image" + accessibilityHint="Opens ways of sharing image"> <Image style={[styles.image, {aspectRatio}]} source={uri} accessible={true} // Must set for `accessibilityLabel` to work + accessibilityIgnoresInvertColors accessibilityLabel={alt} + accessibilityHint="" /> {children} </TouchableOpacity> @@ -80,7 +85,9 @@ export function AutoSizedImage({ style={[styles.image, {aspectRatio}]} source={{uri}} accessible={true} // Must set for `accessibilityLabel` to work + accessibilityIgnoresInvertColors accessibilityLabel={alt} + accessibilityHint="" /> {children} </View> diff --git a/src/view/com/util/images/Gallery.tsx b/src/view/com/util/images/Gallery.tsx index 78ced0668..5b6c3384d 100644 --- a/src/view/com/util/images/Gallery.tsx +++ b/src/view/com/util/images/Gallery.tsx @@ -41,16 +41,25 @@ export const GalleryItem: FC<GalleryItemProps> = ({ delayPressIn={DELAY_PRESS_IN} onPress={() => onPress?.(index)} onPressIn={() => onPressIn?.(index)} - onLongPress={() => onLongPress?.(index)}> + onLongPress={() => onLongPress?.(index)} + accessibilityRole="button" + accessibilityLabel="View image" + accessibilityHint=""> <Image source={{uri: image.thumb}} style={imageStyle} accessible={true} accessibilityLabel={image.alt} + accessibilityHint="" + accessibilityIgnoresInvertColors /> </TouchableOpacity> {image.alt === '' ? null : ( - <Pressable onPress={onPressAltText}> + <Pressable + onPress={onPressAltText} + accessibilityRole="button" + accessibilityLabel="View alt text" + accessibilityHint="Opens modal with alt text"> <Text style={styles.alt}>ALT</Text> </Pressable> )} diff --git a/src/view/com/util/images/Image.tsx b/src/view/com/util/images/Image.tsx index e3d0d7fcc..e779fa378 100644 --- a/src/view/com/util/images/Image.tsx +++ b/src/view/com/util/images/Image.tsx @@ -8,5 +8,7 @@ export function HighPriorityImage({source, ...props}: HighPriorityImageProps) { const updatedSource = { uri: typeof source === 'object' && source ? source.uri : '', } satisfies ImageSource - return <Image source={updatedSource} {...props} /> + return ( + <Image accessibilityIgnoresInvertColors source={updatedSource} {...props} /> + ) } diff --git a/src/view/com/util/images/ImageHorzList.tsx b/src/view/com/util/images/ImageHorzList.tsx index 5c232e0b4..88494bba3 100644 --- a/src/view/com/util/images/ImageHorzList.tsx +++ b/src/view/com/util/images/ImageHorzList.tsx @@ -16,15 +16,33 @@ interface Props { } export function ImageHorzList({images, onPress, style}: Props) { + const numImages = images.length return ( <View style={[styles.flexRow, style]}> {images.map(({thumb, alt}, i) => ( - <TouchableWithoutFeedback key={i} onPress={() => onPress?.(i)}> + <TouchableWithoutFeedback + key={i} + onPress={() => onPress?.(i)} + accessible={true} + accessibilityLabel={`Open image ${i} of ${numImages}`} + accessibilityHint="Opens image in viewer" + accessibilityActions={[{name: 'press', label: 'Press'}]} + onAccessibilityAction={action => { + switch (action.nativeEvent.actionName) { + case 'press': + onPress?.(0) + break + default: + break + } + }}> <Image source={{uri: thumb}} style={styles.image} accessible={true} - accessibilityLabel={alt} + accessibilityIgnoresInvertColors + accessibilityHint={alt} + accessibilityLabel="" /> </TouchableWithoutFeedback> ))} diff --git a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx b/src/view/com/util/load-latest/LoadLatestBtn.web.tsx index 1b6f18b62..839685029 100644 --- a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtn.web.tsx @@ -23,7 +23,10 @@ export const LoadLatestBtn = ({ <TouchableOpacity style={[pal.view, pal.borderDark, styles.loadLatest]} onPress={onPress} - hitSlop={HITSLOP}> + hitSlop={HITSLOP} + accessibilityRole="button" + accessibilityLabel={`Load new ${label}`} + accessibilityHint=""> <Text type="md-bold" style={pal.text}> <UpIcon size={16} strokeWidth={1} style={[pal.text, styles.icon]} /> Load new {label} diff --git a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx index 75a812760..5279696a2 100644 --- a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx @@ -23,7 +23,10 @@ export const LoadLatestBtn = observer( }, ]} onPress={onPress} - hitSlop={HITSLOP}> + hitSlop={HITSLOP} + accessibilityRole="button" + accessibilityLabel={`Load new ${label}`} + accessibilityHint={`Loads new ${label}`}> <LinearGradient colors={[gradients.blueLight.start, gradients.blueLight.end]} start={{x: 0, y: 0}} diff --git a/src/view/com/util/moderation/ContentHider.tsx b/src/view/com/util/moderation/ContentHider.tsx index 74fb479ad..0f3e47d61 100644 --- a/src/view/com/util/moderation/ContentHider.tsx +++ b/src/view/com/util/moderation/ContentHider.tsx @@ -55,7 +55,14 @@ export function ContentHider({ </Text> <TouchableOpacity style={styles.showBtn} - onPress={() => setOverride(v => !v)}> + onPress={() => setOverride(v => !v)} + accessibilityLabel={override ? 'Hide post' : 'Show post'} + // TODO: The text labelling should be split up so controls have unique roles + accessibilityHint={ + override + ? 'Re-hide post' + : 'Shows post hidden based on your moderation settings' + }> <Text type="md" style={pal.link}> {override ? 'Hide' : 'Show'} </Text> diff --git a/src/view/com/util/moderation/PostHider.tsx b/src/view/com/util/moderation/PostHider.tsx index b3c4c9593..2cc7ea62b 100644 --- a/src/view/com/util/moderation/PostHider.tsx +++ b/src/view/com/util/moderation/PostHider.tsx @@ -46,7 +46,8 @@ export function PostHider({ </Text> <TouchableOpacity style={styles.showBtn} - onPress={() => setOverride(v => !v)}> + onPress={() => setOverride(v => !v)} + accessibilityRole="button"> <Text type="md" style={pal.link}> {override ? 'Hide' : 'Show'} post </Text> diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index 6a7759840..929c85adc 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -136,7 +136,10 @@ export function PostEmbeds({ <Pressable onPress={() => { onPressAltText(alt) - }}> + }} + accessibilityRole="button" + accessibilityLabel="View alt text" + accessibilityHint="Opens modal with alt text"> <Text style={styles.alt}>ALT</Text> </Pressable> )} |