Skip to content

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

Open
davidmigloz wants to merge 2 commits into
flutter:mainfrom
davidmigloz:upstream-shellroute-navigator-semantics-boundary
Open

[go_router] Fix ShellRoute chrome dropped from semantics tree by ModalBarrier#12353
davidmigloz wants to merge 2 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

1 participant