Skip to content

feat(ui): composer & design-system a11y pass#126

Merged
xsahil03x merged 37 commits into
mainfrom
feat/composer-a11y
Jul 6, 2026
Merged

feat(ui): composer & design-system a11y pass#126
xsahil03x merged 37 commits into
mainfrom
feat/composer-a11y

Conversation

@xsahil03x

@xsahil03x xsahil03x commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

Design-system-wide accessibility pass touching most reusable primitives in stream_core_flutter. Focus areas: screen-reader announcement, focus management, and semantic tree structure. No product/chat-domain code — all changes are generic.

New generic a11y primitives:

  • StreamAccessibilityAutofocus — behavior-only wrapper that requests SR entry focus on its child, with bounded retry, SR-toggle reactivity, and a per-route uniqueness debug assertion.
  • StreamSemanticsTransitionAnnouncer — observes a Listenable, computes a transition message, dispatches via StreamSemanticsAnnouncer.

Component updates (each in its own commit):

  • StreamButton — Semantics + Tooltip moved outside ElevatedButton, wrapped in MergeSemantics so selected/tooltip merge cleanly with the button's own node.
  • StreamStepper — announces as a single slider node with value/increasedValue/decreasedValue/onIncrease/onDecrease; internal +/- buttons and text field are excluded from SR.
  • StreamListTile — exposes focusNode / autofocus (mirrors Material ListTile).
  • StreamBadgeNotification, StreamFileTypeIcon — accept optional semanticLabel.
  • StreamSnackbar — new onVisible callback (fires after entrance, or synchronously when SR is active) for SR announcement hooks.
  • StreamAppBar, StreamSheetHeader — title/subtitle merged; wrapped in Semantics(header: true, namesRoute: true) by default with excludeHeaderSemantics opt-out; auto-implied leading icon gets MaterialLocalizations back/close tooltip.
  • StreamBottomAppBar — grouped in an outer Semantics(container: true); title/subtitle merge without heading/namesRoute (secondary toolbar).
  • StreamTextInput — auto-focus-on-a11y-focus behavior gated to desktop platforms; error text moved to its own live-region container.
  • Message composer attachment components — new semanticLabel + mergeRemoveAction props; content sub-trees excluded so the tile is one focus stop with a remove tap action.

Skill doc:

  • chore(claude): expand flutter-a11y skill with focus, dispatch, role-annotation guidance — adds guidance on role-annotation vs. container wrapping, FocusTraversalGroup being keyboard-only, and FocusSemanticEvent platform mapping / onDidGainAccessibilityFocus non-round-trip.

Test plan

  • melos run test:flutter from the core repo passes
  • melos run analyze passes
  • melos run format passes
  • Manual TalkBack sweep on Android: verify no regression in swipe-nav announcement across the button, stepper, app bar, sheet header, snackbar, and attachment surfaces
  • Manual VoiceOver sweep on iOS: same as above

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added accessibility-focused improvements across core components, including optional labels, tooltips, autofocus, focus handling, and better screen-reader announcements.
    • Added a semantics debugger toggle in the gallery to help inspect accessibility behavior.
    • Added support for more accessible sheets, snackbars, app bars, buttons, text inputs, steppers, list tiles, badges, and message attachments.
  • Bug Fixes

    • Improved screen-reader behavior for selected, disabled, and live-updating content.
    • Refined focus and traversal order so interactive elements are announced more consistently.

@xsahil03x
xsahil03x requested a review from a team as a code owner July 2, 2026 22:22
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@xsahil03x, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 40 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7dcb55e7-6c96-4cc9-899b-22b5455fcf5f

📥 Commits

Reviewing files that changed from the base of the PR and between 6251ab1 and cc738a9.

📒 Files selected for processing (3)
  • packages/stream_core/lib/src/ws/client/engine/web_socket_engine.dart
  • packages/stream_core_flutter/test/components/message_composer/attachment/file_attachment_semantics_test.dart
  • packages/stream_core_flutter/test/components/message_composer/attachment/media_attachment_semantics_test.dart
📝 Walkthrough

Walkthrough

This PR adds Flutter accessibility guidance, new shared a11y helpers, a gallery semantics debugger, and semantics/focus/announcement updates across core components and their tests.

Changes

Accessibility feature rollout

Layer / File(s) Summary
Accessibility skill documentation
.claude/skills/flutter-a11y/SKILL.md, packages/stream_core_flutter/CHANGELOG.md
Adds a Flutter accessibility workflow guide and a new 0.5.0 changelog entry listing the rollout items.
Core a11y widgets
packages/stream_core_flutter/lib/src/a11y/*, packages/stream_core_flutter/lib/core.dart, packages/stream_core_flutter/test/a11y/*
Adds autofocus and announcement helpers, exports them, and covers focus dispatch and announcement timing/dispatch behavior with tests.
Text input labeling and helper affinity
packages/stream_core_flutter/lib/src/theme/components/stream_text_input_theme*.dart, packages/stream_core_flutter/lib/src/components/common/stream_text_input.dart, apps/design_system_gallery/lib/components/common/stream_text_input.dart, packages/stream_core_flutter/test/components/common/stream_text_input_test.dart
Adds label and helper-placement props, rewires text-input semantics, updates gallery samples, and adds accessibility tests.
Button, file icon, and badge semantics
packages/stream_core_flutter/lib/src/components/buttons/stream_button.dart, packages/stream_core_flutter/lib/src/components/accessories/stream_file_type_icon.dart, packages/stream_core_flutter/lib/src/components/badge/stream_*.dart, packages/stream_core_flutter/test/components/buttons/*, packages/stream_core_flutter/test/components/badge/*
Adds autofocus, tooltip, and semantic-label support plus merged semantics behavior, with widget tests.
Stepper slider semantics
packages/stream_core_flutter/lib/src/components/controls/stream_stepper.dart, packages/stream_core_flutter/test/components/controls/stream_stepper_semantics_test.dart
Wraps the stepper in slider semantics with increase/decrease actions and adds semantics coverage.
Message composer attachment semantics
packages/stream_core_flutter/lib/src/components/message_composer/attachment/*.dart, packages/stream_core_flutter/test/components/message_composer/attachment/*
Threads semantic labels through attachment widgets, merges removal semantics, and adds semantics-tree tests.
Sheet and snackbar semantics
packages/stream_core_flutter/lib/src/components/sheet/stream_sheet.dart, packages/stream_core_flutter/lib/src/components/snackbar/stream_snackbar*.dart, packages/stream_core_flutter/test/components/sheet/*, packages/stream_core_flutter/test/components/snackbar/*
Adds route/barrier semantics and snackbar visibility/accessibility-navigation behavior with tests.
Toolbar semantics
packages/stream_core_flutter/lib/src/components/toolbar/stream_app_bar.dart, packages/stream_core_flutter/lib/src/components/toolbar/stream_bottom_app_bar.dart, packages/stream_core_flutter/lib/src/components/toolbar/stream_sheet_header.dart, packages/stream_core_flutter/test/components/toolbar/*
Adds header-semantics opt-outs, grouped semantics wrappers, localized tooltips, and focus-isolation tests.
Gallery semantics debugger tooling
apps/design_system_gallery/lib/widgets/scoped_semantics_debugger.dart, apps/design_system_gallery/lib/widgets/toolbar/semantics_debugger_toggle.dart, apps/design_system_gallery/lib/config/preview_configuration.dart, apps/design_system_gallery/lib/core/preview_wrapper.dart, apps/design_system_gallery/lib/widgets/toolbar/toolbar.dart
Adds a scoped semantics overlay and toolbar toggle for preview-time semantics inspection.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

Suggested reviewers: renefloor, VelikovPetar

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description omits required template sections, including Linear/GitHub fields, CLA checklist, screenshots/videos, and the prescribed PR format. Rewrite it using the repository template and add the missing metadata, CLA checkboxes, testing notes, and screenshots/videos section.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: a design-system accessibility pass across composer and UI components.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/composer-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.

xsahil03x and others added 23 commits July 3, 2026 00:34
A developer tool that overlays role-colored semantic node
boundaries on the previewed widget, scoped to the preview area
only. Toggle via the toolbar's debug section:

- Green for text fields, blue for buttons, purple for selected
- Cyan for other tappables, neutral gray for decorative
- Dashed outline highlights MergeSemantics roots so over-collapse
  bugs are visible at a glance

For actual SR announcements, real TalkBack / VoiceOver on a device
is still the ground truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Skip enter/exit animations when MediaQuery.accessibleNavigation is on,
matching ScaffoldMessenger's pattern. Prevents screen readers from
reading content mid-fade.

Fix onDismiss to call hideCurrent() so semantic dismissals resolve with
.dismiss instead of .remove — aligning with Flutter's SnackBar where a
semantic dismiss is a user-initiated close.

Migrate matchesSemantics → isSemantics in tests (deprecation) and add
coverage for the animation-skip behavior under FakeAccessibilityFeatures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match Flutter's TextField semantics exactly by wrapping the inner
EditableText with ExcludeSemantics and re-emitting the field semantics
on the outer chassis. The chassis Semantics now carries textField,
readOnly, enabled, focused, label (hint while empty), value, hint
(error helper), maxValueLength / currentValueLength, and gated
onTap / onFocus actions — keeping the screen reader on a single
focus stop that mirrors the visible bounds.

Leading / trailing slots get OrdinalSortKey so logical reading order
(leading → field → trailing) survives RTL. Decorative icons are wrapped
in ExcludeSemantics so they don't pollute the tree. StreamHelperText in
the error state becomes a live region so screen readers announce
validation failures the moment they appear.

Add focused-on-a11y test coverage: guideline checks, label/hint/value
contract, disabled vs read-only action gating, focus tracking, helper
state, maxLength, RTL traversal, and verification that interactive
trailing widgets stay as their own semantic node rather than merging
into the chassis.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire up tooltip on StreamButton.icon so the message renders as a
Material Tooltip on hover / long-press *and* surfaces to assistive
technologies as the button's accessibility label. Expose autofocus on
both constructors so consumers can drop focus on the primary action of
a freshly-opened dialog, sheet, or picker without plumbing FocusNodes.

Move the selection flag onto the button's own semantic node by
wrapping the inner child in Semantics(selected:) — props.isSelected
already drove the visual state via WidgetState.selected but didn't
reach screen readers, so filter chips / tab-strip buttons silently
failed to announce "selected".

Add coverage for the icon-button label / enabled / selected /
tap-target contract and verify that a regular StreamButton derives
its label from its child Text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Document the workflow used while landing the composer a11y pass:
how to manage Semantics in design-system components, how to audit
with Flutter's semantics testing APIs, and how to debug the
semantics tree when something's off.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add five recipes / pitfalls surfaced while landing the composer a11y
work:

- Pitfall: Semantics(container: false) merges *upward* into ancestor
  containers, not downward into descendant containers. Common mistake
  is to wrap ElevatedButton from the outside hoping a selected: flag
  lands on the button's node; the dartdoc spells out the upward
  direction, with the explicitChildNodes caveat.
- Recipe: "Re-emitting semantics on a custom chassis" — for chrome
  around an inner editable (TextField, Switch, Image), suppress the
  inner's auto-emitted node and re-emit a full semantic node on the
  outer chassis. Includes when to prefer ExcludeSemantics + outer
  Semantics over MergeSemantics.
- Recipe: OrdinalSortKey for logical traversal order in RTL when
  visual and logical orders diverge (leading/trailing in a Row).
- Subsection: "Gating SR-specific behavior" — replaces the one-liner
  with concrete adaptations (skip transient animations, extend
  timers, promote swipe-only affordances), mirroring
  ScaffoldMessenger's pattern.
- Testing: prefer tester.semantics.performAction(find.semantics.byLabel,
  SemanticsAction.tap) over the lower-level
  tester.platformDispatcher.onSemanticsActionEvent! for new tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expose a persistent labelText that renders above the bordered input
area and becomes the field's accessibility label — announced by screen
readers regardless of whether the field is empty or filled. When
labelText is null, the field falls back to the hint while empty so SR
users still hear an identifier.

Add a StreamHelperAffinity enum (inside | outside) on
StreamTextInputStyle so the design team can choose whether helper
text sits inside the bordered chassis (default, backwards compatible)
or below it as an external caption. Set globally via
StreamTextInputTheme or per-instance via the style: prop.

Theme owns the new labelStyle and helperAffinity fields; the inner
label renders single-line with ellipsis overflow to keep layout
predictable. Tests cover the semantic-label contract, visible label
rendering, helper affinity positioning, and theme inheritance.

Gallery playground gains Label Text and Helper Affinity knobs; the
real-world login form switches to labelText for Email / Password.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wrap SemanticsService.sendAnnouncement with platform-aware delivery so
features dispatching screen-reader announcements don't each have to
re-implement the same workarounds:

- On VoiceOver (iOS and macOS) polite announcements queue behind the
  screen reader's current speech and can be silently dropped. The
  helper schedules them after a 1-second delay on those platforms,
  matching what ExpansionTile does internally.
- Assertive announcements interrupt the current speech and dispatch
  immediately on every platform, so the delay is skipped.
- A single shared timer means rapid calls cancel the previous pending
  delivery and only the latest message is announced.
- Platform-channel errors are surfaced via FlutterError.reportError
  instead of disappearing silently.

Also expose tooltip(...) as a thin pass-through to
SemanticsService.tooltip for API parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Wrap sheet body in Semantics(scopesRoute, explicitChildNodes) so
  StreamSheetHeader's namesRoute descendant can name the route.
- Add outer Semantics(hitTestBehavior: opaque) so SR taps on the sheet
  don't leak to the barrier — matches Material's showModalBottomSheet.
- Override buildModalBarrier to wire barrierOnTapHint through as
  semanticsOnTapHint on the barrier; SR reads "double tap to <hint>"
  (e.g. "close attachment picker") instead of "activate".
- Add barrierOnTapHint parameter on showStreamSheet with a scrimOnTapHint
  fallback so undecorated callers get the platform-standard hint.
- Override semanticsDismissible so SR dismiss action tracks
  barrierDismissible.

Add semantics tests covering the route scope, header title as the route
name on Android, and the drag handle exposing a working Dismiss action.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nnotation guidance

Adds three sections capturing lessons from the composer a11y pass:

- Role-annotation vs. container wrapping — role-only tabPanel matches
  Flutter's own TabBarView and avoids introducing an extra SR stop.
- FocusTraversalGroup is for keyboards, not screen readers — clarifies
  that Focus and Semantics are separate trees.
- Programmatic focus dispatch (FocusSemanticEvent) — narrow use case
  guidance, notes that onDidGainAccessibilityFocus doesn't round-trip
  from these dispatches.

Also swaps a project-specific factory reference for a generic
factory/builder-pattern description so the skill stays product-neutral.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A behavior-only wrapper that requests screen-reader focus on its child
shortly after mount. Targets the case where the platform's automatic
focus decision on route entry (typically the first interactive element
in the toolbar) is not the element the user needs to interact with.

Highlights:

- Only moves the SR reading cursor; input focus is untouched so the
  soft keyboard stays closed.
- Passthrough when MediaQuery.accessibleNavigation is false; responds
  to the screen reader being toggled at runtime.
- Debug-only assertion catches multiple instances competing for focus
  on the same route (checked per-route via ModalRoute.of, deferred to
  postFrame to avoid framework focus-tracking cascades).
- Retry cadence bounded (defaults: 300ms interval, 1500ms window) to
  avoid choppy re-announcement from repeated platform focus events.

Ships with tests covering the SR-off passthrough, SR-toggle-mid-session,
retry cadence, window close, dispose cleanup, and the per-route
uniqueness assertion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A behavior-only wrapper that observes a Listenable, snapshots its value
on each notification, derives a transition message via a resolver, and
dispatches it through StreamSemanticsAnnouncer. Baseline is captured at
init so the first transition compares against a real value rather than
a synthetic default.

Useful for announcing add/remove or state-change transitions on
collections and value streams (attachment lists, upload progress,
mention state) without wiring imperative SR announcement calls into
every mutation site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rapper

Moves the Semantics(selected:) wrapper outside the ElevatedButton and
adds an outer MergeSemantics + optional Tooltip. The merge collapses
the two nodes (isSelected annotation + button's own label/role/state)
into a single reported node so SR announces "label, selected, button"
correctly.

Also brings the button's semantic node shape closer to Material's
IconButton — the wrapping Semantics/Tooltip sits at the tap-target
bounds rather than inside the ElevatedButton child.

Adds two tests:
- Color resolution across enabled/disabled/selected states.
- Semantic parity vs. Flutter's IconButton (same label + role +
  focusability + hasEnabledState flags).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps the stepper in a Semantics(slider: true, excludeSemantics: true)
node that exposes value, increasedValue, decreasedValue, onIncrease,
and onDecrease. Descendant Semantics from the internal +/- buttons and
read-only text field are excluded so SR sees a single slider node
instead of three separate focus stops.

Adds an optional semanticLabel prop so callers can describe what the
value represents ("Quantity", "Number of items"); announced alongside
the current value with the platform's adjustable hint appended.

Sighted users still tap the +/- buttons; SR users swipe up/down on the
slider node.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds pass-through focusNode and autofocus parameters mirroring
Material's ListTile. Enables callers to programmatically focus a tile
(e.g., landing focus on the first-enabled option when a picker opens)
without wrapping in an external Focus widget.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…TypeIcon

Both components accept an optional semanticLabel so callers can
override the SR announcement — useful when the visible label is a
bare count ("5") that would be ambiguous on its own ("5 unread"), or
when the file-type icon needs to describe the file kind.

StreamFileTypeIcon defaults to ExcludeSemantics when no label is
given, matching the convention that decorative icons should not
introduce SR stops.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fires after the snackbar's entrance animation completes. When
MediaQuery.accessibleNavigation is true (SR active), the entrance
animation is skipped and this fires synchronously on display.

Provides a hook to push an explicit SemanticsService.sendAnnouncement
for screen readers that don't reliably observe newly-mounted live
regions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps the title/subtitle column in Semantics(header: true) so SR
announces it as a heading. On Android/Fuchsia/Linux/Windows, sets
namesRoute: true so the platform can use the title as the route's
accessible name (iOS/macOS handle route names differently, so it's
left null there).

Also plumbs MaterialLocalizations tooltips (closeButtonTooltip,
backButtonTooltip) onto the auto-implied leading icon so it announces
"Back" / "Close" instead of an unlabeled icon.

New excludeHeaderSemantics prop lets callers opt out — useful when the
title widget carries its own heading semantics or when the bar is
shown in a sub-section that shouldn't identify the route.

Adds tests covering:
- Localized back/close tooltips on the auto-implied leading.
- Heading flag on the title.
- namesRoute on Android.
- Focus isolation between multiple StreamAppBars.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the StreamAppBar treatment for sheet headers: title/subtitle
merged as one announcement, wrapped in Semantics(header: true,
namesRoute: true) by default so SR treats it as the sheet's heading
and accessible name.

Also plumbs the localized close/back tooltip onto the auto-implied
leading icon, and adds the excludeHeaderSemantics opt-out for cases
where the title widget provides its own heading semantics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps the bottom bar in Semantics(container: true) so its leading,
title, subtitle, and trailing slots are grouped as one region and
don't intermix with surrounding page content during SR traversal.

Uses MergeSemantics on the title/subtitle column so they read as a
single announcement — but unlike StreamAppBar / StreamSheetHeader, the
bottom bar deliberately does not claim the route's accessible name or
expose the title as a heading (it's a secondary toolbar).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The onDidGainAccessibilityFocus / onDidLoseAccessibilityFocus handlers
that request input focus (and consequently open the IME) are now
attached only on desktop platforms (macOS / Linux / Windows). On
mobile (iOS / Android), where focusing an editable text field opens
the soft keyboard, the handlers are omitted so SR users can swipe
onto the field without triggering the keyboard.

Also moves the error text into its own live-region Semantics container
(liveRegion: hasError) so SR auto-announces validation errors on
appearance instead of piggybacking on the field's hint.

Test updated to assert the error string lives in its own live-region
node and is not duplicated onto the field hint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
StreamMessageComposerAttachment gains two new props:

- semanticLabel — SR announcement (typically the type prefix "File",
  "Photo", "Video").
- mergeRemoveAction — when true (default), the whole tile (content +
  remove button) collapses into a single SR stop whose tap action maps
  to onRemovePressed. Appropriate for tiles whose child has no
  interactive elements (file row, image thumbnail, unsupported chip).

Subclasses updated to forward the semantic label:
- StreamMessageComposerFileAttachment
- StreamMessageComposerMediaAttachment (ExcludeSemantics on the
  visual child so its label is the sole announcement).
- StreamMessageComposerUnsupportedAttachment

Adds new tests covering the semantic tree shape for file and media
attachments (label, merge behavior, remove action).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace deprecated hasFlag(SemanticsFlag.x) with flagsCollection.x
  (or flagsCollection.toStrings() when iterating).
- Replace deprecated tester.binding.pipelineOwner with
  tester.binding.rootPipelineOwner (or
  RendererBinding.instance.rootPipelineOwner where tester isn't in scope).
- Remove unused _fmt helper in file_attachment_semantics_test.dart.
- Remove now-unnecessary imports and add const constructor usages
  flagged by dart fix --apply.

Fixes the analyze CI job (dart analyze --fatal-infos).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@xsahil03x
xsahil03x force-pushed the feat/composer-a11y branch from 9ef7700 to 6f3d14e Compare July 2, 2026 22:35
Adds a 0.5.0 section listing the new a11y primitives
(StreamAccessibilityAutofocus, StreamSemanticsAnnouncer,
StreamSemanticsTransitionAnnouncer) and the component-level a11y
improvements shipped in this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@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: 14

🧹 Nitpick comments (4)
packages/stream_core_flutter/test/components/message_composer/attachment/file_attachment_semantics_test.dart (1)

36-69: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test performs no assertions.

This test only debugPrints the traversal/semantics tree and never calls expect(), so it will pass regardless of whether the actual semantics regress. Consider asserting expected labels/flags (e.g. via isSemantics(...) matchers as used in stream_button_test.dart) for title, subtitle, and the remove action node.

🤖 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
`@packages/stream_core_flutter/test/components/message_composer/attachment/file_attachment_semantics_test.dart`
around lines 36 - 69, The semantic test for StreamMessageComposerFileAttachment
currently only dumps traversal output and never verifies behavior, so it can’t
catch regressions. Update the test in the file_attachment_semantics_test widget
test to add explicit expectations using semantics matchers or similar
assertions, referencing StreamMessageComposerFileAttachment, the title/subtitle
Text nodes, and the remove action triggered by onRemovePressed, so the test
checks labels/flags/actions instead of only calling debugPrint.
packages/stream_core_flutter/test/components/message_composer/attachment/media_attachment_semantics_test.dart (1)

15-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test performs no assertions.

Same gap as file_attachment_semantics_test.dart: only debugPrints the traversal/tree, with no expect() verifying the two semanticLabel values ("Photo 1"/"Photo 2") or remove-action merging actually appear as expected.

🤖 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
`@packages/stream_core_flutter/test/components/message_composer/attachment/media_attachment_semantics_test.dart`
around lines 15 - 56, The semantics test only prints the traversal/tree and
never verifies behavior, so add explicit assertions in
media_attachment_semantics_test.dart. In StreamMessageComposerMediaAttachment’s
test, replace the debug-only inspection with expect checks against
tester.semantics.simulatedAccessibilityTraversal() and/or the root SemanticsNode
to confirm both semanticLabel values (“Photo 1” and “Photo 2”) are present and
that the remove action is merged as expected.
packages/stream_core_flutter/test/components/buttons/stream_button_vs_icon_button_semantics_test.dart (1)

42-425: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Diagnostic-only tests: no assertions anywhere in this file.

Every testWidgets here only debugPrints semantics data for manual comparison; none assert on the result, so this file always "passes" and adds no regression coverage despite looking like a semantics test suite. Consider converting the interesting comparisons into real expect(..., matchesSemantics(...)) assertions, or moving this exploratory script out of the automated test/ tree (e.g., into a dev script or the skill docs) so it isn't mistaken for coverage.

🤖 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
`@packages/stream_core_flutter/test/components/buttons/stream_button_vs_icon_button_semantics_test.dart`
around lines 42 - 425, These tests are diagnostic-only and never assert
anything, so they provide no real coverage. Update the relevant testWidgets
cases in stream_button_vs_icon_button_semantics_test to use actual expectations
with semantics matchers instead of only debugPrint, focusing on the comparison
helpers like capturedEvents, _fmt, and the IconButton/StreamButton/ToggleButtons
checks. If they are meant to stay exploratory, move them out of the automated
test suite so the file is not treated as regression coverage.
packages/stream_core_flutter/test/components/snackbar/stream_snackbar_test.dart (1)

850-916: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add coverage for onVisible and timed auto-dismiss under accessible navigation.

The two new tests here only assert FadeTransition opacity/text presence, not that StreamSnackbarProps.onVisible actually fires or that a timed (non-persistent) snackbar still auto-dismisses when accessibleNavigation is true. See the related concern raised in stream_snackbar_messenger.dart (Lines 704-710, 758-764) — the current implementation may silently skip both of these under accessible navigation, and this test suite wouldn't currently catch it.

🤖 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
`@packages/stream_core_flutter/test/components/snackbar/stream_snackbar_test.dart`
around lines 850 - 916, The accessibleNavigation tests in StreamSnackbarHost
currently verify only opacity and text visibility, but they do not cover
StreamSnackbarProps.onVisible or timed auto-dismiss behavior. Update the
accessibleNavigation group in stream_snackbar_test.dart to assert that onVisible
is invoked when a snackbar is shown and that a non-persistent snackbar still
closes automatically after its duration even with accessible navigation enabled.
Use the existing StreamSnackbarMessenger, StreamSnackbarHost, and
controller.closed expectations to keep the coverage tied to the same behavior
paths.
🤖 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 @.claude/skills/flutter-a11y/SKILL.md:
- Around line 265-271: The matcher/version guidance is incorrect:
`isSemantics(...)` is not a Flutter 3.43 replacement for `matchesSemantics`, and
the current SDK pin means the docs should not point readers to an unavailable
API. Update the semantics matcher table and note in `SKILL.md` to reflect the
correct relationship between `containsSemantics(...)`, `matchesSemantics(...)`,
and `isSemantics(...)`, and make sure the version guidance only mentions APIs
supported by the pinned Flutter range. Use the existing matcher names in that
section to locate and revise the affected text.

In `@apps/design_system_gallery/lib/widgets/scoped_semantics_debugger.dart`:
- Around line 229-236: The selection-color check in _colorForNode is treating
both explicitly selected and explicitly not-selected nodes as selected, which
makes the role visualization misleading. Update the logic to only use the purple
color when SemanticsNode.getSemanticsData().flagsCollection.isSelected is
Tristate.isTrue, and let Tristate.isFalse fall through to the later role checks
in _colorForNode.
- Around line 42-83: The `ScopedSemanticsDebugger` currently calls
`View.pipelineOwnerOf(context)` from `_attach()`, which can be reached from
`initState()` and trigger an inherited-widget lifecycle assert. Keep
`ensureSemantics()` and the `WidgetsBinding` observer setup in `_attach()`, but
move the pipeline-owner lookup/rebind logic into `didChangeDependencies()` so it
runs after dependencies are available. Update the existing
`didChangeDependencies()`, `_attach()`, and `_detach()` paths to ensure listener
registration is still balanced when `widget.enabled` changes.

In
`@packages/stream_core_flutter/lib/src/a11y/stream_accessibility_autofocus.dart`:
- Around line 96-118: Handle runtime changes to widget.enabled in
StreamAccessibilityAutofocus: add a didUpdateWidget override to detect enabled
toggles and start/stop the attempt window accordingly, since
didChangeDependencies only tracks MediaQuery.accessibleNavigationOf changes.
Also guard _requestFocus so it no-ops when widget.enabled is false, ensuring the
class behaves as a passthrough when disabled and can begin autofocus when
re-enabled without waiting for a MediaQuery change.

In
`@packages/stream_core_flutter/lib/src/components/common/stream_text_input.dart`:
- Around line 477-493: The desktop accessibility focus handlers in
stream_text_input should not move real input focus into a disabled
StreamTextInput. Update handleDidGainAccessibilityFocus to also check
props.enabled before calling _effectiveFocusNode.requestFocus, using the same
enabled gate already used by onFocus, and keep the existing
canRequestFocus/hasFocus checks in the StreamTextInput build logic.
- Around line 437-441: The label style default is not resolving per widget
state, so disabled inputs never dim like textStyle and hintStyle do. Update the
labelStyle fallback in StreamTextInputTheme/StreamTextInput to use a state-aware
style resolver (for example the same WidgetStateTextStyle pattern used by
textStyle and hintStyle) so WidgetStateProperty.resolveAs can apply
disabled-state changes. Make sure the default label style matches the documented
theme behavior and the actual referenced typography token used in the
implementation.
- Around line 467-475: The helper text semantics in StreamTextInput are applying
liveRegion twice, once on the outer Semantics wrapper and again inside
StreamHelperText, which can cause duplicate announcements. Update the
helperWidget branch so the outer Semantics around props.helperText keeps only
the sortKey (_helperSortKey) and remove the liveRegion flag there, leaving
StreamHelperText to manage the live-region behavior.

In
`@packages/stream_core_flutter/lib/src/components/controls/stream_stepper.dart`:
- Around line 225-261: The StreamStepper semantics wrapper currently hides
descendant semantics but does not prevent focus traversal or pointer
interaction. Update the widget around the Row in StreamStepper so it also
includes a FocusScope configured to block focus for descendants and a
pointer-blocking wrapper such as IgnorePointer or AbsorbPointer to keep taps
from reaching the inner StreamTextInput and buttons. Keep the existing Semantics
configuration intact, and make sure the new wrappers are applied in the same
build path as the slider semantics so the descendant focus and hit-testing
behavior match the stream_stepper_semantics_test expectations.

In
`@packages/stream_core_flutter/lib/src/components/message_composer/attachment/stream_message_composer_attachment.dart`:
- Around line 180-190: The attachment semantics in
stream_message_composer_attachment.dart currently drops props.semanticLabel when
mergeRemoveAction is false because the ternary returns bare innerContent in that
branch. Update the container-building logic around the semantics wrapper so the
label is always applied, and only conditionally add the
MergeSemantics/onTap/onTapHint remove action behavior when mergeRemoveAction is
true. Use the existing props.semanticLabel, props.mergeRemoveAction, and
innerContent logic to keep the announced label consistent in both branches.

In
`@packages/stream_core_flutter/lib/src/theme/components/stream_text_input_theme.dart`:
- Around line 232-241: The default label styling for Stream text input is
inconsistent with the documented disabled-state behavior and font description.
Update the _StreamTextInputDefaults.labelStyle getter in stream_text_input.dart
so it uses a state-aware WidgetStateTextStyle.resolveWith pattern like textStyle
and hintStyle, returning textDisabled when disabled and the normal label color
otherwise. Also align the docs/comments for labelStyle with the actual default
typography, or change the default to match the documented captionEmphasis if
that is the intended design.

In
`@packages/stream_core_flutter/test/a11y/stream_accessibility_autofocus_test.dart`:
- Around line 204-218: The test widget tree in the accessibility autofocus test
is missing const constructor usage, triggering prefer_const_constructors under
dart analyze --fatal-infos. Update the pumpWidget setup to use const where
possible for MaterialApp, MediaQuery, Scaffold, and Column, keeping the existing
StreamAccessibilityAutofocus widgets and SizedBox children intact so the test
remains functionally unchanged.

In
`@packages/stream_core_flutter/test/components/buttons/stream_button_color_resolution_test.dart`:
- Around line 19-22: The test setup in StreamTheme is passing a redundant
default value for brightness, which triggers avoid_redundant_argument_values
under analysis. Update the StreamTheme constructions in
stream_button_color_resolution_test to omit brightness: Brightness.light where
the default already applies, and make the same adjustment in the repeated setup
near the other StreamTheme instance so both use the default consistently.

In
`@packages/stream_core_flutter/test/components/message_composer/attachment/file_attachment_semantics_test.dart`:
- Around line 7-26: Remove the unused _fmt helper from the
file_attachment_semantics_test test file, since it is not referenced anywhere
and causes the analyze step to fail with unused_element. Delete the standalone
_fmt(SemanticsData d) function and keep the existing inline semantics formatting
logic used in the test assertions so the test still covers the same behavior.

In
`@packages/stream_core_flutter/test/components/snackbar/stream_snackbar_test.dart`:
- Around line 433-440: The snackbar semantics test uses isSemantics in
stream_snackbar_test, but that matcher is not available on the oldest Flutter
SDK still allowed by the package constraint. Update the compatibility choice in
StreamSnackbar tests by either bumping the Flutter constraint in pubspec.yaml to
a version that includes isSemantics, or replace this assertion with
matchesSemantics in the StreamSnackbar test to keep the test compiling on
supported SDKs.

---

Nitpick comments:
In
`@packages/stream_core_flutter/test/components/buttons/stream_button_vs_icon_button_semantics_test.dart`:
- Around line 42-425: These tests are diagnostic-only and never assert anything,
so they provide no real coverage. Update the relevant testWidgets cases in
stream_button_vs_icon_button_semantics_test to use actual expectations with
semantics matchers instead of only debugPrint, focusing on the comparison
helpers like capturedEvents, _fmt, and the IconButton/StreamButton/ToggleButtons
checks. If they are meant to stay exploratory, move them out of the automated
test suite so the file is not treated as regression coverage.

In
`@packages/stream_core_flutter/test/components/message_composer/attachment/file_attachment_semantics_test.dart`:
- Around line 36-69: The semantic test for StreamMessageComposerFileAttachment
currently only dumps traversal output and never verifies behavior, so it can’t
catch regressions. Update the test in the file_attachment_semantics_test widget
test to add explicit expectations using semantics matchers or similar
assertions, referencing StreamMessageComposerFileAttachment, the title/subtitle
Text nodes, and the remove action triggered by onRemovePressed, so the test
checks labels/flags/actions instead of only calling debugPrint.

In
`@packages/stream_core_flutter/test/components/message_composer/attachment/media_attachment_semantics_test.dart`:
- Around line 15-56: The semantics test only prints the traversal/tree and never
verifies behavior, so add explicit assertions in
media_attachment_semantics_test.dart. In StreamMessageComposerMediaAttachment’s
test, replace the debug-only inspection with expect checks against
tester.semantics.simulatedAccessibilityTraversal() and/or the root SemanticsNode
to confirm both semanticLabel values (“Photo 1” and “Photo 2”) are present and
that the remove action is merged as expected.

In
`@packages/stream_core_flutter/test/components/snackbar/stream_snackbar_test.dart`:
- Around line 850-916: The accessibleNavigation tests in StreamSnackbarHost
currently verify only opacity and text visibility, but they do not cover
StreamSnackbarProps.onVisible or timed auto-dismiss behavior. Update the
accessibleNavigation group in stream_snackbar_test.dart to assert that onVisible
is invoked when a snackbar is shown and that a non-persistent snackbar still
closes automatically after its duration even with accessible navigation enabled.
Use the existing StreamSnackbarMessenger, StreamSnackbarHost, and
controller.closed expectations to keep the coverage tied to the same behavior
paths.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: b6031289-a129-4399-bf5d-81de156f9081

📥 Commits

Reviewing files that changed from the base of the PR and between 31ca84f and fe2d32d.

📒 Files selected for processing (42)
  • .claude/skills/flutter-a11y/SKILL.md
  • apps/design_system_gallery/lib/components/common/stream_text_input.dart
  • apps/design_system_gallery/lib/config/preview_configuration.dart
  • apps/design_system_gallery/lib/core/preview_wrapper.dart
  • apps/design_system_gallery/lib/widgets/scoped_semantics_debugger.dart
  • apps/design_system_gallery/lib/widgets/toolbar/semantics_debugger_toggle.dart
  • apps/design_system_gallery/lib/widgets/toolbar/toolbar.dart
  • packages/stream_core_flutter/lib/core.dart
  • packages/stream_core_flutter/lib/src/a11y/stream_accessibility_autofocus.dart
  • packages/stream_core_flutter/lib/src/a11y/stream_semantics_announcer.dart
  • packages/stream_core_flutter/lib/src/a11y/stream_semantics_transition_announcer.dart
  • packages/stream_core_flutter/lib/src/components/accessories/stream_file_type_icon.dart
  • packages/stream_core_flutter/lib/src/components/badge/stream_badge_notification.dart
  • packages/stream_core_flutter/lib/src/components/buttons/stream_button.dart
  • packages/stream_core_flutter/lib/src/components/common/stream_text_input.dart
  • packages/stream_core_flutter/lib/src/components/controls/stream_stepper.dart
  • packages/stream_core_flutter/lib/src/components/list/stream_list_tile.dart
  • packages/stream_core_flutter/lib/src/components/message_composer/attachment/stream_message_composer_attachment.dart
  • packages/stream_core_flutter/lib/src/components/message_composer/attachment/stream_message_composer_file_attachment.dart
  • packages/stream_core_flutter/lib/src/components/message_composer/attachment/stream_message_composer_media_attachment.dart
  • packages/stream_core_flutter/lib/src/components/message_composer/attachment/stream_message_composer_unsupported_attachment.dart
  • packages/stream_core_flutter/lib/src/components/sheet/stream_sheet.dart
  • packages/stream_core_flutter/lib/src/components/snackbar/stream_snackbar.dart
  • packages/stream_core_flutter/lib/src/components/snackbar/stream_snackbar_messenger.dart
  • packages/stream_core_flutter/lib/src/components/toolbar/stream_app_bar.dart
  • packages/stream_core_flutter/lib/src/components/toolbar/stream_bottom_app_bar.dart
  • packages/stream_core_flutter/lib/src/components/toolbar/stream_sheet_header.dart
  • packages/stream_core_flutter/lib/src/theme/components/stream_text_input_theme.dart
  • packages/stream_core_flutter/lib/src/theme/components/stream_text_input_theme.g.theme.dart
  • packages/stream_core_flutter/test/a11y/stream_accessibility_autofocus_test.dart
  • packages/stream_core_flutter/test/a11y/stream_semantics_announcer_test.dart
  • packages/stream_core_flutter/test/components/buttons/stream_button_color_resolution_test.dart
  • packages/stream_core_flutter/test/components/buttons/stream_button_test.dart
  • packages/stream_core_flutter/test/components/buttons/stream_button_vs_icon_button_semantics_test.dart
  • packages/stream_core_flutter/test/components/common/stream_text_input_test.dart
  • packages/stream_core_flutter/test/components/controls/stream_stepper_semantics_test.dart
  • packages/stream_core_flutter/test/components/message_composer/attachment/file_attachment_semantics_test.dart
  • packages/stream_core_flutter/test/components/message_composer/attachment/media_attachment_semantics_test.dart
  • packages/stream_core_flutter/test/components/sheet/stream_sheet_test.dart
  • packages/stream_core_flutter/test/components/snackbar/stream_snackbar_test.dart
  • packages/stream_core_flutter/test/components/toolbar/stream_app_bar_focus_isolation_test.dart
  • packages/stream_core_flutter/test/components/toolbar/stream_app_bar_test.dart

Comment thread .claude/skills/flutter-a11y/SKILL.md Outdated
- Format two files flagged by dart format --set-exit-if-changed.
- Rewrite the 0.5.0 changelog block to only list public-API surface
  changes; drop the internal semantic-tree improvements that don't
  change the widget contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@xsahil03x
xsahil03x force-pushed the feat/composer-a11y branch from 30d3d48 to bcb5383 Compare July 2, 2026 22:40
xsahil03x and others added 3 commits July 3, 2026 00:54
- StreamTextInput: drop the outer helper Semantics' liveRegion — the
  inner StreamHelperText already sets liveRegion on its own container,
  and nesting two liveRegion nodes can cause duplicate SR announcements.
- StreamMessageComposerAttachment: apply semanticLabel in both branches
  of mergeRemoveAction so the label is announced regardless of whether
  the tile collapses into a single tap-to-remove focus stop.
- StreamStepper tests: rewrite the boundary-action assertions to check
  the semantic data directly (hasAction) instead of invoking through
  the deprecated pipelineOwner.semanticsOwner path; drop the
  focus-isolation tests that asserted FocusScope / IgnorePointer
  wrappers not present in the impl (readOnly text input + Semantics
  excludeSemantics is sufficient for the intended SR-only slider
  abstraction).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rAttachment

The prop was added defensively for a hypothetical future case (voice
recording tile with play/pause controls needing the remove button as a
separate SR stop), but every caller uses the default (true), no tests
exercise false, and the branch for false introduced the bug where
semanticLabel was silently dropped.

The remaining behavior — whole tile is one SR focus stop, tap activates
remove, the overlaid remove control is ExcludeSemantics'd — is what
every caller actually wants. If the voice-recording case emerges, the
prop can be re-added then with a real usage justifying it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n test env

RendererBinding.rootPipelineOwner.semanticsOwner is null in the test
binding (the semantic root lives on a descendant PipelineOwner in tests),
so switching from tester.binding.pipelineOwner to rootPipelineOwner broke
the sheet + app-bar-focus-isolation tests that assert on the semantic
tree. Revert to the deprecated tester.binding.pipelineOwner path with an
inline // ignore: deprecated_member_use comment.

Flutter's own tests still use the deprecated path for the same reason
(see e.g. packages/flutter/test/widgets/text_semantics_test.dart).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.44501% with 53 lines in your changes missing coverage. Please review.
✅ Project coverage is 45.80%. Comparing base (31ca84f) to head (cc738a9).

Files with missing lines Patch % Lines
...rc/a11y/stream_semantics_transition_announcer.dart 0.00% 27 Missing ⚠️
...tream_message_composer_unsupported_attachment.dart 0.00% 8 Missing ⚠️
...r/lib/src/components/common/stream_text_input.dart 94.82% 6 Missing ⚠️
...tter/lib/src/components/list/stream_list_tile.dart 0.00% 4 Missing ⚠️
...ment/stream_message_composer_media_attachment.dart 66.66% 3 Missing ⚠️
.../components/accessories/stream_file_type_icon.dart 66.66% 2 Missing ⚠️
...r/lib/src/a11y/stream_accessibility_autofocus.dart 98.24% 1 Missing ⚠️
...flutter/lib/src/components/sheet/stream_sheet.dart 97.36% 1 Missing ⚠️
...r/lib/src/components/snackbar/stream_snackbar.dart 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #126      +/-   ##
==========================================
+ Coverage   37.26%   45.80%   +8.54%     
==========================================
  Files         175      178       +3     
  Lines        6986     7243     +257     
==========================================
+ Hits         2603     3318     +715     
+ Misses       4383     3925     -458     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

- ScopedSemanticsDebugger: move `View.pipelineOwnerOf(context)` lookup
  out of `_attach()` (called from `initState`, where
  `dependOnInheritedWidgetOfExactType` isn't safe). The listener is
  wired up in `didChangeDependencies` instead, which fires right after
  `initState` on first mount and on every owner change.

- ScopedSemanticsDebugger: `_colorForNode` no longer paints
  explicitly-not-selected nodes with the "selected" color. Check
  `isSelected == Tristate.isTrue` instead of `!= Tristate.none`.

- flutter-a11y skill: correct matcher table — `isSemantics` was added
  in Flutter 3.41 to replace `containsSemantics`, not `matchesSemantics`.
  `matchesSemantics` is still a distinct, non-deprecated matcher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@xsahil03x
xsahil03x force-pushed the feat/composer-a11y branch from 7ace3d3 to 4ed5aa1 Compare July 2, 2026 23:18
…nabled

If enabled is toggled to false while a retry timer is running, callbacks
still fire but no longer produce FocusSemanticEvent dispatches. Cheaper
than adding didUpdateWidget + full timer teardown, and matches the
widget's stated use case (opt-in on first mount, not dynamic toggling).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@xsahil03x
xsahil03x force-pushed the feat/composer-a11y branch from 2de5b4c to 60d02ba Compare July 3, 2026 09:45
xsahil03x and others added 2 commits July 3, 2026 11:54
- StreamMessageComposerAttachment: use
  MaterialLocalizations.of(context).deleteButtonTooltip for the remove
  onTapHint instead of the hardcoded 'remove' — localized alongside
  every other Material tooltip in the app, no extra translation
  infrastructure needed.
- StreamMessageComposerFileAttachment: expose semanticLabel as a prop
  and drop the hardcoded 'File' default. The chat layer is responsible
  for passing the localized string (matches the pattern already used
  by StreamMessageComposerMediaAttachment).

Core stays localization-free — all locale-sensitive strings either
come from MaterialLocalizations or are passed in by the consumer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match the docstring shape used by
StreamMessageComposerMediaAttachmentProps.semanticLabel — summary line,
accessibility-modes note, example type prefixes, and the
SemanticsProperties.label reference — on
StreamMessageComposerFileAttachmentProps.semanticLabel too.

Also refresh the base StreamMessageComposerAttachmentProps.semanticLabel
doc: the tap hint is now MaterialLocalizations.deleteButtonTooltip (not
the previous hardcoded "remove") — the doc referenced the old wording.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@VelikovPetar VelikovPetar 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.

Looks very good to me!
Left some minor remarks, but probably would be good to also get a review from someone else who is more familiar with Flutter accessibility before merging the PR!

),
);

debugPrint('=== ACCESSIBILITY TRAVERSAL ORDER (what SR walks) ===');

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.

Should we maybe add some assertions in this tests? As far as I can see, we only have print logs


debugPrint('\n=== FULL TREE ===');
final root = RendererBinding.instance.rootPipelineOwner.semanticsOwner?.rootSemanticsNode;
debugPrint(root?.toStringDeep() ?? '(none)');

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.

Same as above, I don't think we are asserting anything here

@Brazol Brazol 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.

LGTM, added small comment about platform check

xsahil03x and others added 4 commits July 6, 2026 02:03
Provides an alternative screen-reader label that replaces the badge's
rendered icon and duration when set. Consumers can pass a spelled-out
duration (e.g. '1 minute 23 seconds') so TalkBack/VoiceOver read the
value unambiguously instead of the M:SS visual form. The badge itself
wraps its content in Semantics + ExcludeSemantics so the caller's
label fully replaces the visual reading.

Dartdoc mirrors Flutter's own `Text.semanticsLabel` style: state what
the property does and what it replaces, nothing more.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ions

The file/media attachment semantic tests previously dumped the
semantic tree via `debugPrint` — useful during authoring, but they
didn't gate anything, as flagged in review.

Replace the diagnostics with concrete expectations against the
merged-node contract:

- File attachment: one SR focus stop whose label includes both the
  title and subtitle Text, with a tap action and 'Delete' onTapHint.
- Media attachment (two tiles): one SR node per tile with the
  caller-supplied semanticLabel, tap action, and 'Delete' onTapHint;
  no leaked semantics from the ExcludeSemantics'd remove control;
  correct left-to-right traversal order.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two focused expectations for the property added in 7042a57:

- When null, the badge's visual duration text is announced verbatim by
  screen readers (e.g. TalkBack reads '1:23').
- When set, the caller-supplied label replaces the visual content in
  the semantic tree — the ExcludeSemantics wrapping prevents the SR
  from also announcing the M:SS form, so the badge speaks only the
  spelled-out duration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@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

🧹 Nitpick comments (1)
packages/stream_core_flutter/lib/src/components/badge/stream_media_badge.dart (1)

84-92: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider container: true for the label override wrapper.

Without container: true, this Semantics(label: label, ...) node can merge upward into an ancestor's semantics node instead of remaining its own node, depending on where StreamMediaBadge is composited (e.g., overlaid inside a media attachment tile that itself sets a semanticLabel). The provided test wraps the badge directly under Scaffold, so it doesn't exercise this nested scenario.

♿ Proposed fix
     if (semanticsLabel case final label?) {
       badge = Semantics(
+        container: true,
         label: label,
         child: ExcludeSemantics(child: badge),
       );
     }
🤖 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
`@packages/stream_core_flutter/lib/src/components/badge/stream_media_badge.dart`
around lines 84 - 92, The semantics override in StreamMediaBadge should stay as
its own node instead of merging into an ancestor. Update the Semantics wrapper
in the badge-building logic to use container semantics when applying the
optional semanticsLabel override, keeping the ExcludeSemantics child behavior
intact. Make this change in the badge composition path around the label override
so nested uses of StreamMediaBadge preserve the intended accessibility label.
🤖 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
`@packages/stream_core_flutter/test/components/message_composer/attachment/file_attachment_semantics_test.dart`:
- Line 29: The semantics lookup in file_attachment_semantics_test uses a regular
string with escaped dots, which violates the use_raw_strings lint. Update the
tester.getSemantics call to use a raw string for the RegExp pattern in the
message composer attachment semantics test, keeping the same matching behavior
while satisfying analyzer rules.

---

Nitpick comments:
In
`@packages/stream_core_flutter/lib/src/components/badge/stream_media_badge.dart`:
- Around line 84-92: The semantics override in StreamMediaBadge should stay as
its own node instead of merging into an ancestor. Update the Semantics wrapper
in the badge-building logic to use container semantics when applying the
optional semanticsLabel override, keeping the ExcludeSemantics child behavior
intact. Make this change in the badge composition path around the label override
so nested uses of StreamMediaBadge preserve the intended accessibility label.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0754a911-b991-4ab3-9580-f5a28d20e4de

📥 Commits

Reviewing files that changed from the base of the PR and between fe2d32d and 6251ab1.

📒 Files selected for processing (45)
  • .claude/skills/flutter-a11y/SKILL.md
  • apps/design_system_gallery/lib/components/common/stream_text_input.dart
  • apps/design_system_gallery/lib/config/preview_configuration.dart
  • apps/design_system_gallery/lib/core/preview_wrapper.dart
  • apps/design_system_gallery/lib/widgets/scoped_semantics_debugger.dart
  • apps/design_system_gallery/lib/widgets/toolbar/semantics_debugger_toggle.dart
  • apps/design_system_gallery/lib/widgets/toolbar/toolbar.dart
  • packages/stream_core_flutter/CHANGELOG.md
  • packages/stream_core_flutter/lib/core.dart
  • packages/stream_core_flutter/lib/src/a11y/stream_accessibility_autofocus.dart
  • packages/stream_core_flutter/lib/src/a11y/stream_semantics_announcer.dart
  • packages/stream_core_flutter/lib/src/a11y/stream_semantics_transition_announcer.dart
  • packages/stream_core_flutter/lib/src/components/accessories/stream_file_type_icon.dart
  • packages/stream_core_flutter/lib/src/components/badge/stream_badge_notification.dart
  • packages/stream_core_flutter/lib/src/components/badge/stream_media_badge.dart
  • packages/stream_core_flutter/lib/src/components/buttons/stream_button.dart
  • packages/stream_core_flutter/lib/src/components/common/stream_text_input.dart
  • packages/stream_core_flutter/lib/src/components/controls/stream_stepper.dart
  • packages/stream_core_flutter/lib/src/components/list/stream_list_tile.dart
  • packages/stream_core_flutter/lib/src/components/message_composer/attachment/stream_message_composer_attachment.dart
  • packages/stream_core_flutter/lib/src/components/message_composer/attachment/stream_message_composer_file_attachment.dart
  • packages/stream_core_flutter/lib/src/components/message_composer/attachment/stream_message_composer_media_attachment.dart
  • packages/stream_core_flutter/lib/src/components/message_composer/attachment/stream_message_composer_unsupported_attachment.dart
  • packages/stream_core_flutter/lib/src/components/sheet/stream_sheet.dart
  • packages/stream_core_flutter/lib/src/components/snackbar/stream_snackbar.dart
  • packages/stream_core_flutter/lib/src/components/snackbar/stream_snackbar_messenger.dart
  • packages/stream_core_flutter/lib/src/components/toolbar/stream_app_bar.dart
  • packages/stream_core_flutter/lib/src/components/toolbar/stream_bottom_app_bar.dart
  • packages/stream_core_flutter/lib/src/components/toolbar/stream_sheet_header.dart
  • packages/stream_core_flutter/lib/src/theme/components/stream_text_input_theme.dart
  • packages/stream_core_flutter/lib/src/theme/components/stream_text_input_theme.g.theme.dart
  • packages/stream_core_flutter/test/a11y/stream_accessibility_autofocus_test.dart
  • packages/stream_core_flutter/test/a11y/stream_semantics_announcer_test.dart
  • packages/stream_core_flutter/test/components/badge/stream_media_badge_semantics_test.dart
  • packages/stream_core_flutter/test/components/buttons/stream_button_color_resolution_test.dart
  • packages/stream_core_flutter/test/components/buttons/stream_button_test.dart
  • packages/stream_core_flutter/test/components/buttons/stream_button_vs_icon_button_semantics_test.dart
  • packages/stream_core_flutter/test/components/common/stream_text_input_test.dart
  • packages/stream_core_flutter/test/components/controls/stream_stepper_semantics_test.dart
  • packages/stream_core_flutter/test/components/message_composer/attachment/file_attachment_semantics_test.dart
  • packages/stream_core_flutter/test/components/message_composer/attachment/media_attachment_semantics_test.dart
  • packages/stream_core_flutter/test/components/sheet/stream_sheet_test.dart
  • packages/stream_core_flutter/test/components/snackbar/stream_snackbar_test.dart
  • packages/stream_core_flutter/test/components/toolbar/stream_app_bar_focus_isolation_test.dart
  • packages/stream_core_flutter/test/components/toolbar/stream_app_bar_test.dart
✅ Files skipped from review due to trivial changes (2)
  • packages/stream_core_flutter/lib/src/components/message_composer/attachment/stream_message_composer_unsupported_attachment.dart
  • packages/stream_core_flutter/lib/src/theme/components/stream_text_input_theme.g.theme.dart
🚧 Files skipped from review as they are similar to previous changes (34)
  • packages/stream_core_flutter/lib/core.dart
  • packages/stream_core_flutter/test/components/buttons/stream_button_test.dart
  • apps/design_system_gallery/lib/widgets/toolbar/toolbar.dart
  • apps/design_system_gallery/lib/widgets/toolbar/semantics_debugger_toggle.dart
  • apps/design_system_gallery/lib/config/preview_configuration.dart
  • packages/stream_core_flutter/test/components/buttons/stream_button_color_resolution_test.dart
  • apps/design_system_gallery/lib/core/preview_wrapper.dart
  • packages/stream_core_flutter/test/a11y/stream_semantics_announcer_test.dart
  • packages/stream_core_flutter/test/components/toolbar/stream_app_bar_test.dart
  • packages/stream_core_flutter/lib/src/components/snackbar/stream_snackbar_messenger.dart
  • packages/stream_core_flutter/test/components/common/stream_text_input_test.dart
  • packages/stream_core_flutter/test/a11y/stream_accessibility_autofocus_test.dart
  • packages/stream_core_flutter/test/components/toolbar/stream_app_bar_focus_isolation_test.dart
  • packages/stream_core_flutter/lib/src/components/badge/stream_badge_notification.dart
  • packages/stream_core_flutter/lib/src/a11y/stream_semantics_transition_announcer.dart
  • packages/stream_core_flutter/test/components/sheet/stream_sheet_test.dart
  • packages/stream_core_flutter/lib/src/components/accessories/stream_file_type_icon.dart
  • packages/stream_core_flutter/lib/src/components/toolbar/stream_bottom_app_bar.dart
  • packages/stream_core_flutter/lib/src/theme/components/stream_text_input_theme.dart
  • packages/stream_core_flutter/lib/src/components/buttons/stream_button.dart
  • packages/stream_core_flutter/lib/src/components/toolbar/stream_sheet_header.dart
  • packages/stream_core_flutter/lib/src/components/list/stream_list_tile.dart
  • packages/stream_core_flutter/test/components/buttons/stream_button_vs_icon_button_semantics_test.dart
  • apps/design_system_gallery/lib/components/common/stream_text_input.dart
  • packages/stream_core_flutter/lib/src/components/message_composer/attachment/stream_message_composer_media_attachment.dart
  • packages/stream_core_flutter/lib/src/components/controls/stream_stepper.dart
  • packages/stream_core_flutter/lib/src/a11y/stream_accessibility_autofocus.dart
  • packages/stream_core_flutter/lib/src/a11y/stream_semantics_announcer.dart
  • packages/stream_core_flutter/test/components/snackbar/stream_snackbar_test.dart
  • packages/stream_core_flutter/lib/src/components/snackbar/stream_snackbar.dart
  • packages/stream_core_flutter/lib/src/components/sheet/stream_sheet.dart
  • packages/stream_core_flutter/lib/src/components/toolbar/stream_app_bar.dart
  • apps/design_system_gallery/lib/widgets/scoped_semantics_debugger.dart
  • packages/stream_core_flutter/lib/src/components/common/stream_text_input.dart

@xsahil03x
xsahil03x merged commit 7d0421a into main Jul 6, 2026
9 checks passed
@xsahil03x
xsahil03x deleted the feat/composer-a11y branch July 6, 2026 00:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants