fix accessibility label in notifications (#4305)

* fix accessibility label in notifications

* add accessibility options to expand post

* inherit from outside, but always include `activate`

* include option to disable label/hint on previewable avatar

* fix hidden elements still being read on voiceover

* make it work for followers too

* extract variable

* fix hint

* update wording elsewhere
zio/stable
Hailey 2024-05-31 13:02:18 -07:00 committed by GitHub
parent b51640fbc0
commit 708a80e7a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 93 additions and 49 deletions

View File

@ -194,10 +194,36 @@ let FeedItem = ({
]} ]}
href={itemHref} href={itemHref}
noFeedback noFeedback
accessible={ accessible={!isAuthorsExpanded}
(item.type === 'post-like' && authors.length === 1) || accessibilityActions={
item.type === 'repost' authors.length > 1
? [
{
name: 'toggleAuthorsExpanded',
label: isAuthorsExpanded
? _(msg`Collapse list of users`)
: _(msg`Expand list of users`),
},
]
: [
{
name: 'viewProfile',
label: _(
msg`View ${
authors[0].profile.displayName || authors[0].profile.handle
}'s profile`,
),
},
]
} }
onAccessibilityAction={e => {
if (e.nativeEvent.actionName === 'activate') {
onBeforePress()
}
if (e.nativeEvent.actionName === 'toggleAuthorsExpanded') {
onToggleAuthorsExpanded()
}
}}
onBeforePress={onBeforePress}> onBeforePress={onBeforePress}>
<View style={[styles.layoutIcon, a.pr_sm]}> <View style={[styles.layoutIcon, a.pr_sm]}>
{/* TODO: Prevent conditional rendering and move toward composable {/* TODO: Prevent conditional rendering and move toward composable
@ -332,16 +358,14 @@ function CondensedAuthorsList({
profile={authors[0].profile} profile={authors[0].profile}
moderation={authors[0].moderation.ui('avatar')} moderation={authors[0].moderation.ui('avatar')}
type={authors[0].profile.associated?.labeler ? 'labeler' : 'user'} type={authors[0].profile.associated?.labeler ? 'labeler' : 'user'}
accessible={false}
/> />
</View> </View>
) )
} }
return ( return (
<TouchableOpacity <TouchableOpacity
accessibilityLabel={_(msg`Show users`)} accessibilityRole="none"
accessibilityHint={_(
msg`Opens an expanded list of users in this notification`,
)}
onPress={onToggleAuthorsExpanded}> onPress={onToggleAuthorsExpanded}>
<View style={styles.avis}> <View style={styles.avis}>
{authors.slice(0, MAX_AUTHORS).map(author => ( {authors.slice(0, MAX_AUTHORS).map(author => (
@ -351,6 +375,7 @@ function CondensedAuthorsList({
profile={author.profile} profile={author.profile}
moderation={author.moderation.ui('avatar')} moderation={author.moderation.ui('avatar')}
type={author.profile.associated?.labeler ? 'labeler' : 'user'} type={author.profile.associated?.labeler ? 'labeler' : 'user'}
accessible={false}
/> />
</View> </View>
))} ))}
@ -392,48 +417,45 @@ function ExpandedAuthorsList({
}, [heightInterp, visible]) }, [heightInterp, visible])
return ( return (
<Animated.View <Animated.View style={[heightStyle, styles.overflowHidden]}>
style={[ {visible &&
heightStyle, authors.map(author => (
styles.overflowHidden, <NewLink
visible ? s.mb10 : undefined, key={author.profile.did}
]}> label={author.profile.displayName || author.profile.handle}
{authors.map(author => ( accessibilityHint={_(msg`Opens this profile`)}
<NewLink to={makeProfileLink({
key={author.profile.did} did: author.profile.did,
label={_(msg`See profile`)} handle: author.profile.handle,
to={makeProfileLink({ })}
did: author.profile.did, style={styles.expandedAuthor}>
handle: author.profile.handle, <View style={styles.expandedAuthorAvi}>
})} <ProfileHoverCard did={author.profile.did}>
style={styles.expandedAuthor}> <UserAvatar
<View style={styles.expandedAuthorAvi}> size={35}
<ProfileHoverCard did={author.profile.did}> avatar={author.profile.avatar}
<UserAvatar moderation={author.moderation.ui('avatar')}
size={35} type={author.profile.associated?.labeler ? 'labeler' : 'user'}
avatar={author.profile.avatar} />
moderation={author.moderation.ui('avatar')} </ProfileHoverCard>
type={author.profile.associated?.labeler ? 'labeler' : 'user'} </View>
/> <View style={s.flex1}>
</ProfileHoverCard> <Text
</View> type="lg-bold"
<View style={s.flex1}> numberOfLines={1}
<Text style={pal.text}
type="lg-bold" lineHeight={1.2}>
numberOfLines={1} {sanitizeDisplayName(
style={pal.text} author.profile.displayName || author.profile.handle,
lineHeight={1.2}> )}
{sanitizeDisplayName( &nbsp;
author.profile.displayName || author.profile.handle, <Text style={[pal.textLight]} lineHeight={1.2}>
)} {sanitizeHandle(author.profile.handle)}
&nbsp; </Text>
<Text style={[pal.textLight]} lineHeight={1.2}>
{sanitizeHandle(author.profile.handle)}
</Text> </Text>
</Text> </View>
</View> </NewLink>
</NewLink> ))}
))}
</Animated.View> </Animated.View>
) )
} }

View File

@ -64,6 +64,8 @@ export const Link = memo(function Link({
anchorNoUnderline, anchorNoUnderline,
navigationAction, navigationAction,
onBeforePress, onBeforePress,
accessibilityActions,
onAccessibilityAction,
...props ...props
}: Props) { }: Props) {
const t = useTheme() const t = useTheme()
@ -89,6 +91,11 @@ export const Link = memo(function Link({
[closeModal, navigation, navigationAction, href, openLink, onBeforePress], [closeModal, navigation, navigationAction, href, openLink, onBeforePress],
) )
const accessibilityActionsWithActivate = [
...(accessibilityActions || []),
{name: 'activate', label: title},
]
if (noFeedback) { if (noFeedback) {
return ( return (
<WebAuxClickWrapper> <WebAuxClickWrapper>
@ -97,6 +104,14 @@ export const Link = memo(function Link({
onPress={onPress} onPress={onPress}
accessible={accessible} accessible={accessible}
accessibilityRole="link" accessibilityRole="link"
accessibilityActions={accessibilityActionsWithActivate}
onAccessibilityAction={e => {
if (e.nativeEvent.actionName === 'activate') {
onPress()
} else {
onAccessibilityAction?.(e)
}
}}
{...props} {...props}
android_ripple={{ android_ripple={{
color: t.atoms.bg_contrast_25.backgroundColor, color: t.atoms.bg_contrast_25.backgroundColor,

View File

@ -53,6 +53,7 @@ interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
profile: AppBskyActorDefs.ProfileViewBasic profile: AppBskyActorDefs.ProfileViewBasic
disableHoverCard?: boolean disableHoverCard?: boolean
onBeforePress?: () => void onBeforePress?: () => void
accessible?: boolean
} }
const BLUR_AMOUNT = isWeb ? 5 : 100 const BLUR_AMOUNT = isWeb ? 5 : 100
@ -386,6 +387,7 @@ let PreviewableUserAvatar = ({
profile, profile,
disableHoverCard, disableHoverCard,
onBeforePress, onBeforePress,
accessible = true,
...rest ...rest
}: PreviewableUserAvatarProps): React.ReactNode => { }: PreviewableUserAvatarProps): React.ReactNode => {
const {_} = useLingui() const {_} = useLingui()
@ -399,7 +401,12 @@ let PreviewableUserAvatar = ({
return ( return (
<ProfileHoverCard did={profile.did} disable={disableHoverCard}> <ProfileHoverCard did={profile.did} disable={disableHoverCard}>
<Link <Link
label={_(msg`See profile`)} label={
accessible
? _(msg`${profile.displayName || profile.handle}'s avatar`)
: undefined
}
accessibilityHint={accessible ? _(msg`Opens this profile`) : undefined}
to={makeProfileLink({ to={makeProfileLink({
did: profile.did, did: profile.did,
handle: profile.handle, handle: profile.handle,