From e7c7b2b084beabee74918007b735d11641095ff6 Mon Sep 17 00:00:00 2001 From: feruzm Date: Tue, 23 Jun 2026 06:22:17 +0000 Subject: [PATCH 1/2] feat(a11y): label post action-row + overflow-menu controls for screen readers Extends the upvote a11y work (#3286) across the whole post action surface so VoiceOver/TalkBack announce every interactive control with a meaningful, count-aware name and role. Central (optional, additive props): TextWithIcon and IconButton now forward accessibilityLabel/accessibilityHint and expose accessibilityRole="button" when interactive (gated on a press handler), plus accessibilityState. Call sites labeled: - Feed card (postCardActionsPanel): votes / reblog / comment / tip - Single post bar (postDisplayView): votes / reblog / comment / tip / view-stats - Comment & wave row (commentView): votes / reply / tip / edit / delete / show-hide replies - Overflow menus: post options on feed cards (postCardHeader) and on comments/waves (postHeaderDescription) Counts are interpolated ({count} votes/reblogs/comments/tips/views) and every formatMessage carries a defaultMessage so non-English locales fall back to English instead of raw message IDs. 16 new post.a11y_* strings (en-US). --- node_modules | 1 + .../view/textWithIcon/textWithIconView.tsx | 9 ++++ src/components/comment/view/commentView.tsx | 32 +++++++++++++ .../iconButton/view/iconButtonView.tsx | 6 +++ .../children/postCardActionsPanel.tsx | 37 ++++++++++++++- .../postCard/children/postCardHeader.tsx | 4 ++ .../view/postHeaderDescription.tsx | 4 ++ .../postView/view/postDisplayView.tsx | 45 +++++++++++++++++++ src/config/locales/en-US.json | 16 +++++++ 9 files changed, 153 insertions(+), 1 deletion(-) create mode 120000 node_modules diff --git a/node_modules b/node_modules new file mode 120000 index 0000000000..323b2a6355 --- /dev/null +++ b/node_modules @@ -0,0 +1 @@ +/root/ecency-mobile/node_modules \ No newline at end of file diff --git a/src/components/basicUIElements/view/textWithIcon/textWithIconView.tsx b/src/components/basicUIElements/view/textWithIcon/textWithIconView.tsx index d5ae7b6b66..9f3a1f90cf 100644 --- a/src/components/basicUIElements/view/textWithIcon/textWithIconView.tsx +++ b/src/components/basicUIElements/view/textWithIcon/textWithIconView.tsx @@ -16,6 +16,8 @@ const TextWithIcon = ({ textStyle, onLongPress, isLoading, + accessibilityLabel, + accessibilityHint, }) => { const [ltext, setLtext] = useState(text); useEffect(() => { @@ -23,6 +25,9 @@ const TextWithIcon = ({ }, [text]); const _iconStyle = [styles.icon, iconStyle, iconSize && { fontSize: iconSize }]; + // Interactive only when both clickable and wired to a press handler — mirror the + // existing `disabled` condition so screen readers expose the right role/state. + const _interactive = !!(isClickable && onPress); return ( @@ -31,6 +36,10 @@ const TextWithIcon = ({ disabled={!isClickable || !onPress} onPress={() => onPress && onPress()} onLongPress={() => onLongPress && onLongPress()} + accessibilityRole={_interactive ? 'button' : undefined} + accessibilityLabel={accessibilityLabel} + accessibilityHint={accessibilityHint} + accessibilityState={{ disabled: !_interactive }} > {isLoading ? ( diff --git a/src/components/comment/view/commentView.tsx b/src/components/comment/view/commentView.tsx index ae4011a6f2..45a7be25a6 100644 --- a/src/components/comment/view/commentView.tsx +++ b/src/components/comment/view/commentView.tsx @@ -215,6 +215,14 @@ const CommentView = ({ } text={_totalVotes} textStyle={styles.voteCountText} + accessibilityLabel={intl.formatMessage( + { id: 'post.a11y_votes', defaultMessage: '{count} votes' }, + { count: _totalVotes || 0 }, + )} + accessibilityHint={intl.formatMessage({ + id: 'post.a11y_voters_hint', + defaultMessage: 'View voters', + })} /> {isLoggedIn && ( @@ -236,6 +252,10 @@ const CommentView = ({ name="gift-outline" onPress={_handleOnTipPress} iconType="MaterialCommunityIcons" + accessibilityLabel={intl.formatMessage({ + id: 'post.a11y_tip', + defaultMessage: 'Send tip', + })} /> )} @@ -248,6 +268,10 @@ const CommentView = ({ name="create" onPress={() => handleOnEditPress && handleOnEditPress(comment)} iconType="MaterialIcons" + accessibilityLabel={intl.formatMessage({ + id: 'post.a11y_edit', + defaultMessage: 'Edit comment', + })} /> {!childCount && !_totalVotes && comment.isDeletable && ( )} @@ -288,6 +316,10 @@ const CommentView = ({ iconSize={16} onPress={() => _showSubCommentsToggle()} text="" + accessibilityLabel={intl.formatMessage({ + id: repliesToggle ? 'post.a11y_hide_replies' : 'post.a11y_show_replies', + defaultMessage: repliesToggle ? 'Hide replies' : 'Show replies', + })} /> )} diff --git a/src/components/iconButton/view/iconButtonView.tsx b/src/components/iconButton/view/iconButtonView.tsx index 0e6f6b7bcb..0040fce420 100644 --- a/src/components/iconButton/view/iconButtonView.tsx +++ b/src/components/iconButton/view/iconButtonView.tsx @@ -25,6 +25,8 @@ const IconButton = ({ size, style, isLoading, + accessibilityLabel, + accessibilityHint, }) => ( !isLoading && onLongPress && onLongPress()} + accessibilityRole={onPress ? 'button' : undefined} + accessibilityLabel={accessibilityLabel} + accessibilityHint={accessibilityHint} + accessibilityState={{ disabled: !!disabled }} > {!isLoading ? ( { + const intl = useIntl(); + const _onVotersPress = () => { handleCardInteraction(PostCardActionIds.NAVIGATE, { name: ROUTES.SCREENS.VOTERS, @@ -63,7 +66,19 @@ const PostCardActionsPanelComponent = ({ content, handleCardInteraction }: Props } /> - + handleCardInteraction(PostCardActionIds.REPLY)} + accessibilityLabel={intl.formatMessage( + { id: 'post.a11y_comments', defaultMessage: '{count} comments' }, + { count: get(content, 'children', 0) }, + )} + accessibilityHint={intl.formatMessage({ + id: 'post.a11y_reply_hint', + defaultMessage: 'Reply', + })} /> diff --git a/src/components/postCard/children/postCardHeader.tsx b/src/components/postCard/children/postCardHeader.tsx index b26f917815..a7ed86ec43 100644 --- a/src/components/postCard/children/postCardHeader.tsx +++ b/src/components/postCard/children/postCardHeader.tsx @@ -109,6 +109,10 @@ const PostCardHeaderComponent = ({ name="dots-horizontal" onPress={() => handleCardInteraction(PostCardActionIds.OPTIONS)} size={24} + accessibilityLabel={intl.formatMessage({ + id: 'post.a11y_post_options', + defaultMessage: 'Post options', + })} /> diff --git a/src/components/postElements/headerDescription/view/postHeaderDescription.tsx b/src/components/postElements/headerDescription/view/postHeaderDescription.tsx index 7a8de77120..5f622f43fd 100644 --- a/src/components/postElements/headerDescription/view/postHeaderDescription.tsx +++ b/src/components/postElements/headerDescription/view/postHeaderDescription.tsx @@ -182,6 +182,10 @@ class PostHeaderDescription extends PureComponent { name="dots-horizontal" onPress={() => handleOnDotPress && handleOnDotPress()} iconType="MaterialCommunityIcons" + accessibilityLabel={intl.formatMessage({ + id: 'post.a11y_post_options', + defaultMessage: 'Post options', + })} /> )} diff --git a/src/components/postView/view/postDisplayView.tsx b/src/components/postView/view/postDisplayView.tsx index deafda50e2..202ddc273e 100644 --- a/src/components/postView/view/postDisplayView.tsx +++ b/src/components/postView/view/postDisplayView.tsx @@ -243,6 +243,14 @@ const PostDisplayView = ({ onPress={handleVotersIconPress} text={activeVotesCount} textMarginLeft={20} + accessibilityLabel={intl.formatMessage( + { id: 'post.a11y_votes', defaultMessage: '{count} votes' }, + { count: activeVotesCount || 0 }, + )} + accessibilityHint={intl.formatMessage({ + id: 'post.a11y_voters_hint', + defaultMessage: 'View voters', + })} /> {isLoggedIn && ( )} {!isLoggedIn && ( @@ -274,6 +298,10 @@ const PostDisplayView = ({ isClickable text={get(post, 'children', 0)} textMarginLeft={20} + accessibilityLabel={intl.formatMessage( + { id: 'post.a11y_comments', defaultMessage: '{count} comments' }, + { count: get(post, 'children', 0) }, + )} /> )} @@ -286,6 +314,14 @@ const PostDisplayView = ({ text={tipsQuery.data?.meta?.count || 0} textMarginLeft={20} isLoading={tipsQuery.isLoading} + accessibilityLabel={intl.formatMessage( + { id: 'post.a11y_tips', defaultMessage: '{count} tips' }, + { count: tipsQuery.data?.meta?.count || 0 }, + )} + accessibilityHint={intl.formatMessage({ + id: 'post.a11y_tip', + defaultMessage: 'Send tip', + })} /> @@ -306,6 +342,7 @@ const PostDisplayView = ({ _handleOnTipPress, tipsQuery.data?.meta?.count, tipsQuery.isLoading, + intl, ], ); @@ -394,6 +431,14 @@ const PostDisplayView = ({ text={getAbbreviatedNumber(postStatsQuery.data?.visits || 0)} textMarginLeft={4} isLoading={postStatsQuery.isLoading} + accessibilityLabel={intl.formatMessage( + { id: 'post.a11y_views', defaultMessage: '{count} views' }, + { count: postStatsQuery.data?.visits || 0 }, + )} + accessibilityHint={intl.formatMessage({ + id: 'post.a11y_stats_hint', + defaultMessage: 'View post stats', + })} /> diff --git a/src/config/locales/en-US.json b/src/config/locales/en-US.json index 7da72bcffc..843d9342d4 100644 --- a/src/config/locales/en-US.json +++ b/src/config/locales/en-US.json @@ -868,6 +868,22 @@ "downvoted": "Downvoted", "upvote_hint": "Double tap to open vote options", "payout_details": "Payout details", + "a11y_votes": "{count} votes", + "a11y_voters_hint": "View voters", + "a11y_reblogs": "{count} reblogs", + "a11y_reblogs_hint": "View reblogs", + "a11y_comments": "{count} comments", + "a11y_comments_hint": "View comments", + "a11y_reply_hint": "Reply", + "a11y_tip": "Send tip", + "a11y_tips": "{count} tips", + "a11y_edit": "Edit comment", + "a11y_delete": "Delete comment", + "a11y_show_replies": "Show replies", + "a11y_hide_replies": "Hide replies", + "a11y_post_options": "Post options", + "a11y_views": "{count} views", + "a11y_stats_hint": "View post stats", "image": "Image", "reveal_muted": "MUTED\nTap to reveal content", "in": "in", From 9751612bc50a790023488eed386d70722b519632 Mon Sep 17 00:00:00 2001 From: feruzm Date: Tue, 23 Jun 2026 06:37:50 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix(a11y):=20address=20review=20=E2=80=94?= =?UTF-8?q?=20plural=20counts,=20gate=20a11y=20state,=20untrack=20node=5Fm?= =?UTF-8?q?odules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove the accidentally-committed node_modules symlink (it broke CI's yarn install --frozen-lockfile; node_modules stays gitignored/untracked). - Use ICU plural for count labels (votes/reblogs/comments/tips/views) so a count of 1 reads "1 vote" not "1 votes". en-US strings + inline defaultMessage fallbacks both updated. - TextWithIcon: set accessibilityState only on interactive instances so decorative/display-only ones don't announce as "dimmed". - IconButton: report disabled while isLoading, since onPress is suppressed then. - commentView votes: gate isClickable + the "View voters" hint on _totalVotes > 0 so a zero-vote control isn't announced as an actionable button. --- node_modules | 1 - .../view/textWithIcon/textWithIconView.tsx | 4 ++- src/components/comment/view/commentView.tsx | 24 ++++++++++----- .../iconButton/view/iconButtonView.tsx | 4 ++- .../children/postCardActionsPanel.tsx | 15 ++++++++-- .../postView/view/postDisplayView.tsx | 30 +++++++++++++++---- src/config/locales/en-US.json | 10 +++---- 7 files changed, 64 insertions(+), 24 deletions(-) delete mode 120000 node_modules diff --git a/node_modules b/node_modules deleted file mode 120000 index 323b2a6355..0000000000 --- a/node_modules +++ /dev/null @@ -1 +0,0 @@ -/root/ecency-mobile/node_modules \ No newline at end of file diff --git a/src/components/basicUIElements/view/textWithIcon/textWithIconView.tsx b/src/components/basicUIElements/view/textWithIcon/textWithIconView.tsx index 9f3a1f90cf..a7c1e752fb 100644 --- a/src/components/basicUIElements/view/textWithIcon/textWithIconView.tsx +++ b/src/components/basicUIElements/view/textWithIcon/textWithIconView.tsx @@ -39,7 +39,9 @@ const TextWithIcon = ({ accessibilityRole={_interactive ? 'button' : undefined} accessibilityLabel={accessibilityLabel} accessibilityHint={accessibilityHint} - accessibilityState={{ disabled: !_interactive }} + // Only advertise state for actual buttons; decorative/display-only instances + // must not announce as "dimmed" (disabled) on VoiceOver/TalkBack. + accessibilityState={_interactive ? { disabled: false } : undefined} > {isLoading ? ( diff --git a/src/components/comment/view/commentView.tsx b/src/components/comment/view/commentView.tsx index 45a7be25a6..136e1dc166 100644 --- a/src/components/comment/view/commentView.tsx +++ b/src/components/comment/view/commentView.tsx @@ -209,20 +209,27 @@ const CommentView = ({ iconSize={20} wrapperStyle={styles.leftButton} iconType="MaterialCommunityIcons" - isClickable + isClickable={_totalVotes > 0} onPress={() => handleOnVotersPress && _totalVotes > 0 && handleOnVotersPress(activeVotes, comment) } text={_totalVotes} textStyle={styles.voteCountText} accessibilityLabel={intl.formatMessage( - { id: 'post.a11y_votes', defaultMessage: '{count} votes' }, + { + id: 'post.a11y_votes', + defaultMessage: '{count, plural, one {# vote} other {# votes}}', + }, { count: _totalVotes || 0 }, )} - accessibilityHint={intl.formatMessage({ - id: 'post.a11y_voters_hint', - defaultMessage: 'View voters', - })} + accessibilityHint={ + _totalVotes > 0 + ? intl.formatMessage({ + id: 'post.a11y_voters_hint', + defaultMessage: 'View voters', + }) + : undefined + } /> {!isLoading ? ( handleCardInteraction(PostCardActionIds.REPLY)} accessibilityLabel={intl.formatMessage( - { id: 'post.a11y_comments', defaultMessage: '{count} comments' }, + { + id: 'post.a11y_comments', + defaultMessage: '{count, plural, one {# comment} other {# comments}}', + }, { count: get(content, 'children', 0) }, )} accessibilityHint={intl.formatMessage({ diff --git a/src/components/postView/view/postDisplayView.tsx b/src/components/postView/view/postDisplayView.tsx index 202ddc273e..b7245e4e19 100644 --- a/src/components/postView/view/postDisplayView.tsx +++ b/src/components/postView/view/postDisplayView.tsx @@ -244,7 +244,10 @@ const PostDisplayView = ({ text={activeVotesCount} textMarginLeft={20} accessibilityLabel={intl.formatMessage( - { id: 'post.a11y_votes', defaultMessage: '{count} votes' }, + { + id: 'post.a11y_votes', + defaultMessage: '{count, plural, one {# vote} other {# votes}}', + }, { count: activeVotesCount || 0 }, )} accessibilityHint={intl.formatMessage({ @@ -261,7 +264,10 @@ const PostDisplayView = ({ text={post?.reblogs ?? 0} textMarginLeft={20} accessibilityLabel={intl.formatMessage( - { id: 'post.a11y_reblogs', defaultMessage: '{count} reblogs' }, + { + id: 'post.a11y_reblogs', + defaultMessage: '{count, plural, one {# reblog} other {# reblogs}}', + }, { count: post?.reblogs ?? 0 }, )} accessibilityHint={intl.formatMessage({ @@ -281,7 +287,10 @@ const PostDisplayView = ({ onPress={_scrollToComments} isLoading={!isLoadedComments} accessibilityLabel={intl.formatMessage( - { id: 'post.a11y_comments', defaultMessage: '{count} comments' }, + { + id: 'post.a11y_comments', + defaultMessage: '{count, plural, one {# comment} other {# comments}}', + }, { count: get(post, 'children', 0) }, )} accessibilityHint={intl.formatMessage({ @@ -299,7 +308,10 @@ const PostDisplayView = ({ text={get(post, 'children', 0)} textMarginLeft={20} accessibilityLabel={intl.formatMessage( - { id: 'post.a11y_comments', defaultMessage: '{count} comments' }, + { + id: 'post.a11y_comments', + defaultMessage: '{count, plural, one {# comment} other {# comments}}', + }, { count: get(post, 'children', 0) }, )} /> @@ -315,7 +327,10 @@ const PostDisplayView = ({ textMarginLeft={20} isLoading={tipsQuery.isLoading} accessibilityLabel={intl.formatMessage( - { id: 'post.a11y_tips', defaultMessage: '{count} tips' }, + { + id: 'post.a11y_tips', + defaultMessage: '{count, plural, one {# tip} other {# tips}}', + }, { count: tipsQuery.data?.meta?.count || 0 }, )} accessibilityHint={intl.formatMessage({ @@ -432,7 +447,10 @@ const PostDisplayView = ({ textMarginLeft={4} isLoading={postStatsQuery.isLoading} accessibilityLabel={intl.formatMessage( - { id: 'post.a11y_views', defaultMessage: '{count} views' }, + { + id: 'post.a11y_views', + defaultMessage: '{count, plural, one {# view} other {# views}}', + }, { count: postStatsQuery.data?.visits || 0 }, )} accessibilityHint={intl.formatMessage({ diff --git a/src/config/locales/en-US.json b/src/config/locales/en-US.json index 843d9342d4..5e709b78aa 100644 --- a/src/config/locales/en-US.json +++ b/src/config/locales/en-US.json @@ -868,21 +868,21 @@ "downvoted": "Downvoted", "upvote_hint": "Double tap to open vote options", "payout_details": "Payout details", - "a11y_votes": "{count} votes", + "a11y_votes": "{count, plural, one {# vote} other {# votes}}", "a11y_voters_hint": "View voters", - "a11y_reblogs": "{count} reblogs", + "a11y_reblogs": "{count, plural, one {# reblog} other {# reblogs}}", "a11y_reblogs_hint": "View reblogs", - "a11y_comments": "{count} comments", + "a11y_comments": "{count, plural, one {# comment} other {# comments}}", "a11y_comments_hint": "View comments", "a11y_reply_hint": "Reply", "a11y_tip": "Send tip", - "a11y_tips": "{count} tips", + "a11y_tips": "{count, plural, one {# tip} other {# tips}}", "a11y_edit": "Edit comment", "a11y_delete": "Delete comment", "a11y_show_replies": "Show replies", "a11y_hide_replies": "Hide replies", "a11y_post_options": "Post options", - "a11y_views": "{count} views", + "a11y_views": "{count, plural, one {# view} other {# views}}", "a11y_stats_hint": "View post stats", "image": "Image", "reveal_muted": "MUTED\nTap to reveal content",