From 8e75edc112022da06703c9b2585dd7f6b9300a77 Mon Sep 17 00:00:00 2001 From: Hashem Date: Thu, 27 Aug 2026 22:12:18 +0100 Subject: [PATCH 1/2] Add route semantics opt-out to page routes --- packages/cupertino_ui/lib/src/route.dart | 28 ++++++++++++++ .../change_2026_08_27_route_semantics.yaml | 3 ++ packages/cupertino_ui/test/route_test.dart | 38 +++++++++++++++++++ packages/material_ui/lib/src/page.dart | 28 ++++++++++++++ .../change_2026_08_27_route_semantics.yaml | 3 ++ packages/material_ui/test/page_test.dart | 38 +++++++++++++++++++ 6 files changed, 138 insertions(+) create mode 100644 packages/cupertino_ui/pending_changelogs/change_2026_08_27_route_semantics.yaml create mode 100644 packages/material_ui/pending_changelogs/change_2026_08_27_route_semantics.yaml diff --git a/packages/cupertino_ui/lib/src/route.dart b/packages/cupertino_ui/lib/src/route.dart index 2645b62300ca..6560cba3ed42 100644 --- a/packages/cupertino_ui/lib/src/route.dart +++ b/packages/cupertino_ui/lib/src/route.dart @@ -97,6 +97,8 @@ mixin CupertinoRouteTransitionMixin on PageRoute { @protected Widget buildContent(BuildContext context); + bool get _includeRouteSemantics => true; + /// {@template cupertino_ui.CupertinoRouteTransitionMixin.title} /// A title string for this route. /// @@ -194,6 +196,9 @@ mixin CupertinoRouteTransitionMixin on PageRoute { Animation secondaryAnimation, ) { final Widget child = buildContent(context); + if (!_includeRouteSemantics) { + return child; + } return Semantics(scopesRoute: true, explicitChildNodes: true, child: child); } @@ -309,6 +314,7 @@ class CupertinoPageRoute extends PageRoute with CupertinoRouteTransitionMi this.maintainState = true, super.fullscreenDialog, super.allowSnapshotting = true, + this.includeRouteSemantics = true, super.barrierDismissible = false, }) { assert(opaque); @@ -330,6 +336,21 @@ class CupertinoPageRoute extends PageRoute with CupertinoRouteTransitionMi @override final bool maintainState; + /// {@template cupertino_ui.CupertinoPageRoute.includeRouteSemantics} + /// Whether this route introduces a route scope in the semantics tree. + /// + /// Defaults to true. When true, screen readers can treat pushes and pops of + /// this route as navigation to a new screen and announce the change to users. + /// + /// Set this to false for routes that update only part of the screen, such as + /// tab or shell content in a nested navigator. This prevents screen readers + /// from treating the route as a new screen. + /// {@endtemplate} + final bool includeRouteSemantics; + + @override + bool get _includeRouteSemantics => includeRouteSemantics; + @override String get debugLabel => '${super.debugLabel}(${settings.name})'; } @@ -362,6 +383,9 @@ class _PageBasedCupertinoPageRoute extends PageRoute with CupertinoRouteTr @override bool get fullscreenDialog => _page.fullscreenDialog; + @override + bool get _includeRouteSemantics => _page.includeRouteSemantics; + @override String get debugLabel => '${super.debugLabel}(${_page.name})'; } @@ -390,6 +414,7 @@ class CupertinoPage extends Page { this.title, this.fullscreenDialog = false, this.allowSnapshotting = true, + this.includeRouteSemantics = true, super.canPop, super.onPopInvoked, super.key, @@ -413,6 +438,9 @@ class CupertinoPage extends Page { /// {@macro flutter.widgets.TransitionRoute.allowSnapshotting} final bool allowSnapshotting; + /// {@macro cupertino_ui.CupertinoPageRoute.includeRouteSemantics} + final bool includeRouteSemantics; + @override Route createRoute(BuildContext context) { return _PageBasedCupertinoPageRoute(page: this, allowSnapshotting: allowSnapshotting); diff --git a/packages/cupertino_ui/pending_changelogs/change_2026_08_27_route_semantics.yaml b/packages/cupertino_ui/pending_changelogs/change_2026_08_27_route_semantics.yaml new file mode 100644 index 000000000000..03eb1b65a2b5 --- /dev/null +++ b/packages/cupertino_ui/pending_changelogs/change_2026_08_27_route_semantics.yaml @@ -0,0 +1,3 @@ +changelog: | + - Adds an option for `CupertinoPageRoute` and `CupertinoPage` to opt out of introducing a semantics route scope. +version: minor diff --git a/packages/cupertino_ui/test/route_test.dart b/packages/cupertino_ui/test/route_test.dart index ebd17dcd5f79..125e8e21ef0e 100644 --- a/packages/cupertino_ui/test/route_test.dart +++ b/packages/cupertino_ui/test/route_test.dart @@ -2340,6 +2340,44 @@ void main() { expect(find.text('Visible'), findsOneWidget); }); + testWidgets('CupertinoPageRoute can opt out of route semantics', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + + await tester.pumpWidget( + CupertinoApp( + onGenerateRoute: (RouteSettings settings) { + return CupertinoPageRoute( + includeRouteSemantics: false, + builder: (BuildContext context) => const Text('Page'), + ); + }, + ), + ); + + expect(find.semantics.byFlag(SemanticsFlag.scopesRoute), findsNothing); + handle.dispose(); + }); + + testWidgets('CupertinoPage can opt out of route semantics', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + + await tester.pumpWidget( + buildNavigator( + view: tester.view, + pages: const >[ + CupertinoPage(includeRouteSemantics: false, child: Text('Page')), + ], + onPopPage: (Route route, dynamic result) { + assert(false); // The test shouldn't call this. + return true; + }, + ), + ); + + expect(find.semantics.byFlag(SemanticsFlag.scopesRoute), findsNothing); + handle.dispose(); + }); + testWidgets('CupertinoPage works', (WidgetTester tester) async { final LocalKey pageKey = UniqueKey(); final detector = TransitionDetector(); diff --git a/packages/material_ui/lib/src/page.dart b/packages/material_ui/lib/src/page.dart index 6aee37edc010..da7c34863004 100644 --- a/packages/material_ui/lib/src/page.dart +++ b/packages/material_ui/lib/src/page.dart @@ -41,6 +41,7 @@ class MaterialPageRoute extends PageRoute with MaterialRouteTransitionMixi this.maintainState = true, super.fullscreenDialog, super.allowSnapshotting = true, + this.includeRouteSemantics = true, super.barrierDismissible = false, super.traversalEdgeBehavior, super.directionalTraversalEdgeBehavior, @@ -57,6 +58,21 @@ class MaterialPageRoute extends PageRoute with MaterialRouteTransitionMixi @override final bool maintainState; + /// {@template material_ui.MaterialPageRoute.includeRouteSemantics} + /// Whether this route introduces a route scope in the semantics tree. + /// + /// Defaults to true. When true, screen readers can treat pushes and pops of + /// this route as navigation to a new screen and announce the change to users. + /// + /// Set this to false for routes that update only part of the screen, such as + /// tab or shell content in a nested navigator. This prevents screen readers + /// from treating the route as a new screen. + /// {@endtemplate} + final bool includeRouteSemantics; + + @override + bool get _includeRouteSemantics => includeRouteSemantics; + @override String get debugLabel => '${super.debugLabel}(${settings.name})'; } @@ -87,6 +103,8 @@ mixin MaterialRouteTransitionMixin on PageRoute { @protected Widget buildContent(BuildContext context); + bool get _includeRouteSemantics => true; + @override Duration get transitionDuration => _getPageTransitionBuilder(navigator!.context)?.transitionDuration ?? @@ -191,6 +209,9 @@ mixin MaterialRouteTransitionMixin on PageRoute { Animation secondaryAnimation, ) { final Widget result = buildContent(context); + if (!_includeRouteSemantics) { + return result; + } return Semantics(scopesRoute: true, explicitChildNodes: true, child: result); } @@ -233,6 +254,7 @@ class MaterialPage extends Page { this.maintainState = true, this.fullscreenDialog = false, this.allowSnapshotting = true, + this.includeRouteSemantics = true, super.key, super.canPop, super.onPopInvoked, @@ -253,6 +275,9 @@ class MaterialPage extends Page { /// {@macro flutter.widgets.TransitionRoute.allowSnapshotting} final bool allowSnapshotting; + /// {@macro material_ui.MaterialPageRoute.includeRouteSemantics} + final bool includeRouteSemantics; + @override Route createRoute(BuildContext context) { return _PageBasedMaterialPageRoute(page: this, allowSnapshotting: allowSnapshotting); @@ -282,6 +307,9 @@ class _PageBasedMaterialPageRoute extends PageRoute with MaterialRouteTran @override bool get fullscreenDialog => _page.fullscreenDialog; + @override + bool get _includeRouteSemantics => _page.includeRouteSemantics; + @override String get debugLabel => '${super.debugLabel}(${_page.name})'; } diff --git a/packages/material_ui/pending_changelogs/change_2026_08_27_route_semantics.yaml b/packages/material_ui/pending_changelogs/change_2026_08_27_route_semantics.yaml new file mode 100644 index 000000000000..1fcd9dd6e871 --- /dev/null +++ b/packages/material_ui/pending_changelogs/change_2026_08_27_route_semantics.yaml @@ -0,0 +1,3 @@ +changelog: | + - Adds an option for `MaterialPageRoute` and `MaterialPage` to opt out of introducing a semantics route scope. +version: minor diff --git a/packages/material_ui/test/page_test.dart b/packages/material_ui/test/page_test.dart index 77aa9594a7b9..ed70ad94fe7d 100644 --- a/packages/material_ui/test/page_test.dart +++ b/packages/material_ui/test/page_test.dart @@ -1196,6 +1196,44 @@ void main() { }), ); + testWidgets('MaterialPageRoute can opt out of route semantics', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + + await tester.pumpWidget( + MaterialApp( + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + includeRouteSemantics: false, + builder: (BuildContext context) => const Text('Page'), + ); + }, + ), + ); + + expect(find.semantics.byFlag(SemanticsFlag.scopesRoute), findsNothing); + handle.dispose(); + }); + + testWidgets('MaterialPage can opt out of route semantics', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + + await tester.pumpWidget( + buildNavigator( + view: tester.view, + pages: const >[ + MaterialPage(includeRouteSemantics: false, child: Text('Page')), + ], + onPopPage: (Route route, dynamic result) { + assert(false); // The test shouldn't call this. + return true; + }, + ), + ); + + expect(find.semantics.byFlag(SemanticsFlag.scopesRoute), findsNothing); + handle.dispose(); + }); + testWidgets('MaterialPage works', (WidgetTester tester) async { final LocalKey pageKey = UniqueKey(); final detector = TransitionDetector(); From 9a0602678136e45030f1c1b334ac05d1e16b1ba9 Mon Sep 17 00:00:00 2001 From: Hashem Date: Fri, 28 Aug 2026 17:53:36 +0100 Subject: [PATCH 2/2] Expose route semantics on transition mixins --- packages/cupertino_ui/lib/src/route.dart | 33 ++++++++++++------------ packages/material_ui/lib/src/page.dart | 33 ++++++++++++------------ 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/packages/cupertino_ui/lib/src/route.dart b/packages/cupertino_ui/lib/src/route.dart index 6560cba3ed42..838855dc17fa 100644 --- a/packages/cupertino_ui/lib/src/route.dart +++ b/packages/cupertino_ui/lib/src/route.dart @@ -97,7 +97,17 @@ mixin CupertinoRouteTransitionMixin on PageRoute { @protected Widget buildContent(BuildContext context); - bool get _includeRouteSemantics => true; + /// {@template cupertino_ui.CupertinoRouteTransitionMixin.includeRouteSemantics} + /// Whether this route introduces a route scope in the semantics tree. + /// + /// Defaults to true. When true, screen readers can treat pushes and pops of + /// this route as navigation to a new screen and announce the change to users. + /// + /// Set this to false for routes that update only part of the screen, such as + /// tab or shell content in a nested navigator. This prevents screen readers + /// from treating the route as a new screen. + /// {@endtemplate} + bool get includeRouteSemantics => true; /// {@template cupertino_ui.CupertinoRouteTransitionMixin.title} /// A title string for this route. @@ -196,7 +206,7 @@ mixin CupertinoRouteTransitionMixin on PageRoute { Animation secondaryAnimation, ) { final Widget child = buildContent(context); - if (!_includeRouteSemantics) { + if (!includeRouteSemantics) { return child; } return Semantics(scopesRoute: true, explicitChildNodes: true, child: child); @@ -336,20 +346,9 @@ class CupertinoPageRoute extends PageRoute with CupertinoRouteTransitionMi @override final bool maintainState; - /// {@template cupertino_ui.CupertinoPageRoute.includeRouteSemantics} - /// Whether this route introduces a route scope in the semantics tree. - /// - /// Defaults to true. When true, screen readers can treat pushes and pops of - /// this route as navigation to a new screen and announce the change to users. - /// - /// Set this to false for routes that update only part of the screen, such as - /// tab or shell content in a nested navigator. This prevents screen readers - /// from treating the route as a new screen. - /// {@endtemplate} - final bool includeRouteSemantics; - + /// {@macro cupertino_ui.CupertinoRouteTransitionMixin.includeRouteSemantics} @override - bool get _includeRouteSemantics => includeRouteSemantics; + final bool includeRouteSemantics; @override String get debugLabel => '${super.debugLabel}(${settings.name})'; @@ -384,7 +383,7 @@ class _PageBasedCupertinoPageRoute extends PageRoute with CupertinoRouteTr bool get fullscreenDialog => _page.fullscreenDialog; @override - bool get _includeRouteSemantics => _page.includeRouteSemantics; + bool get includeRouteSemantics => _page.includeRouteSemantics; @override String get debugLabel => '${super.debugLabel}(${_page.name})'; @@ -438,7 +437,7 @@ class CupertinoPage extends Page { /// {@macro flutter.widgets.TransitionRoute.allowSnapshotting} final bool allowSnapshotting; - /// {@macro cupertino_ui.CupertinoPageRoute.includeRouteSemantics} + /// {@macro cupertino_ui.CupertinoRouteTransitionMixin.includeRouteSemantics} final bool includeRouteSemantics; @override diff --git a/packages/material_ui/lib/src/page.dart b/packages/material_ui/lib/src/page.dart index da7c34863004..6574955c991f 100644 --- a/packages/material_ui/lib/src/page.dart +++ b/packages/material_ui/lib/src/page.dart @@ -58,20 +58,9 @@ class MaterialPageRoute extends PageRoute with MaterialRouteTransitionMixi @override final bool maintainState; - /// {@template material_ui.MaterialPageRoute.includeRouteSemantics} - /// Whether this route introduces a route scope in the semantics tree. - /// - /// Defaults to true. When true, screen readers can treat pushes and pops of - /// this route as navigation to a new screen and announce the change to users. - /// - /// Set this to false for routes that update only part of the screen, such as - /// tab or shell content in a nested navigator. This prevents screen readers - /// from treating the route as a new screen. - /// {@endtemplate} - final bool includeRouteSemantics; - + /// {@macro material_ui.MaterialRouteTransitionMixin.includeRouteSemantics} @override - bool get _includeRouteSemantics => includeRouteSemantics; + final bool includeRouteSemantics; @override String get debugLabel => '${super.debugLabel}(${settings.name})'; @@ -103,7 +92,17 @@ mixin MaterialRouteTransitionMixin on PageRoute { @protected Widget buildContent(BuildContext context); - bool get _includeRouteSemantics => true; + /// {@template material_ui.MaterialRouteTransitionMixin.includeRouteSemantics} + /// Whether this route introduces a route scope in the semantics tree. + /// + /// Defaults to true. When true, screen readers can treat pushes and pops of + /// this route as navigation to a new screen and announce the change to users. + /// + /// Set this to false for routes that update only part of the screen, such as + /// tab or shell content in a nested navigator. This prevents screen readers + /// from treating the route as a new screen. + /// {@endtemplate} + bool get includeRouteSemantics => true; @override Duration get transitionDuration => @@ -209,7 +208,7 @@ mixin MaterialRouteTransitionMixin on PageRoute { Animation secondaryAnimation, ) { final Widget result = buildContent(context); - if (!_includeRouteSemantics) { + if (!includeRouteSemantics) { return result; } return Semantics(scopesRoute: true, explicitChildNodes: true, child: result); @@ -275,7 +274,7 @@ class MaterialPage extends Page { /// {@macro flutter.widgets.TransitionRoute.allowSnapshotting} final bool allowSnapshotting; - /// {@macro material_ui.MaterialPageRoute.includeRouteSemantics} + /// {@macro material_ui.MaterialRouteTransitionMixin.includeRouteSemantics} final bool includeRouteSemantics; @override @@ -308,7 +307,7 @@ class _PageBasedMaterialPageRoute extends PageRoute with MaterialRouteTran bool get fullscreenDialog => _page.fullscreenDialog; @override - bool get _includeRouteSemantics => _page.includeRouteSemantics; + bool get includeRouteSemantics => _page.includeRouteSemantics; @override String get debugLabel => '${super.debugLabel}(${_page.name})';