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

@ -56,7 +56,10 @@ export const Composer = observer(
}
return (
<Animated.View style={[styles.wrapper, pal.view, wrapperAnimStyle]}>
<Animated.View
style={[styles.wrapper, pal.view, wrapperAnimStyle]}
aria-modal
accessibilityViewIsModal>
<ComposePost
replyTo={replyTo}
onPost={onPost}

View file

@ -31,7 +31,7 @@ export const Composer = observer(
}
return (
<View style={styles.mask}>
<View style={styles.mask} aria-modal accessibilityViewIsModal>
<View style={[styles.container, pal.view, pal.border]}>
<ComposePost
replyTo={replyTo}

View file

@ -1,4 +1,4 @@
import React from 'react'
import React, {ComponentProps} from 'react'
import {
Linking,
SafeAreaView,
@ -50,6 +50,8 @@ export const DrawerContent = observer(() => {
const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile} =
useNavigationTabState()
const {notifications} = store.me
// events
// =
@ -120,7 +122,11 @@ export const DrawerContent = observer(() => {
]}>
<SafeAreaView style={s.flex1}>
<View style={styles.main}>
<TouchableOpacity testID="profileCardButton" onPress={onPressProfile}>
<TouchableOpacity
testID="profileCardButton"
accessibilityLabel="Profile"
accessibilityHint="Navigates to your profile"
onPress={onPressProfile}>
<UserAvatar size={80} avatar={store.me.avatar} />
<Text
type="title-lg"
@ -164,6 +170,8 @@ export const DrawerContent = observer(() => {
)
}
label="Search"
accessibilityLabel="Search"
accessibilityHint="Search through users and posts"
bold={isAtSearch}
onPress={onPressSearch}
/>
@ -184,6 +192,8 @@ export const DrawerContent = observer(() => {
)
}
label="Home"
accessibilityLabel="Home"
accessibilityHint="Navigates to default feed"
bold={isAtHome}
onPress={onPressHome}
/>
@ -204,7 +214,13 @@ export const DrawerContent = observer(() => {
)
}
label="Notifications"
count={store.me.notifications.unreadCountLabel}
accessibilityLabel={
notifications.unreadCountLabel === '1'
? 'Notifications: 1 unread notification'
: `Notifications: ${notifications.unreadCountLabel} unread notifications`
}
accessibilityHint="Opens notification feed"
count={notifications.unreadCountLabel}
bold={isAtNotifications}
onPress={onPressNotifications}
/>
@ -225,6 +241,8 @@ export const DrawerContent = observer(() => {
)
}
label="Profile"
accessibilityLabel="Profile"
accessibilityHint="See profile display name, avatar, description, and other profile items"
onPress={onPressProfile}
/>
<MenuItem
@ -236,6 +254,8 @@ export const DrawerContent = observer(() => {
/>
}
label="Settings"
accessibilityLabel="Settings"
accessibilityHint="Manage settings for your account, like handle, content moderation, and app passwords"
onPress={onPressSettings}
/>
</View>
@ -243,6 +263,13 @@ export const DrawerContent = observer(() => {
<View style={styles.footer}>
{!isWeb && (
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel="Toggle dark mode"
accessibilityHint={
theme.colorScheme === 'dark'
? 'Sets display to light mode'
: 'Sets display to dark mode'
}
onPress={onDarkmodePress}
style={[
styles.footerBtn,
@ -258,6 +285,9 @@ export const DrawerContent = observer(() => {
</TouchableOpacity>
)}
<TouchableOpacity
accessibilityRole="link"
accessibilityLabel="Send feedback"
accessibilityHint="Opens Google Forms feedback link"
onPress={onPressFeedback}
style={[
styles.footerBtn,
@ -281,25 +311,30 @@ export const DrawerContent = observer(() => {
)
})
function MenuItem({
icon,
label,
count,
bold,
onPress,
}: {
interface MenuItemProps extends ComponentProps<typeof TouchableOpacity> {
icon: JSX.Element
label: string
count?: string
bold?: boolean
onPress: () => void
}) {
}
function MenuItem({
icon,
label,
accessibilityLabel,
count,
bold,
onPress,
}: MenuItemProps) {
const pal = usePalette('default')
return (
<TouchableOpacity
testID={`menuItemButton-${label}`}
style={styles.menuItem}
onPress={onPress}>
onPress={onPress}
accessibilityRole="menuitem"
accessibilityLabel={accessibilityLabel}
accessibilityHint="">
<View style={[styles.menuItemIconWrapper]}>
{icon}
{count ? (
@ -332,6 +367,7 @@ const InviteCodes = observer(() => {
const {track} = useAnalytics()
const store = useStores()
const pal = usePalette('default')
const {invitesAvailable} = store.me
const onPress = React.useCallback(() => {
track('Menu:ItemClicked', {url: '#invite-codes'})
store.shell.closeDrawer()
@ -341,7 +377,14 @@ const InviteCodes = observer(() => {
<TouchableOpacity
testID="menuItemInviteCodes"
style={[styles.inviteCodes]}
onPress={onPress}>
onPress={onPress}
accessibilityRole="button"
accessibilityLabel={
invitesAvailable === 1
? 'Invite codes: 1 available'
: `Invite codes: ${invitesAvailable} available`
}
accessibilityHint="Opens list of invite codes">
<FontAwesomeIcon
icon="ticket"
style={[

View file

@ -1,4 +1,4 @@
import React from 'react'
import React, {ComponentProps} from 'react'
import {
Animated,
GestureResponderEvent,
@ -94,6 +94,8 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
)
}
onPress={onPressHome}
accessibilityLabel="Go home"
accessibilityHint="Navigates to feed home"
/>
<Btn
testID="bottomBarSearchBtn"
@ -113,6 +115,7 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
)
}
onPress={onPressSearch}
accessibilityRole="search"
/>
<Btn
testID="bottomBarNotificationsBtn"
@ -133,6 +136,8 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
}
onPress={onPressNotifications}
notificationCount={store.me.notifications.unreadCountLabel}
accessibilityLabel="Notifications"
accessibilityHint="Navigates to notifications"
/>
<Btn
testID="bottomBarProfileBtn"
@ -154,31 +159,43 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
</View>
}
onPress={onPressProfile}
accessibilityLabel="Profile"
accessibilityHint="Navigates to profile"
/>
</Animated.View>
)
})
interface BtnProps
extends Pick<
ComponentProps<typeof TouchableOpacity>,
'accessibilityRole' | 'accessibilityHint' | 'accessibilityLabel'
> {
testID?: string
icon: JSX.Element
notificationCount?: string
onPress?: (event: GestureResponderEvent) => void
onLongPress?: (event: GestureResponderEvent) => void
}
function Btn({
testID,
icon,
notificationCount,
onPress,
onLongPress,
}: {
testID?: string
icon: JSX.Element
notificationCount?: string
onPress?: (event: GestureResponderEvent) => void
onLongPress?: (event: GestureResponderEvent) => void
}) {
accessibilityHint,
accessibilityLabel,
}: BtnProps) {
return (
<TouchableOpacity
testID={testID}
style={styles.ctrl}
onPress={onLongPress ? onPress : undefined}
onPressIn={onLongPress ? undefined : onPress}
onLongPress={onLongPress}>
onLongPress={onLongPress}
accessibilityLabel={accessibilityLabel}
accessibilityHint={accessibilityHint}>
{notificationCount ? (
<View style={[styles.notificationCount]}>
<Text style={styles.notificationCountLabel}>{notificationCount}</Text>

View file

@ -2,7 +2,11 @@ import React from 'react'
import {observer} from 'mobx-react-lite'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {PressableWithHover} from 'view/com/util/PressableWithHover'
import {useNavigation, useNavigationState} from '@react-navigation/native'
import {
useLinkProps,
useNavigation,
useNavigationState,
} from '@react-navigation/native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
@ -59,7 +63,10 @@ function BackBtn() {
<TouchableOpacity
testID="viewHeaderBackOrMenuBtn"
onPress={onPressBack}
style={styles.backBtn}>
style={styles.backBtn}
accessibilityRole="button"
accessibilityLabel="Go back"
accessibilityHint="Navigates to the previous screen">
<FontAwesomeIcon
size={24}
icon="angle-left"
@ -86,25 +93,28 @@ const NavItem = observer(
}
return getCurrentRoute(state).name
})
const isCurrent = isTab(currentRouteName, pathName)
const {onPress} = useLinkProps({to: href})
return (
<PressableWithHover
style={styles.navItemWrapper}
hoverStyle={pal.viewLight}>
<Link href={href} style={styles.navItem}>
<View style={[styles.navItemIconWrapper]}>
{isCurrent ? iconFilled : icon}
{typeof count === 'string' && count ? (
<Text type="button" style={styles.navItemCount}>
{count}
</Text>
) : null}
</View>
<Text type="title" style={[isCurrent ? s.bold : s.normal, pal.text]}>
{label}
</Text>
</Link>
hoverStyle={pal.viewLight}
onPress={onPress}
accessibilityLabel={label}
accessibilityHint={`Navigates to ${label}`}>
<View style={[styles.navItemIconWrapper]}>
{isCurrent ? iconFilled : icon}
{typeof count === 'string' && count ? (
<Text type="button" style={styles.navItemCount}>
{count}
</Text>
) : null}
</View>
<Text type="title" style={[isCurrent ? s.bold : s.normal, pal.text]}>
{label}
</Text>
</PressableWithHover>
)
},
@ -115,7 +125,12 @@ function ComposeBtn() {
const onPressCompose = () => store.shell.openComposer({})
return (
<TouchableOpacity style={[styles.newPostBtn]} onPress={onPressCompose}>
<TouchableOpacity
style={[styles.newPostBtn]}
onPress={onPressCompose}
accessibilityRole="button"
accessibilityLabel="New post"
accessibilityHint="Opens post composer">
<View style={styles.newPostBtnIconWrapper}>
<ComposeIcon2
size={19}
@ -202,7 +217,7 @@ const styles = StyleSheet.create({
profileCard: {
marginVertical: 10,
width: 60,
width: 90,
paddingLeft: 12,
},
@ -215,21 +230,18 @@ const styles = StyleSheet.create({
},
navItemWrapper: {
paddingHorizontal: 12,
borderRadius: 8,
},
navItem: {
flexDirection: 'row',
alignItems: 'center',
paddingTop: 12,
paddingBottom: 12,
paddingHorizontal: 12,
padding: 12,
borderRadius: 8,
gap: 10,
},
navItemIconWrapper: {
alignItems: 'center',
justifyContent: 'center',
width: 28,
height: 28,
marginRight: 10,
marginTop: 2,
},
navItemCount: {

View file

@ -61,7 +61,14 @@ export const DesktopRightNav = observer(function DesktopRightNav() {
<View>
<TouchableOpacity
style={[styles.darkModeToggle]}
onPress={onDarkmodePress}>
onPress={onDarkmodePress}
accessibilityRole="button"
accessibilityLabel="Toggle dark mode"
accessibilityHint={
mode === 'Dark'
? 'Sets display to light mode'
: 'Sets display to dark mode'
}>
<View style={[pal.viewLight, styles.darkModeToggleIcon]}>
<MoonIcon size={18} style={pal.textLight} />
</View>
@ -78,13 +85,22 @@ const InviteCodes = observer(() => {
const store = useStores()
const pal = usePalette('default')
const {invitesAvailable} = store.me
const onPress = React.useCallback(() => {
store.shell.openModal({name: 'invite-codes'})
}, [store])
return (
<TouchableOpacity
style={[styles.inviteCodes, pal.border]}
onPress={onPress}>
onPress={onPress}
accessibilityRole="button"
accessibilityLabel={
invitesAvailable === 1
? 'Invite codes: 1 available'
: `Invite codes: ${invitesAvailable} available`
}
accessibilityHint="Opens list of invite codes">
<FontAwesomeIcon
icon="ticket"
style={[

View file

@ -67,10 +67,16 @@ export const DesktopSearch = observer(function DesktopSearch() {
onBlur={() => setIsInputFocused(false)}
onChangeText={onChangeQuery}
onSubmitEditing={onSubmit}
accessibilityRole="search"
/>
{query ? (
<View style={styles.cancelBtn}>
<TouchableOpacity onPress={onPressCancelSearch}>
<TouchableOpacity
onPress={onPressCancelSearch}
accessibilityRole="button"
accessibilityLabel="Cancel search"
accessibilityHint="Exits inputting search query"
onAccessibilityEscape={onPressCancelSearch}>
<Text type="lg" style={[pal.link]}>
Cancel
</Text>

View file

@ -46,7 +46,9 @@ const ShellInner = observer(() => {
{!isDesktop && store.shell.isDrawerOpen && (
<TouchableOpacity
onPress={() => store.shell.closeDrawer()}
style={styles.drawerMask}>
style={styles.drawerMask}
accessibilityLabel="Close navigation footer"
accessibilityHint="Closes bottom navigation bar">
<View style={styles.drawerContainer}>
<DrawerContent />
</View>