Skip to content

compose: Focus the message composer on entry for screen readers#6511

Draft
andremion wants to merge 2 commits into
developfrom
andrerego/and-1248-focus-composer-on-entry
Draft

compose: Focus the message composer on entry for screen readers#6511
andremion wants to merge 2 commits into
developfrom
andrerego/and-1248-focus-composer-on-entry

Conversation

@andremion

@andremion andremion commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

AND-1248

Goal

When a screen reader user opens a message view, the reading cursor should land on the message composer input, so they can start typing right away (WCAG 2.4.3 Focus Order, 1.3.1). Today the cursor lands somewhere in the middle of the message list, and where it lands varies with the content.

The root cause is that the message list uses reverseLayout = true, which scrambles the accessibility traversal order (Compose bug b/232293357). So traversal-order solutions (traversalIndex / isTraversalGroup) cannot reliably hoist the composer to the top, and Compose exposes no public API to set initial accessibility focus.

Implementation

ComposerScreenReaderEntryFocus in ChannelScreen moves the reading cursor onto the composer input when a message view opens. It only runs when an explore-by-touch service (TalkBack) is active, and it moves the reading cursor only, not input focus, so the keyboard stays closed and the user double-taps to type.

  • It dispatches ACTION_ACCESSIBILITY_FOCUS to the node tagged as the composer input. There is no public Compose API for this, so the node id is resolved reflectively via AndroidComposeView.getSemanticsOwner(). The lookup fails safe: if anything cannot be resolved, focus is simply left untouched. A consumer-rules.pro keep-rule preserves that method in R8-minified release builds.
  • The message list loads asynchronously and the screen reader re-asserts its own initial focus when content appears. To not lose that race, the cursor is re-applied while the list settles (keyed on the item count) within a short bounded window, after which it stops so later incoming messages do not pull focus back.
  • A thread is also a message view. The logic re-arms when the view switches between the channel and a thread (keyed on parentMessageId), so opening a thread in place focuses the composer too.

Shared accessibility helpers live in a new AccessibilityUtils.kt. The existing private touch-exploration helper in GiphyMessageContent was consolidated into it.

Testing

Requires TalkBack on a device or emulator (with the soft keyboard enabled so it behaves like a physical device).

  1. Turn TalkBack on.
  2. From the channel list, open a channel. The reading cursor should land on the message composer input, and the keyboard should stay closed.
  3. Double-tap. The keyboard should open and the input should be ready to type.
  4. Open a thread in place from a message's reply footer. The cursor should land on the composer input again.
  5. Open a channel from the Threads tab. The cursor should land on the composer input.
  6. Send or receive a few messages after the screen settles. The cursor should not jump back to the composer.
  7. Turn TalkBack off and confirm normal use is unchanged (the composer is not auto-focused and no keyboard opens on entry).

Automated coverage: AccessibilityUtilsTest (touch-exploration state, semantics node lookup, fail-safe behavior) and the existing MessageComposerScreenTest / ChannelScreenTest.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved screen reader (TalkBack) focus management: message composer input now receives proper accessibility focus when messages open, enhancing the experience for screen reader users.
  • Refactor

    • Consolidated accessibility utilities into a shared module for improved code reusability and consistency.
    • Updated test infrastructure with standardized composer input test tagging.

@andremion andremion added the pr:bug Bug fix label Jun 22, 2026
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

PR checklist ✅

All required conditions are satisfied:

  • Title length is OK (or ignored by label).
  • At least one pr: label exists.
  • Sections ### Goal, ### Implementation, and ### Testing are filled, or the PR is bot-authored.
  • An issue is linked (Linear ticket or GitHub issue), or the PR is bot-authored.

🎉 Great job! This PR is ready for review.

@andremion

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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 commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Introduces shared Compose accessibility utilities (rememberIsTouchExplorationEnabled, requestAccessibilityFocusForTestTag, semanticsNodeIdForTestTag) in a new AccessibilityUtils.kt, a ComposerInputTestTag constant, a ComposerScreenReaderEntryFocus composable in ChannelScreen that re-applies TalkBack focus to the composer input on channel/thread entry, a ProGuard keep rule for getSemanticsOwner(), and corresponding tests.

Changes

TalkBack Focus Management for Message Composer Entry

Layer / File(s) Summary
Shared accessibility utilities, test-tag constant, and ProGuard rule
...ui/util/AccessibilityUtils.kt, ...composer/ComposerTestTags.kt, consumer-rules.pro
AccessibilityUtils.kt adds rememberIsTouchExplorationEnabled, requestAccessibilityFocusForTestTag, semanticsNodeIdForTestTag, and a private recursive findByTestTag. ComposerTestTags.kt declares the ComposerInputTestTag internal constant. consumer-rules.pro adds a -keepclassmembers rule to retain getSemanticsOwner() from AndroidComposeView in R8-minified builds.
Adopt shared utilities in GiphyMessageContent and MessageComposerInputCenterContent
...messages/GiphyMessageContent.kt, ...composer/internal/MessageComposerInputCenterContent.kt
GiphyMessageContent removes its file-local rememberIsTouchExplorationEnabled composable and imports the shared one. MessageComposerInputCenterContent replaces the hardcoded "Stream_ComposerInputField" string with ComposerInputTestTag on the BasicTextField modifier.
ComposerScreenReaderEntryFocus composable in ChannelScreen
...messages/ChannelScreen.kt
Adds a private ComposerScreenReaderEntryFocus composable that, when touch exploration is active, defers an accessibility focus request to the composer input via withFrameNanos, keys re-application on viewKey and message item count, gates repetition with a rememberSaveable entryFocusSettled flag, and stops after a 2-second window. Invoked from ChannelScreen before the main content box.
Tests
...ui/util/AccessibilityUtilsTest.kt, ...composer/MessageComposerScreenTest.kt
AccessibilityUtilsTest adds five Robolectric/Compose tests covering touch-exploration state reflection, semantics node id lookup, and safe-failure of focus dispatch on non-Compose views. MessageComposerScreenTest updates two assertions to use ComposerInputTestTag instead of the hardcoded string.

Sequence Diagram(s)

sequenceDiagram
  participant ChannelScreen
  participant ComposerScreenReaderEntryFocus
  participant rememberIsTouchExplorationEnabled
  participant requestAccessibilityFocusForTestTag
  participant semanticsNodeIdForTestTag
  participant AndroidComposeView

  ChannelScreen->>ComposerScreenReaderEntryFocus: composition (listViewModel)
  ComposerScreenReaderEntryFocus->>rememberIsTouchExplorationEnabled: observe TalkBack state
  rememberIsTouchExplorationEnabled-->>ComposerScreenReaderEntryFocus: isTouchExplorationEnabled=true
  Note over ComposerScreenReaderEntryFocus: LaunchedEffect(viewKey, itemCount)
  ComposerScreenReaderEntryFocus->>ComposerScreenReaderEntryFocus: withFrameNanos (defer 1 frame)
  ComposerScreenReaderEntryFocus->>requestAccessibilityFocusForTestTag: ComposerInputTestTag
  requestAccessibilityFocusForTestTag->>semanticsNodeIdForTestTag: ComposerInputTestTag
  semanticsNodeIdForTestTag->>AndroidComposeView: reflect getSemanticsOwner()
  AndroidComposeView-->>semanticsNodeIdForTestTag: SemanticsOwner
  semanticsNodeIdForTestTag-->>requestAccessibilityFocusForTestTag: nodeId
  requestAccessibilityFocusForTestTag->>AndroidComposeView: ACTION_ACCESSIBILITY_FOCUS(nodeId)
  Note over ComposerScreenReaderEntryFocus: LaunchedEffect(viewKey) → delay(2000ms) → entryFocusSettled=true
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • GetStream/stream-chat-android#6483: Modifies GiphyMessageContent.kt to adjust TalkBack focus/announcement behavior, directly overlapping with this PR's removal of the file-local touch-exploration composable.
  • GetStream/stream-chat-android#6498: Also touches GiphyMessageContent.kt to change the accessibility semantics structure of the Giphy preview, which is the same component refactored here.

Suggested labels

pr:improvement, released

Suggested reviewers

  • gpunto

🐇 Hop, hop! The focus is set,
On composer it lands — no TalkBack regret!
getSemanticsOwner won't disappear,
Reflected in builds, both far and near.
Screen readers rejoice, the composer's in sight,
A bunny tapped testTag — everything's right! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: enabling screen reader users to have focus land on the message composer when opening a message view.
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.
Description check ✅ Passed The PR description is thorough and well-structured, covering the goal, implementation details, testing instructions, and technical decisions comprehensively.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch andrerego/and-1248-focus-composer-on-entry

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.

@andremion andremion added pr:improvement Improvement and removed pr:bug Bug fix labels Jun 22, 2026
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.90 MB 5.90 MB 0.00 MB 🟢
stream-chat-android-ui-components 11.15 MB 11.15 MB 0.00 MB 🟢
stream-chat-android-compose 12.61 MB 12.61 MB 0.00 MB 🟢

When a screen reader is active, move the reading cursor to the message
composer input as a message view opens, without moving input focus, so
the keyboard stays closed and the user double-taps to type. This uses
ACTION_ACCESSIBILITY_FOCUS instead of FocusRequester (which would open
the keyboard), and re-applies focus while the message list loads so the
screen reader's own initial placement does not override it. Gated on
touch exploration, so sighted users are unaffected.

Compose exposes no public API to move accessibility focus, so the node
is resolved reflectively via AndroidComposeView.getSemanticsOwner; a
consumer ProGuard rule keeps that method for minified release builds,
and the lookup fails safe otherwise.

Share the composer input test tag as a single constant and reuse the
touch-exploration helper across the composer and the Giphy preview.
@andremion andremion force-pushed the andrerego/and-1248-focus-composer-on-entry branch from eea0942 to dfaa6c6 Compare June 22, 2026 15:24
@sonarqubecloud

Copy link
Copy Markdown

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

Labels

pr:improvement Improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant