Skip to content

[go_router] Fix ShellRoute chrome dropped from semantics tree by ModalBarrier - #12353

Merged
auto-submit[bot] merged 3 commits into
flutter:mainfrom
davidmigloz:upstream-shellroute-navigator-semantics-boundary
Sep 22, 2026
Merged

auto-submit[bot] merged 3 commits into
flutter:mainfrom
davidmigloz:upstream-shellroute-navigator-semantics-boundary

Conversation

@davidmigloz

Copy link
Copy Markdown
Contributor

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:

Before: 6 nodes, the entire sidebar subtree is missing
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.

After: sidebar fully present, routed content unchanged (two nodes for the repro's own toggle switch omitted for brevity)
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.

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 Use AccessibilityFocusBlockType.blockSubtree to replace BlockSemantics in modal routes. 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

If you need help, consider asking for advice on the #hackers-new channel on Discord.

Footnotes

  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. ↩ ↩2

…lBarrier

Every ModalRoute (e.g. the MaterialPageRoute go_router builds for each
GoRoute) creates a ModalBarrier wrapped in BlockSemantics, which drops the
semantics of siblings painted before it up to the nearest semantics
boundary. A nested Navigator (as built for ShellRoute/StatefulShellRoute
branches) does not itself establish a semantics boundary, so the block
escapes the shell's Navigator and drops earlier-painted shell chrome, e.g.
a side-rail or app bar in a Row/Column-based shell. Bottom-nav shells are
unaffected only because Scaffold happens to paint its body before its
chrome.

Wraps the Navigator built for ShellRoute/StatefulShellRoute branches (but
not the root GoRouter navigator, which has no earlier-painted siblings by
construction) in Semantics(container: true) to contain the block. This is
the workaround independently confirmed by a Flutter framework team member
on the upstream issue.

Fixes flutter/flutter#135656
Related: flutter/flutter#150978
…dary

StatefulShellRoute branches flow through the same _buildPageForShellRoute
path as ShellRoute, so they are already covered by the
Semantics(container: true) fix, but no test proved it. Adds a test to the
existing 'Shell navigator semantics boundary' group using
StatefulShellRoute.indexedStack, mirroring the ShellRoute chrome test.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request updates go_router to wrap nested navigators for ShellRoute and StatefulShellRoute in a Semantics widget with container: true. This change ensures that shell chrome painted before the navigator is not dropped from the semantics tree by the active route's ModalBarrier. Unit tests verifying this behavior and a pending changelog entry have also been added. As there are no review comments, no further feedback is provided.

@github-actions github-actions Bot added p: go_router triage-framework Should be looked at in framework triage labels Aug 3, 2026
davidmigloz added a commit to davidmigloz/flutter_packages that referenced this pull request Aug 3, 2026
…lBarrier

Every ModalRoute builds a ModalBarrier wrapped in BlockSemantics, which
drops the semantics of siblings painted before it up to the nearest
semantics boundary. The nested Navigator built for
ShellRoute/StatefulShellRoute branches establishes no such boundary, so
the block escapes it and prunes earlier-painted shell chrome (side rail,
app bar in Row/Column shells). Wraps shell navigators (not the root
navigator) in Semantics(container: true) to contain the block.

Squash of the two commits on the upstream PR branch:
flutter#12353
Fixes flutter/flutter#135656
@Piinks
Piinks requested a review from hannah-hyj August 4, 2026 22:16

@hannah-hyj hannah-hyj left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks like a clean fix !

@hannah-hyj
hannah-hyj requested a review from chunhtai August 31, 2026 20:36
@elliette

Copy link
Copy Markdown
Member

[from triage] gentle ping @chunhtai

@chunhtai chunhtai 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

@chunhtai chunhtai added CICD Run CI/CD autosubmit Merge PR when tree becomes green via auto submit App labels Sep 22, 2026
@auto-submit
auto-submit Bot merged commit 63ee971 into flutter:main Sep 22, 2026
14 checks passed
pull Bot pushed a commit to edisplay/flutter that referenced this pull request Sep 24, 2026
…er#193289)

Roll Packages from c2b58e1a97fb to 431ea69a42e2 (56 revisions)

flutter/packages@c2b58e1...431ea69

2026-09-24 [email protected] [cross_file]
Updates cross_file to a package separated federated plugin
(flutter/packages#11010)
2026-09-23 [email protected] [material_ui]
Remove static access of `copyWith` from `dart fix` golden tests for
`ThemeData/TextTheme` (flutter/packages#12994)
2026-09-23 [email protected] [camera_avfoundation] Replace
deprecated high-resolution capture APIs (flutter/packages#12372)
2026-09-23 [email protected] [pigeon] update nullish checks to use
new isNullish method (flutter/packages#12985)
2026-09-23 [email protected] [ci] Assign batch release PR approver
as reviewer on sync-back PR (flutter/packages#12976)
2026-09-23 [email protected] [image_picker] Fix scaling 10-bit images
(flutter/packages#12557)
2026-09-23 [email protected] [material_ui] Improve
Autocomplete, DrawerHeader, and Tooltip accessibility
(flutter/packages#12918)
2026-09-23 [email protected] [google_fonts] Decouple GoogleFontsLite
for full tree-shaking and add full feature parity
(flutter/packages#12830)
2026-09-23 [email protected] [file_selector]
Ignore flakey `file_selector_android` tests (flutter/packages#12992)
2026-09-23 [email protected] [CI] Remove
bringup from `Mac_arm64 build_all_packages` targets
(flutter/packages#12978)
2026-09-22 [email protected] [material_ui]
Unskip 5 passing web tests in bottom_app_bar_test and text_field_test
(flutter/packages#12983)
2026-09-22 [email protected]
[cupertino_ui] Unskip 5 passing web tests in
adaptive_text_selection_toolbar_test and text_field_test
(flutter/packages#12984)
2026-09-22 [email protected] [go_router] Fix ShellRoute chrome dropped
from semantics tree by ModalBarrier (flutter/packages#12353)
2026-09-22 [email protected] [material_ui]
Add contrastLevel for M3 ColorScheme (flutter/packages#12743)
2026-09-22 [email protected] [go_router] Preserve nested pushes
during config updates (flutter/packages#12611)
2026-09-22 [email protected] [material_ui]
Migrate M3 Chip template to use new gen_defaults
(flutter/packages#12847)
2026-09-22 [email protected] [material_ui] Fix
LocalHistoryEntry leak when double tapping Drawer scrim
(flutter/packages#12552)
2026-09-22 [email protected] Update suggested reviewers
(flutter/packages#12974)
2026-09-22 [email protected] Sync release-material_ui-1.4.0 to
main (flutter/packages#12966)
2026-09-22 [email protected] Sync release-cupertino_ui-1.1.1 to
main (flutter/packages#12965)
2026-09-21 [email protected] [ci] Update repo
for 3.47 stable release (flutter/packages#12959)
2026-09-21 [email protected] Manual roll Flutter from
27fec0e to 4fcd90b (1 revision) (flutter/packages#12904)
2026-09-21 [email protected] Revert "[go_router_builder] Migrate to
material_ui" (flutter/packages#12962)
2026-09-21 [email protected]
[material_ui] Update `material_ui` tests to prevent them from failing
when framework `TextStyle` changes (flutter/packages#12728)
2026-09-21 [email protected] [go_router_builder] Migrate to
material_ui (flutter/packages#12913)
2026-09-21 [email protected] [material_ui,
cupertino_ui] Bump Flutter version from to 3.47 (flutter/packages#12944)
2026-09-21 [email protected] [cupertino_ui] Work around dart2wasm
optional parameter inference bug in example checkbox tests
(flutter/packages#12961)
2026-09-21 [email protected] [cupertino_ui] Work around dart2wasm
optional parameter inference bug in checkbox_test.dart
(flutter/packages#12956)
2026-09-21 [email protected] [google_sign_in] PR 3/4 Migrate
ViewProvider and GID SDK wrappers from Objective-C to Swift
(flutter/packages#12657)
2026-09-21 [email protected] [google_sign_in] PR 2/4 Migrate
the plugin class from Objective-C to Swift. (flutter/packages#12655)
2026-09-20 [email protected]
[cross_file_darwin] iOS/macOS implementation of `cross_file`
(flutter/packages#12910)
2026-09-20 [email protected]
[cross_file_web] Web implementation of `cross_file`
(flutter/packages#12908)
2026-09-20 [email protected] [material_ui]
Migrate M3 `InputChip` template to use new gen_defaults
(flutter/packages#12850)
2026-09-20 [email protected] [material_ui]
Migrate M3 Checkbox defaults to new gen_defaults
(flutter/packages#12816)
2026-09-20 [email protected] [material_ui]
Migrate M3 `FilterChip` template to use new gen_defaults
(flutter/packages#12849)
2026-09-19 [email protected] [material_ui] Announce PopupMenuButton with a
child as a button (flutter/packages#12585)
2026-09-19 [email protected] Roll Flutter (stable) from
9584c67 to 6a19cca (7 revisions) (flutter/packages#12950)
2026-09-18 [email protected] [material_ui]
Reduce the `material_ui` non-wasm web test suite
(flutter/packages#12763)
2026-09-18 [email protected] Replace
`Mac_x64` builders with `Mac_arm64` ones (flutter/packages#12941)
2026-09-18 [email protected]
[in_app_purchase_storekit] Use `_$SKPaymentDiscountWrapperToJson`
(flutter/packages#12721)
2026-09-18 [email protected] [In_app _purchase] README
excerpt examples and changelog (flutter/packages#12739)
2026-09-18 [email protected] [material_ui] Add alternative
input method for `RangeSlider` in `NavigationMode.directional`
(flutter/packages#12630)
2026-09-18 [email protected]
[cross_file_android] Android implementation of `cross_file`
(flutter/packages#12909)
2026-09-18 [email protected]
[camera_platform_interface] Adds videoOutputPath support to
startVideoRecording (flutter/packages#12667)
2026-09-17 [email protected] [cupertino_ui] Migrate a
snippet in `CupertinoCheckbox`'s API doc to `{@example}` and add unit
tests (flutter/packages#12180)
2026-09-17 [email protected] [material_ui]
Default gen_defaults color helper prefix (flutter/packages#12848)
...
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autosubmit Merge PR when tree becomes green via auto submit App CICD Run CI/CD p: go_router triage-framework Should be looked at in framework triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[go_router] Semantics disappearing in ShellRoute

4 participants