mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Refine accessibility
This commit is contained in:
@ -55,6 +55,7 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
|
||||
({ item }: { item: ComposeStateDraft }) => {
|
||||
return (
|
||||
<Pressable
|
||||
accessibilityHint={t('content.draftsList.content.accessibilityHint')}
|
||||
style={[styles.draft, { backgroundColor: theme.backgroundDefault }]}
|
||||
onPress={async () => {
|
||||
setCheckingAttachments(true)
|
||||
@ -181,7 +182,10 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
|
||||
visible={checkingAttachments}
|
||||
children={
|
||||
<View
|
||||
style={[styles.modal, { backgroundColor: theme.backgroundOverlayInvert }]}
|
||||
style={[
|
||||
styles.modal,
|
||||
{ backgroundColor: theme.backgroundOverlayInvert }
|
||||
]}
|
||||
children={
|
||||
<Text
|
||||
children='检查附件在服务器的状态…'
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useContext } from 'react'
|
||||
@ -22,6 +23,7 @@ export interface Props {
|
||||
const ComposeEditAttachmentImage: React.FC<Props> = ({ index }) => {
|
||||
const { t } = useTranslation('screenCompose')
|
||||
const { theme } = useTheme()
|
||||
const { screenReaderEnabled } = useAccessibility()
|
||||
|
||||
const { composeState, composeDispatch } = useContext(ComposeContext)
|
||||
const theAttachmentRemote = composeState.attachments.uploads[index].remote!
|
||||
@ -160,9 +162,11 @@ const ComposeEditAttachmentImage: React.FC<Props> = ({ index }) => {
|
||||
</Animated.View>
|
||||
</PanGestureHandler>
|
||||
</View>
|
||||
<Text style={[styles.imageFocusText, { color: theme.primaryDefault }]}>
|
||||
{t('content.editAttachment.content.imageFocus')}
|
||||
</Text>
|
||||
{screenReaderEnabled ? null : (
|
||||
<Text style={[styles.imageFocusText, { color: theme.primaryDefault }]}>
|
||||
{t('content.editAttachment.content.imageFocus')}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -22,6 +22,9 @@ const ComposeEditAttachmentSubmit: React.FC<Props> = ({ index }) => {
|
||||
|
||||
return (
|
||||
<HeaderRight
|
||||
accessibilityLabel={t(
|
||||
'content.editAttachment.header.right.accessibilityLabel'
|
||||
)}
|
||||
type='icon'
|
||||
content='Save'
|
||||
loading={isSubmitting}
|
||||
@ -39,8 +42,8 @@ const ComposeEditAttachmentSubmit: React.FC<Props> = ({ index }) => {
|
||||
) {
|
||||
formData.append(
|
||||
'focus',
|
||||
`${theAttachment.meta.focus.x || 0},${-theAttachment.meta.focus.y ||
|
||||
0}`
|
||||
`${theAttachment.meta?.focus?.x || 0},${-theAttachment.meta?.focus
|
||||
?.y || 0}`
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -4,8 +4,20 @@ import { useSearchQuery } from '@utils/queryHooks/search'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { forEach, groupBy, sortBy } from 'lodash'
|
||||
import React, { useCallback, useContext, useEffect, useMemo } from 'react'
|
||||
import { FlatList, StyleSheet, View } from 'react-native'
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef
|
||||
} from 'react'
|
||||
import {
|
||||
AccessibilityInfo,
|
||||
findNodeHandle,
|
||||
FlatList,
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native'
|
||||
import { Circle } from 'react-native-animated-spinkit'
|
||||
import ComposeActions from './Root/Actions'
|
||||
import ComposePosting from './Posting'
|
||||
@ -44,6 +56,15 @@ const ComposeRoot = React.memo(
|
||||
const { reduceMotionEnabled } = useAccessibility()
|
||||
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 { isFetching, data, refetch } = useSearchQuery({
|
||||
@ -106,6 +127,16 @@ const ComposeRoot = React.memo(
|
||||
[composeState]
|
||||
)
|
||||
|
||||
const ListFooter = useCallback(
|
||||
() => (
|
||||
<ComposeRootFooter
|
||||
accessibleRefAttachments={accessibleRefAttachments}
|
||||
accessibleRefEmojis={accessibleRefEmojis}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
<FlatList
|
||||
@ -113,14 +144,14 @@ const ComposeRoot = React.memo(
|
||||
ListEmptyComponent={listEmpty}
|
||||
keyboardShouldPersistTaps='always'
|
||||
ListHeaderComponent={ComposeRootHeader}
|
||||
ListFooterComponent={ComposeRootFooter}
|
||||
ListFooterComponent={ListFooter}
|
||||
ItemSeparatorComponent={ComponentSeparator}
|
||||
// @ts-ignore
|
||||
data={data ? data[composeState.tag?.type] : undefined}
|
||||
keyExtractor={() => Math.random().toString()}
|
||||
/>
|
||||
<ComposeActions />
|
||||
<ComposeDrafts />
|
||||
<ComposeDrafts accessibleRefDrafts={accessibleRefDrafts} />
|
||||
<ComposePosting />
|
||||
</View>
|
||||
)
|
||||
|
@ -164,20 +164,50 @@ const ComposeActions: React.FC = () => {
|
||||
|
||||
return (
|
||||
<View
|
||||
accessibilityRole='toolbar'
|
||||
style={[
|
||||
styles.additions,
|
||||
{ backgroundColor: theme.backgroundDefault, borderTopColor: theme.border }
|
||||
{
|
||||
backgroundColor: theme.backgroundDefault,
|
||||
borderTopColor: theme.border
|
||||
}
|
||||
]}
|
||||
>
|
||||
<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}
|
||||
children={<Icon name='Aperture' size={24} color={attachmentColor} />}
|
||||
/>
|
||||
<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}
|
||||
children={<Icon name='BarChart2' size={24} color={pollColor} />}
|
||||
/>
|
||||
<Pressable
|
||||
accessibilityRole='button'
|
||||
accessibilityLabel={t(
|
||||
'content.root.actions.visibility.accessibilityLabel',
|
||||
{ visibility: composeState.visibility }
|
||||
)}
|
||||
accessibilityState={{ disabled: composeState.visibilityLock }}
|
||||
style={styles.button}
|
||||
onPress={visibilityOnPress}
|
||||
children={
|
||||
<Icon
|
||||
@ -190,18 +220,34 @@ const ComposeActions: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
<Pressable
|
||||
accessibilityRole='button'
|
||||
accessibilityLabel={t(
|
||||
'content.root.actions.spoiler.accessibilityLabel'
|
||||
)}
|
||||
accessibilityState={{ expanded: composeState.spoiler.active }}
|
||||
style={styles.button}
|
||||
onPress={spoilerOnPress}
|
||||
children={
|
||||
<Icon
|
||||
name='AlertTriangle'
|
||||
size={24}
|
||||
color={
|
||||
composeState.spoiler.active ? theme.primaryDefault : theme.secondary
|
||||
composeState.spoiler.active
|
||||
? theme.primaryDefault
|
||||
: theme.secondary
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<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}
|
||||
children={<Icon name='Smile' size={24} color={emojiColor} />}
|
||||
/>
|
||||
@ -210,6 +256,12 @@ const ComposeActions: React.FC = () => {
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100%'
|
||||
},
|
||||
additions: {
|
||||
height: 45,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
|
@ -3,13 +3,17 @@ import { useNavigation } from '@react-navigation/native'
|
||||
import { getInstanceDrafts } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
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 { StyleSheet, View } from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
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 navigation = useNavigation()
|
||||
const { composeState } = useContext(ComposeContext)
|
||||
@ -24,6 +28,7 @@ const ComposeDrafts: React.FC = () => {
|
||||
if (!composeState.dirty && instanceDrafts?.length) {
|
||||
return (
|
||||
<View
|
||||
ref={accessibleRefDrafts}
|
||||
style={styles.base}
|
||||
children={
|
||||
<Button
|
||||
|
@ -3,15 +3,30 @@ import ComposeEmojis from '@screens/Compose/Root/Footer/Emojis'
|
||||
import ComposePoll from '@screens/Compose/Root/Footer/Poll'
|
||||
import ComposeReply from '@screens/Compose/Root/Footer/Reply'
|
||||
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)
|
||||
|
||||
return (
|
||||
<>
|
||||
{composeState.emoji.active ? <ComposeEmojis /> : null}
|
||||
{composeState.attachments.uploads.length ? <ComposeAttachments /> : null}
|
||||
{composeState.emoji.active ? (
|
||||
<ComposeEmojis accessibleRefEmojis={accessibleRefEmojis} />
|
||||
) : null}
|
||||
{composeState.attachments.uploads.length ? (
|
||||
<ComposeAttachments
|
||||
accessibleRefAttachments={accessibleRefAttachments}
|
||||
/>
|
||||
) : null}
|
||||
{composeState.poll.active ? <ComposePoll /> : null}
|
||||
{composeState.replyToStatus ? <ComposeReply /> : null}
|
||||
</>
|
||||
|
@ -8,6 +8,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, {
|
||||
RefObject,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
@ -28,9 +29,13 @@ import ComposeContext from '../../utils/createContext'
|
||||
import { ExtendedAttachment } from '../../utils/types'
|
||||
import addAttachment from './addAttachment'
|
||||
|
||||
export interface Props {
|
||||
accessibleRefAttachments: RefObject<View>
|
||||
}
|
||||
|
||||
const DEFAULT_HEIGHT = 200
|
||||
|
||||
const ComposeAttachments: React.FC = () => {
|
||||
const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
||||
const { showActionSheetWithOptions } = useActionSheet()
|
||||
const { composeState, composeDispatch } = useContext(ComposeContext)
|
||||
const { t } = useTranslation('screenCompose')
|
||||
@ -153,6 +158,10 @@ const ComposeAttachments: React.FC = () => {
|
||||
) : (
|
||||
<View style={styles.actions}>
|
||||
<Button
|
||||
accessibilityLabel={t(
|
||||
'content.root.footer.attachments.remove.accessibilityLabel',
|
||||
{ attachment: index + 1 }
|
||||
)}
|
||||
type='icon'
|
||||
content='X'
|
||||
spacing='M'
|
||||
@ -169,6 +178,10 @@ const ComposeAttachments: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
accessibilityLabel={t(
|
||||
'content.root.footer.attachments.edit.accessibilityLabel',
|
||||
{ attachment: index + 1 }
|
||||
)}
|
||||
type='icon'
|
||||
content='Edit'
|
||||
spacing='M'
|
||||
@ -192,6 +205,10 @@ const ComposeAttachments: React.FC = () => {
|
||||
const listFooter = useMemo(
|
||||
() => (
|
||||
<Pressable
|
||||
accessible
|
||||
accessibilityLabel={t(
|
||||
'content.root.footer.attachments.upload.accessibilityLabel'
|
||||
)}
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
@ -233,7 +250,7 @@ const ComposeAttachments: React.FC = () => {
|
||||
[]
|
||||
)
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
<View style={styles.base} ref={accessibleRefAttachments} accessible>
|
||||
<Pressable style={styles.sensitive} onPress={sensitiveOnPress}>
|
||||
<Icon
|
||||
name={composeState.attachments.sensitive ? 'CheckCircle' : 'Circle'}
|
||||
|
@ -3,14 +3,30 @@ import haptics from '@components/haptics'
|
||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useCallback, useContext, useMemo } from 'react'
|
||||
import { Pressable, SectionList, StyleSheet, Text, View } from 'react-native'
|
||||
import React, {
|
||||
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 validUrl from 'valid-url'
|
||||
import updateText from '../../updateText'
|
||||
import ComposeContext from '../../utils/createContext'
|
||||
|
||||
const SingleEmoji = ({ emoji }: { emoji: Mastodon.Emoji }) => {
|
||||
const { t } = useTranslation()
|
||||
const { reduceMotionEnabled } = useAccessibility()
|
||||
|
||||
const { composeState, composeDispatch } = useContext(ComposeContext)
|
||||
@ -29,6 +45,12 @@ const SingleEmoji = ({ emoji }: { emoji: Mastodon.Emoji }) => {
|
||||
if (validUrl.isHttpsUri(uri)) {
|
||||
return (
|
||||
<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 }}
|
||||
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 { theme } = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
const tagEmojis = findNodeHandle(accessibleRefEmojis.current)
|
||||
if (composeState.emoji.active) {
|
||||
tagEmojis && AccessibilityInfo.setAccessibilityFocus(tagEmojis)
|
||||
}
|
||||
}, [composeState.emoji.active])
|
||||
|
||||
const listHeader = useCallback(
|
||||
({ section: { title } }) => (
|
||||
<Text style={[styles.group, { color: theme.secondary }]}>{title}</Text>
|
||||
@ -73,6 +106,8 @@ const ComposeEmojis: React.FC = () => {
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
<SectionList
|
||||
accessible
|
||||
ref={accessibleRefEmojis}
|
||||
horizontal
|
||||
keyboardShouldPersistTaps='always'
|
||||
sections={composeState.emoji.emojis || []}
|
||||
|
@ -48,6 +48,10 @@ const ComposePoll: React.FC = () => {
|
||||
color={theme.secondary}
|
||||
/>
|
||||
<TextInput
|
||||
accessibilityLabel={t(
|
||||
'content.root.footer.poll.option.placeholder.accessibilityLabel',
|
||||
{ index: i + 1 }
|
||||
)}
|
||||
keyboardAppearance={mode}
|
||||
{...(i === 0 && firstRender && { autoFocus: true })}
|
||||
style={[
|
||||
@ -80,6 +84,19 @@ const ComposePoll: React.FC = () => {
|
||||
<View style={styles.controlAmount}>
|
||||
<View style={styles.firstButton}>
|
||||
<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={() => {
|
||||
analytics('compose_poll_reduce_press')
|
||||
total > 2 &&
|
||||
@ -95,6 +112,19 @@ const ComposePoll: React.FC = () => {
|
||||
/>
|
||||
</View>
|
||||
<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={() => {
|
||||
analytics('compose_poll_increase_press')
|
||||
total < 4 &&
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useContext } from 'react'
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StyleSheet, Text, TextInput } from 'react-native'
|
||||
import formatText from '../../formatText'
|
||||
@ -45,7 +45,6 @@ const ComposeSpoilerInput: React.FC = () => {
|
||||
payload: { selection: { start, end } }
|
||||
})
|
||||
}}
|
||||
ref={composeState.textInputFocus.refs.spoiler}
|
||||
scrollEnabled={false}
|
||||
onFocus={() =>
|
||||
composeDispatch({
|
||||
|
@ -37,7 +37,7 @@ const composeInitialState: Omit<ComposeState, 'timestamp'> = {
|
||||
replyToStatus: undefined,
|
||||
textInputFocus: {
|
||||
current: 'text',
|
||||
refs: { text: createRef(), spoiler: createRef() }
|
||||
refs: { text: createRef() }
|
||||
}
|
||||
}
|
||||
|
||||
|
2
src/screens/Compose/utils/types.d.ts
vendored
2
src/screens/Compose/utils/types.d.ts
vendored
@ -63,7 +63,7 @@ export type ComposeState = {
|
||||
replyToStatus?: Mastodon.Status
|
||||
textInputFocus: {
|
||||
current: 'text' | 'spoiler'
|
||||
refs: { text: RefObject<TextInput>; spoiler: RefObject<TextInput> }
|
||||
refs: { text: RefObject<TextInput> }
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user