From 252a3b4a011993aa18fa7e235492fff23b885b88 Mon Sep 17 00:00:00 2001 From: m1roxx Date: Thu, 27 Aug 2026 15:15:46 +0500 Subject: [PATCH 1/4] [go_router] Expose Navigator clipBehavior on ShellRoute and StatefulShellBranch Nested shell Navigators always clipped their contents, so a sub-route could not paint outside the bounds the shell laid out for it (a box shadow or an overflowing menu got cut off). Adds a `clipBehavior` parameter to `ShellRoute` and `StatefulShellBranch` that is forwarded to the `Navigator` each of them builds. It defaults to `Clip.hardEdge`, which is the `Navigator` default, so existing behavior is unchanged. Branches configure this individually because each `StatefulShellBranch` builds its own `Navigator`, matching how `observers` and `restorationScopeId` already work. --- packages/go_router/CHANGELOG.md | 4 + packages/go_router/lib/src/builder.dart | 12 +- packages/go_router/lib/src/route.dart | 35 ++++- packages/go_router/pubspec.yaml | 2 +- .../test/shell_route_clip_behavior_test.dart | 148 ++++++++++++++++++ 5 files changed, 196 insertions(+), 5 deletions(-) create mode 100644 packages/go_router/test/shell_route_clip_behavior_test.dart diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index fee39de46aeb..9d70c491e7a4 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,7 @@ +## 18.1.0 + +- Adds `clipBehavior` to `ShellRoute` and `StatefulShellBranch`, forwarded to the nested `Navigator`. Set it to `Clip.none` to let sub-routes paint outside the bounds of the shell, for example to render a box shadow. + ## 18.0.0 - Migrates to material_ui and cupertino_ui. diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index e58662646681..bef8098cf26a 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -118,6 +118,7 @@ class RouteBuilder { errorBuilder: errorBuilder, errorPageBuilder: errorPageBuilder, requestFocus: requestFocus, + clipBehavior: Clip.hardEdge, ), ); } @@ -137,6 +138,7 @@ class _CustomNavigator extends StatefulWidget { required this.errorBuilder, required this.errorPageBuilder, required this.requestFocus, + required this.clipBehavior, }); final GlobalKey navigatorKey; @@ -157,6 +159,9 @@ class _CustomNavigator extends StatefulWidget { final GoRouterPageBuilder? errorPageBuilder; final bool requestFocus; + /// The clip behavior forwarded to the [Navigator] built by this widget. + final Clip clipBehavior; + @override State createState() => _CustomNavigatorState(); } @@ -292,8 +297,9 @@ class _CustomNavigatorState extends State<_CustomNavigator> { ShellRouteMatch match, RouteMatchList matchList, List? observers, - String? restorationScopeId, - ) { + String? restorationScopeId, { + Clip clipBehavior = Clip.hardEdge, + }) { return PopScope( // Prevent ShellRoute from being popped, for example // by an iOS back gesture, when the route has active sub-routes. @@ -315,6 +321,7 @@ class _CustomNavigatorState extends State<_CustomNavigator> { errorBuilder: widget.errorBuilder, errorPageBuilder: widget.errorPageBuilder, requestFocus: widget.requestFocus, + clipBehavior: clipBehavior, ), ); }, @@ -454,6 +461,7 @@ class _CustomNavigatorState extends State<_CustomNavigator> { pages: _pages!, observers: widget.observers, onPopPage: _handlePopPage, + clipBehavior: widget.clipBehavior, ), ), ); diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 3f36a7967c6f..6b98be4f8cb0 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -56,8 +56,9 @@ typedef NavigatorBuilder = ShellRouteMatch match, RouteMatchList matchList, List? observers, - String? restorationScopeId, - ); + String? restorationScopeId, { + Clip clipBehavior, + }); /// Signature for function used in [RouteBase.onExit]. /// @@ -600,6 +601,7 @@ class ShellRouteContext { List? observers, bool notifyRootObserver, String? restorationScopeId, + Clip clipBehavior, ) { final effectiveObservers = [...?observers]; @@ -616,6 +618,7 @@ class ShellRouteContext { routeMatchList, effectiveObservers, restorationScopeId, + clipBehavior: clipBehavior, ); } } @@ -728,6 +731,7 @@ class ShellRoute extends ShellRouteBase { super.parentNavigatorKey, GlobalKey? navigatorKey, this.restorationScopeId, + this.clipBehavior = Clip.hardEdge, }) : assert(routes.isNotEmpty), navigatorKey = navigatorKey ?? GlobalKey(), super._() { @@ -765,6 +769,7 @@ class ShellRoute extends ShellRouteBase { observers, notifyRootObserver, restorationScopeId, + clipBehavior, ); return builder!(context, state, navigator); } @@ -783,6 +788,7 @@ class ShellRoute extends ShellRouteBase { observers, notifyRootObserver, restorationScopeId, + clipBehavior, ); return pageBuilder!(context, state, navigator); } @@ -804,6 +810,18 @@ class ShellRoute extends ShellRouteBase { /// its history. final String? restorationScopeId; + /// The clip behavior of the [Navigator] built for this route. + /// + /// The nested Navigator clips its contents by default, so that the route + /// transitions of its sub-routes are not painted outside the bounds the + /// shell lays out for them. Set this to [Clip.none] when a sub-route needs + /// to paint outside those bounds, for example to render a box shadow or an + /// overflowing menu. Note that this also allows route transition animations + /// to paint outside the bounds of the shell. + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + @override GlobalKey navigatorKeyForSubRoute(RouteBase subRoute) { assert(routes.contains(subRoute)); @@ -1127,6 +1145,7 @@ class StatefulShellBranch { this.restorationScopeId, this.observers, this.preload = false, + this.clipBehavior = Clip.hardEdge, }) : navigatorKey = navigatorKey ?? GlobalKey() { assert(() { ShellRouteBase._debugCheckSubRouteParentNavigatorKeys(routes, this.navigatorKey); @@ -1162,6 +1181,16 @@ class StatefulShellBranch { /// The observers parameter is used by the [Navigator] built for this branch. final List? observers; + /// The clip behavior of the [Navigator] built for this branch. + /// + /// Each branch of a [StatefulShellRoute] builds its own [Navigator], so the + /// clip behavior is configured per branch rather than on the shell route. + /// + /// See [ShellRoute.clipBehavior] for a description of the behavior. + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + /// Whether this route branch should be eagerly loaded when navigating to the /// associated StatefulShellRoute for the first time. /// @@ -1409,6 +1438,7 @@ class StatefulNavigationShellState extends State with R branch.observers, route.notifyRootObserver, branch.restorationScopeId, + branch.clipBehavior, ); } @@ -1438,6 +1468,7 @@ class StatefulNavigationShellState extends State with R matchList, branch.observers, branch.restorationScopeId, + clipBehavior: branch.clipBehavior, ); final _StatefulShellBranchState branchState = _branchStateFor(branch, false); diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index 2f36afd5095f..1bd36c35e39b 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -1,7 +1,7 @@ name: go_router description: A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more -version: 18.0.0 +version: 18.1.0 repository: https://github.com/flutter/packages/tree/main/packages/go_router issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22 diff --git a/packages/go_router/test/shell_route_clip_behavior_test.dart b/packages/go_router/test/shell_route_clip_behavior_test.dart new file mode 100644 index 000000000000..7549a07d8664 --- /dev/null +++ b/packages/go_router/test/shell_route_clip_behavior_test.dart @@ -0,0 +1,148 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; +import 'package:material_ui/material_ui.dart'; + +import 'test_helpers.dart'; + +/// Reads the clip behavior of the [Navigator] identified by [navigatorKey]. +/// +/// Offstage widgets are included so that the Navigators of inactive +/// [StatefulShellBranch]es can be inspected too. +Clip clipBehaviorOf(WidgetTester tester, GlobalKey navigatorKey) => + tester.widget(find.byKey(navigatorKey, skipOffstage: false)).clipBehavior; + +void main() { + group('ShellRoute', () { + testWidgets('clips the nested Navigator by default', (WidgetTester tester) async { + final navigatorKey = GlobalKey(debugLabel: 'shell'); + await createRouter([ + ShellRoute( + navigatorKey: navigatorKey, + builder: (_, _, Widget child) => child, + routes: [GoRoute(path: '/', builder: (_, _) => const Text('Home'))], + ), + ], tester); + + expect(clipBehaviorOf(tester, navigatorKey), Clip.hardEdge); + }); + + testWidgets('forwards clipBehavior to the nested Navigator', (WidgetTester tester) async { + final navigatorKey = GlobalKey(debugLabel: 'shell'); + await createRouter([ + ShellRoute( + navigatorKey: navigatorKey, + clipBehavior: Clip.none, + builder: (_, _, Widget child) => child, + routes: [GoRoute(path: '/', builder: (_, _) => const Text('Home'))], + ), + ], tester); + + expect(clipBehaviorOf(tester, navigatorKey), Clip.none); + }); + + testWidgets('forwards clipBehavior to the nested Navigator when using pageBuilder', ( + WidgetTester tester, + ) async { + final navigatorKey = GlobalKey(debugLabel: 'shell'); + await createRouter([ + ShellRoute( + navigatorKey: navigatorKey, + clipBehavior: Clip.antiAlias, + pageBuilder: (_, _, Widget child) => MaterialPage(child: child), + routes: [GoRoute(path: '/', builder: (_, _) => const Text('Home'))], + ), + ], tester); + + expect(clipBehaviorOf(tester, navigatorKey), Clip.antiAlias); + }); + }); + + group('StatefulShellBranch', () { + testWidgets('clips the branch Navigator by default', (WidgetTester tester) async { + final navigatorKey = GlobalKey(debugLabel: 'branch'); + await createRouter([ + StatefulShellRoute.indexedStack( + builder: (_, _, StatefulNavigationShell shell) => shell, + branches: [ + StatefulShellBranch( + navigatorKey: navigatorKey, + routes: [GoRoute(path: '/', builder: (_, _) => const Text('A'))], + ), + ], + ), + ], tester); + + expect(clipBehaviorOf(tester, navigatorKey), Clip.hardEdge); + }); + + testWidgets('forwards clipBehavior per branch', (WidgetTester tester) async { + final keyA = GlobalKey(debugLabel: 'a'); + final keyB = GlobalKey(debugLabel: 'b'); + final root = GlobalKey(debugLabel: 'root'); + await createRouter( + [ + StatefulShellRoute.indexedStack( + builder: (_, _, StatefulNavigationShell shell) => shell, + branches: [ + StatefulShellBranch( + navigatorKey: keyA, + clipBehavior: Clip.none, + routes: [GoRoute(path: '/a', builder: (_, _) => const Text('A'))], + ), + StatefulShellBranch( + navigatorKey: keyB, + routes: [GoRoute(path: '/b', builder: (_, _) => const Text('B'))], + ), + ], + ), + ], + tester, + navigatorKey: root, + initialLocation: '/a', + ); + + expect(clipBehaviorOf(tester, keyA), Clip.none); + + root.currentContext!.go('/b'); + await tester.pumpAndSettle(); + + // Each branch keeps its own clip behavior; the loaded branches stay in + // the tree because StatefulShellRoute preserves their state. + expect(clipBehaviorOf(tester, keyA), Clip.none); + expect(clipBehaviorOf(tester, keyB), Clip.hardEdge); + }); + + testWidgets('forwards clipBehavior to preloaded branches', (WidgetTester tester) async { + final keyA = GlobalKey(debugLabel: 'a'); + final keyB = GlobalKey(debugLabel: 'b'); + await createRouter( + [ + StatefulShellRoute.indexedStack( + builder: (_, _, StatefulNavigationShell shell) => shell, + branches: [ + StatefulShellBranch( + navigatorKey: keyA, + routes: [GoRoute(path: '/a', builder: (_, _) => const Text('A'))], + ), + StatefulShellBranch( + navigatorKey: keyB, + preload: true, + clipBehavior: Clip.none, + routes: [GoRoute(path: '/b', builder: (_, _) => const Text('B'))], + ), + ], + ), + ], + tester, + initialLocation: '/a', + ); + await tester.pumpAndSettle(); + + expect(clipBehaviorOf(tester, keyB), Clip.none); + }); + }); +} From 582fabb285f1520cb256475661fefa8b223210c6 Mon Sep 17 00:00:00 2001 From: Ilyas Nugmanov Date: Thu, 3 Sep 2026 11:53:19 +0500 Subject: [PATCH 2/4] [go_router] Use a pending changelog for the batch release process go_router opts into batch release in ci_config.yaml, so CHANGELOG.md and the pubspec version must not be edited directly. Revert both and describe the change in pending_changelogs instead. --- packages/go_router/CHANGELOG.md | 4 ---- .../pending_changelogs/shell_route_clip_behavior.yaml | 4 ++++ packages/go_router/pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 packages/go_router/pending_changelogs/shell_route_clip_behavior.yaml diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 9d70c491e7a4..fee39de46aeb 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,7 +1,3 @@ -## 18.1.0 - -- Adds `clipBehavior` to `ShellRoute` and `StatefulShellBranch`, forwarded to the nested `Navigator`. Set it to `Clip.none` to let sub-routes paint outside the bounds of the shell, for example to render a box shadow. - ## 18.0.0 - Migrates to material_ui and cupertino_ui. diff --git a/packages/go_router/pending_changelogs/shell_route_clip_behavior.yaml b/packages/go_router/pending_changelogs/shell_route_clip_behavior.yaml new file mode 100644 index 000000000000..a2fb2bbf61e2 --- /dev/null +++ b/packages/go_router/pending_changelogs/shell_route_clip_behavior.yaml @@ -0,0 +1,4 @@ +changelog: | + - Adds `clipBehavior` to `ShellRoute` and `StatefulShellBranch`, forwarded to the nested `Navigator`. Set it to `Clip.none` to let sub-routes paint outside the bounds of the shell, for example to render a box shadow. + - **BREAKING CHANGE**: `NavigatorBuilder` takes a new optional named `clipBehavior` parameter. Functions passed as `ShellRouteContext.navigatorBuilder` must accept it. +version: major diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index 1bd36c35e39b..2f36afd5095f 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -1,7 +1,7 @@ name: go_router description: A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more -version: 18.1.0 +version: 18.0.0 repository: https://github.com/flutter/packages/tree/main/packages/go_router issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22 From db18e3746bfa898f1ce120cfa11d0ae5006b00e6 Mon Sep 17 00:00:00 2001 From: Ilyas Nugmanov Date: Fri, 4 Sep 2026 11:45:20 +0500 Subject: [PATCH 3/4] [go_router] Resolve the clip behavior without changing NavigatorBuilder Keep the NavigatorBuilder typedef and ShellRouteContext unchanged, and resolve the clip behavior in builder.dart from the shell route and the Navigator key instead, so the change stays additive. Downgrade the pending changelog to a minor version. --- packages/go_router/lib/src/builder.dart | 29 ++++++++++++++++--- packages/go_router/lib/src/route.dart | 11 ++----- .../shell_route_clip_behavior.yaml | 3 +- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index bef8098cf26a..7f25185e90ab 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -41,6 +41,28 @@ typedef _ErrorBuilderForAppType = Widget Function(BuildContext context, GoRouter typedef PopPageWithRouteMatchCallback = bool Function(Route route, dynamic result, RouteMatchBase match); +/// The clip behavior of the [Navigator] identified by [navigatorKey]. +/// +/// A [ShellRoute] builds a single Navigator and carries the clip behavior +/// itself, while a [StatefulShellRoute] builds one per branch, so the branch +/// owning [navigatorKey] carries it. [ShellRouteBase] cannot be subclassed +/// outside of this library, so those are the only two cases. +Clip _clipBehaviorFor(ShellRouteBase route, GlobalKey navigatorKey) { + switch (route) { + case ShellRoute(): + return route.clipBehavior; + case StatefulShellRoute(): + for (final StatefulShellBranch branch in route.branches) { + if (branch.navigatorKey == navigatorKey) { + return branch.clipBehavior; + } + } + return Clip.hardEdge; + default: + return Clip.hardEdge; + } +} + /// Builds the top-level Navigator for GoRouter. class RouteBuilder { /// [RouteBuilder] constructor. @@ -297,9 +319,8 @@ class _CustomNavigatorState extends State<_CustomNavigator> { ShellRouteMatch match, RouteMatchList matchList, List? observers, - String? restorationScopeId, { - Clip clipBehavior = Clip.hardEdge, - }) { + String? restorationScopeId, + ) { return PopScope( // Prevent ShellRoute from being popped, for example // by an iOS back gesture, when the route has active sub-routes. @@ -321,7 +342,7 @@ class _CustomNavigatorState extends State<_CustomNavigator> { errorBuilder: widget.errorBuilder, errorPageBuilder: widget.errorPageBuilder, requestFocus: widget.requestFocus, - clipBehavior: clipBehavior, + clipBehavior: _clipBehaviorFor(match.route, navigatorKey), ), ); }, diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 6b98be4f8cb0..302c6dd17a2c 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -56,9 +56,8 @@ typedef NavigatorBuilder = ShellRouteMatch match, RouteMatchList matchList, List? observers, - String? restorationScopeId, { - Clip clipBehavior, - }); + String? restorationScopeId, + ); /// Signature for function used in [RouteBase.onExit]. /// @@ -601,7 +600,6 @@ class ShellRouteContext { List? observers, bool notifyRootObserver, String? restorationScopeId, - Clip clipBehavior, ) { final effectiveObservers = [...?observers]; @@ -618,7 +616,6 @@ class ShellRouteContext { routeMatchList, effectiveObservers, restorationScopeId, - clipBehavior: clipBehavior, ); } } @@ -769,7 +766,6 @@ class ShellRoute extends ShellRouteBase { observers, notifyRootObserver, restorationScopeId, - clipBehavior, ); return builder!(context, state, navigator); } @@ -788,7 +784,6 @@ class ShellRoute extends ShellRouteBase { observers, notifyRootObserver, restorationScopeId, - clipBehavior, ); return pageBuilder!(context, state, navigator); } @@ -1438,7 +1433,6 @@ class StatefulNavigationShellState extends State with R branch.observers, route.notifyRootObserver, branch.restorationScopeId, - branch.clipBehavior, ); } @@ -1468,7 +1462,6 @@ class StatefulNavigationShellState extends State with R matchList, branch.observers, branch.restorationScopeId, - clipBehavior: branch.clipBehavior, ); final _StatefulShellBranchState branchState = _branchStateFor(branch, false); diff --git a/packages/go_router/pending_changelogs/shell_route_clip_behavior.yaml b/packages/go_router/pending_changelogs/shell_route_clip_behavior.yaml index a2fb2bbf61e2..676408c8e9db 100644 --- a/packages/go_router/pending_changelogs/shell_route_clip_behavior.yaml +++ b/packages/go_router/pending_changelogs/shell_route_clip_behavior.yaml @@ -1,4 +1,3 @@ changelog: | - Adds `clipBehavior` to `ShellRoute` and `StatefulShellBranch`, forwarded to the nested `Navigator`. Set it to `Clip.none` to let sub-routes paint outside the bounds of the shell, for example to render a box shadow. - - **BREAKING CHANGE**: `NavigatorBuilder` takes a new optional named `clipBehavior` parameter. Functions passed as `ShellRouteContext.navigatorBuilder` must accept it. -version: major +version: minor From 919818fe9d00de686aae9bf092fe03bdd5c2c9d4 Mon Sep 17 00:00:00 2001 From: m1roxx Date: Fri, 25 Sep 2026 13:04:08 +0500 Subject: [PATCH 4/4] [go_router] Address review feedback on clipBehavior - Share the clipBehavior docs through a doc template - Default _CustomNavigator.clipBehavior to Clip.hardEdge - Include clipBehavior in ShellRoute.debugFillProperties - Forward clipBehavior from ShellRouteData.$route and StatefulShellBranchData.$branch - Shorten the pending changelog entry --- packages/go_router/lib/src/builder.dart | 7 ++- packages/go_router/lib/src/route.dart | 7 ++- packages/go_router/lib/src/route_data.dart | 4 ++ .../shell_route_clip_behavior.yaml | 2 +- .../test/shell_route_clip_behavior_test.dart | 60 +++++++++++++++++++ 5 files changed, 73 insertions(+), 7 deletions(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 7f25185e90ab..5f16f3cbe09d 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -140,7 +140,6 @@ class RouteBuilder { errorBuilder: errorBuilder, errorPageBuilder: errorPageBuilder, requestFocus: requestFocus, - clipBehavior: Clip.hardEdge, ), ); } @@ -160,7 +159,7 @@ class _CustomNavigator extends StatefulWidget { required this.errorBuilder, required this.errorPageBuilder, required this.requestFocus, - required this.clipBehavior, + this.clipBehavior = Clip.hardEdge, }); final GlobalKey navigatorKey; @@ -181,7 +180,9 @@ class _CustomNavigator extends StatefulWidget { final GoRouterPageBuilder? errorPageBuilder; final bool requestFocus; - /// The clip behavior forwarded to the [Navigator] built by this widget. + /// The clip behavior of the [Navigator] built by this widget. + /// + /// {@macro go_router.ShellRoute.clipBehavior} final Clip clipBehavior; @override diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 302c6dd17a2c..421c73dad15a 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -807,6 +807,7 @@ class ShellRoute extends ShellRouteBase { /// The clip behavior of the [Navigator] built for this route. /// + /// {@template go_router.ShellRoute.clipBehavior} /// The nested Navigator clips its contents by default, so that the route /// transitions of its sub-routes are not painted outside the bounds the /// shell lays out for them. Set this to [Clip.none] when a sub-route needs @@ -815,6 +816,7 @@ class ShellRoute extends ShellRouteBase { /// to paint outside the bounds of the shell. /// /// Defaults to [Clip.hardEdge]. + /// {@endtemplate} final Clip clipBehavior; @override @@ -827,6 +829,7 @@ class ShellRoute extends ShellRouteBase { void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty>('navigatorKey', navigatorKey)); + properties.add(EnumProperty('clipBehavior', clipBehavior, defaultValue: Clip.hardEdge)); } } @@ -1181,9 +1184,7 @@ class StatefulShellBranch { /// Each branch of a [StatefulShellRoute] builds its own [Navigator], so the /// clip behavior is configured per branch rather than on the shell route. /// - /// See [ShellRoute.clipBehavior] for a description of the behavior. - /// - /// Defaults to [Clip.hardEdge]. + /// {@macro go_router.ShellRoute.clipBehavior} final Clip clipBehavior; /// Whether this route branch should be eagerly loaded when navigating to the diff --git a/packages/go_router/lib/src/route_data.dart b/packages/go_router/lib/src/route_data.dart index 6cca5b1ba11b..b4db662c5cfa 100644 --- a/packages/go_router/lib/src/route_data.dart +++ b/packages/go_router/lib/src/route_data.dart @@ -306,6 +306,7 @@ abstract class ShellRouteData extends RouteData { bool notifyRootObserver = true, List? observers, String? restorationScopeId, + Clip clipBehavior = Clip.hardEdge, }) { T factoryImpl(GoRouterState state) { return (_stateObjectExpando[state] ??= factory(state)) as T; @@ -330,6 +331,7 @@ abstract class ShellRouteData extends RouteData { observers: observers, restorationScopeId: restorationScopeId, redirect: redirect, + clipBehavior: clipBehavior, ); } @@ -440,6 +442,7 @@ abstract class StatefulShellBranchData { String? initialLocation, String? restorationScopeId, bool preload = false, + Clip clipBehavior = Clip.hardEdge, }) { return StatefulShellBranch( routes: routes, @@ -448,6 +451,7 @@ abstract class StatefulShellBranchData { initialLocation: initialLocation, restorationScopeId: restorationScopeId, preload: preload, + clipBehavior: clipBehavior, ); } } diff --git a/packages/go_router/pending_changelogs/shell_route_clip_behavior.yaml b/packages/go_router/pending_changelogs/shell_route_clip_behavior.yaml index 676408c8e9db..7072db593161 100644 --- a/packages/go_router/pending_changelogs/shell_route_clip_behavior.yaml +++ b/packages/go_router/pending_changelogs/shell_route_clip_behavior.yaml @@ -1,3 +1,3 @@ changelog: | - - Adds `clipBehavior` to `ShellRoute` and `StatefulShellBranch`, forwarded to the nested `Navigator`. Set it to `Clip.none` to let sub-routes paint outside the bounds of the shell, for example to render a box shadow. + - Adds `clipBehavior` to `ShellRoute` and `StatefulShellBranch`, forwarded to the nested `Navigator`. version: minor diff --git a/packages/go_router/test/shell_route_clip_behavior_test.dart b/packages/go_router/test/shell_route_clip_behavior_test.dart index 7549a07d8664..2fecbcdc98fa 100644 --- a/packages/go_router/test/shell_route_clip_behavior_test.dart +++ b/packages/go_router/test/shell_route_clip_behavior_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import 'package:material_ui/material_ui.dart'; @@ -15,8 +16,38 @@ import 'test_helpers.dart'; Clip clipBehaviorOf(WidgetTester tester, GlobalKey navigatorKey) => tester.widget(find.byKey(navigatorKey, skipOffstage: false)).clipBehavior; +class _ShellRouteData extends ShellRouteData { + const _ShellRouteData(); + + @override + Widget builder(BuildContext context, GoRouterState state, Widget navigator) => navigator; +} + +/// Reads the description of the `clipBehavior` diagnostics property of the +/// given [route], or null if the property is hidden at its default value. +String? clipBehaviorDiagnosticOf(ShellRoute route) { + final builder = DiagnosticPropertiesBuilder(); + route.debugFillProperties(builder); + return builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .where((DiagnosticsNode node) => node.name == 'clipBehavior') + .map((DiagnosticsNode node) => node.toDescription()) + .firstOrNull; +} + void main() { group('ShellRoute', () { + test('includes a non-default clipBehavior in debugFillProperties', () { + ShellRoute shellRoute({Clip clipBehavior = Clip.hardEdge}) => ShellRoute( + clipBehavior: clipBehavior, + builder: (_, _, Widget child) => child, + routes: [GoRoute(path: '/', builder: (_, _) => const Text('Home'))], + ); + + expect(clipBehaviorDiagnosticOf(shellRoute()), isNull); + expect(clipBehaviorDiagnosticOf(shellRoute(clipBehavior: Clip.none)), 'none'); + }); + testWidgets('clips the nested Navigator by default', (WidgetTester tester) async { final navigatorKey = GlobalKey(debugLabel: 'shell'); await createRouter([ @@ -145,4 +176,33 @@ void main() { expect(clipBehaviorOf(tester, keyB), Clip.none); }); }); + + group('typed routes', () { + test(r'ShellRouteData.$route forwards clipBehavior', () { + final routes = [GoRoute(path: '/', builder: (_, _) => const Text('Home'))]; + + expect( + ShellRouteData.$route(factory: (_) => const _ShellRouteData(), routes: routes).clipBehavior, + Clip.hardEdge, + ); + expect( + ShellRouteData.$route( + factory: (_) => const _ShellRouteData(), + clipBehavior: Clip.none, + routes: routes, + ).clipBehavior, + Clip.none, + ); + }); + + test(r'StatefulShellBranchData.$branch forwards clipBehavior', () { + final routes = [GoRoute(path: '/', builder: (_, _) => const Text('Home'))]; + + expect(StatefulShellBranchData.$branch(routes: routes).clipBehavior, Clip.hardEdge); + expect( + StatefulShellBranchData.$branch(routes: routes, clipBehavior: Clip.none).clipBehavior, + Clip.none, + ); + }); + }); }