Repository navigation
Use AccessibilityFocusBlockType.blockSubtree to replace BlockSemantics in modal routes. - #181519
Closed
hannah-hyj wants to merge 2 commits into
Closed
hannah-hyj wants to merge 2 commits into
hannah-hyj wants to merge 2 commits into
Conversation
Contributor
There was a problem hiding this comment.
Code Review
The pull request successfully replaces BlockSemantics with AccessibilityFocusBlockType.blockSubtree in modal routes, which is a more granular and efficient way to manage accessibility focus. The changes to _SemanticsParentData for operator== and hashCode are correctly implemented to include the new accessibilityFocusBlockType field. The new test cases thoroughly validate the intended behavior of blocking accessibility focus for non-current modal routes and dialogs, and correctly verify the unblocking when routes are popped. Overall, the implementation is sound and well-tested.
chunhtai
reviewed
Jan 30, 2026
| focusScopeNode.canRequestFocus = !ignoreEvents; | ||
| return IgnorePointer(ignoring: ignoreEvents, child: child); | ||
| child: Semantics( | ||
| accessibilityFocusBlockType: widget.route.isCurrent |
Contributor
There was a problem hiding this comment.
I think we may also need to do this for modalbarrier
auto-submit Bot
pushed a commit
to flutter/packages
that referenced
this pull request
Sep 22, 2026
…lBarrier (#12353) Shell chrome painted before a `ShellRoute` or `StatefulShellRoute` navigator (a side rail, or an app bar in a `Row`/`Column` based shell) disappears from the semantics tree. Screen readers cannot reach it at all: the nodes are not merely unnamed, they do not exist. The mechanism: every `ModalRoute` builds a `ModalBarrier` wrapped in `BlockSemantics`, which drops the semantics of everything painted before it up to the nearest semantics boundary. The nested `Navigator` that go_router builds for shell routes does not establish such a boundary, so the block escapes the shell's navigator and prunes the shell's own chrome. Bottom-nav shells are unaffected only because `Scaffold` happens to paint its body before its bars. This PR wraps the navigator built for `ShellRoute`/`StatefulShellRoute` branches in `Semantics(container: true)`, which contains the block. The root navigator is left unwrapped, since it has no earlier-painted siblings by construction. This is the workaround a framework team member confirmed on the linked issue; applying it inside go_router fixes it for every shell consumer without app-side patches. Semantics tree of a minimal repro (a `Row` shell: 220px sidebar with three nav buttons, routed content on the right), before and after, captured with `debugDumpSemanticsTree`: <details> <summary>Before: 6 nodes, the entire sidebar subtree is missing</summary> ``` SemanticsNode#0 │ Rect.fromLTRB(0.0, 0.0, 2400.0, 1800.0) │ └─SemanticsNode#1 │ Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) scaled by 3.0x │ textDirection: ltr │ sortKey: OrdinalSortKey#39327(order: 0.0) │ └─SemanticsNode#2 │ Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) │ flags: scopesRoute │ └─SemanticsNode#3 │ Rect.fromLTRB(221.0, 0.0, 800.0, 600.0) │ sortKey: OrdinalSortKey#39327(order: 0.0) │ └─SemanticsNode#4 │ Rect.fromLTRB(0.0, 0.0, 579.0, 600.0) │ flags: scopesRoute │ └─SemanticsNode#5 Rect.fromLTRB(85.5, 284.0, 493.5, 316.0) label: "Dashboard content" textDirection: ltr ``` Node `#3` starts at `x=221`, right of the 220px sidebar plus a 1px divider. There is no node anywhere for the sidebar: no title, no navigation container, no buttons. The sidebar is painted before the `/dashboard` route (`#4`, `scopesRoute`) inside the same enclosing semantics scope, which is exactly what that route's `BlockSemantics` drops. </details> <details> <summary>After: sidebar fully present, routed content unchanged (two nodes for the repro's own toggle switch omitted for brevity)</summary> ``` SemanticsNode#0 │ Rect.fromLTRB(0.0, 0.0, 2400.0, 1800.0) │ └─SemanticsNode#1 │ Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) scaled by 3.0x │ textDirection: ltr │ sortKey: OrdinalSortKey#39327(order: 0.0) │ └─SemanticsNode#2 │ Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) │ flags: scopesRoute │ ├─SemanticsNode#3 │ Rect.fromLTRB(16.0, 16.0, 204.0, 76.0) │ label: "Nested Navigator Semantics" │ textDirection: ltr │ ├─SemanticsNode#4 │ │ Rect.fromLTRB(0.0, 92.0, 220.0, 260.0) │ │ label: "Main navigation" │ │ textDirection: ltr │ │ │ ├─SemanticsNode#5 │ │ Rect.fromLTRB(0.0, 0.0, 220.0, 56.0) │ │ actions: focus, tap │ │ flags: isSelected, isButton, hasEnabledState, isEnabled, │ │ isFocusable, hasSelectedState │ │ label: "Dashboard" │ │ textDirection: ltr │ │ │ ├─SemanticsNode#6 │ │ Rect.fromLTRB(0.0, 56.0, 220.0, 112.0) │ │ actions: focus, tap │ │ flags: isButton, hasEnabledState, isEnabled, isFocusable, │ │ hasSelectedState │ │ label: "Settings" │ │ textDirection: ltr │ │ │ └─SemanticsNode#7 │ Rect.fromLTRB(0.0, 112.0, 220.0, 168.0) │ actions: focus, tap │ flags: isButton, hasEnabledState, isEnabled, isFocusable, │ hasSelectedState │ label: "Reports" │ textDirection: ltr │ └─SemanticsNode#10 │ Rect.fromLTRB(221.0, 0.0, 800.0, 600.0) │ sortKey: OrdinalSortKey#39327(order: 0.0) │ └─SemanticsNode#11 │ Rect.fromLTRB(0.0, 0.0, 579.0, 600.0) │ flags: scopesRoute │ └─SemanticsNode#12 Rect.fromLTRB(85.5, 284.0, 493.5, 316.0) label: "Dashboard content" textDirection: ltr ``` The routed content node ("Dashboard content") is byte-identical in both dumps. The fix does not change the routed content's semantics, only whether the chrome painted before the shell navigator survives alongside it. </details> Notes for review: - Tests: the fix commit adds a `Shell navigator semantics boundary` group to `builder_test.dart` (chrome survives, structural wrap present, root navigator not wrapped), and a second commit adds a `StatefulShellRoute.indexedStack` regression test covering branch switching. Removing the wrap makes the chrome tests fail with `Found 0 widgets with a semantics label`. - Version/CHANGELOG: go_router uses batch release, so this PR adds a file under `pending_changelogs/` (`version: patch`) instead of touching `pubspec.yaml` or `CHANGELOG.md`. - Interaction with flutter/flutter#181519 (replacing `BlockSemantics` in modal routes with `AccessibilityFocusBlockType.blockSubtree`): the fix establishes a semantics container boundary at the shell navigator, which is where a nested navigator should scope its routes' blocking regardless of the blocking mechanism. If that migration later makes the containment unnecessary, the wrap stays harmless. Fixes flutter/flutter#135656 Related: flutter/flutter#55758, flutter/flutter#150978 ## Pre-Review Checklist [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
vashworth
pushed a commit
to vashworth/packages
that referenced
this pull request
Sep 24, 2026
…lBarrier (flutter#12353) Shell chrome painted before a `ShellRoute` or `StatefulShellRoute` navigator (a side rail, or an app bar in a `Row`/`Column` based shell) disappears from the semantics tree. Screen readers cannot reach it at all: the nodes are not merely unnamed, they do not exist. The mechanism: every `ModalRoute` builds a `ModalBarrier` wrapped in `BlockSemantics`, which drops the semantics of everything painted before it up to the nearest semantics boundary. The nested `Navigator` that go_router builds for shell routes does not establish such a boundary, so the block escapes the shell's navigator and prunes the shell's own chrome. Bottom-nav shells are unaffected only because `Scaffold` happens to paint its body before its bars. This PR wraps the navigator built for `ShellRoute`/`StatefulShellRoute` branches in `Semantics(container: true)`, which contains the block. The root navigator is left unwrapped, since it has no earlier-painted siblings by construction. This is the workaround a framework team member confirmed on the linked issue; applying it inside go_router fixes it for every shell consumer without app-side patches. Semantics tree of a minimal repro (a `Row` shell: 220px sidebar with three nav buttons, routed content on the right), before and after, captured with `debugDumpSemanticsTree`: <details> <summary>Before: 6 nodes, the entire sidebar subtree is missing</summary> ``` SemanticsNode#0 │ Rect.fromLTRB(0.0, 0.0, 2400.0, 1800.0) │ └─SemanticsNode#1 │ Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) scaled by 3.0x │ textDirection: ltr │ sortKey: OrdinalSortKey#39327(order: 0.0) │ └─SemanticsNode#2 │ Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) │ flags: scopesRoute │ └─SemanticsNode#3 │ Rect.fromLTRB(221.0, 0.0, 800.0, 600.0) │ sortKey: OrdinalSortKey#39327(order: 0.0) │ └─SemanticsNode#4 │ Rect.fromLTRB(0.0, 0.0, 579.0, 600.0) │ flags: scopesRoute │ └─SemanticsNode#5 Rect.fromLTRB(85.5, 284.0, 493.5, 316.0) label: "Dashboard content" textDirection: ltr ``` Node `flutter#3` starts at `x=221`, right of the 220px sidebar plus a 1px divider. There is no node anywhere for the sidebar: no title, no navigation container, no buttons. The sidebar is painted before the `/dashboard` route (`flutter#4`, `scopesRoute`) inside the same enclosing semantics scope, which is exactly what that route's `BlockSemantics` drops. </details> <details> <summary>After: sidebar fully present, routed content unchanged (two nodes for the repro's own toggle switch omitted for brevity)</summary> ``` SemanticsNode#0 │ Rect.fromLTRB(0.0, 0.0, 2400.0, 1800.0) │ └─SemanticsNode#1 │ Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) scaled by 3.0x │ textDirection: ltr │ sortKey: OrdinalSortKey#39327(order: 0.0) │ └─SemanticsNode#2 │ Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) │ flags: scopesRoute │ ├─SemanticsNode#3 │ Rect.fromLTRB(16.0, 16.0, 204.0, 76.0) │ label: "Nested Navigator Semantics" │ textDirection: ltr │ ├─SemanticsNode#4 │ │ Rect.fromLTRB(0.0, 92.0, 220.0, 260.0) │ │ label: "Main navigation" │ │ textDirection: ltr │ │ │ ├─SemanticsNode#5 │ │ Rect.fromLTRB(0.0, 0.0, 220.0, 56.0) │ │ actions: focus, tap │ │ flags: isSelected, isButton, hasEnabledState, isEnabled, │ │ isFocusable, hasSelectedState │ │ label: "Dashboard" │ │ textDirection: ltr │ │ │ ├─SemanticsNode#6 │ │ Rect.fromLTRB(0.0, 56.0, 220.0, 112.0) │ │ actions: focus, tap │ │ flags: isButton, hasEnabledState, isEnabled, isFocusable, │ │ hasSelectedState │ │ label: "Settings" │ │ textDirection: ltr │ │ │ └─SemanticsNode#7 │ Rect.fromLTRB(0.0, 112.0, 220.0, 168.0) │ actions: focus, tap │ flags: isButton, hasEnabledState, isEnabled, isFocusable, │ hasSelectedState │ label: "Reports" │ textDirection: ltr │ └─SemanticsNode#10 │ Rect.fromLTRB(221.0, 0.0, 800.0, 600.0) │ sortKey: OrdinalSortKey#39327(order: 0.0) │ └─SemanticsNode#11 │ Rect.fromLTRB(0.0, 0.0, 579.0, 600.0) │ flags: scopesRoute │ └─SemanticsNode#12 Rect.fromLTRB(85.5, 284.0, 493.5, 316.0) label: "Dashboard content" textDirection: ltr ``` The routed content node ("Dashboard content") is byte-identical in both dumps. The fix does not change the routed content's semantics, only whether the chrome painted before the shell navigator survives alongside it. </details> Notes for review: - Tests: the fix commit adds a `Shell navigator semantics boundary` group to `builder_test.dart` (chrome survives, structural wrap present, root navigator not wrapped), and a second commit adds a `StatefulShellRoute.indexedStack` regression test covering branch switching. Removing the wrap makes the chrome tests fail with `Found 0 widgets with a semantics label`. - Version/CHANGELOG: go_router uses batch release, so this PR adds a file under `pending_changelogs/` (`version: patch`) instead of touching `pubspec.yaml` or `CHANGELOG.md`. - Interaction with flutter/flutter#181519 (replacing `BlockSemantics` in modal routes with `AccessibilityFocusBlockType.blockSubtree`): the fix establishes a semantics container boundary at the shell navigator, which is where a nested navigator should scope its routes' blocking regardless of the blocking mechanism. If that migration later makes the containment unnecessary, the wrap stays harmless. Fixes flutter/flutter#135656 Related: flutter/flutter#55758, flutter/flutter#150978 ## Pre-Review Checklist [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
issue: #166258
The way
BlockSemanticsworks : it drops those blocked semantics nodes from tree.The way AccessibilityFocusBlockType works: the nodes are still in the semantics tree but with a flag "isAccessibilityFocusBlocked: true "
This will stop a live region node from being dropped from the tree and added to the tree again, and thus live region announcing things again.
Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.
Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the
gemini-code-assistbot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.