[go_router] Fix SelectionArea dead zones after ShellRoute navigation#11062
[go_router] Fix SelectionArea dead zones after ShellRoute navigation#11062davidmigloz wants to merge 2 commits intoflutter:mainfrom
Conversation
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
There was a problem hiding this comment.
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
Renzo-Olivares
left a comment
There was a problem hiding this comment.
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.
|
Thanks for the feedback @Renzo-Olivares ! I initially took the Would this be the right direction?
If this direction looks correct to you, I’ll proceed with it. |
|
@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. |
Description
When a
SelectionAreawraps aShellRoute's child (a common pattern for enabling text selection across an app), offstage pages kept theirTextwidgets registered with theSelectableRegion. 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 viaModalRoute.of(context)and wraps them withSelectionContainer.disabledto unregister their selectables.StatefulShellRoute: Wraps inactive branches withSelectionContainer.disabledin_IndexedStackedRouteBranchContainer._buildRouteBranchContainer. Navigator state is preserved viaGlobalObjectKeyreparenting.Reproduction
Fixes flutter/flutter#182573
Tests
Added 12 new widget tests in
builder_test.dartcovering: