Skip to content

[go_router] Fix SelectionArea dead zones after ShellRoute navigation#11062

Open
davidmigloz wants to merge 2 commits intoflutter:mainfrom
davidmigloz:fix-selection-dead-zones
Open

[go_router] Fix SelectionArea dead zones after ShellRoute navigation#11062
davidmigloz wants to merge 2 commits intoflutter:mainfrom
davidmigloz:fix-selection-dead-zones

Conversation

@davidmigloz
Copy link
Contributor

Description

When a SelectionArea wraps a ShellRoute's child (a common pattern for enabling text selection across an app), offstage pages kept their Text widgets registered with the SelectableRegion. These invisible selectables intercepted drag-to-select events, creating "dead zones" on the currently visible page where selection silently failed.

Fix

ShellRoute: Adds _OffstageSelectionDisabler — a lightweight widget inserted around each route's content that detects non-current routes via ModalRoute.of(context) and wraps them with SelectionContainer.disabled to unregister their selectables.

StatefulShellRoute: Wraps inactive branches with SelectionContainer.disabled in _IndexedStackedRouteBranchContainer._buildRouteBranchContainer. Navigator state is preserved via GlobalObjectKey reparenting.

Reproduction

Fixes flutter/flutter#182573

Tests

Added 12 new widget tests in builder_test.dart covering:

  • ShellRoute: builder, pageBuilder, back navigation, deep linking, 3+ level nesting, CustomTransitionPage, pop navigation
  • StatefulShellRoute: branch selection toggle, navigator state preservation, composition with route-level disabling

When a SelectionArea wraps a ShellRoute's child (e.g. in a Scaffold body),
offstage pages kept their Text widgets registered with the SelectableRegion.
These invisible selectables intercepted drag-to-select events, creating
dead zones on the active page.

For ShellRoute: adds _OffstageSelectionDisabler that detects non-current
routes via ModalRoute.of(context) and wraps them with
SelectionContainer.disabled.

For StatefulShellRoute: wraps inactive branches with
SelectionContainer.disabled in _IndexedStackedRouteBranchContainer.

Fixes flutter/flutter#182573
@github-actions github-actions bot added p: go_router triage-framework Should be looked at in framework triage labels Feb 18, 2026
Copy link

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

Choose a reason for hiding this comment

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

Code Review

The pull request successfully addresses the issue of text selection "dead zones" by disabling selection for offstage routes. However, the current implementation of conditionally wrapping widgets with SelectionContainer.disabled or _OffstageSelectionDisabler will cause the entire subtree (including Navigators and their pages) to be unmounted and remounted whenever a route's visibility changes. This leads to a complete loss of state (e.g., scroll positions, text field contents) for those routes. I have suggested an alternative approach that toggles the registrar property of a persistent SelectionContainer to maintain the widget tree structure and preserve state.

- Convert _OffstageSelectionDisabler to StatefulWidget with GlobalKey-based
  reparenting to preserve descendant State across tree structure changes
- Add _SelectionGuard StatefulWidget in route.dart for StatefulShellRoute
  branch containers with the same GlobalKey preservation pattern
- Fix type parameter mismatch: try both <dynamic> and <void> in
  _addSelectionGuardToPage so user pageBuilder pages (which default to
  <dynamic>) get the selection guard applied
- Switch to pending_changelogs/ instead of direct CHANGELOG.md/pubspec.yaml
  edits (revert version back to 17.1.0)
- Fix omit_obvious_local_variable_types lint in 3 test declarations
- Add 4 new tests: state preservation for ShellRoute and
  StatefulShellRoute, and <dynamic> type parameter coverage for
  NoTransitionPage and CustomTransitionPage
@davidmigloz davidmigloz requested a review from chunhtai February 18, 2026 23:55
@Renzo-Olivares Renzo-Olivares self-requested a review February 19, 2026 23:39
Copy link
Contributor

@Renzo-Olivares Renzo-Olivares left a comment

Choose a reason for hiding this comment

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

Hi @davidmigloz, thank you for your contribution as well as your patience on review! I think the fix for this issue should probably live in the framework that way we can fix the root issue. What do you think about submitting a change to https://github.com/flutter/flutter ? Generally any route we are making offstage/not the current route we should wrap with a SelectionContainer.disabled similar to how you've done in this pull request. Happy to help review and help with any questions. If that's not something you want to do then I'm also happy to take over as well.

@davidmigloz
Copy link
Contributor Author

davidmigloz commented Mar 5, 2026

Thanks for the feedback @Renzo-Olivares !

I initially took the go_router path because it was the quickest way for me to unblock the affected app, but I agree this should be fixed structurally in the framework.

Would this be the right direction?

  • Apply the fix at the route framework layer (ModalRoute scope in routes.dart)
  • Ensure non-current route subtrees do not register with the active SelectionArea (so they cannot receive selection events or contribute stale selectable geometry)
  • Re-enable normal selection registration when the route becomes current again
  • Preserve subtree state across these transitions
  • After the framework fix lands, re-evaluate this go_router PR and keep only any residual package-specific guard if still needed (e.g., inactive StatefulShellRoute branch containers)

If this direction looks correct to you, I’ll proceed with it.

@Renzo-Olivares
Copy link
Contributor

@davidmigloz That sounds like a great plan! Thank you for writing such a detailed outline.

Some related issues to look out for : flutter/flutter#119772 and flutter/flutter#151536. These have similar root causes where the selectables will try to register themselves despite not being in the current route, though in those specific issues because the initial route is a nested route and we have not laid out the route route, when attempting to access the transform of selectables in the root route we get a crash. We could get a similar crash in your reproduction if we set the initial route to a nested route.

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] SelectionArea dead zones after ShellRoute navigation — offstage page selectables intercept drag events

3 participants