[go_router] Fix ShellRoute chrome dropped from semantics tree by ModalBarrier (#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 https://github.com/flutter/flutter/issues/135656
Related: https://github.com/flutter/flutter/issues/55758, https://github.com/flutter/flutter/issues/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 repo is a companion repo to the main flutter repo. It contains the source code for Flutter's first-party packages (i.e., packages developed by the core Flutter team). Check the packages directory to see all packages.
These packages are also available on pub.
Please file any issues, bugs, or feature requests in the main flutter repo. Issues pertaining to this repository are labeled “package”.
If you wish to contribute a new package to the Flutter ecosystem, please see the documentation for developing packages. You can store your package source code in any GitHub repository (the present repo is only intended for packages developed by the core Flutter team). Once your package is ready you can publish to the pub repository.
If you wish to contribute a change to any of the existing packages in this repo, please review our contribution guide, and send a pull request.
These are the packages hosted in this repository: