diff options
author | Paul Frazee <pfrazee@gmail.com> | 2023-07-03 15:57:53 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-07-03 15:57:53 -0500 |
commit | bc55241c9ae731f633f8b93a9b7eac7635070148 (patch) | |
tree | 2f75cf5fc31bb8214f848a4326c1af731000b842 /src | |
parent | 0163ba0af8cd42e2a76b3ae66efc0777afdc2544 (diff) | |
download | voidsky-bc55241c9ae731f633f8b93a9b7eac7635070148.tar.zst |
[APP-724] Collection of accessibility fixes (#949)
* Fix: include alt text on the web lightbox image * a11y: Dont read the 'ALT' label * a11y: remove a wrapper behavior from posts This appears to have been introduced with the goal of creating meta actions on posts, but the behavior seems counter-productive. The accessibility inspector was unable to access individual items within the post and therefore most content was simply skipped. There may be a way to support the post actions without losing the ability to access the inner elements but I couldnt find it. -prf * a11y: apply alt tags to image wrappers so they get read * a11y: set Link accessibilityLabel to the title if none set * a11y: skip the SANDBOX watermark * a11y: improve post meta to not read UI and give a useful date * ally: improve post controls * a11y: add labels to lightbox images on mobile * fix types
Diffstat (limited to 'src')
19 files changed, 80 insertions, 148 deletions
diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index c226d25cc..6dba2f011 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -89,7 +89,9 @@ export const Gallery = observer(function ({gallery}: Props) { openAltTextModal(store, image) }} style={[styles.altTextControl, altTextControlStyle]}> - <Text style={styles.altTextControlLabel}>ALT</Text> + <Text style={styles.altTextControlLabel} accessible={false}> + ALT + </Text> {image.altText.length > 0 ? ( <FontAwesomeIcon icon="check" diff --git a/src/view/com/lightbox/ImageViewing/@types/index.ts b/src/view/com/lightbox/ImageViewing/@types/index.ts index 4a08e2394..8400e12e4 100644 --- a/src/view/com/lightbox/ImageViewing/@types/index.ts +++ b/src/view/com/lightbox/ImageViewing/@types/index.ts @@ -6,8 +6,6 @@ * */ -import {ImageURISource, ImageRequireSource} from 'react-native' - export type Dimensions = { width: number height: number @@ -18,4 +16,4 @@ export type Position = { y: number } -export type ImageSource = ImageURISource | ImageRequireSource +export type ImageSource = {uri: string; alt?: string} diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx index 01a53ff6f..b900f9afe 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx @@ -133,6 +133,8 @@ const ImageItem = ({ source={imageSrc} style={imageStylesWithOpacity} onLoad={onLoaded} + accessibilityLabel={imageSrc.alt} + accessibilityHint="" /> {(!isLoaded || !imageDimensions) && <ImageLoading />} </ScrollView> diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx index 658735724..ebf0b1d28 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx @@ -128,7 +128,9 @@ const ImageItem = ({ onPress={doubleTapToZoomEnabled ? handleDoubleTap : undefined} onLongPress={onLongPressHandler} delayLongPress={delayLongPress} - accessibilityRole="image"> + accessibilityRole="image" + accessibilityLabel={imageSrc.alt} + accessibilityHint=""> <Animated.Image source={imageSrc} style={imageStylesWithOpacity} diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx index b496e0d95..072bfebfa 100644 --- a/src/view/com/lightbox/Lightbox.tsx +++ b/src/view/com/lightbox/Lightbox.tsx @@ -109,7 +109,7 @@ export const Lightbox = observer(function Lightbox() { const opts = store.shell.activeLightbox as models.ProfileImageLightbox return ( <ImageView - images={[{uri: opts.profileView.avatar}]} + images={[{uri: opts.profileView.avatar || ''}]} imageIndex={0} visible onRequestClose={onClose} @@ -120,7 +120,7 @@ export const Lightbox = observer(function Lightbox() { const opts = store.shell.activeLightbox as models.ImagesLightbox return ( <ImageView - images={opts.images.map(({uri}) => ({uri}))} + images={opts.images.map(img => ({...img}))} imageIndex={opts.index} visible onRequestClose={onClose} diff --git a/src/view/com/lightbox/Lightbox.web.tsx b/src/view/com/lightbox/Lightbox.web.tsx index f6aa26a3b..6d79dad36 100644 --- a/src/view/com/lightbox/Lightbox.web.tsx +++ b/src/view/com/lightbox/Lightbox.web.tsx @@ -109,6 +109,8 @@ function LightboxInner({ accessibilityIgnoresInvertColors source={imgs[index]} style={styles.image} + accessibilityLabel={imgs[index].alt} + accessibilityHint="" /> {canGoLeft && ( <TouchableOpacity diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 83a51f7aa..e1c73c0d5 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -1,6 +1,6 @@ -import React, {useCallback, useMemo} from 'react' +import React, {useMemo} from 'react' import {observer} from 'mobx-react-lite' -import {AccessibilityActionEvent, Linking, StyleSheet, View} from 'react-native' +import {Linking, StyleSheet, View} from 'react-native' import Clipboard from '@react-native-clipboard/clipboard' import {AtUri, AppBskyFeedDefs} from '@atproto/api' import { @@ -138,40 +138,6 @@ export const PostThreadItem = observer(function PostThreadItem({ ) }, [item, store]) - const accessibilityActions = useMemo( - () => [ - { - name: 'reply', - label: 'Reply', - }, - { - name: 'repost', - label: item.post.viewer?.repost ? 'Undo repost' : 'Repost', - }, - {name: 'like', label: item.post.viewer?.like ? 'Unlike' : 'Like'}, - ], - [item.post.viewer?.like, item.post.viewer?.repost], - ) - - const onAccessibilityAction = useCallback( - (event: AccessibilityActionEvent) => { - switch (event.nativeEvent.actionName) { - case 'like': - onPressToggleLike() - break - case 'reply': - onPressReply() - break - case 'repost': - onPressToggleRepost() - break - default: - break - } - }, - [onPressReply, onPressToggleLike, onPressToggleRepost], - ) - if (!record) { return <ErrorMessage message="Invalid or unsupported post record" /> } @@ -193,9 +159,7 @@ export const PostThreadItem = observer(function PostThreadItem({ <PostHider testID={`postThreadItem-by-${item.post.author.handle}`} style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]} - moderation={item.moderation.thread} - accessibilityActions={accessibilityActions} - onAccessibilityAction={onAccessibilityAction}> + moderation={item.moderation.thread}> <PostSandboxWarning /> <View style={styles.layout}> <View style={styles.layoutAvi}> @@ -369,9 +333,7 @@ export const PostThreadItem = observer(function PostThreadItem({ pal.view, item._showParentReplyLine && styles.noTopBorder, ]} - moderation={item.moderation.thread} - accessibilityActions={accessibilityActions} - onAccessibilityAction={onAccessibilityAction}> + moderation={item.moderation.thread}> {item._showParentReplyLine && ( <View style={[ diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index c74abb894..12ab0e901 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -1,6 +1,5 @@ -import React, {useCallback, useEffect, useMemo, useState} from 'react' +import React, {useEffect, useState} from 'react' import { - AccessibilityActionEvent, ActivityIndicator, Linking, StyleProp, @@ -200,47 +199,11 @@ const PostLoaded = observer( ) }, [item, setDeleted, store]) - const accessibilityActions = useMemo( - () => [ - { - name: 'reply', - label: 'Reply', - }, - { - name: 'repost', - label: item.post.viewer?.repost ? 'Undo repost' : 'Repost', - }, - {name: 'like', label: item.post.viewer?.like ? 'Unlike' : 'Like'}, - ], - [item.post.viewer?.like, item.post.viewer?.repost], - ) - - const onAccessibilityAction = useCallback( - (event: AccessibilityActionEvent) => { - switch (event.nativeEvent.actionName) { - case 'like': - onPressToggleLike() - break - case 'reply': - onPressReply() - break - case 'repost': - onPressToggleRepost() - break - default: - break - } - }, - [onPressReply, onPressToggleLike, onPressToggleRepost], - ) - return ( <PostHider href={itemHref} style={[styles.outer, pal.view, pal.border, style]} - moderation={item.moderation.list} - accessibilityActions={accessibilityActions} - onAccessibilityAction={onAccessibilityAction}> + moderation={item.moderation.list}> {showReplyLine && <View style={styles.replyLine} />} <View style={styles.layout}> <View style={styles.layoutAvi}> diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 7a292378e..6ec2c80f4 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -1,6 +1,6 @@ -import React, {useCallback, useMemo, useState} from 'react' +import React, {useMemo, useState} from 'react' import {observer} from 'mobx-react-lite' -import {AccessibilityActionEvent, Linking, StyleSheet, View} from 'react-native' +import {Linking, StyleSheet, View} from 'react-native' import Clipboard from '@react-native-clipboard/clipboard' import {AtUri} from '@atproto/api' import { @@ -158,40 +158,6 @@ export const FeedItem = observer(function ({ moderation = {behavior: ModerationBehaviorCode.Show} } - const accessibilityActions = useMemo( - () => [ - { - name: 'reply', - label: 'Reply', - }, - { - name: 'repost', - label: item.post.viewer?.repost ? 'Undo repost' : 'Repost', - }, - {name: 'like', label: item.post.viewer?.like ? 'Unlike' : 'Like'}, - ], - [item.post.viewer?.like, item.post.viewer?.repost], - ) - - const onAccessibilityAction = useCallback( - (event: AccessibilityActionEvent) => { - switch (event.nativeEvent.actionName) { - case 'like': - onPressToggleLike() - break - case 'reply': - onPressReply() - break - case 'repost': - onPressToggleRepost() - break - default: - break - } - }, - [onPressReply, onPressToggleLike, onPressToggleRepost], - ) - if (!record || deleted) { return <View /> } @@ -201,9 +167,7 @@ export const FeedItem = observer(function ({ testID={`feedItem-by-${item.post.author.handle}`} style={outerStyles} href={itemHref} - moderation={moderation} - accessibilityActions={accessibilityActions} - onAccessibilityAction={onAccessibilityAction}> + moderation={moderation}> {isThreadChild && ( <View style={[styles.topReplyLine, {borderColor: pal.colors.replyLine}]} diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index f753f01cc..7ff896344 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -88,6 +88,10 @@ export const Link = observer(function Link({ props.dataSet.noUnderline = 1 } + if (title && !props.accessibilityLabel) { + props.accessibilityLabel = title + } + return ( <TouchableOpacity testID={testID} @@ -171,6 +175,7 @@ export const DesktopWebTextLink = observer(function DesktopWebTextLink({ text, numberOfLines, lineHeight, + ...props }: { testID?: string type?: TypographyVariant @@ -179,6 +184,9 @@ export const DesktopWebTextLink = observer(function DesktopWebTextLink({ text: string | JSX.Element numberOfLines?: number lineHeight?: number + accessible?: boolean + accessibilityLabel?: string + accessibilityHint?: string }) { if (isDesktopWeb) { return ( @@ -190,6 +198,7 @@ export const DesktopWebTextLink = observer(function DesktopWebTextLink({ text={text} numberOfLines={numberOfLines} lineHeight={lineHeight} + {...props} /> ) } @@ -199,7 +208,8 @@ export const DesktopWebTextLink = observer(function DesktopWebTextLink({ type={type} style={style} numberOfLines={numberOfLines} - lineHeight={lineHeight}> + lineHeight={lineHeight} + {...props}> {text} </Text> ) diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index 45651e4e5..628c88722 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -2,7 +2,7 @@ import React from 'react' import {StyleSheet, View} from 'react-native' import {Text} from './text/Text' import {DesktopWebTextLink} from './Link' -import {ago} from 'lib/strings/time' +import {ago, niceDate} from 'lib/strings/time' import {usePalette} from 'lib/hooks/usePalette' import {useStores} from 'state/index' import {UserAvatar} from './UserAvatar' @@ -57,7 +57,11 @@ export const PostMeta = observer(function (opts: PostMetaOpts) { text={sanitizeDisplayName(displayName)} href={`/profile/${opts.authorHandle}`} /> - <Text type="md" style={pal.textLight} lineHeight={1.2}> + <Text + type="md" + style={pal.textLight} + lineHeight={1.2} + accessible={false}> · </Text> <DesktopWebTextLink @@ -65,6 +69,8 @@ export const PostMeta = observer(function (opts: PostMetaOpts) { style={[styles.metaItem, pal.textLight]} lineHeight={1.2} text={ago(opts.timestamp)} + accessibilityLabel={niceDate(opts.timestamp)} + accessibilityHint="" href={opts.postHref} /> </View> @@ -122,7 +128,7 @@ export const PostMeta = observer(function (opts: PostMetaOpts) { href={`/profile/${opts.authorHandle}`} /> </View> - <Text type="md" style={pal.textLight} lineHeight={1.2}> + <Text type="md" style={pal.textLight} lineHeight={1.2} accessible={false}> · </Text> <DesktopWebTextLink @@ -130,6 +136,8 @@ export const PostMeta = observer(function (opts: PostMetaOpts) { style={[styles.metaItem, pal.textLight]} lineHeight={1.2} text={ago(opts.timestamp)} + accessibilityLabel={niceDate(opts.timestamp)} + accessibilityHint="" href={opts.postHref} /> </View> diff --git a/src/view/com/util/PostSandboxWarning.tsx b/src/view/com/util/PostSandboxWarning.tsx index 54495aa9b..21f5f7b90 100644 --- a/src/view/com/util/PostSandboxWarning.tsx +++ b/src/view/com/util/PostSandboxWarning.tsx @@ -10,7 +10,10 @@ export function PostSandboxWarning() { if (store.session.isSandbox) { return ( <View style={styles.container}> - <Text type="title-2xl" style={[pal.text, styles.text]}> + <Text + type="title-2xl" + style={[pal.text, styles.text]} + accessible={false}> SANDBOX </Text> </View> diff --git a/src/view/com/util/forms/DropdownButton.tsx b/src/view/com/util/forms/DropdownButton.tsx index c6e650077..ad216d97e 100644 --- a/src/view/com/util/forms/DropdownButton.tsx +++ b/src/view/com/util/forms/DropdownButton.tsx @@ -50,6 +50,8 @@ interface DropdownButtonProps { openToRight?: boolean rightOffset?: number bottomOffset?: number + accessibilityLabel?: string + accessibilityHint?: string } export function DropdownButton({ @@ -63,6 +65,7 @@ export function DropdownButton({ openToRight = false, rightOffset = 0, bottomOffset = 0, + accessibilityLabel, }: PropsWithChildren<DropdownButtonProps>) { const ref1 = useRef<TouchableOpacity>(null) const ref2 = useRef<View>(null) @@ -128,8 +131,8 @@ export function DropdownButton({ hitSlop={HITSLOP} ref={ref1} accessibilityRole="button" - accessibilityLabel={`Opens ${numItems} options`} - accessibilityHint={`Opens ${numItems} options`}> + accessibilityLabel={accessibilityLabel || `Opens ${numItems} options`} + accessibilityHint=""> {children} </TouchableOpacity> ) @@ -246,7 +249,9 @@ export function PostDropdownBtn({ testID={testID} style={style} items={dropdownItems} - menuWidth={isWeb ? 220 : 200}> + menuWidth={isWeb ? 220 : 200} + accessibilityLabel="Additional post actions" + accessibilityHint=""> {children} </DropdownButton> ) @@ -335,6 +340,7 @@ const DropdownItems = ({ key={index} style={[styles.menuItem]} onPress={() => onPressItem(index)} + accessibilityRole="button" accessibilityLabel={item.label} accessibilityHint={`Option ${index + 1} of ${numItems}`}> {item.icon && ( diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx index e6aba46f3..9c6f25cae 100644 --- a/src/view/com/util/images/AutoSizedImage.tsx +++ b/src/view/com/util/images/AutoSizedImage.tsx @@ -64,15 +64,14 @@ export function AutoSizedImage({ delayPressIn={DELAY_PRESS_IN} style={[styles.container, style]} accessible={true} - accessibilityLabel="Share image" - accessibilityHint="Opens ways of sharing image"> + accessibilityRole="button" + accessibilityLabel={alt || 'Image'} + accessibilityHint="Tap to view fully"> <Image style={[styles.image, {aspectRatio}]} source={uri} - accessible={true} // Must set for `accessibilityLabel` to work + accessible={false} // Must set for `accessibilityLabel` to work accessibilityIgnoresInvertColors - accessibilityLabel={alt} - accessibilityHint="" /> {children} </TouchableOpacity> diff --git a/src/view/com/util/images/Gallery.tsx b/src/view/com/util/images/Gallery.tsx index a7a64b171..01a7d574a 100644 --- a/src/view/com/util/images/Gallery.tsx +++ b/src/view/com/util/images/Gallery.tsx @@ -34,7 +34,7 @@ export const GalleryItem: FC<GalleryItemProps> = ({ onPressIn={onPressIn ? () => onPressIn(index) : undefined} onLongPress={onLongPress ? () => onLongPress(index) : undefined} accessibilityRole="button" - accessibilityLabel="View image" + accessibilityLabel={image.alt || 'Image'} accessibilityHint=""> <Image source={{uri: image.thumb}} @@ -47,7 +47,9 @@ export const GalleryItem: FC<GalleryItemProps> = ({ </TouchableOpacity> {image.alt === '' ? null : ( <View style={styles.altContainer}> - <Text style={styles.alt}>ALT</Text> + <Text style={styles.alt} accessible={false}> + ALT + </Text> </View> )} </View> diff --git a/src/view/com/util/moderation/PostHider.tsx b/src/view/com/util/moderation/PostHider.tsx index 50ccf595b..f2b6dbddd 100644 --- a/src/view/com/util/moderation/PostHider.tsx +++ b/src/view/com/util/moderation/PostHider.tsx @@ -72,8 +72,7 @@ export function PostHider({ style={style} href={href} noFeedback - accessible={true} - accessibilityRole="none" + accessible={false} {...props}> {children} </Link> diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index 12d4c48c8..cd6db408c 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -19,6 +19,7 @@ import {Text} from '../text/Text' import {PostDropdownBtn} from '../forms/DropdownButton' import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons' import {s, colors} from 'lib/styles' +import {pluralize} from 'lib/strings/helpers' import {useTheme} from 'lib/ThemeContext' import {useStores} from 'state/index' import {RepostButton} from './RepostButton' @@ -170,7 +171,9 @@ export function PostCtrls(opts: PostCtrlsOpts) { hitSlop={HITSLOP} onPress={opts.onPressReply} accessibilityRole="button" - accessibilityLabel="Reply" + accessibilityLabel={`Reply (${opts.replyCount} ${ + opts.replyCount === 1 ? 'reply' : 'replies' + })`} accessibilityHint="reply composer"> <CommentBottomArrow style={[defaultCtrlColor, opts.big ? s.mt2 : styles.mt1]} @@ -190,7 +193,9 @@ export function PostCtrls(opts: PostCtrlsOpts) { hitSlop={HITSLOP} onPress={onPressToggleLikeWrapper} accessibilityRole="button" - accessibilityLabel={opts.isLiked ? 'Unlike' : 'Like'} + accessibilityLabel={`${opts.isLiked ? 'Unlike' : 'Like'} (${ + opts.likeCount + } ${pluralize(opts.likeCount || 0, 'like')})`} accessibilityHint=""> {opts.isLiked ? ( <HeartIconSolid diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx index 59f7f6ee1..4338e4c59 100644 --- a/src/view/com/util/post-ctrls/RepostButton.tsx +++ b/src/view/com/util/post-ctrls/RepostButton.tsx @@ -4,6 +4,7 @@ import {RepostIcon} from 'lib/icons' import {s, colors} from 'lib/styles' import {useTheme} from 'lib/ThemeContext' import {Text} from '../text/Text' +import {pluralize} from 'lib/strings/helpers' import {useStores} from 'state/index' const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5} @@ -49,7 +50,9 @@ export const RepostButton = ({ onPress={onPressToggleRepostWrapper} style={styles.control} accessibilityRole="button" - accessibilityLabel={isReposted ? 'Undo repost' : 'Repost'} + accessibilityLabel={`${ + isReposted ? 'Undo repost' : 'Repost' + } (${repostCount} ${pluralize(repostCount || 0, 'repost')})`} accessibilityHint=""> <RepostIcon style={ diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index edcef7039..372b36359 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -129,7 +129,9 @@ export function PostEmbeds({ style={styles.singleImage}> {alt === '' ? null : ( <View style={styles.altContainer}> - <Text style={styles.alt}>ALT</Text> + <Text style={styles.alt} accessible={false}> + ALT + </Text> </View> )} </AutoSizedImage> |