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>
This commit is contained in:
Ollie H 2023-05-01 18:38:47 -07:00 committed by GitHub
parent c75c888de2
commit 83959c595d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
86 changed files with 2479 additions and 1827 deletions

View file

@ -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>
)

View file

@ -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>
)

View file

@ -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,
},
})

View file

@ -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

View file

@ -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={

View file

@ -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}

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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}}

View file

@ -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}

View file

@ -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

View file

@ -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}

View file

@ -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}

View file

@ -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>

View file

@ -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>
)}

View file

@ -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} />
)
}

View file

@ -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>
))}

View file

@ -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}

View file

@ -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}}

View file

@ -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>

View file

@ -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>

View file

@ -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>
)}