Skip to content

feat(a11y): Message actions announcement and back focus - #7229

Merged
OtavioStasiak merged 45 commits into
developfrom
feat.a11y-action-sheet-focus-and-hint
May 21, 2026
Merged

feat(a11y): Message actions announcement and back focus#7229
OtavioStasiak merged 45 commits into
developfrom
feat.a11y-action-sheet-focus-and-hint

Conversation

@OtavioStasiak

@OtavioStasiak OtavioStasiak commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Proposed changes

Improve the actionSheet behavior on a11y.

  • Action Sheet handle is accessible;
  • open message actions is available on a11y actions;
  • When we close MessageActions the focus went back to the message, and not from top of the View;

Issue(s)

https://rocketchat.atlassian.net/browse/MA-263

How to test or reproduce

  • Open the app;
  • Go to Room;
  • Turn Screen reader on;
  • Focus on a message;
  • Open ActionSheet;
  • Navigate through the action sheet;
  • Close ActionSheet focusing on handle;

Screenshots

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • Improvement (non-breaking change which improves a current function)
  • New feature (non-breaking change which adds functionality)
  • Documentation update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc
  • I have signed the CLA
  • Lint and unit tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works (if applicable)
  • I have added necessary documentation (if applicable)
  • Any dependent changes have been merged and published in downstream modules

Further comments

Summary by CodeRabbit

  • Accessibility Improvements

    • Improved focus handling for action sheets and message actions (restores screen‑reader focus on open/close; Android focus deferred briefly for TalkBack).
    • Localized labels and hints for dismissing action sheets and showing message actions across many languages.
  • New Features

    • Track-and-restore last-focused message for reliable focus return.
    • Enhanced message accessibility labels and an accessibility action to "Show message actions".
  • Tests

    • Added tests covering message accessibility labels and actions.

Review Change Stack

@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds accessibility focus tracking and restoration for message action sheets: shared last-focused message ref, message accessibility label/action utilities and tests, Touch/Handle ref forwarding, action-sheet header focus on present, and focus restoration on dismiss.

Changes

Accessibility Focus Restoration Workflow

Layer / File(s) Summary
Accessibility Ref Tracker
app/lib/a11y/useLastFocusedMessageRef.ts
New utility that maintains a module-scoped RefObject<View | null> with markAsLastFocused(), get(), and clear() for tracking the last focused message.
Message Accessibility Helpers & Tests
app/lib/a11y/useMessageAccessibilityLabel.ts, app/lib/a11y/useMessageAccessibilityActions.ts, app/lib/a11y/*test.ts
Adds buildMessageAccessibilityLabel / useMessageAccessibilityLabel and useMessageAccessibilityActions, plus comprehensive tests covering label composition, translation/read-receipt/thread hints, and action availability.
Message & Touch integration
app/containers/message/Message.tsx, app/containers/message/Touch.tsx
Integrates accessibility label/actions into MessageTouchable, tracks last-focused message on long-press, forwards componentRef through Touch, and wires onAccessibilityAction to trigger message actions.
ActionSheet Handle ForwardRef
app/containers/ActionSheet/Handle.tsx
Converts Handle to forwardRef, forwards the RectButton ref, and sources accessibility label/hint from i18n keys A11y_close_action_sheet / A11y_close_action_sheet_hint.
ActionSheet Focus on Present
app/containers/ActionSheet/ActionSheet.tsx
Adds handleRef, focusHandle() (uses findNodeHandle + AccessibilityInfo.setAccessibilityFocus) and onDidPresent wired into TrueSheet; on Android the focus is deferred ~300ms.
MessageActions Focus Restoration
app/containers/MessageActions/index.tsx
Captures last-focused message ref via useLastFocusedMessageRef, provides onClose to restore accessibility focus to the saved native node after dismiss animation, clears the stored ref, and passes onClose to showActionSheet.
Localization entries
app/i18n/locales/*
Adds keys: A11y_close_action_sheet, A11y_close_action_sheet_hint, and Show_message_actions (English) and A11y_close_action_sheet across many locale files.

Sequence Diagram

sequenceDiagram
  participant User
  participant MessageTouchable
  participant lastFocusedRef as lastFocusedMessageRef
  participant MessageActions
  participant ActionSheet
  participant Handle
  participant AccessibilityInfo

  User->>MessageTouchable: Long press / accessibility action
  MessageTouchable->>lastFocusedRef: markAsLastFocused()
  MessageTouchable->>MessageActions: open message actions
  MessageActions->>ActionSheet: showActionSheet(onClose)
  ActionSheet->>ActionSheet: onDidPresent
  ActionSheet->>Handle: focusHandle -> findNodeHandle(handleRef)
  Handle->>AccessibilityInfo: setAccessibilityFocus(header) (defer 300ms on Android)

  User->>ActionSheet: Dismiss
  MessageActions->>lastFocusedRef: get() -> messageRef
  MessageActions->>AccessibilityInfo: setAccessibilityFocus(messageRef) (after animation)
  MessageActions->>lastFocusedRef: clear()
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • diegolmello
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly matches the main feature: improving accessibility for message actions by implementing announcements and refocusing after the action sheet closes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • MA-263: Request failed with status code 401

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@OtavioStasiak OtavioStasiak changed the title feat: announcement and focus feat(a11y): Message actions announcement and back focus Apr 23, 2026
@OtavioStasiak
OtavioStasiak temporarily deployed to experimental_ios_build April 23, 2026 16:42 — with GitHub Actions Inactive
@OtavioStasiak
OtavioStasiak temporarily deployed to experimental_android_build April 23, 2026 16:42 — with GitHub Actions Inactive
@OtavioStasiak
OtavioStasiak had a problem deploying to upload_experimental_android April 23, 2026 17:18 — with GitHub Actions Error
@github-actions

Copy link
Copy Markdown

Android Build Available

Rocket.Chat Experimental 4.72.0.108617

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNR-q4kPySm10tUnMpVhAS_0xW-fB4K8AaSET3MuDoXjn9hXgrmFsio_VEihbYHT191fB6aC8vRNjhGD5Ibp

@github-actions

Copy link
Copy Markdown

iOS Build Available

Rocket.Chat Experimental 4.72.0.108618

@OtavioStasiak
OtavioStasiak had a problem deploying to official_android_build May 18, 2026 14:29 — with GitHub Actions Error
@OtavioStasiak
OtavioStasiak temporarily deployed to approve_e2e_testing May 18, 2026 16:02 — with GitHub Actions Inactive
@OtavioStasiak
OtavioStasiak had a problem deploying to experimental_ios_build May 18, 2026 16:05 — with GitHub Actions Error
@OtavioStasiak
OtavioStasiak had a problem deploying to official_android_build May 18, 2026 16:05 — with GitHub Actions Error
@OtavioStasiak
OtavioStasiak had a problem deploying to experimental_android_build May 18, 2026 16:05 — with GitHub Actions Error
@OtavioStasiak

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
.maestro/tests/e2ee/flows/verify-message-unread.yaml (1)

16-31: ⚡ Quick win

Consider unifying unread verification to use ID-based selector on both platforms.

Android uses the read-receipt-unread testID while iOS uses a text pattern match (.*Message was not read.*). The text pattern is fragile since "Message was not read" is sourced from i18n translations (Message_was_not_read in en.json) and will break if the string is translated or modified. Since the read-receipt-unread testID is defined in the ReadReceipt component and available for both platforms, verify whether iOS can use it instead of the text pattern for consistency and resilience.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.maestro/tests/e2ee/flows/verify-message-unread.yaml around lines 16 - 31,
Unify the unread verification to use the ID-based selector on both platforms by
replacing the iOS text regex check with the same id selector used for Android:
update the iOS runFlow block that calls extendedWaitUntil so it looks for id:
'read-receipt-unread' (the testID defined in the ReadReceipt component) with the
same timeout; ensure the runFlow entries for platform: iOS and platform: Android
both use extendedWaitUntil -> visible -> id: 'read-receipt-unread' to make the
check resilient to i18n changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.maestro/tests/e2ee/flows/verify-message-unread.yaml:
- Around line 28-31: The YAML uses a fragile text-regex matcher in the
extendedWaitUntil step (visible: text: '.*Message was not read.*'); replace this
with an ID-based selector or accessibility identifier for the unread receipt
element (e.g., use the iOS accessibilityIdentifier/testID exposed for the unread
receipt) so the step targets the element by stable id rather than localized
text; update the extendedWaitUntil visible selector to use the id/accessibility
selector form your test framework supports (or an iOS predicate using
accessibilityIdentifier) and fall back to a less fragile locator only if the id
is unavailable.

---

Nitpick comments:
In @.maestro/tests/e2ee/flows/verify-message-unread.yaml:
- Around line 16-31: Unify the unread verification to use the ID-based selector
on both platforms by replacing the iOS text regex check with the same id
selector used for Android: update the iOS runFlow block that calls
extendedWaitUntil so it looks for id: 'read-receipt-unread' (the testID defined
in the ReadReceipt component) with the same timeout; ensure the runFlow entries
for platform: iOS and platform: Android both use extendedWaitUntil -> visible ->
id: 'read-receipt-unread' to make the check resilient to i18n changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 77aa5ef0-32ce-4187-856e-949df63142f3

📥 Commits

Reviewing files that changed from the base of the PR and between b97c8b2 and 3ce211b.

⛔ Files ignored due to path filters (1)
  • app/containers/message/__snapshots__/Message.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (2)
  • .maestro/tests/e2ee/flows/verify-message-unread.yaml
  • .maestro/tests/room/threads.yaml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .maestro/tests/room/threads.yaml
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: E2E Run iOS (12) / ios-test
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-03-05T14:28:10.004Z
Learnt from: Rohit3523
Repo: RocketChat/Rocket.Chat.ReactNative PR: 6997
File: .maestro/tests/room/message-markdown-click.yaml:28-39
Timestamp: 2026-03-05T14:28:10.004Z
Learning: In Maestro YAML selector fields (text, id) within the Rocket.Chat React Native repository, use the contains pattern '.*keyword.*' (leading and trailing '.*') for matching text. The pattern '.*keyword*.' is incorrect and will fail to match cases where the keyword appears at the end of the element's text. This guideline applies to all Maestro YAML selector fields across the codebase.

Applied to files:

  • .maestro/tests/e2ee/flows/verify-message-unread.yaml
🔇 Additional comments (1)
.maestro/tests/e2ee/flows/verify-message-unread.yaml (1)

16-31: Clarify the connection to PR objectives.

This change updates the unread message verification flow with platform-specific checks. However, the PR objectives focus on action sheet accessibility (handle accessibility, focus restoration).

Could you clarify how the action sheet accessibility changes necessitated this update to the unread receipt verification? Understanding the connection will help verify this change is complete and correct.

Comment thread .maestro/tests/e2ee/flows/verify-message-unread.yaml
@OtavioStasiak

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@diegolmello diegolmello left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few items from review that I'd like resolved before merge:

🔴 Required: i18n keys only translated to English

en.json adds four new keys but every one of the 24 non-English locales adds only two:

Key en other locales
A11y_close_action_sheet
A11y_press_to_view_thread
A11y_close_action_sheet_hint
Show_message_actions

The PR body claims "Localized labels and hints for dismissing action sheets and showing message actions across many languages" — but a Spanish/German/Japanese screen-reader user will currently hear "Dismisses the action sheet" and "Show message actions" in English (i18n-js falls back to the default locale). Either translate the missing two keys across all 24 locales, or trim the claim from the PR description.

🟡 Important: Dead IMessageA11y plumbing + duplicate useMessageAccessibilityLabel call

In app/containers/message/Message.tsx:

  • IMessageA11y (lines 107-111) declares accessibilityActions, onAccessibilityAction, and handleLongPress, but none of the three are read anywhere inside Message. MessageTouchable passes handleLongPress={!isDisabled ? handleLongPress : undefined} at line 223; the child silently discards it. Misleading for future readers, and it also defeats React.memo (fresh closure every render).
  • useMessageAccessibilityLabel(props) is called in both Message (line 112) and MessageTouchable (line 179). The work involves i18n.t(...), toLocaleTimeString(), and mention/channel string substitution — running twice per message render. Only the MessageTouchable instance feeds the outer Touch; Message only uses its copy in the early-return path for thread reply/sequential/info/ignored.

Suggest dropping the unused interface fields and computing the label once in the parent.

🟡 Important: Verify duplicate a11y props on Touch outer + inner

Touch.tsx:85-110 now forwards accessible, accessibilityRole, accessibilityLabel, accessibilityHint, accessibilityActions, and onAccessibilityAction to both the outer KeyboardComponent and the inner View. The LoadMore.test.tsx.snap diff confirms the duplication. Two sibling accessible nodes with identical labels typically result in double announcements on iOS VoiceOver and can confuse swipe navigation on TalkBack.

Could you confirm this was tested end-to-end with VoiceOver and TalkBack on a long-pressable message and share what's announced? If the inner View no longer needs accessible, dropping it would also explain (and possibly let you revert) the Maestro iOS testID workaround in verify-message-unread.yaml — that change looks like a side effect of the outer Touch absorbing nested testIDs on iOS.

Happy to pair on the dual-label investigation if useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants