Skip to content

Improve Giphy preview TalkBack accessibility#6483

Open
andremion wants to merge 5 commits into
developfrom
fix/compose-giphy-preview-a11y
Open

Improve Giphy preview TalkBack accessibility#6483
andremion wants to merge 5 commits into
developfrom
fix/compose-giphy-preview-a11y

Conversation

@andremion

@andremion andremion commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

AND-1180

Goal

The ephemeral Giphy preview (rendered after a /giphy command) had three accessibility gaps:

  1. No signal that the preview appeared. TalkBack focus stayed on the composer's voice-record button; users had to swipe blindly into the message list to find the bubble.
  2. Bubble announced only "Giphy". No alt text, no "Only visible to you" context, no preview-state framing — the Giphy image had contentDescription = null, the inner combinedClickable created a nested accessibility merge boundary, and the visible-to-you row was a sibling focus.
  3. No confirmation after Send / Shuffle / Cancel. TalkBack focus drifted unpredictably after the bubble unmounted, with no signal that the action took effect.

Implementation

Three feature-scoped commits.

1. Name the Giphy image for TalkBack.

Set contentDescription on the inner StreamAsyncImage in GiphyAttachmentContent to attachment.title?.takeIf(String::isNotBlank). When the attachment has a non-blank title (typically the search query supplied by the Giphy API), TalkBack reads it together with the existing "Giphy" badge — e.g. "Hello, giphy". Improves both the ephemeral preview path and sent Giphy messages.

2. Focus the preview bubble on appearance.

Wrap the visible-to-you row + Giphy image in GiphyMessageContent in a focusable Column with Modifier.semantics(mergeDescendants = true) {}, and requestFocus() on it 100 ms after the message id is composed. Mirrors the precedent in MessageComposerAudioRecordingLockedContent.

Adds interactive: Boolean = true to GiphyAttachmentContent and GiphyAttachmentContentParams. The ephemeral preview passes false, which skips the inner combinedClickable — its handlers are no-ops in the ephemeral case, and the inner clickable was a nested accessibility merge boundary that prevented the image alt and "Giphy" badge from merging into the bubble's focus.

TalkBack now announces the bubble as one focus, e.g. "Only visible to you, Hello, Giphy".

Note

The interactive workaround. The cleaner design is to make onItemClick (and AttachmentState.onLongItemClick) nullable and gate combinedClickable on handler != null, which removes the need for a separate flag. That's a binary-breaking type change on a published API, so this PR takes the additive route. An inline comment in GiphyAttachmentContent.kt marks the spot; v8 should refactor to nullable handlers and drop interactive.

3. Announce action state changes.

Wrap each Send / Shuffle / Cancel button's onClick to fire view.announceForAccessibility before forwarding to onGiphyActionClick:

  • SendGiphy → "Giphy sent"
  • ShuffleGiphy → "Giphy shuffled"
  • CancelGiphy → "Giphy preview cancelled"

The announce queues before the state change, so users hear the confirmation regardless of where TalkBack focus lands afterward (the focus drift itself is a TalkBack fallback-recovery race after the bubble unmounts, not deterministically fixable from this composable).

New strings translated across the 7 supported locales. No public API breaking changes; interactive is an additive defaulted param.

Testing

Enable TalkBack on a physical device. Run the Compose sample.

  1. Open a channel and type /giphy hello, then submit the command.
  2. The ephemeral preview bubble appears in the message list. Expected: TalkBack focus moves to the bubble within ~100 ms and announces "Only visible to you, Hello, Giphy" as a single focus.
  3. Swipe right to reach the Send button. Double-tap to send.
  4. Expected: TalkBack announces "Giphy sent". The sent Giphy appears in the message list; focus drifts to a nearby message (the drift itself is a TalkBack fallback timing behaviour and may vary).
  5. Repeat with /giphy welcome. On the preview, swipe to Shuffle and double-tap.
  6. Expected: TalkBack announces "Giphy shuffled". The bubble stays mounted with a new Giphy. Swipe back to the bubble; the alt text reflects the new content.
  7. Repeat with /giphy something. On the preview, swipe to Cancel and double-tap.
  8. Expected: TalkBack announces "Giphy preview cancelled". The bubble unmounts.
  9. Disable TalkBack and re-test all three actions to confirm the visible behaviour is unchanged.

Summary by CodeRabbit

  • New Features

    • Giphy attachments now support configurable interactivity control
    • Enhanced accessibility for Giphy message interactions with TalkBack announcements and improved focus handling
  • Localization

    • Added translations for Giphy action states (cancelled, sent, shuffled) across Spanish, French, Hindi, Indonesian, Italian, Japanese, and Korean

@andremion andremion requested a review from a team as a code owner June 2, 2026 09:03
@github-actions

github-actions Bot commented Jun 2, 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 andremion added the pr:improvement Improvement label Jun 2, 2026
@andremion

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
✅ 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.

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.86 MB 5.86 MB 0.00 MB 🟢
stream-chat-android-ui-components 11.09 MB 11.09 MB 0.00 MB 🟢
stream-chat-android-compose 12.50 MB 12.52 MB 0.02 MB 🟢

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR enhances Giphy attachments with an interactive control flag and improves accessibility for the message preview flow. The interactive parameter gates click/long-click handlers, while the preview area gains focus management and screen reader announcements for action outcomes across eight supported languages.

Changes

Giphy interactive parameter and accessibility

Layer / File(s) Summary
Interactive parameter contract and implementation
stream-chat-android-compose/api/stream-chat-android-compose.api, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/GiphyAttachmentContent.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt
GiphyAttachmentContent adds an interactive: Boolean = true parameter; when false, click and long-click handlers are not attached to the container. The parameter is threaded through GiphyAttachmentContentParams, documented in KDoc, and forwarded by ChatComponentFactory. Content description now uses attachment title when available instead of null.
GiphyMessageContent accessibility enhancements
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/GiphyMessageContent.kt
Focus management via FocusRequester and delayed LaunchedEffect directs TalkBack focus to the preview region. Action buttons (Send, Shuffle, Cancel) announce their outcomes to accessibility services. The preview region is rendered with interactive=false to prevent duplicate handlers. New constant PreviewFocusRequestDelayMs controls focus activation delay. Supporting imports for focus, semantics, and coroutines are added.
Localization strings for Giphy action states
stream-chat-android-compose/src/main/res/values/strings.xml, stream-chat-android-compose/src/main/res/values-es/strings.xml, stream-chat-android-compose/src/main/res/values-fr/strings.xml, stream-chat-android-compose/src/main/res/values-hi/strings.xml, stream-chat-android-compose/src/main/res/values-in/strings.xml, stream-chat-android-compose/src/main/res/values-it/strings.xml, stream-chat-android-compose/src/main/res/values-ja/strings.xml, stream-chat-android-compose/src/main/res/values-ko/strings.xml
New string resources for Giphy message-list action outcomes (cancelled, sent, shuffled) are added in English and translated across Spanish, French, Hindi, Indonesian, Italian, Japanese, and Korean locales.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Suggested labels

pr:improvement

Suggested reviewers

  • gpunto
  • VelikovPetar

Poem

🐰 Click and focus dance in the Giphy stream,
Interactive flags control the dream—
TalkBack hears the shuffle, send, and sway,
Across eight tongues, we guide the way! 🌍✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% 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 and concisely describes the main change: improving Giphy preview accessibility for TalkBack screen readers.
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 comprehensively addresses the template requirements: Goal is clearly stated (three accessibility gaps), Implementation details the three feature-scoped changes with rationale, Testing provides step-by-step TalkBack verification instructions. UI Changes and GIF sections are omitted but not critical for accessibility-focused backend changes.

✏️ 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 fix/compose-giphy-preview-a11y

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/GiphyMessageContent.kt`:
- Around line 84-100: The current LaunchedEffect(message.id) always delays and
calls previewFocusRequester.requestFocus() and the preview node is always
.focusable(), which steals focus even when TalkBack is off; modify the logic to
check accessibility touch exploration before applying focus behavior: obtain
AccessibilityManager and check isTouchExplorationEnabled (or equivalent), wrap
the .focusRequester(previewFocusRequester) and .focusable() modifiers behind
that condition on the preview composable, and only call
previewFocusRequester.requestFocus() within LaunchedEffect when
isTouchExplorationEnabled is true (still keep the existing delay using
PreviewFocusRequestDelayMs); ensure you reference previewFocusRequester,
LaunchedEffect(message.id), PreviewFocusRequestDelayMs, focusRequester,
focusable(), and requestFocus() when making the change.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0271b33b-153c-45a7-a0bf-89040710fd4a

📥 Commits

Reviewing files that changed from the base of the PR and between fa7a6d3 and 92e5abc.

📒 Files selected for processing (13)
  • stream-chat-android-compose/api/stream-chat-android-compose.api
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/GiphyAttachmentContent.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/GiphyMessageContent.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt
  • stream-chat-android-compose/src/main/res/values-es/strings.xml
  • stream-chat-android-compose/src/main/res/values-fr/strings.xml
  • stream-chat-android-compose/src/main/res/values-hi/strings.xml
  • stream-chat-android-compose/src/main/res/values-in/strings.xml
  • stream-chat-android-compose/src/main/res/values-it/strings.xml
  • stream-chat-android-compose/src/main/res/values-ja/strings.xml
  • stream-chat-android-compose/src/main/res/values-ko/strings.xml
  • stream-chat-android-compose/src/main/res/values/strings.xml

@andremion andremion marked this pull request as draft June 2, 2026 09:12
@andremion andremion force-pushed the fix/compose-giphy-preview-a11y branch 2 times, most recently from e696962 to c5f6111 Compare June 2, 2026 11:03
@andremion andremion marked this pull request as ready for review June 2, 2026 12:43
@andremion andremion enabled auto-merge (squash) June 2, 2026 12:43
@andremion andremion disabled auto-merge June 2, 2026 13:30
@andremion andremion force-pushed the fix/compose-giphy-preview-a11y branch from 365016a to 9f6169e Compare June 2, 2026 13:31
andremion added 4 commits June 8, 2026 11:11
The Giphy image had `contentDescription = null`, so TalkBack only
announced the "Giphy" overlay badge on focus. Set the image's
`contentDescription` to the attachment title (when non-blank), so
screen-reader users hear the search query together with the badge,
e.g. "Hello, giphy". When the title is blank or absent, falls back
to the existing "Giphy" announcement.

Improves both the ephemeral preview path and sent Giphy messages.
After a `/giphy` command, the ephemeral preview rendered in the
message list but TalkBack focus stayed on the composer. Screen-
reader users had no signal the preview arrived.

Wrap the visible-to-you row + the Giphy image in a focusable
`Column` with `Modifier.semantics(mergeDescendants = true) {}`,
and request focus on it 100 ms after the message id is first
composed. Mirrors the precedent in
`MessageComposerAudioRecordingLockedContent`.

Adds `interactive: Boolean = true` to `GiphyAttachmentContent`
and `GiphyAttachmentContentParams`. The ephemeral preview passes
`false`, which skips the inner `combinedClickable` — both its
handlers are no-ops in the ephemeral case, and the inner
clickable was a nested accessibility merge boundary that kept
the image alt and "Giphy" badge from merging into the bubble's
focus.

TalkBack now announces the bubble as one focus, e.g. "Only
visible to you, Hello, Giphy".
After tapping Send / Shuffle / Cancel on the ephemeral Giphy
preview, screen-reader users got no confirmation that their
action took effect — focus drifted depending on TalkBack's
fallback-recovery race after the bubble unmounted.

Wrap each action button's onClick to fire a polite live region
announce before forwarding to `onGiphyActionClick`:

- `SendGiphy` → "Giphy sent"
- `ShuffleGiphy` → "Giphy shuffled"
- `CancelGiphy` → "Giphy preview cancelled"

The announce queues before the state change, so users hear the
confirmation regardless of where TalkBack focus lands afterward.

New strings translated across the 7 supported locales.
The new `GiphyMessageContent`, the `interactive = false` branch in
`GiphyAttachmentContent`, and the `attachment.title.takeIf(String::isNotBlank)`
null / blank paths had no test exercising them.

Parameterize the existing `internal fun GiphyAttachmentContent()` preview
helper to accept `interactive` and `title` with backward-compatible defaults,
and add a sibling `internal fun GiphyMessageContent()` helper following the
same fixture-data pattern.

Add three Paparazzi snapshot tests in `AttachmentsContentTest`:

- `giphy attachment content non-interactive` covers the `applyIf(interactive)`
  false branch.
- `giphy attachment content blank title` covers the `takeIf(String::isNotBlank)`
  null path.
- `giphy message content` covers `GiphyMessageContent`'s static rendering plus
  the new factory forwarding in `ChatComponentFactory.GiphyAttachmentContent`
  and the new `interactive` field in `GiphyAttachmentContentParams`.

Runtime-only paths (`LaunchedEffect`, `requestFocus()`, accessibility listener
callbacks, button `onClick` lambdas) remain uncovered — they require UI tests
beyond snapshot scope.
@andremion andremion force-pushed the fix/compose-giphy-preview-a11y branch from 23d1e65 to 03801fc Compare June 8, 2026 10:13
@sonarqubecloud

sonarqubecloud Bot commented Jun 8, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
71.4% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

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