1
0
mirror of https://github.com/tooot-app/app synced 2025-04-15 02:42:04 +02:00

Refine accessibility

This commit is contained in:
Zhiyuan Zheng 2021-04-09 21:43:12 +02:00
parent 9258f4b934
commit d4b28df091
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
57 changed files with 661 additions and 142 deletions

View File

@ -31,6 +31,7 @@ const ComponentAccount: React.FC<Props> = ({
return ( return (
<Pressable <Pressable
accessibilityRole='button'
style={[styles.itemDefault, styles.itemAccount]} style={[styles.itemDefault, styles.itemAccount]}
onPress={customOnPress || onPress} onPress={customOnPress || onPress}
> >

View File

@ -4,6 +4,7 @@ import layoutAnimation from '@utils/styles/layoutAnimation'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useMemo, useRef } from 'react' import React, { useEffect, useMemo, useRef } from 'react'
import { import {
AccessibilityProps,
Pressable, Pressable,
StyleProp, StyleProp,
StyleSheet, StyleSheet,
@ -14,15 +15,18 @@ import {
import { Flow } from 'react-native-animated-spinkit' import { Flow } from 'react-native-animated-spinkit'
export interface Props { export interface Props {
accessibilityLabel?: AccessibilityProps['accessibilityLabel']
accessibilityHint?: AccessibilityProps['accessibilityHint']
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
type: 'icon' | 'text' type: 'icon' | 'text'
content: string content: string
selected?: boolean
loading?: boolean loading?: boolean
destructive?: boolean destructive?: boolean
disabled?: boolean disabled?: boolean
active?: boolean
strokeWidth?: number strokeWidth?: number
size?: 'S' | 'M' | 'L' size?: 'S' | 'M' | 'L'
@ -34,13 +38,15 @@ export interface Props {
} }
const Button: React.FC<Props> = ({ const Button: React.FC<Props> = ({
accessibilityLabel,
accessibilityHint,
style: customStyle, style: customStyle,
type, type,
content, content,
selected,
loading = false, loading = false,
destructive = false, destructive = false,
disabled = false, disabled = false,
active = false,
strokeWidth, strokeWidth,
size = 'M', size = 'M',
spacing = 'S', spacing = 'S',
@ -57,7 +63,7 @@ const Button: React.FC<Props> = ({
} else { } else {
mounted.current = true mounted.current = true
} }
}, [content, loading, disabled, active]) }, [content, loading, disabled])
const loadingSpinkit = useMemo( const loadingSpinkit = useMemo(
() => ( () => (
@ -68,40 +74,22 @@ const Button: React.FC<Props> = ({
[mode] [mode]
) )
const colorContent = useMemo(() => { const mainColor = useMemo(() => {
if (active) { if (selected) {
return theme.blue return theme.blue
} else if (overlay) {
return theme.primaryOverlay
} else if (disabled || loading) {
return theme.disabled
} else { } else {
if (overlay) { if (destructive) {
return theme.primaryOverlay return theme.red
} else { } else {
if (disabled) { return theme.primaryDefault
return theme.secondary
} else {
if (destructive) {
return theme.red
} else {
return theme.primaryDefault
}
}
} }
} }
}, [mode, disabled]) }, [mode, disabled, loading, selected])
const colorBorder = useMemo(() => {
if (active) {
return theme.blue
} else {
if (disabled || loading) {
return theme.secondary
} else {
if (destructive) {
return theme.red
} else {
return theme.primaryDefault
}
}
}
}, [mode, loading, disabled])
const colorBackground = useMemo(() => { const colorBackground = useMemo(() => {
if (overlay) { if (overlay) {
return theme.backgroundOverlayInvert return theme.backgroundOverlayInvert
@ -117,7 +105,7 @@ const Button: React.FC<Props> = ({
<> <>
<Icon <Icon
name={content} name={content}
color={colorContent} color={mainColor}
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
style={{ opacity: loading ? 0 : 1 }} style={{ opacity: loading ? 0 : 1 }}
size={StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1)} size={StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1)}
@ -130,7 +118,7 @@ const Button: React.FC<Props> = ({
<> <>
<Text <Text
style={{ style={{
color: colorContent, color: mainColor,
fontSize: fontSize:
StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1), StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1),
fontWeight: destructive fontWeight: destructive
@ -145,7 +133,7 @@ const Button: React.FC<Props> = ({
</> </>
) )
} }
}, [mode, content, loading, disabled, active]) }, [mode, content, loading, disabled])
enum spacingMapping { enum spacingMapping {
XS = 'S', XS = 'S',
@ -156,11 +144,20 @@ const Button: React.FC<Props> = ({
return ( return (
<Pressable <Pressable
accessible
accessibilityLabel={accessibilityLabel}
accessibilityHint={accessibilityHint}
accessibilityRole='button'
accessibilityState={{
selected,
disabled: disabled || selected,
busy: loading
}}
style={[ style={[
styles.button, styles.button,
{ {
borderWidth: overlay ? 0 : 1, borderWidth: overlay ? 0 : 1,
borderColor: colorBorder, borderColor: mainColor,
backgroundColor: colorBackground, backgroundColor: colorBackground,
paddingVertical: StyleConstants.Spacing[spacing], paddingVertical: StyleConstants.Spacing[spacing],
paddingHorizontal: paddingHorizontal:
@ -171,7 +168,7 @@ const Button: React.FC<Props> = ({
testID='base' testID='base'
onPress={onPress} onPress={onPress}
children={children} children={children}
disabled={disabled || active || loading} disabled={selected || disabled || loading}
/> />
) )
} }

View File

@ -1,6 +1,7 @@
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useMemo, useRef, useState } from 'react' import React, { useCallback, useMemo, useRef, useState } from 'react'
import { import {
AccessibilityProps,
Image, Image,
ImageStyle, ImageStyle,
Pressable, Pressable,
@ -18,6 +19,9 @@ import { Blurhash } from 'react-native-blurhash'
// preview, original, remote -> first show preview, then original, if original failed, then remote // preview, original, remote -> first show preview, then original, if original failed, then remote
export interface Props { export interface Props {
accessibilityLabel?: AccessibilityProps['accessibilityLabel']
accessibilityHint?: AccessibilityProps['accessibilityHint']
hidden?: boolean hidden?: boolean
uri: { preview?: string; original?: string; remote?: string } uri: { preview?: string; original?: string; remote?: string }
blurhash?: string blurhash?: string
@ -36,6 +40,8 @@ export interface Props {
const GracefullyImage = React.memo( const GracefullyImage = React.memo(
({ ({
accessibilityLabel,
accessibilityHint,
hidden = false, hidden = false,
uri, uri,
blurhash, blurhash,
@ -103,10 +109,7 @@ const GracefullyImage = React.memo(
} else { } else {
return ( return (
<View <View
style={[ style={[styles.blurhash, { backgroundColor: theme.disabled }]}
styles.blurhash,
{ backgroundColor: theme.disabled }
]}
/> />
) )
} }
@ -117,6 +120,11 @@ const GracefullyImage = React.memo(
return ( return (
<Pressable <Pressable
{...(onPress
? { accessibilityRole: 'imagebutton' }
: { accessibilityRole: 'image' })}
accessibilityLabel={accessibilityLabel}
accessibilityHint={accessibilityHint}
style={[style, dimension, { backgroundColor: theme.shimmerDefault }]} style={[style, dimension, { backgroundColor: theme.shimmerDefault }]}
{...(onPress {...(onPress
? hidden ? hidden

View File

@ -28,7 +28,11 @@ const ComponentHashtag: React.FC<Props> = ({
}, []) }, [])
return ( return (
<Pressable style={styles.itemDefault} onPress={customOnPress || onPress}> <Pressable
accessibilityRole='button'
style={styles.itemDefault}
onPress={customOnPress || onPress}
>
<Text style={[styles.itemHashtag, { color: theme.primaryDefault }]}> <Text style={[styles.itemHashtag, { color: theme.primaryDefault }]}>
#{hashtag.name} #{hashtag.name}
</Text> </Text>

View File

@ -2,10 +2,20 @@ import Icon from '@components/Icon'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native' import {
AccessibilityProps,
Pressable,
StyleSheet,
Text,
View
} from 'react-native'
import { Flow } from 'react-native-animated-spinkit' import { Flow } from 'react-native-animated-spinkit'
export interface Props { export interface Props {
accessibilityLabel?: string
accessibilityHint?: string
accessibilityState?: AccessibilityProps['accessibilityState']
type?: 'icon' | 'text' type?: 'icon' | 'text'
content: string content: string
native?: boolean native?: boolean
@ -18,6 +28,11 @@ export interface Props {
} }
const HeaderRight: React.FC<Props> = ({ const HeaderRight: React.FC<Props> = ({
// Accessibility - Start
accessibilityLabel,
accessibilityHint,
accessibilityState,
// Accessibility - End
type = 'icon', type = 'icon',
content, content,
native = true, native = true,
@ -75,6 +90,10 @@ const HeaderRight: React.FC<Props> = ({
return ( return (
<Pressable <Pressable
accessibilityLabel={accessibilityLabel}
accessibilityHint={accessibilityHint}
accessibilityRole='button'
accessibilityState={accessibilityState}
onPress={onPress} onPress={onPress}
children={children} children={children}
disabled={disabled || loading} disabled={disabled || loading}

View File

@ -1,8 +1,10 @@
import React, { createElement } from 'react' import React, { createElement } from 'react'
import { StyleProp, View, ViewStyle } from 'react-native' import { AccessibilityProps, StyleProp, View, ViewStyle } from 'react-native'
import * as FeatherIcon from 'react-native-feather' import * as FeatherIcon from 'react-native-feather'
export interface Props { export interface Props {
accessibilityLabel?: AccessibilityProps['accessibilityLabel']
name: string name: string
size: number size: number
color: string color: string
@ -12,6 +14,7 @@ export interface Props {
} }
const Icon: React.FC<Props> = ({ const Icon: React.FC<Props> = ({
accessibilityLabel,
name, name,
size, size,
color, color,
@ -21,6 +24,7 @@ const Icon: React.FC<Props> = ({
}) => { }) => {
return ( return (
<View <View
accessibilityLabel={accessibilityLabel}
style={[ style={[
style, style,
{ {

View File

@ -1,5 +1,6 @@
import Button from '@components/Button' import Button from '@components/Button'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { useAppsQuery } from '@utils/queryHooks/apps' import { useAppsQuery } from '@utils/queryHooks/apps'
import { useInstanceQuery } from '@utils/queryHooks/instance' import { useInstanceQuery } from '@utils/queryHooks/instance'
import { getInstances } from '@utils/slices/instancesSlice' import { getInstances } from '@utils/slices/instancesSlice'
@ -7,7 +8,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import * as WebBrowser from 'expo-web-browser' import * as WebBrowser from 'expo-web-browser'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import React, { useCallback, useMemo, useState } from 'react' import React, { RefObject, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
Alert, Alert,
@ -19,6 +20,7 @@ import {
TextInput, TextInput,
View View
} from 'react-native' } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { Placeholder } from 'rn-placeholder' import { Placeholder } from 'rn-placeholder'
import analytics from './analytics' import analytics from './analytics'
@ -26,16 +28,19 @@ import InstanceAuth from './Instance/Auth'
import InstanceInfo from './Instance/Info' import InstanceInfo from './Instance/Info'
export interface Props { export interface Props {
scrollViewRef?: RefObject<ScrollView>
disableHeaderImage?: boolean disableHeaderImage?: boolean
goBack?: boolean goBack?: boolean
} }
const ComponentInstance: React.FC<Props> = ({ const ComponentInstance: React.FC<Props> = ({
scrollViewRef,
disableHeaderImage, disableHeaderImage,
goBack = false goBack = false
}) => { }) => {
const { t } = useTranslation('componentInstance') const { t } = useTranslation('componentInstance')
const { mode, theme } = useTheme() const { mode, theme } = useTheme()
const { screenReaderEnabled } = useAccessibility()
const instances = useSelector(getInstances, () => true) const instances = useSelector(getInstances, () => true)
const [domain, setDomain] = useState<string>() const [domain, setDomain] = useState<string>()
@ -139,6 +144,8 @@ const ComponentInstance: React.FC<Props> = ({
<View style={styles.base}> <View style={styles.base}>
<View style={styles.inputRow}> <View style={styles.inputRow}>
<TextInput <TextInput
accessible={false}
accessibilityRole='none'
style={[ style={[
styles.prefix, styles.prefix,
{ {
@ -148,12 +155,8 @@ const ComponentInstance: React.FC<Props> = ({
} }
]} ]}
editable={false} editable={false}
children={ placeholder='https://'
<Text placeholderTextColor={theme.primaryDefault}
style={{ color: theme.primaryDefault }}
children='https://'
/>
}
/> />
<TextInput <TextInput
style={[ style={[
@ -172,10 +175,14 @@ const ComponentInstance: React.FC<Props> = ({
keyboardType='url' keyboardType='url'
textContentType='URL' textContentType='URL'
onSubmitEditing={onSubmitEditing} onSubmitEditing={onSubmitEditing}
placeholder={t('server.textInput.placeholder')} placeholder={' ' + t('server.textInput.placeholder')}
placeholderTextColor={theme.secondary} placeholderTextColor={theme.secondary}
returnKeyType='go' returnKeyType='go'
keyboardAppearance={mode} keyboardAppearance={mode}
{...(scrollViewRef && {
onFocus: () =>
setTimeout(() => scrollViewRef.current?.scrollToEnd(), 150)
})}
/> />
<Button <Button
type='text' type='text'
@ -229,9 +236,21 @@ const ComponentInstance: React.FC<Props> = ({
color={theme.secondary} color={theme.secondary}
style={styles.disclaimerIcon} style={styles.disclaimerIcon}
/> />
<Text style={[styles.disclaimerText, { color: theme.secondary }]}> <Text
style={[styles.disclaimerText, { color: theme.secondary }]}
accessibilityRole='link'
onPress={() => {
if (screenReaderEnabled) {
analytics('view_privacy')
WebBrowser.openBrowserAsync(
'https://tooot.app/privacy-policy'
)
}
}}
>
{t('server.disclaimer.base')} {t('server.disclaimer.base')}
<Text <Text
accessible
style={{ color: theme.blue }} style={{ color: theme.blue }}
onPress={() => { onPress={() => {
analytics('view_privacy') analytics('view_privacy')
@ -265,8 +284,7 @@ const styles = StyleSheet.create({
}, },
prefix: { prefix: {
borderBottomWidth: 1, borderBottomWidth: 1,
...StyleConstants.FontStyle.M, ...StyleConstants.FontStyle.M
paddingRight: StyleConstants.Spacing.XS
}, },
textInput: { textInput: {
flex: 1, flex: 1,

View File

@ -16,8 +16,10 @@ const InstanceInfo = React.memo(
const { theme } = useTheme() const { theme } = useTheme()
return ( return (
<View style={[styles.base, style]}> <View style={[styles.base, style]} accessible>
<Text style={[styles.header, { color: theme.primaryDefault }]}>{header}</Text> <Text style={[styles.header, { color: theme.primaryDefault }]}>
{header}
</Text>
{content ? ( {content ? (
<Text style={[styles.content, { color: theme.primaryDefault }]}> <Text style={[styles.content, { color: theme.primaryDefault }]}>
{content} {content}

View File

@ -7,7 +7,11 @@ export interface Props {
} }
const MenuContainer: React.FC<Props> = ({ children }) => { const MenuContainer: React.FC<Props> = ({ children }) => {
return <View style={styles.base}>{children}</View> return (
<View style={styles.base}>
{children}
</View>
)
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@ -1,4 +1,5 @@
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { ColorDefinitions } from '@utils/styles/themes' import { ColorDefinitions } from '@utils/styles/themes'
@ -41,6 +42,7 @@ const MenuRow: React.FC<Props> = ({
onPress onPress
}) => { }) => {
const { theme } = useTheme() const { theme } = useTheme()
const { screenReaderEnabled } = useAccessibility()
const loadingSpinkit = useMemo( const loadingSpinkit = useMemo(
() => ( () => (
@ -55,11 +57,22 @@ const MenuRow: React.FC<Props> = ({
) )
return ( return (
<View style={styles.base}> <View
style={styles.base}
accessible
accessibilityRole={switchValue ? 'switch' : 'button'}
accessibilityState={switchValue ? { checked: switchValue } : undefined}
>
<TapGestureHandler <TapGestureHandler
onHandlerStateChange={({ nativeEvent }) => onHandlerStateChange={async ({ nativeEvent }) => {
nativeEvent.state === State.ACTIVE && !loading && onPress && onPress() if (nativeEvent.state === State.ACTIVE && !loading) {
} if (screenReaderEnabled && switchOnValueChange) {
switchOnValueChange()
} else {
if (onPress) onPress()
}
}
}}
> >
<View style={styles.core}> <View style={styles.core}>
<View style={styles.front}> <View style={styles.front}>

View File

@ -3,6 +3,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { getTheme } from '@utils/styles/themes' import { getTheme } from '@utils/styles/themes'
import React from 'react' import React from 'react'
import { AccessibilityInfo } from 'react-native'
import FlashMessage, { import FlashMessage, {
hideMessage, hideMessage,
showMessage showMessage
@ -36,6 +37,8 @@ const displayMessage = ({
mode: 'light' | 'dark' mode: 'light' | 'dark'
type: 'success' | 'error' | 'warning' type: 'success' | 'error' | 'warning'
}) => { }) => {
AccessibilityInfo.announceForAccessibility(message + '.' + description)
enum iconMapping { enum iconMapping {
success = 'CheckCircle', success = 'CheckCircle',
error = 'XCircle', error = 'XCircle',
@ -98,7 +101,10 @@ const Message = React.memo(
...StyleConstants.FontStyle.M, ...StyleConstants.FontStyle.M,
fontWeight: StyleConstants.Font.Weight.Bold fontWeight: StyleConstants.Font.Weight.Bold
}} }}
textStyle={{ color: theme.primaryDefault, ...StyleConstants.FontStyle.S }} textStyle={{
color: theme.primaryDefault,
...StyleConstants.FontStyle.S
}}
// @ts-ignore // @ts-ignore
textProps={{ numberOfLines: 2 }} textProps={{ numberOfLines: 2 }}
/> />

View File

@ -4,6 +4,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { adaptiveScale } from '@utils/styles/scaling' import { adaptiveScale } from '@utils/styles/scaling'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, Text } from 'react-native' import { StyleSheet, Text } from 'react-native'
import FastImage from 'react-native-fast-image' import FastImage from 'react-native-fast-image'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
@ -27,6 +28,7 @@ const ParseEmojis = React.memo(
adaptiveSize = false, adaptiveSize = false,
fontBold = false fontBold = false
}: Props) => { }: Props) => {
const { t } = useTranslation('componentParse')
const { reduceMotionEnabled } = useAccessibility() const { reduceMotionEnabled } = useAccessibility()
const adaptiveFontsize = useSelector(getSettingsFontsize) const adaptiveFontsize = useSelector(getSettingsFontsize)
@ -69,10 +71,10 @@ const ParseEmojis = React.memo(
return emojiShortcode === `:${emoji.shortcode}:` return emojiShortcode === `:${emoji.shortcode}:`
}) })
if (emojiIndex === -1) { if (emojiIndex === -1) {
return <Text key={emojiShortcode}>{emojiShortcode}</Text> return <Text key={emojiShortcode + i}>{emojiShortcode}</Text>
} else { } else {
if (i === 0) { if (i === 0) {
return <Text key={emojiShortcode}> </Text> return <Text key={emojiShortcode + i}> </Text>
} else { } else {
const uri = reduceMotionEnabled const uri = reduceMotionEnabled
? emojis[emojiIndex].static_url ? emojis[emojiIndex].static_url
@ -80,7 +82,7 @@ const ParseEmojis = React.memo(
if (validUrl.isHttpsUri(uri)) { if (validUrl.isHttpsUri(uri)) {
return ( return (
<FastImage <FastImage
key={emojiShortcode} key={emojiShortcode + i}
source={{ uri }} source={{ uri }}
style={styles.image} style={styles.image}
/> />
@ -91,7 +93,7 @@ const ParseEmojis = React.memo(
} }
} }
} else { } else {
return <Text key={str}>{str}</Text> return <Text key={i}>{str}</Text>
} }
}) })
) : ( ) : (

View File

@ -53,6 +53,7 @@ const renderNode = ({
: true : true
return ( return (
<Text <Text
accessible
key={index} key={index}
style={{ style={{
color: theme.blue, color: theme.blue,
@ -251,6 +252,7 @@ const ParseHTML = React.memo(
/> />
{expandAllow ? ( {expandAllow ? (
<Pressable <Pressable
accessibilityLabel=''
onPress={() => { onPress={() => {
analytics('status_readmore', { allow: expandAllow, expanded }) analytics('status_readmore', { allow: expandAllow, expanded })
layoutAnimation() layoutAnimation()

View File

@ -62,6 +62,7 @@ const TimelineDefault: React.FC<Props> = ({
return ( return (
<Pressable <Pressable
accessible={highlighted ? false : true}
style={[ style={[
styles.statusView, styles.statusView,
{ {
@ -84,6 +85,7 @@ const TimelineDefault: React.FC<Props> = ({
<TimelineAvatar <TimelineAvatar
queryKey={disableOnPress ? undefined : queryKey} queryKey={disableOnPress ? undefined : queryKey}
account={actualStatus.account} account={actualStatus.account}
highlighted={highlighted}
/> />
<TimelineHeaderDefault <TimelineHeaderDefault
queryKey={disableOnPress ? undefined : queryKey} queryKey={disableOnPress ? undefined : queryKey}

View File

@ -84,7 +84,11 @@ const TimelineNotifications: React.FC<Props> = ({
}} }}
> >
<View style={styles.header}> <View style={styles.header}>
<TimelineAvatar queryKey={queryKey} account={actualAccount} /> <TimelineAvatar
queryKey={queryKey}
account={actualAccount}
highlighted={highlighted}
/>
<TimelineHeaderNotification <TimelineHeaderNotification
queryKey={queryKey} queryKey={queryKey}
notification={notification} notification={notification}

View File

@ -269,12 +269,28 @@ const TimelineActions: React.FC<Props> = ({
> >
<View style={styles.actions}> <View style={styles.actions}>
<Pressable <Pressable
{...(highlighted
? {
accessibilityLabel: t(
'shared.actions.reply.accessibilityLabel'
),
accessibilityRole: 'button'
}
: { accessibilityLabel: '' })}
style={styles.action} style={styles.action}
onPress={onPressReply} onPress={onPressReply}
children={childrenReply} children={childrenReply}
/> />
<Pressable <Pressable
{...(highlighted
? {
accessibilityLabel: t(
'shared.actions.reblogged.accessibilityLabel'
),
accessibilityRole: 'button'
}
: { accessibilityLabel: '' })}
style={styles.action} style={styles.action}
onPress={onPressReblog} onPress={onPressReblog}
children={childrenReblog} children={childrenReblog}
@ -284,12 +300,28 @@ const TimelineActions: React.FC<Props> = ({
/> />
<Pressable <Pressable
{...(highlighted
? {
accessibilityLabel: t(
'shared.actions.favourited.accessibilityLabel'
),
accessibilityRole: 'button'
}
: { accessibilityLabel: '' })}
style={styles.action} style={styles.action}
onPress={onPressFavourite} onPress={onPressFavourite}
children={childrenFavourite} children={childrenFavourite}
/> />
<Pressable <Pressable
{...(highlighted
? {
accessibilityLabel: t(
'shared.actions.bookmarked.accessibilityLabel'
),
accessibilityRole: 'button'
}
: { accessibilityLabel: '' })}
style={styles.action} style={styles.action}
onPress={onPressBookmark} onPress={onPressBookmark}
children={childrenBookmark} children={childrenBookmark}

View File

@ -28,6 +28,16 @@ const TimelineActionsUsers = React.memo(
<View style={styles.base}> <View style={styles.base}>
{status.reblogs_count > 0 ? ( {status.reblogs_count > 0 ? (
<Text <Text
accessibilityLabel={t(
'shared.actionsUsers.reblogged_by.accessibilityLabel',
{
count: status.reblogs_count
}
)}
accessibilityHint={t(
'shared.actionsUsers.reblogged_by.accessibilityHint'
)}
accessibilityRole='button'
style={[styles.text, { color: theme.secondary }]} style={[styles.text, { color: theme.secondary }]}
onPress={() => { onPress={() => {
analytics('timeline_shared_actionsusers_press_boosted', { analytics('timeline_shared_actionsusers_press_boosted', {
@ -41,13 +51,23 @@ const TimelineActionsUsers = React.memo(
}) })
}} }}
> >
{t('shared.actionsUsers.reblogged_by', { {t('shared.actionsUsers.reblogged_by.text', {
count: status.reblogs_count count: status.reblogs_count
})} })}
</Text> </Text>
) : null} ) : null}
{status.favourites_count > 0 ? ( {status.favourites_count > 0 ? (
<Text <Text
accessibilityLabel={t(
'shared.actionsUsers.favourited_by.accessibilityLabel',
{
count: status.reblogs_count
}
)}
accessibilityHint={t(
'shared.actionsUsers.favourited_by.accessibilityHint'
)}
accessibilityRole='button'
style={[styles.text, { color: theme.secondary }]} style={[styles.text, { color: theme.secondary }]}
onPress={() => { onPress={() => {
analytics('timeline_shared_actionsusers_press_boosted', { analytics('timeline_shared_actionsusers_press_boosted', {
@ -61,7 +81,7 @@ const TimelineActionsUsers = React.memo(
}) })
}} }}
> >
{t('shared.actionsUsers.favourited_by', { {t('shared.actionsUsers.favourited_by.text', {
count: status.favourites_count count: status.favourites_count
})} })}
</Text> </Text>

View File

@ -52,6 +52,7 @@ const AttachmentAudio: React.FC<Props> = ({
return ( return (
<View <View
accessibilityLabel={audio.description}
style={[ style={[
styles.base, styles.base,
{ {

View File

@ -23,6 +23,7 @@ const AttachmentImage = React.memo(
return ( return (
<View style={styles.base}> <View style={styles.base}>
<GracefullyImage <GracefullyImage
accessibilityLabel={image.description}
hidden={sensitiveShown} hidden={sensitiveShown}
uri={{ original: image.preview_url, remote: image.remote_url }} uri={{ original: image.preview_url, remote: image.remote_url }}
blurhash={image.blurhash} blurhash={image.blurhash}

View File

@ -47,7 +47,11 @@ const AttachmentUnsupported: React.FC<Props> = ({
<Text <Text
style={[ style={[
styles.text, styles.text,
{ color: attachment.blurhash ? theme.backgroundDefault : theme.primaryDefault } {
color: attachment.blurhash
? theme.backgroundDefault
: theme.primaryDefault
}
]} ]}
> >
{t('shared.attachment.unsupported.text')} {t('shared.attachment.unsupported.text')}
@ -58,9 +62,9 @@ const AttachmentUnsupported: React.FC<Props> = ({
content={t('shared.attachment.unsupported.button')} content={t('shared.attachment.unsupported.button')}
size='S' size='S'
overlay overlay
onPress={async () => { onPress={() => {
analytics('timeline_shared_attachment_unsupported_press') analytics('timeline_shared_attachment_unsupported_press')
attachment.remote_url && (await openLink(attachment.remote_url)) attachment.remote_url && openLink(attachment.remote_url)
}} }}
/> />
) : null} ) : null}

View File

@ -62,6 +62,7 @@ const AttachmentVideo: React.FC<Props> = ({
]} ]}
> >
<Video <Video
accessibilityLabel={video.description}
ref={videoPlayer} ref={videoPlayer}
style={{ style={{
width: '100%', width: '100%',

View File

@ -5,14 +5,17 @@ import { StackNavigationProp } from '@react-navigation/stack'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
export interface Props { export interface Props {
queryKey?: QueryKeyTimeline queryKey?: QueryKeyTimeline
account: Mastodon.Account account: Mastodon.Account
highlighted: boolean
} }
const TimelineAvatar = React.memo( const TimelineAvatar = React.memo(
({ queryKey, account }: Props) => { ({ queryKey, account, highlighted }: Props) => {
const { t } = useTranslation('componentTimeline')
const navigation = useNavigation< const navigation = useNavigation<
StackNavigationProp<Nav.TabLocalStackParamList> StackNavigationProp<Nav.TabLocalStackParamList>
>() >()
@ -26,6 +29,14 @@ const TimelineAvatar = React.memo(
return ( return (
<GracefullyImage <GracefullyImage
{...(highlighted && {
accessibilityLabel: t('shared.avatar.accessibilityLabel', {
name: account.display_name
}),
accessibilityHint: t('shared.avatar.accessibilityHint', {
name: account.display_name
})
})}
onPress={onPress} onPress={onPress}
uri={{ original: account.avatar_static }} uri={{ original: account.avatar_static }}
dimension={{ dimension={{
@ -35,8 +46,7 @@ const TimelineAvatar = React.memo(
style={{ style={{
borderRadius: StyleConstants.Avatar.M, borderRadius: StyleConstants.Avatar.M,
overflow: 'hidden', overflow: 'hidden',
marginRight: StyleConstants.Spacing.S, marginRight: StyleConstants.Spacing.S
backgroundColor: 'red'
}} }}
/> />
) )

View File

@ -18,6 +18,8 @@ const TimelineCard = React.memo(
return ( return (
<Pressable <Pressable
accessible
accessibilityRole='link'
style={[styles.card, { borderColor: theme.border }]} style={[styles.card, { borderColor: theme.border }]}
onPress={async () => { onPress={async () => {
analytics('timeline_shared_card_press') analytics('timeline_shared_card_press')

View File

@ -4,6 +4,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, StyleSheet, View } from 'react-native' import { Pressable, StyleSheet, View } from 'react-native'
import HeaderSharedAccount from './HeaderShared/Account' import HeaderSharedAccount from './HeaderShared/Account'
import HeaderSharedApplication from './HeaderShared/Application' import HeaderSharedApplication from './HeaderShared/Application'
@ -19,6 +20,7 @@ export interface Props {
const TimelineHeaderDefault = React.memo( const TimelineHeaderDefault = React.memo(
({ queryKey, rootQueryKey, status }: Props) => { ({ queryKey, rootQueryKey, status }: Props) => {
const { t } = useTranslation('componentTimeline')
const navigation = useNavigation() const navigation = useNavigation()
const { theme } = useTheme() const { theme } = useTheme()
@ -36,6 +38,7 @@ const TimelineHeaderDefault = React.memo(
{queryKey ? ( {queryKey ? (
<Pressable <Pressable
accessibilityHint={t('shared.header.actions.accessibilityHint')}
style={styles.action} style={styles.action}
onPress={() => onPress={() =>
navigation.navigate('Screen-Actions', { navigation.navigate('Screen-Actions', {

View File

@ -2,6 +2,7 @@ import { ParseEmojis } from '@root/components/Parse'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, Text, View } from 'react-native' import { StyleSheet, Text, View } from 'react-native'
export interface Props { export interface Props {
@ -11,12 +12,19 @@ export interface Props {
const HeaderSharedAccount = React.memo( const HeaderSharedAccount = React.memo(
({ account, withoutName = false }: Props) => { ({ account, withoutName = false }: Props) => {
const { t } = useTranslation('componentTimeline')
const { theme } = useTheme() const { theme } = useTheme()
return ( return (
<View style={styles.base}> <View style={styles.base}>
{withoutName ? null : ( {withoutName ? null : (
<Text style={styles.name} numberOfLines={1}> <Text
accessibilityHint={t(
'shared.header.shared.account.name.accessibilityHint'
)}
style={styles.name}
numberOfLines={1}
>
<ParseEmojis <ParseEmojis
content={account.display_name || account.username} content={account.display_name || account.username}
emojis={account.emojis} emojis={account.emojis}
@ -25,6 +33,9 @@ const HeaderSharedAccount = React.memo(
</Text> </Text>
)} )}
<Text <Text
accessibilityHint={t(
'shared.header.shared.account.account.accessibilityHint'
)}
style={[styles.acct, { color: theme.secondary }]} style={[styles.acct, { color: theme.secondary }]}
numberOfLines={1} numberOfLines={1}
> >

View File

@ -17,6 +17,7 @@ const HeaderSharedApplication = React.memo(
return application && application.name !== 'Web' ? ( return application && application.name !== 'Web' ? (
<Text <Text
accessibilityRole='link'
onPress={async () => { onPress={async () => {
analytics('timeline_shared_header_application_press', { analytics('timeline_shared_header_application_press', {
application application

View File

@ -2,6 +2,7 @@ import Icon from '@components/Icon'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet } from 'react-native' import { StyleSheet } from 'react-native'
export interface Props { export interface Props {
@ -10,10 +11,12 @@ export interface Props {
const HeaderSharedMuted = React.memo( const HeaderSharedMuted = React.memo(
({ muted }: Props) => { ({ muted }: Props) => {
const { t } = useTranslation('componentTimeline')
const { theme } = useTheme() const { theme } = useTheme()
return muted ? ( return muted ? (
<Icon <Icon
accessibilityLabel={t('shared.header.shared.muted.accessibilityLabel')}
name='VolumeX' name='VolumeX'
size={StyleConstants.Font.Size.S} size={StyleConstants.Font.Size.S}
color={theme.secondary} color={theme.secondary}

View File

@ -2,6 +2,7 @@ import Icon from '@components/Icon'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet } from 'react-native' import { StyleSheet } from 'react-native'
export interface Props { export interface Props {
@ -10,12 +11,16 @@ export interface Props {
const HeaderSharedVisibility = React.memo( const HeaderSharedVisibility = React.memo(
({ visibility }: Props) => { ({ visibility }: Props) => {
const { t } = useTranslation('componentTimeline')
const { theme } = useTheme() const { theme } = useTheme()
switch (visibility) { switch (visibility) {
case 'private': case 'private':
return ( return (
<Icon <Icon
accessibilityLabel={t(
'shared.header.shared.visibility.private.accessibilityLabel'
)}
name='Lock' name='Lock'
size={StyleConstants.Font.Size.S} size={StyleConstants.Font.Size.S}
color={theme.secondary} color={theme.secondary}
@ -25,6 +30,9 @@ const HeaderSharedVisibility = React.memo(
case 'direct': case 'direct':
return ( return (
<Icon <Icon
accessibilityLabel={t(
'shared.header.shared.visibility.direct.accessibilityLabel'
)}
name='Mail' name='Mail'
size={StyleConstants.Font.Size.S} size={StyleConstants.Font.Size.S}
color={theme.secondary} color={theme.secondary}

View File

@ -3,6 +3,9 @@
"apply": "Apply", "apply": "Apply",
"cancel": "Cancel" "cancel": "Cancel"
}, },
"customEmoji": {
"accessibilityLabel": "Custom emoji {{emoji}}"
},
"message": { "message": {
"success": { "success": {
"message": "{{function}} succeed" "message": "{{function}} succeed"

View File

@ -29,19 +29,33 @@
} }
}, },
"actions": { "actions": {
"favourited": { "reply": {
"function": "Favourite toot" "accessibilityLabel": "Reply to this toot"
}, },
"reblogged": { "reblogged": {
"accessibilityLabel": "Boost this toot",
"function": "Boost toot" "function": "Boost toot"
}, },
"favourited": {
"accessibilityLabel": "Add this toot to favourites",
"function": "Favourite toot"
},
"bookmarked": { "bookmarked": {
"accessibilityLabel": "Add this toot to bookmarks",
"function": "Bookmark toot" "function": "Bookmark toot"
} }
}, },
"actionsUsers": { "actionsUsers": {
"reblogged_by": "$t(screenTabs:shared.users.statuses.reblogged_by)", "reblogged_by": {
"favourited_by": "$t(screenTabs:shared.users.statuses.favourited_by)" "accessibilityLabel": "{{count}} users have boosted this toot",
"accessibilityHint": "Tap to know the users",
"text": "$t(screenTabs:shared.users.statuses.reblogged_by)"
},
"favourited_by": {
"accessibilityLabel": "{{count}} users have favourited this toot",
"accessibilityHint": "Tap to know the users",
"text": "$t(screenTabs:shared.users.statuses.favourited_by)"
}
}, },
"attachment": { "attachment": {
"sensitive": { "sensitive": {
@ -52,13 +66,36 @@
"button": "Try remote link" "button": "Try remote link"
} }
}, },
"avatar": {
"accessibilityLabel": "Avatar of {{name}}",
"accessibilityHint": "Tap to go to {{name}}'s page"
},
"content": { "content": {
"expandHint": "hidden content" "expandHint": "hidden content"
}, },
"fullConversation": "Read conversations", "fullConversation": "Read conversations",
"header": { "header": {
"shared": { "shared": {
"application": "Tooted with {{application}}" "account": {
"name": {
"accessibilityHint": "User's display name"
},
"account": {
"accessibilityHint": "User's account"
}
},
"application": "Tooted with {{application}}",
"muted": {
"accessibilityLabel": "Toot muted"
},
"visibility": {
"direct": {
"accessibilityLabel": "Toot is a direct message"
},
"private": {
"accessibilityLabel": "Toot is visible to followers only"
}
}
}, },
"conversation": { "conversation": {
"withAccounts": "With", "withAccounts": "With",
@ -67,6 +104,7 @@
} }
}, },
"actions": { "actions": {
"accessibilityHint": "Actions for this toot, such as its posted user, toot itself",
"account": { "account": {
"heading": "About user", "heading": "About user",
"mute": { "mute": {

View File

@ -45,15 +45,38 @@
}, },
"footer": { "footer": {
"attachments": { "attachments": {
"sensitive": "Mark attachments as sensitive" "sensitive": "Mark attachments as sensitive",
"remove": {
"accessibilityLabel": "Remove uploaded attachment, number {{attachment}}"
},
"edit": {
"accessibilityLabel": "Edit uploaded attachment, number {{attachment}}"
},
"upload": {
"accessibilityLabel": "Upload more attachments"
}
},
"emojis": {
"accessibilityHint": "Tap to add emoji to toot"
}, },
"poll": { "poll": {
"option": { "option": {
"placeholder": { "placeholder": {
"accessibilityLabel": "Field number {{index}}",
"single": "Single choice", "single": "Single choice",
"multiple": "Multiple choice" "multiple": "Multiple choice"
} }
}, },
"quantity": {
"reduce": {
"accessibilityLabel": "Reduce poll options to {{amount}}",
"accessibilityHint": "Minimum poll options quantity reached, currently has {{amount}}"
},
"increase": {
"accessibilityLabel": "Increase poll options to {{amount}}",
"accessibilityHint": "Maximum poll options quantity reached, currently has {{amount}}"
}
},
"multiple": { "multiple": {
"heading": "Choice type", "heading": "Choice type",
"options": { "options": {
@ -79,6 +102,8 @@
}, },
"actions": { "actions": {
"attachment": { "attachment": {
"accessibilityLabel": "Upload attachment",
"accessibilityHint": "Poll function will be disabled when there is any attachment",
"actions": { "actions": {
"options": { "options": {
"library": "Upload from photo library", "library": "Upload from photo library",
@ -113,7 +138,12 @@
} }
} }
}, },
"poll": {
"accessibilityLabel": "Add poll",
"accessibilityHint": "Attachment function will be disabled when poll is active"
},
"visibility": { "visibility": {
"accessibilityLabel": "Toot visibility is {{visibility}}",
"title": "Toot visibility", "title": "Toot visibility",
"options": { "options": {
"public": "Public", "public": "Public",
@ -122,6 +152,13 @@
"direct": "Direct message", "direct": "Direct message",
"cancel": "$t(common:buttons.cancel)" "cancel": "$t(common:buttons.cancel)"
} }
},
"spoiler": {
"accessibilityLabel": "Spoiler"
},
"emoji": {
"accessibilityLabel": "Add emoji",
"accessibilityHint": "Open emoji selection panel, swipe horizontally to change page"
} }
}, },
"drafts": "Draft ({{count}})", "drafts": "Draft ({{count}})",
@ -131,6 +168,7 @@
"header": { "header": {
"title": "Edit attachment", "title": "Edit attachment",
"right": { "right": {
"accessibilityLabel": "Save editing attachment",
"failed": { "failed": {
"title": "Editing failed", "title": "Editing failed",
"button": "Try again" "button": "Try again"
@ -150,6 +188,7 @@
"title": "Draft" "title": "Draft"
}, },
"content": { "content": {
"accessibilityHint": "Saved draft, tap to edit this draft",
"textEmpty": "Content empty" "textEmpty": "Content empty"
} }
} }

View File

@ -1,8 +1,12 @@
{ {
"content": { "content": {
"actions": {
"accessibilityLabel": "More actions of this image",
"accessibilityHint": "You can save or share this image"
},
"options": { "options": {
"save": "Save image", "save": "Save image",
"share": "Share iamge", "share": "Share image",
"cancel": "$t(common:buttons.cancel)" "cancel": "$t(common:buttons.cancel)"
}, },
"save": { "save": {

View File

@ -17,6 +17,18 @@
"name": "About me" "name": "About me"
} }
}, },
"common": {
"search": {
"accessibilityLabel": "Search",
"accessibilityHint": "Search for hashtags, users or toots"
}
},
"notifications": {
"filter": {
"accessibilityLabel": "Filter",
"accessibilityHint": "Filter shown notifications' types"
}
},
"me": { "me": {
"stacks": { "stacks": {
"bookmarks": { "bookmarks": {
@ -175,6 +187,10 @@
}, },
"shared": { "shared": {
"account": { "account": {
"actions": {
"accessibilityLabel": "Actions for user {{user}}",
"accessibilityHint": "You can mute, blokc, report or share this user"
},
"moved": "User moved", "moved": "User moved",
"created_at": "Registered on: {{date}}", "created_at": "Registered on: {{date}}",
"summary": { "summary": {

View File

@ -224,7 +224,7 @@ const ScreenAnnouncements: React.FC<ScreenAnnouncementsProp> = ({
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
/> />
<HeaderCenter content={t('screenAnnouncements:heading')} /> <HeaderCenter content={t('screenAnnouncements:heading')} />
<View style={{ opacity: 0 }}> <View style={{ opacity: 0 }} accessible={false}>
<HeaderRight <HeaderRight
content='MoreHorizontal' content='MoreHorizontal'
native={false} native={false}

View File

@ -55,6 +55,7 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
({ item }: { item: ComposeStateDraft }) => { ({ item }: { item: ComposeStateDraft }) => {
return ( return (
<Pressable <Pressable
accessibilityHint={t('content.draftsList.content.accessibilityHint')}
style={[styles.draft, { backgroundColor: theme.backgroundDefault }]} style={[styles.draft, { backgroundColor: theme.backgroundDefault }]}
onPress={async () => { onPress={async () => {
setCheckingAttachments(true) setCheckingAttachments(true)
@ -181,7 +182,10 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
visible={checkingAttachments} visible={checkingAttachments}
children={ children={
<View <View
style={[styles.modal, { backgroundColor: theme.backgroundOverlayInvert }]} style={[
styles.modal,
{ backgroundColor: theme.backgroundOverlayInvert }
]}
children={ children={
<Text <Text
children='检查附件在服务器的状态…' children='检查附件在服务器的状态…'

View File

@ -1,3 +1,4 @@
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react' import React, { useContext } from 'react'
@ -22,6 +23,7 @@ export interface Props {
const ComposeEditAttachmentImage: React.FC<Props> = ({ index }) => { const ComposeEditAttachmentImage: React.FC<Props> = ({ index }) => {
const { t } = useTranslation('screenCompose') const { t } = useTranslation('screenCompose')
const { theme } = useTheme() const { theme } = useTheme()
const { screenReaderEnabled } = useAccessibility()
const { composeState, composeDispatch } = useContext(ComposeContext) const { composeState, composeDispatch } = useContext(ComposeContext)
const theAttachmentRemote = composeState.attachments.uploads[index].remote! const theAttachmentRemote = composeState.attachments.uploads[index].remote!
@ -160,9 +162,11 @@ const ComposeEditAttachmentImage: React.FC<Props> = ({ index }) => {
</Animated.View> </Animated.View>
</PanGestureHandler> </PanGestureHandler>
</View> </View>
<Text style={[styles.imageFocusText, { color: theme.primaryDefault }]}> {screenReaderEnabled ? null : (
{t('content.editAttachment.content.imageFocus')} <Text style={[styles.imageFocusText, { color: theme.primaryDefault }]}>
</Text> {t('content.editAttachment.content.imageFocus')}
</Text>
)}
</> </>
) )
} }

View File

@ -22,6 +22,9 @@ const ComposeEditAttachmentSubmit: React.FC<Props> = ({ index }) => {
return ( return (
<HeaderRight <HeaderRight
accessibilityLabel={t(
'content.editAttachment.header.right.accessibilityLabel'
)}
type='icon' type='icon'
content='Save' content='Save'
loading={isSubmitting} loading={isSubmitting}
@ -39,8 +42,8 @@ const ComposeEditAttachmentSubmit: React.FC<Props> = ({ index }) => {
) { ) {
formData.append( formData.append(
'focus', 'focus',
`${theAttachment.meta.focus.x || 0},${-theAttachment.meta.focus.y || `${theAttachment.meta?.focus?.x || 0},${-theAttachment.meta?.focus
0}` ?.y || 0}`
) )
} }

View File

@ -4,8 +4,20 @@ import { useSearchQuery } from '@utils/queryHooks/search'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { forEach, groupBy, sortBy } from 'lodash' import { forEach, groupBy, sortBy } from 'lodash'
import React, { useCallback, useContext, useEffect, useMemo } from 'react' import React, {
import { FlatList, StyleSheet, View } from 'react-native' useCallback,
useContext,
useEffect,
useMemo,
useRef
} from 'react'
import {
AccessibilityInfo,
findNodeHandle,
FlatList,
StyleSheet,
View
} from 'react-native'
import { Circle } from 'react-native-animated-spinkit' import { Circle } from 'react-native-animated-spinkit'
import ComposeActions from './Root/Actions' import ComposeActions from './Root/Actions'
import ComposePosting from './Posting' import ComposePosting from './Posting'
@ -44,6 +56,15 @@ const ComposeRoot = React.memo(
const { reduceMotionEnabled } = useAccessibility() const { reduceMotionEnabled } = useAccessibility()
const { theme } = useTheme() const { theme } = useTheme()
const accessibleRefDrafts = useRef(null)
const accessibleRefAttachments = useRef(null)
const accessibleRefEmojis = useRef(null)
useEffect(() => {
const tagDrafts = findNodeHandle(accessibleRefDrafts.current)
tagDrafts && AccessibilityInfo.setAccessibilityFocus(tagDrafts)
}, [accessibleRefDrafts.current])
const { composeState, composeDispatch } = useContext(ComposeContext) const { composeState, composeDispatch } = useContext(ComposeContext)
const { isFetching, data, refetch } = useSearchQuery({ const { isFetching, data, refetch } = useSearchQuery({
@ -106,6 +127,16 @@ const ComposeRoot = React.memo(
[composeState] [composeState]
) )
const ListFooter = useCallback(
() => (
<ComposeRootFooter
accessibleRefAttachments={accessibleRefAttachments}
accessibleRefEmojis={accessibleRefEmojis}
/>
),
[]
)
return ( return (
<View style={styles.base}> <View style={styles.base}>
<FlatList <FlatList
@ -113,14 +144,14 @@ const ComposeRoot = React.memo(
ListEmptyComponent={listEmpty} ListEmptyComponent={listEmpty}
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
ListHeaderComponent={ComposeRootHeader} ListHeaderComponent={ComposeRootHeader}
ListFooterComponent={ComposeRootFooter} ListFooterComponent={ListFooter}
ItemSeparatorComponent={ComponentSeparator} ItemSeparatorComponent={ComponentSeparator}
// @ts-ignore // @ts-ignore
data={data ? data[composeState.tag?.type] : undefined} data={data ? data[composeState.tag?.type] : undefined}
keyExtractor={() => Math.random().toString()} keyExtractor={() => Math.random().toString()}
/> />
<ComposeActions /> <ComposeActions />
<ComposeDrafts /> <ComposeDrafts accessibleRefDrafts={accessibleRefDrafts} />
<ComposePosting /> <ComposePosting />
</View> </View>
) )

View File

@ -164,20 +164,50 @@ const ComposeActions: React.FC = () => {
return ( return (
<View <View
accessibilityRole='toolbar'
style={[ style={[
styles.additions, styles.additions,
{ backgroundColor: theme.backgroundDefault, borderTopColor: theme.border } {
backgroundColor: theme.backgroundDefault,
borderTopColor: theme.border
}
]} ]}
> >
<Pressable <Pressable
accessibilityRole='button'
accessibilityLabel={t(
'content.root.actions.attachment.accessibilityLabel'
)}
accessibilityHint={t(
'content.root.actions.attachment.accessibilityHint'
)}
accessibilityState={{
disabled: composeState.poll.active
}}
style={styles.button}
onPress={attachmentOnPress} onPress={attachmentOnPress}
children={<Icon name='Aperture' size={24} color={attachmentColor} />} children={<Icon name='Aperture' size={24} color={attachmentColor} />}
/> />
<Pressable <Pressable
accessibilityRole='button'
accessibilityLabel={t('content.root.actions.poll.accessibilityLabel')}
accessibilityHint={t('content.root.actions.poll.accessibilityHint')}
accessibilityState={{
disabled: composeState.attachments.uploads.length ? true : false,
expanded: composeState.poll.active
}}
style={styles.button}
onPress={pollOnPress} onPress={pollOnPress}
children={<Icon name='BarChart2' size={24} color={pollColor} />} children={<Icon name='BarChart2' size={24} color={pollColor} />}
/> />
<Pressable <Pressable
accessibilityRole='button'
accessibilityLabel={t(
'content.root.actions.visibility.accessibilityLabel',
{ visibility: composeState.visibility }
)}
accessibilityState={{ disabled: composeState.visibilityLock }}
style={styles.button}
onPress={visibilityOnPress} onPress={visibilityOnPress}
children={ children={
<Icon <Icon
@ -190,18 +220,34 @@ const ComposeActions: React.FC = () => {
} }
/> />
<Pressable <Pressable
accessibilityRole='button'
accessibilityLabel={t(
'content.root.actions.spoiler.accessibilityLabel'
)}
accessibilityState={{ expanded: composeState.spoiler.active }}
style={styles.button}
onPress={spoilerOnPress} onPress={spoilerOnPress}
children={ children={
<Icon <Icon
name='AlertTriangle' name='AlertTriangle'
size={24} size={24}
color={ color={
composeState.spoiler.active ? theme.primaryDefault : theme.secondary composeState.spoiler.active
? theme.primaryDefault
: theme.secondary
} }
/> />
} }
/> />
<Pressable <Pressable
accessibilityRole='button'
accessibilityLabel={t('content.root.actions.emoji.accessibilityLabel')}
accessibilityHint={t('content.root.actions.emoji.accessibilityHint')}
accessibilityState={{
disabled: composeState.emoji.emojis ? false : true,
expanded: composeState.emoji.active
}}
style={styles.button}
onPress={emojiOnPress} onPress={emojiOnPress}
children={<Icon name='Smile' size={24} color={emojiColor} />} children={<Icon name='Smile' size={24} color={emojiColor} />}
/> />
@ -210,6 +256,12 @@ const ComposeActions: React.FC = () => {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
height: '100%'
},
additions: { additions: {
height: 45, height: 45,
borderTopWidth: StyleSheet.hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,

View File

@ -3,13 +3,17 @@ import { useNavigation } from '@react-navigation/native'
import { getInstanceDrafts } from '@utils/slices/instancesSlice' import { getInstanceDrafts } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation' import layoutAnimation from '@utils/styles/layoutAnimation'
import React, { useContext, useEffect } from 'react' import React, { RefObject, useContext, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyleSheet, View } from 'react-native' import { StyleSheet, View } from 'react-native'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import ComposeContext from '../utils/createContext' import ComposeContext from '../utils/createContext'
const ComposeDrafts: React.FC = () => { export interface Props {
accessibleRefDrafts: RefObject<View>
}
const ComposeDrafts: React.FC<Props> = ({ accessibleRefDrafts }) => {
const { t } = useTranslation('screenCompose') const { t } = useTranslation('screenCompose')
const navigation = useNavigation() const navigation = useNavigation()
const { composeState } = useContext(ComposeContext) const { composeState } = useContext(ComposeContext)
@ -24,6 +28,7 @@ const ComposeDrafts: React.FC = () => {
if (!composeState.dirty && instanceDrafts?.length) { if (!composeState.dirty && instanceDrafts?.length) {
return ( return (
<View <View
ref={accessibleRefDrafts}
style={styles.base} style={styles.base}
children={ children={
<Button <Button

View File

@ -3,15 +3,30 @@ import ComposeEmojis from '@screens/Compose/Root/Footer/Emojis'
import ComposePoll from '@screens/Compose/Root/Footer/Poll' import ComposePoll from '@screens/Compose/Root/Footer/Poll'
import ComposeReply from '@screens/Compose/Root/Footer/Reply' import ComposeReply from '@screens/Compose/Root/Footer/Reply'
import ComposeContext from '@screens/Compose/utils/createContext' import ComposeContext from '@screens/Compose/utils/createContext'
import React, { useContext } from 'react' import React, { RefObject, useContext } from 'react'
import { SectionList, View } from 'react-native'
const ComposeRootFooter: React.FC = () => { export interface Props {
accessibleRefAttachments: RefObject<View>
accessibleRefEmojis: RefObject<SectionList>
}
const ComposeRootFooter: React.FC<Props> = ({
accessibleRefAttachments,
accessibleRefEmojis
}) => {
const { composeState } = useContext(ComposeContext) const { composeState } = useContext(ComposeContext)
return ( return (
<> <>
{composeState.emoji.active ? <ComposeEmojis /> : null} {composeState.emoji.active ? (
{composeState.attachments.uploads.length ? <ComposeAttachments /> : null} <ComposeEmojis accessibleRefEmojis={accessibleRefEmojis} />
) : null}
{composeState.attachments.uploads.length ? (
<ComposeAttachments
accessibleRefAttachments={accessibleRefAttachments}
/>
) : null}
{composeState.poll.active ? <ComposePoll /> : null} {composeState.poll.active ? <ComposePoll /> : null}
{composeState.replyToStatus ? <ComposeReply /> : null} {composeState.replyToStatus ? <ComposeReply /> : null}
</> </>

View File

@ -8,6 +8,7 @@ import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation' import layoutAnimation from '@utils/styles/layoutAnimation'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { import React, {
RefObject,
useCallback, useCallback,
useContext, useContext,
useEffect, useEffect,
@ -28,9 +29,13 @@ import ComposeContext from '../../utils/createContext'
import { ExtendedAttachment } from '../../utils/types' import { ExtendedAttachment } from '../../utils/types'
import addAttachment from './addAttachment' import addAttachment from './addAttachment'
export interface Props {
accessibleRefAttachments: RefObject<View>
}
const DEFAULT_HEIGHT = 200 const DEFAULT_HEIGHT = 200
const ComposeAttachments: React.FC = () => { const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
const { showActionSheetWithOptions } = useActionSheet() const { showActionSheetWithOptions } = useActionSheet()
const { composeState, composeDispatch } = useContext(ComposeContext) const { composeState, composeDispatch } = useContext(ComposeContext)
const { t } = useTranslation('screenCompose') const { t } = useTranslation('screenCompose')
@ -153,6 +158,10 @@ const ComposeAttachments: React.FC = () => {
) : ( ) : (
<View style={styles.actions}> <View style={styles.actions}>
<Button <Button
accessibilityLabel={t(
'content.root.footer.attachments.remove.accessibilityLabel',
{ attachment: index + 1 }
)}
type='icon' type='icon'
content='X' content='X'
spacing='M' spacing='M'
@ -169,6 +178,10 @@ const ComposeAttachments: React.FC = () => {
}} }}
/> />
<Button <Button
accessibilityLabel={t(
'content.root.footer.attachments.edit.accessibilityLabel',
{ attachment: index + 1 }
)}
type='icon' type='icon'
content='Edit' content='Edit'
spacing='M' spacing='M'
@ -192,6 +205,10 @@ const ComposeAttachments: React.FC = () => {
const listFooter = useMemo( const listFooter = useMemo(
() => ( () => (
<Pressable <Pressable
accessible
accessibilityLabel={t(
'content.root.footer.attachments.upload.accessibilityLabel'
)}
style={[ style={[
styles.container, styles.container,
{ {
@ -233,7 +250,7 @@ const ComposeAttachments: React.FC = () => {
[] []
) )
return ( return (
<View style={styles.base}> <View style={styles.base} ref={accessibleRefAttachments} accessible>
<Pressable style={styles.sensitive} onPress={sensitiveOnPress}> <Pressable style={styles.sensitive} onPress={sensitiveOnPress}>
<Icon <Icon
name={composeState.attachments.sensitive ? 'CheckCircle' : 'Circle'} name={composeState.attachments.sensitive ? 'CheckCircle' : 'Circle'}

View File

@ -3,14 +3,30 @@ import haptics from '@components/haptics'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager' import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useContext, useMemo } from 'react' import React, {
import { Pressable, SectionList, StyleSheet, Text, View } from 'react-native' RefObject,
useCallback,
useContext,
useEffect,
useMemo
} from 'react'
import { useTranslation } from 'react-i18next'
import {
AccessibilityInfo,
findNodeHandle,
Pressable,
SectionList,
StyleSheet,
Text,
View
} from 'react-native'
import FastImage from 'react-native-fast-image' import FastImage from 'react-native-fast-image'
import validUrl from 'valid-url' import validUrl from 'valid-url'
import updateText from '../../updateText' import updateText from '../../updateText'
import ComposeContext from '../../utils/createContext' import ComposeContext from '../../utils/createContext'
const SingleEmoji = ({ emoji }: { emoji: Mastodon.Emoji }) => { const SingleEmoji = ({ emoji }: { emoji: Mastodon.Emoji }) => {
const { t } = useTranslation()
const { reduceMotionEnabled } = useAccessibility() const { reduceMotionEnabled } = useAccessibility()
const { composeState, composeDispatch } = useContext(ComposeContext) const { composeState, composeDispatch } = useContext(ComposeContext)
@ -29,6 +45,12 @@ const SingleEmoji = ({ emoji }: { emoji: Mastodon.Emoji }) => {
if (validUrl.isHttpsUri(uri)) { if (validUrl.isHttpsUri(uri)) {
return ( return (
<FastImage <FastImage
accessibilityLabel={t('common:customEmoji.accessibilityLabel', {
emoji: emoji.shortcode
})}
accessibilityHint={t(
'screenCompose:content.root.footer.emojis.accessibilityHint'
)}
source={{ uri: reduceMotionEnabled ? emoji.static_url : emoji.url }} source={{ uri: reduceMotionEnabled ? emoji.static_url : emoji.url }}
style={styles.emoji} style={styles.emoji}
/> />
@ -42,10 +64,21 @@ const SingleEmoji = ({ emoji }: { emoji: Mastodon.Emoji }) => {
) )
} }
const ComposeEmojis: React.FC = () => { export interface Props {
accessibleRefEmojis: RefObject<SectionList>
}
const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
const { composeState } = useContext(ComposeContext) const { composeState } = useContext(ComposeContext)
const { theme } = useTheme() const { theme } = useTheme()
useEffect(() => {
const tagEmojis = findNodeHandle(accessibleRefEmojis.current)
if (composeState.emoji.active) {
tagEmojis && AccessibilityInfo.setAccessibilityFocus(tagEmojis)
}
}, [composeState.emoji.active])
const listHeader = useCallback( const listHeader = useCallback(
({ section: { title } }) => ( ({ section: { title } }) => (
<Text style={[styles.group, { color: theme.secondary }]}>{title}</Text> <Text style={[styles.group, { color: theme.secondary }]}>{title}</Text>
@ -73,6 +106,8 @@ const ComposeEmojis: React.FC = () => {
return ( return (
<View style={styles.base}> <View style={styles.base}>
<SectionList <SectionList
accessible
ref={accessibleRefEmojis}
horizontal horizontal
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
sections={composeState.emoji.emojis || []} sections={composeState.emoji.emojis || []}

View File

@ -48,6 +48,10 @@ const ComposePoll: React.FC = () => {
color={theme.secondary} color={theme.secondary}
/> />
<TextInput <TextInput
accessibilityLabel={t(
'content.root.footer.poll.option.placeholder.accessibilityLabel',
{ index: i + 1 }
)}
keyboardAppearance={mode} keyboardAppearance={mode}
{...(i === 0 && firstRender && { autoFocus: true })} {...(i === 0 && firstRender && { autoFocus: true })}
style={[ style={[
@ -80,6 +84,19 @@ const ComposePoll: React.FC = () => {
<View style={styles.controlAmount}> <View style={styles.controlAmount}>
<View style={styles.firstButton}> <View style={styles.firstButton}>
<Button <Button
{...((total > 2)
? {
accessibilityLabel: t(
'content.root.footer.poll.quantity.reduce.accessibilityLabel',
{ amount: total - 1 }
)
}
: {
accessibilityHint: t(
'content.root.footer.poll.quantity.reduce.accessibilityHint',
{ amount: total }
)
})}
onPress={() => { onPress={() => {
analytics('compose_poll_reduce_press') analytics('compose_poll_reduce_press')
total > 2 && total > 2 &&
@ -95,6 +112,19 @@ const ComposePoll: React.FC = () => {
/> />
</View> </View>
<Button <Button
{...(total < 4
? {
accessibilityLabel: t(
'content.root.footer.poll.quantity.increase.accessibilityLabel',
{ amount: total + 1 }
)
}
: {
accessibilityHint: t(
'content.root.footer.poll.quantity.increase.accessibilityHint',
{ amount: total }
)
})}
onPress={() => { onPress={() => {
analytics('compose_poll_increase_press') analytics('compose_poll_increase_press')
total < 4 && total < 4 &&

View File

@ -1,6 +1,6 @@
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react' import React, { useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyleSheet, Text, TextInput } from 'react-native' import { StyleSheet, Text, TextInput } from 'react-native'
import formatText from '../../formatText' import formatText from '../../formatText'
@ -45,7 +45,6 @@ const ComposeSpoilerInput: React.FC = () => {
payload: { selection: { start, end } } payload: { selection: { start, end } }
}) })
}} }}
ref={composeState.textInputFocus.refs.spoiler}
scrollEnabled={false} scrollEnabled={false}
onFocus={() => onFocus={() =>
composeDispatch({ composeDispatch({

View File

@ -37,7 +37,7 @@ const composeInitialState: Omit<ComposeState, 'timestamp'> = {
replyToStatus: undefined, replyToStatus: undefined,
textInputFocus: { textInputFocus: {
current: 'text', current: 'text',
refs: { text: createRef(), spoiler: createRef() } refs: { text: createRef() }
} }
} }

View File

@ -63,7 +63,7 @@ export type ComposeState = {
replyToStatus?: Mastodon.Status replyToStatus?: Mastodon.Status
textInputFocus: { textInputFocus: {
current: 'text' | 'spoiler' current: 'text' | 'spoiler'
refs: { text: RefObject<TextInput>; spoiler: RefObject<TextInput> } refs: { text: RefObject<TextInput> }
} }
} }

View File

@ -98,6 +98,10 @@ const ImageItem = ({
setImageDimensions setImageDimensions
})} })}
style={{ flex: 1 }} style={{ flex: 1 }}
imageStyle={{
flex: 1,
resizeMode: 'stretch'
}}
/> />
} }
/> />

View File

@ -120,6 +120,8 @@ const HeaderComponent = React.memo(
content={`${currentIndex + 1} / ${imageUrls.length}`} content={`${currentIndex + 1} / ${imageUrls.length}`}
/> />
<HeaderRight <HeaderRight
accessibilityLabel={t('content.actions.accessibilityLabel')}
accessibilityHint={t('content.actions.accessibilityHint')}
content='MoreHorizontal' content='MoreHorizontal'
native={false} native={false}
background background

View File

@ -1,3 +1,4 @@
import GracefullyImage from '@components/GracefullyImage'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { import {
@ -67,32 +68,28 @@ const ScreenTabs = React.memo(
case 'Tab-Notifications': case 'Tab-Notifications':
return <Icon name='Bell' size={size} color={color} /> return <Icon name='Bell' size={size} color={color} />
case 'Tab-Me': case 'Tab-Me':
return instanceActive !== -1 ? ( return (
<Image <GracefullyImage
source={{ key={instanceAccount?.avatarStatic}
uri: instanceAccount?.avatarStatic uri={{ original: instanceAccount?.avatarStatic }}
dimension={{
width: size,
height: size
}} }}
style={{ style={{
width: size,
height: size,
borderRadius: size, borderRadius: size,
overflow: 'hidden',
borderWidth: focused ? 2 : 0, borderWidth: focused ? 2 : 0,
borderColor: focused ? theme.secondary : color borderColor: focused ? theme.secondary : color
}} }}
/> />
) : (
<Icon
name={focused ? 'Meh' : 'Smile'}
size={size}
color={!focused ? theme.secondary : color}
/>
) )
default: default:
return <Icon name='AlertOctagon' size={size} color={color} /> return <Icon name='AlertOctagon' size={size} color={color} />
} }
} }
}), }),
[instanceAccount, instanceActive] [instanceAccount?.avatarStatic, instanceActive]
) )
const tabBarOptions = useMemo( const tabBarOptions = useMemo(
() => ({ () => ({

View File

@ -37,6 +37,8 @@ const TabLocal = React.memo(
}), }),
headerRight: () => ( headerRight: () => (
<HeaderRight <HeaderRight
accessibilityLabel={t('common.search.accessibilityLabel')}
accessibilityHint={t('common.search.accessibilityHint')}
content='Search' content='Search'
onPress={() => { onPress={() => {
analytics('search_tap', { page: 'Local' }) analytics('search_tap', { page: 'Local' })

View File

@ -11,7 +11,8 @@ import {
} from '@utils/slices/instancesSlice' } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import { groupBy } from 'lodash'
import React, { useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyleSheet, Text, View } from 'react-native' import { StyleSheet, Text, View } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler' import { ScrollView } from 'react-native-gesture-handler'
@ -20,10 +21,10 @@ import { useDispatch, useSelector } from 'react-redux'
interface Props { interface Props {
instance: Instance instance: Instance
disabled?: boolean selected?: boolean
} }
const AccountButton: React.FC<Props> = ({ instance, disabled = false }) => { const AccountButton: React.FC<Props> = ({ instance, selected = false }) => {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const navigation = useNavigation() const navigation = useNavigation()
const dispatch = useDispatch() const dispatch = useDispatch()
@ -31,10 +32,10 @@ const AccountButton: React.FC<Props> = ({ instance, disabled = false }) => {
return ( return (
<Button <Button
type='text' type='text'
disabled={disabled} selected={selected}
style={styles.button} style={styles.button}
content={`@${instance.account.acct}@${instance.uri}${ content={`@${instance.account.acct}@${instance.uri}${
disabled ? ' ✓' : '' selected ? ' ✓' : ''
}`} }`}
onPress={() => { onPress={() => {
haptics('Light') haptics('Light')
@ -50,11 +51,17 @@ const AccountButton: React.FC<Props> = ({ instance, disabled = false }) => {
const ScreenMeSwitchRoot: React.FC = () => { const ScreenMeSwitchRoot: React.FC = () => {
const { t } = useTranslation('screenTabs') const { t } = useTranslation('screenTabs')
const { theme } = useTheme() const { theme } = useTheme()
const instances = useSelector(getInstances) const instances = useSelector(getInstances, () => true)
const instanceActive = useSelector(getInstanceActive) const instanceActive = useSelector(getInstanceActive, () => true)
const scrollViewRef = useRef<ScrollView>(null)
return ( return (
<ScrollView style={styles.base} keyboardShouldPersistTaps='always'> <ScrollView
ref={scrollViewRef}
style={styles.base}
keyboardShouldPersistTaps='always'
>
<View style={[styles.firstSection, { borderBottomColor: theme.border }]}> <View style={[styles.firstSection, { borderBottomColor: theme.border }]}>
<Text style={[styles.header, { color: theme.primaryDefault }]}> <Text style={[styles.header, { color: theme.primaryDefault }]}>
{t('me.switch.existing')} {t('me.switch.existing')}
@ -74,7 +81,7 @@ const ScreenMeSwitchRoot: React.FC = () => {
<AccountButton <AccountButton
key={index} key={index}
instance={instance} instance={instance}
disabled={ selected={
instance.url === localAccount.url && instance.url === localAccount.url &&
instance.token === localAccount.token && instance.token === localAccount.token &&
instance.account.id === localAccount.account.id instance.account.id === localAccount.account.id
@ -90,7 +97,11 @@ const ScreenMeSwitchRoot: React.FC = () => {
<Text style={[styles.header, { color: theme.primaryDefault }]}> <Text style={[styles.header, { color: theme.primaryDefault }]}>
{t('me.switch.new')} {t('me.switch.new')}
</Text> </Text>
<ComponentInstance disableHeaderImage goBack /> <ComponentInstance
scrollViewRef={scrollViewRef}
disableHeaderImage
goBack
/>
</View> </View>
</ScrollView> </ScrollView>
) )

View File

@ -34,6 +34,8 @@ const TabNotifications = React.memo(
}), }),
headerRight: () => ( headerRight: () => (
<HeaderRight <HeaderRight
accessibilityLabel={t('notifications.filter.accessibilityLabel')}
accessibilityHint={t('notifications.filter.accessibilityHint')}
content='Filter' content='Filter'
onPress={() => { onPress={() => {
analytics('notificationsfilter_tap') analytics('notificationsfilter_tap')

View File

@ -62,6 +62,8 @@ const TabPublic = React.memo(
), ),
headerRight: () => ( headerRight: () => (
<HeaderRight <HeaderRight
accessibilityLabel={t('common.search.accessibilityLabel')}
accessibilityHint={t('common.search.accessibilityHint')}
content='Search' content='Search'
onPress={() => { onPress={() => {
analytics('search_tap', { page: pages[segment].key }) analytics('search_tap', { page: pages[segment].key })

View File

@ -6,6 +6,7 @@ import { useAccountQuery } from '@utils/queryHooks/account'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useEffect, useMemo, useReducer } from 'react' import React, { useCallback, useEffect, useMemo, useReducer } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, View } from 'react-native' import { StyleSheet, View } from 'react-native'
import { useSharedValue } from 'react-native-reanimated' import { useSharedValue } from 'react-native-reanimated'
import AccountAttachments from './Account/Attachments' import AccountAttachments from './Account/Attachments'
@ -23,6 +24,7 @@ const TabSharedAccount: React.FC<SharedAccountProp> = ({
}, },
navigation navigation
}) => { }) => {
const { t, i18n } = useTranslation('screenTabs')
const { theme } = useTheme() const { theme } = useTheme()
const { data } = useAccountQuery({ id: account.id }) const { data } = useAccountQuery({ id: account.id })
@ -38,6 +40,10 @@ const TabSharedAccount: React.FC<SharedAccountProp> = ({
navigation.setOptions({ navigation.setOptions({
headerRight: () => ( headerRight: () => (
<HeaderRight <HeaderRight
accessibilityLabel={t('shared.account.actions.accessibilityLabel', {
user: data?.acct
})}
accessibilityHint={t('shared.account.actions.accessibilityHint')}
content='MoreHorizontal' content='MoreHorizontal'
onPress={() => { onPress={() => {
analytics('bottomsheet_open_press', { analytics('bottomsheet_open_press', {
@ -54,7 +60,7 @@ const TabSharedAccount: React.FC<SharedAccountProp> = ({
) )
}) })
return updateHeaderRight() return updateHeaderRight()
}, []) }, [i18n.language])
const onScroll = useCallback(({ nativeEvent }) => { const onScroll = useCallback(({ nativeEvent }) => {
scrollY.value = nativeEvent.contentOffset.y scrollY.value = nativeEvent.contentOffset.y

View File

@ -163,6 +163,7 @@ const sharedScreens = (
} }
/> />
<TextInput <TextInput
accessibilityRole='search'
keyboardAppearance={mode} keyboardAppearance={mode}
style={[ style={[
styles.textInput, styles.textInput,

View File

@ -3,46 +3,60 @@ import { AccessibilityInfo } from 'react-native'
type ContextType = { type ContextType = {
reduceMotionEnabled: boolean reduceMotionEnabled: boolean
screenReaderEnabled: boolean
} }
const AccessibilityContext = createContext<ContextType>({ const AccessibilityContext = createContext<ContextType>({
reduceMotionEnabled: false reduceMotionEnabled: false,
screenReaderEnabled: false
}) })
export const useAccessibility = () => useContext(AccessibilityContext) export const useAccessibility = () => useContext(AccessibilityContext)
const AccessibilityManager: React.FC = ({ children }) => { const AccessibilityManager: React.FC = ({ children }) => {
const [reduceMotionEnabled, setReduceMotionEnabled] = useState(false) const [reduceMotionEnabled, setReduceMotionEnabled] = useState(false)
const [screenReaderEnabled, setScreenReaderEnabled] = useState(false)
const handleReduceMotionChanged = (reduceMotionEnabled: boolean) => const handleReduceMotionChanged = (reduceMotionEnabled: boolean) =>
setReduceMotionEnabled(reduceMotionEnabled) setReduceMotionEnabled(reduceMotionEnabled)
const loadReduceMotion = async () => { const handleScreenReaderEnabled = (screenReaderEnabled: boolean) =>
setScreenReaderEnabled(screenReaderEnabled)
const loadAccessibilityInfo = async () => {
const reduceMotion = await AccessibilityInfo.isReduceMotionEnabled() const reduceMotion = await AccessibilityInfo.isReduceMotionEnabled()
const screenReader = await AccessibilityInfo.isScreenReaderEnabled()
setReduceMotionEnabled(reduceMotion) setReduceMotionEnabled(reduceMotion)
setScreenReaderEnabled(screenReader)
} }
useEffect(() => { useEffect(() => {
loadReduceMotion() loadAccessibilityInfo()
AccessibilityInfo.addEventListener( AccessibilityInfo.addEventListener(
'reduceMotionChanged', 'reduceMotionChanged',
handleReduceMotionChanged handleReduceMotionChanged
) )
AccessibilityInfo.addEventListener(
'screenReaderChanged',
handleScreenReaderEnabled
)
return () => { return () => {
AccessibilityInfo.removeEventListener( AccessibilityInfo.removeEventListener(
'reduceMotionChanged', 'reduceMotionChanged',
handleReduceMotionChanged handleReduceMotionChanged
) )
AccessibilityInfo.removeEventListener(
'screenReaderChanged',
handleScreenReaderEnabled
)
} }
}, []) }, [])
return ( return (
<AccessibilityContext.Provider <AccessibilityContext.Provider
value={{ value={{ reduceMotionEnabled, screenReaderEnabled }}
reduceMotionEnabled
}}
> >
{children} {children}
</AccessibilityContext.Provider> </AccessibilityContext.Provider>