diff --git a/packages/flutter/lib/src/cupertino/route.dart b/packages/flutter/lib/src/cupertino/route.dart index b7038f0401..3f12c12434 100644 --- a/packages/flutter/lib/src/cupertino/route.dart +++ b/packages/flutter/lib/src/cupertino/route.dart @@ -864,6 +864,10 @@ class _CupertinoModalPopupRoute extends PopupRoute { /// It is only used when the method is called. Its corresponding widget can be /// safely removed from the tree before the popup is closed. /// +/// The `useRootNavigator` argument is used to determine whether to push the +/// popup to the [Navigator] furthest from or nearest to the given `context`. It +/// is `false` by default. +/// /// The `builder` argument typically builds a [CupertinoActionSheet] widget. /// Content below the widget is dimmed with a [ModalBarrier]. The widget built /// by the `builder` does not share a context with the location that @@ -882,8 +886,10 @@ class _CupertinoModalPopupRoute extends PopupRoute { Future showCupertinoModalPopup({ @required BuildContext context, @required WidgetBuilder builder, + bool useRootNavigator = true, }) { - return Navigator.of(context, rootNavigator: true).push( + assert(useRootNavigator != null); + return Navigator.of(context, rootNavigator: useRootNavigator).push( _CupertinoModalPopupRoute( builder: builder, barrierLabel: 'Dismiss', @@ -933,14 +939,18 @@ Widget _buildCupertinoDialogTransitions(BuildContext context, Animation /// It is only used when the method is called. Its corresponding widget can /// be safely removed from the tree before the dialog is closed. /// -/// Returns a [Future] that resolves to the value (if any) that was passed to -/// [Navigator.pop] when the dialog was closed. +/// The `useRootNavigator` argument is used to determine whether to push the +/// dialog to the [Navigator] furthest from or nearest to the given `context`. +/// By default, `useRootNavigator` is `true` and the dialog route created by +/// this method is pushed to the root navigator. /// -/// The dialog route created by this method is pushed to the root navigator. /// If the application has multiple [Navigator] objects, it may be necessary to /// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the /// dialog rather than just `Navigator.pop(context, result)`. /// +/// Returns a [Future] that resolves to the value (if any) that was passed to +/// [Navigator.pop] when the dialog was closed. +/// /// See also: /// /// * [CupertinoDialog], an iOS-style dialog. @@ -951,8 +961,10 @@ Widget _buildCupertinoDialogTransitions(BuildContext context, Animation Future showCupertinoDialog({ @required BuildContext context, @required WidgetBuilder builder, + bool useRootNavigator = true, }) { assert(builder != null); + assert(useRootNavigator != null); return showGeneralDialog( context: context, barrierDismissible: false, @@ -963,5 +975,6 @@ Future showCupertinoDialog({ return builder(context); }, transitionBuilder: _buildCupertinoDialogTransitions, + useRootNavigator: useRootNavigator, ); } diff --git a/packages/flutter/lib/src/material/about.dart b/packages/flutter/lib/src/material/about.dart index 74b612b03a..4091177c8d 100644 --- a/packages/flutter/lib/src/material/about.dart +++ b/packages/flutter/lib/src/material/about.dart @@ -223,8 +223,8 @@ class AboutListTile extends StatelessWidget { /// The licenses shown on the [LicensePage] are those returned by the /// [LicenseRegistry] API, which can be used to add more licenses to the list. /// -/// The `context` argument is passed to [showDialog], the documentation for -/// which discusses how it is used. +/// The [context] and [useRootNavigator] arguments are passed to [showDialog], +/// the documentation for which discusses how it is used. void showAboutDialog({ @required BuildContext context, String applicationName, @@ -232,10 +232,13 @@ void showAboutDialog({ Widget applicationIcon, String applicationLegalese, List children, + bool useRootNavigator = true, }) { assert(context != null); + assert(useRootNavigator != null); showDialog( context: context, + useRootNavigator: useRootNavigator, builder: (BuildContext context) { return AboutDialog( applicationName: applicationName, @@ -251,7 +254,13 @@ void showAboutDialog({ /// Displays a [LicensePage], which shows licenses for software used by the /// application. /// -/// The arguments correspond to the properties on [LicensePage]. +/// The application arguments correspond to the properties on [LicensePage]. +/// +/// The `context` argument is used to look up the [Navigator] for the page. +/// +/// The `useRootNavigator` argument is used to determine whether to push the +/// page to the [Navigator] furthest from or nearest to the given `context`. It +/// is `false` by default. /// /// If the application has a [Drawer], consider using [AboutListTile] instead /// of calling this directly. @@ -267,9 +276,11 @@ void showLicensePage({ String applicationVersion, Widget applicationIcon, String applicationLegalese, + bool useRootNavigator = false, }) { assert(context != null); - Navigator.push(context, MaterialPageRoute( + assert(useRootNavigator != null); + Navigator.of(context, rootNavigator: useRootNavigator).push(MaterialPageRoute( builder: (BuildContext context) => LicensePage( applicationName: applicationName, applicationVersion: applicationVersion, diff --git a/packages/flutter/lib/src/material/date_picker.dart b/packages/flutter/lib/src/material/date_picker.dart index 2633f093cb..32e45e67ce 100644 --- a/packages/flutter/lib/src/material/date_picker.dart +++ b/packages/flutter/lib/src/material/date_picker.dart @@ -1085,8 +1085,8 @@ typedef SelectableDayPredicate = bool Function(DateTime day); /// provided by [Directionality]. If both [locale] and [textDirection] are not /// null, [textDirection] overrides the direction chosen for the [locale]. /// -/// The [context] argument is passed to [showDialog], the documentation for -/// which discusses how it is used. +/// The [context] and [useRootNavigator] arguments are passed to [showDialog], +/// the documentation for which discusses how it is used. /// /// The [builder] parameter can be used to wrap the dialog widget /// to add inherited widgets like [Theme]. @@ -1133,10 +1133,12 @@ Future showDatePicker({ Locale locale, TextDirection textDirection, TransitionBuilder builder, + bool useRootNavigator = true, }) async { assert(initialDate != null); assert(firstDate != null); assert(lastDate != null); + assert(useRootNavigator != null); assert(!initialDate.isBefore(firstDate), 'initialDate must be on or after firstDate'); assert(!initialDate.isAfter(lastDate), 'initialDate must be on or before lastDate'); assert(!firstDate.isAfter(lastDate), 'lastDate must be on or after firstDate'); @@ -1173,6 +1175,7 @@ Future showDatePicker({ return await showDialog( context: context, + useRootNavigator: useRootNavigator, builder: (BuildContext context) { return builder == null ? child : builder(context, child); }, diff --git a/packages/flutter/lib/src/material/dialog.dart b/packages/flutter/lib/src/material/dialog.dart index 1314cd0a3b..de0c52a851 100644 --- a/packages/flutter/lib/src/material/dialog.dart +++ b/packages/flutter/lib/src/material/dialog.dart @@ -655,20 +655,24 @@ Widget _buildMaterialDialogTransitions(BuildContext context, Animation a /// `showDialog` is originally called from. Use a [StatefulBuilder] or a /// custom [StatefulWidget] if the dialog needs to update dynamically. /// +/// The `child` argument is deprecated, and should be replaced with `builder`. +/// /// The `context` argument is used to look up the [Navigator] and [Theme] for /// the dialog. It is only used when the method is called. Its corresponding /// widget can be safely removed from the tree before the dialog is closed. /// -/// The `child` argument is deprecated, and should be replaced with `builder`. +/// The `useRootNavigator` argument is used to determine whether to push the +/// dialog to the [Navigator] furthest from or nearest to the given `context`. +/// By default, `useRootNavigator` is `true` and the dialog route created by +/// this method is pushed to the root navigator. /// -/// Returns a [Future] that resolves to the value (if any) that was passed to -/// [Navigator.pop] when the dialog was closed. -/// -/// The dialog route created by this method is pushed to the root navigator. /// If the application has multiple [Navigator] objects, it may be necessary to /// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the /// dialog rather than just `Navigator.pop(context, result)`. /// +/// Returns a [Future] that resolves to the value (if any) that was passed to +/// [Navigator.pop] when the dialog was closed. +/// /// See also: /// /// * [AlertDialog], for dialogs that have a row of buttons below a body. @@ -687,8 +691,10 @@ Future showDialog({ 'is appropriate for widgets built in the dialog.' ) Widget child, WidgetBuilder builder, + bool useRootNavigator = true, }) { assert(child == null || builder == null); + assert(useRootNavigator != null); assert(debugCheckHasMaterialLocalizations(context)); final ThemeData theme = Theme.of(context, shadowThemeOnly: true); @@ -711,5 +717,6 @@ Future showDialog({ barrierColor: Colors.black54, transitionDuration: const Duration(milliseconds: 150), transitionBuilder: _buildMaterialDialogTransitions, + useRootNavigator: useRootNavigator, ); } diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart index d54195bb3a..79107c4f6a 100644 --- a/packages/flutter/lib/src/material/popup_menu.dart +++ b/packages/flutter/lib/src/material/popup_menu.dart @@ -790,6 +790,10 @@ class _PopupMenuRoute extends PopupRoute { /// the menu. It is only used when the method is called. Its corresponding /// widget can be safely removed from the tree before the popup menu is closed. /// +/// The `useRootNavigator` argument is used to determine whether to push the +/// menu to the [Navigator] furthest from or nearest to the given `context`. It +/// is `false` by default. +/// /// The `semanticLabel` argument is used by accessibility frameworks to /// announce screen transitions when the menu is opened and closed. If this /// label is not provided, it will default to @@ -814,9 +818,11 @@ Future showMenu({ ShapeBorder shape, Color color, bool captureInheritedThemes = true, + bool useRootNavigator = false, }) { assert(context != null); assert(position != null); + assert(useRootNavigator != null); assert(items != null && items.isNotEmpty); assert(captureInheritedThemes != null); assert(debugCheckHasMaterialLocalizations(context)); @@ -831,7 +837,7 @@ Future showMenu({ label = semanticLabel ?? MaterialLocalizations.of(context)?.popupMenuLabel; } - return Navigator.push(context, _PopupMenuRoute( + return Navigator.of(context, rootNavigator: useRootNavigator).push(_PopupMenuRoute( position: position, items: items, initialValue: initialValue, diff --git a/packages/flutter/lib/src/material/time_picker.dart b/packages/flutter/lib/src/material/time_picker.dart index 5bad4243bc..6ee0ef9827 100644 --- a/packages/flutter/lib/src/material/time_picker.dart +++ b/packages/flutter/lib/src/material/time_picker.dart @@ -1731,8 +1731,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { /// ``` /// {@end-tool} /// -/// The [context] argument is passed to [showDialog], the documentation for -/// which discusses how it is used. +/// The [context] and [useRootNavigator] arguments are passed to [showDialog], +/// the documentation for which discusses how it is used. /// /// The [builder] parameter can be used to wrap the dialog widget /// to add inherited widgets like [Localizations.override], @@ -1780,14 +1780,17 @@ Future showTimePicker({ @required BuildContext context, @required TimeOfDay initialTime, TransitionBuilder builder, + bool useRootNavigator = true, }) async { assert(context != null); assert(initialTime != null); + assert(useRootNavigator != null); assert(debugCheckHasMaterialLocalizations(context)); final Widget dialog = _TimePickerDialog(initialTime: initialTime); return await showDialog( context: context, + useRootNavigator: useRootNavigator, builder: (BuildContext context) { return builder == null ? dialog : builder(context, dialog); }, diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index 2fbf9758c7..1d2f04b265 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -1560,9 +1560,18 @@ class _DialogRoute extends PopupRoute { /// [StatefulWidget] if the dialog needs to update dynamically. The /// `pageBuilder` argument can not be null. /// -/// The `context` argument is used to look up the [Navigator] for the dialog. -/// It is only used when the method is called. Its corresponding widget can -/// be safely removed from the tree before the dialog is closed. +/// The `context` argument is used to look up the [Navigator] for the +/// dialog. It is only used when the method is called. Its corresponding widget +/// can be safely removed from the tree before the dialog is closed. +/// +/// The `useRootNavigator` argument is used to determine whether to push the +/// dialog to the [Navigator] furthest from or nearest to the given `context`. +/// By default, `useRootNavigator` is `true` and the dialog route created by +/// this method is pushed to the root navigator. +/// +/// If the application has multiple [Navigator] objects, it may be necessary to +/// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the +/// dialog rather than just `Navigator.pop(context, result)`. /// /// The `barrierDismissible` argument is used to determine whether this route /// can be dismissed by tapping the modal barrier. This argument defaults @@ -1586,11 +1595,6 @@ class _DialogRoute extends PopupRoute { /// Returns a [Future] that resolves to the value (if any) that was passed to /// [Navigator.pop] when the dialog was closed. /// -/// The dialog route created by this method is pushed to the root navigator. -/// If the application has multiple [Navigator] objects, it may be necessary to -/// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the -/// dialog rather than just `Navigator.pop(context, result)`. -/// /// See also: /// /// * [showDialog], which displays a Material-style dialog. @@ -1603,10 +1607,12 @@ Future showGeneralDialog({ Color barrierColor, Duration transitionDuration, RouteTransitionsBuilder transitionBuilder, + bool useRootNavigator = true, }) { assert(pageBuilder != null); + assert(useRootNavigator != null); assert(!barrierDismissible || barrierLabel != null); - return Navigator.of(context, rootNavigator: true).push(_DialogRoute( + return Navigator.of(context, rootNavigator: useRootNavigator).push(_DialogRoute( pageBuilder: pageBuilder, barrierDismissible: barrierDismissible, barrierLabel: barrierLabel, diff --git a/packages/flutter/test/cupertino/route_test.dart b/packages/flutter/test/cupertino/route_test.dart index cb02e9b750..f33ef83766 100644 --- a/packages/flutter/test/cupertino/route_test.dart +++ b/packages/flutter/test/cupertino/route_test.dart @@ -918,6 +918,164 @@ void main() { expect(homeTapCount, 1); expect(pageTapCount, 1); }); + + testWidgets('showCupertinoModalPopup uses root navigator by default', (WidgetTester tester) async { + final PopupObserver rootObserver = PopupObserver(); + final PopupObserver nestedObserver = PopupObserver(); + + await tester.pumpWidget(CupertinoApp( + navigatorObservers: [rootObserver], + home: Navigator( + observers: [nestedObserver], + onGenerateRoute: (RouteSettings settings) { + return PageRouteBuilder( + pageBuilder: (BuildContext context, Animation _, Animation __) { + return GestureDetector( + onTap: () async { + await showCupertinoModalPopup( + context: context, + builder: (BuildContext context) => const SizedBox(), + ); + }, + child: const Text('tap'), + ); + }, + ); + }, + ), + )); + + // Open the dialog. + await tester.tap(find.text('tap')); + + expect(rootObserver.popupCount, 1); + expect(nestedObserver.popupCount, 0); + }); + + testWidgets('showCupertinoModalPopup uses nested navigator if useRootNavigator is false', (WidgetTester tester) async { + final PopupObserver rootObserver = PopupObserver(); + final PopupObserver nestedObserver = PopupObserver(); + + await tester.pumpWidget(CupertinoApp( + navigatorObservers: [rootObserver], + home: Navigator( + observers: [nestedObserver], + onGenerateRoute: (RouteSettings settings) { + return PageRouteBuilder( + pageBuilder: (BuildContext context, Animation _, Animation __) { + return GestureDetector( + onTap: () async { + await showCupertinoModalPopup( + context: context, + useRootNavigator: false, + builder: (BuildContext context) => const SizedBox(), + ); + }, + child: const Text('tap'), + ); + }, + ); + }, + ), + )); + + // Open the dialog. + await tester.tap(find.text('tap')); + + expect(rootObserver.popupCount, 0); + expect(nestedObserver.popupCount, 1); + }); + + testWidgets('showCupertinoDialog uses root navigator by default', (WidgetTester tester) async { + final DialogObserver rootObserver = DialogObserver(); + final DialogObserver nestedObserver = DialogObserver(); + + await tester.pumpWidget(CupertinoApp( + navigatorObservers: [rootObserver], + home: Navigator( + observers: [nestedObserver], + onGenerateRoute: (RouteSettings settings) { + return PageRouteBuilder( + pageBuilder: (BuildContext context, Animation _, Animation __) { + return GestureDetector( + onTap: () async { + await showCupertinoDialog( + context: context, + builder: (BuildContext context) => const SizedBox(), + ); + }, + child: const Text('tap'), + ); + }, + ); + }, + ), + )); + + // Open the dialog. + await tester.tap(find.text('tap')); + + expect(rootObserver.dialogCount, 1); + expect(nestedObserver.dialogCount, 0); + }); + + testWidgets('showCupertinoDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async { + final DialogObserver rootObserver = DialogObserver(); + final DialogObserver nestedObserver = DialogObserver(); + + await tester.pumpWidget(CupertinoApp( + navigatorObservers: [rootObserver], + home: Navigator( + observers: [nestedObserver], + onGenerateRoute: (RouteSettings settings) { + return PageRouteBuilder( + pageBuilder: (BuildContext context, Animation _, Animation __) { + return GestureDetector( + onTap: () async { + await showCupertinoDialog( + context: context, + useRootNavigator: false, + builder: (BuildContext context) => const SizedBox(), + ); + }, + child: const Text('tap'), + ); + }, + ); + }, + ), + )); + + // Open the dialog. + await tester.tap(find.text('tap')); + + expect(rootObserver.dialogCount, 0); + expect(nestedObserver.dialogCount, 1); + }); } class MockNavigatorObserver extends Mock implements NavigatorObserver {} + +class PopupObserver extends NavigatorObserver { + int popupCount = 0; + + @override + void didPush(Route route, Route previousRoute) { + if (route.toString().contains('_CupertinoModalPopupRoute')) { + popupCount++; + } + super.didPush(route, previousRoute); + } +} + +class DialogObserver extends NavigatorObserver { + int dialogCount = 0; + + @override + void didPush(Route route, Route previousRoute) { + if (route.toString().contains('_DialogRoute')) { + dialogCount++; + } + super.didPush(route, previousRoute); + } +} diff --git a/packages/flutter/test/material/about_test.dart b/packages/flutter/test/material/about_test.dart index 5c716ea9b7..976c2c4996 100644 --- a/packages/flutter/test/material/about_test.dart +++ b/packages/flutter/test/material/about_test.dart @@ -305,6 +305,150 @@ void main() { tileRect = tester.getRect(find.byType(AboutListTile)); expect(tileRect.height, 48.0); }); + + testWidgets('showLicensePage uses nested navigator by default', (WidgetTester tester) async { + final LicensePageObserver rootObserver = LicensePageObserver(); + final LicensePageObserver nestedObserver = LicensePageObserver(); + + await tester.pumpWidget(MaterialApp( + navigatorObservers: [rootObserver], + initialRoute: '/', + onGenerateRoute: (_) { + return PageRouteBuilder( + pageBuilder: (_, __, ___) => Navigator( + observers: [nestedObserver], + onGenerateRoute: (RouteSettings settings) { + return PageRouteBuilder( + pageBuilder: (BuildContext context, _, __) { + return RaisedButton( + onPressed: () { + showLicensePage( + context: context, + applicationName: 'A', + ); + }, + child: const Text('Show License Page'), + ); + }, + ); + }, + ), + ); + }, + )); + + // Open the dialog. + await tester.tap(find.byType(RaisedButton)); + + expect(rootObserver.licensePageCount, 0); + expect(nestedObserver.licensePageCount, 1); + }); + + testWidgets('showLicensePage uses root navigator if useRootNavigator is true', (WidgetTester tester) async { + final LicensePageObserver rootObserver = LicensePageObserver(); + final LicensePageObserver nestedObserver = LicensePageObserver(); + + await tester.pumpWidget(MaterialApp( + navigatorObservers: [rootObserver], + initialRoute: '/', + onGenerateRoute: (_) { + return PageRouteBuilder( + pageBuilder: (_, __, ___) => Navigator( + observers: [nestedObserver], + onGenerateRoute: (RouteSettings settings) { + return PageRouteBuilder( + pageBuilder: (BuildContext context, _, __) { + return RaisedButton( + onPressed: () { + showLicensePage( + context: context, + useRootNavigator: true, + applicationName: 'A', + ); + }, + child: const Text('Show License Page'), + ); + }, + ); + }, + ), + ); + }, + )); + + // Open the dialog. + await tester.tap(find.byType(RaisedButton)); + + expect(rootObserver.licensePageCount, 1); + expect(nestedObserver.licensePageCount, 0); + }); + + testWidgets('showAboutDialog uses root navigator by default', (WidgetTester tester) async { + final AboutDialogObserver rootObserver = AboutDialogObserver(); + final AboutDialogObserver nestedObserver = AboutDialogObserver(); + + await tester.pumpWidget(MaterialApp( + navigatorObservers: [rootObserver], + home: Navigator( + observers: [nestedObserver], + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + builder: (BuildContext context) { + return RaisedButton( + onPressed: () { + showAboutDialog( + context: context, + applicationName: 'A', + ); + }, + child: const Text('Show About Dialog'), + ); + }, + ); + }, + ), + )); + + // Open the dialog. + await tester.tap(find.byType(RaisedButton)); + + expect(rootObserver.dialogCount, 1); + expect(nestedObserver.dialogCount, 0); + }); + + testWidgets('showAboutDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async { + final AboutDialogObserver rootObserver = AboutDialogObserver(); + final AboutDialogObserver nestedObserver = AboutDialogObserver(); + + await tester.pumpWidget(MaterialApp( + navigatorObservers: [rootObserver], + home: Navigator( + observers: [nestedObserver], + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + builder: (BuildContext context) { + return RaisedButton( + onPressed: () { + showAboutDialog( + context: context, + useRootNavigator: false, + applicationName: 'A', + ); + }, + child: const Text('Show About Dialog'), + ); + }, + ); + }, + ), + )); + + // Open the dialog. + await tester.tap(find.byType(RaisedButton)); + + expect(rootObserver.dialogCount, 0); + expect(nestedObserver.dialogCount, 1); + }); } class FakeLicenseEntry extends LicenseEntry { @@ -322,3 +466,27 @@ class FakeLicenseEntry extends LicenseEntry { return []; } } + +class LicensePageObserver extends NavigatorObserver { + int licensePageCount = 0; + + @override + void didPush(Route route, Route previousRoute) { + if (route is MaterialPageRoute) { + licensePageCount++; + } + super.didPush(route, previousRoute); + } +} + +class AboutDialogObserver extends NavigatorObserver { + int dialogCount = 0; + + @override + void didPush(Route route, Route previousRoute) { + if (route.toString().contains('_DialogRoute')) { + dialogCount++; + } + super.didPush(route, previousRoute); + } +} diff --git a/packages/flutter/test/material/date_picker_test.dart b/packages/flutter/test/material/date_picker_test.dart index cf05624b24..a1303a4ecf 100644 --- a/packages/flutter/test/material/date_picker_test.dart +++ b/packages/flutter/test/material/date_picker_test.dart @@ -897,4 +897,90 @@ void _tests() { }); }); + testWidgets('uses root navigator by default', (WidgetTester tester) async { + final DatePickerObserver rootObserver = DatePickerObserver(); + final DatePickerObserver nestedObserver = DatePickerObserver(); + + await tester.pumpWidget(MaterialApp( + navigatorObservers: [rootObserver], + home: Navigator( + observers: [nestedObserver], + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + builder: (BuildContext context) { + return RaisedButton( + onPressed: () { + showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(2018), + lastDate: DateTime(2030), + builder: (BuildContext context, Widget child) { + return const SizedBox(); + }, + ); + }, + child: const Text('Show Date Picker'), + ); + }, + ); + }, + ), + )); + + // Open the dialog. + await tester.tap(find.byType(RaisedButton)); + + expect(rootObserver.datePickerCount, 1); + expect(nestedObserver.datePickerCount, 0); + }); + + testWidgets('uses nested navigator if useRootNavigator is false', (WidgetTester tester) async { + final DatePickerObserver rootObserver = DatePickerObserver(); + final DatePickerObserver nestedObserver = DatePickerObserver(); + + await tester.pumpWidget(MaterialApp( + navigatorObservers: [rootObserver], + home: Navigator( + observers: [nestedObserver], + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + builder: (BuildContext context) { + return RaisedButton( + onPressed: () { + showDatePicker( + context: context, + useRootNavigator: false, + initialDate: DateTime.now(), + firstDate: DateTime(2018), + lastDate: DateTime(2030), + builder: (BuildContext context, Widget child) => const SizedBox(), + ); + }, + child: const Text('Show Date Picker'), + ); + }, + ); + }, + ), + )); + + // Open the dialog. + await tester.tap(find.byType(RaisedButton)); + + expect(rootObserver.datePickerCount, 0); + expect(nestedObserver.datePickerCount, 1); + }); +} + +class DatePickerObserver extends NavigatorObserver { + int datePickerCount = 0; + + @override + void didPush(Route route, Route previousRoute) { + if (route.toString().contains('_DialogRoute')) { + datePickerCount++; + } + super.didPush(route, previousRoute); + } } diff --git a/packages/flutter/test/material/dialog_test.dart b/packages/flutter/test/material/dialog_test.dart index 0bceb811a7..0143eaabd2 100644 --- a/packages/flutter/test/material/dialog_test.dart +++ b/packages/flutter/test/material/dialog_test.dart @@ -646,6 +646,77 @@ void main() { await tester.pump(); }); + testWidgets('showDialog uses root navigator by default', (WidgetTester tester) async { + final DialogObserver rootObserver = DialogObserver(); + final DialogObserver nestedObserver = DialogObserver(); + + await tester.pumpWidget(MaterialApp( + navigatorObservers: [rootObserver], + home: Navigator( + observers: [nestedObserver], + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + builder: (BuildContext context) { + return RaisedButton( + onPressed: () { + showDialog( + context: context, + builder: (BuildContext innerContext) { + return const AlertDialog(title: Text('Title')); + }, + ); + }, + child: const Text('Show Dialog'), + ); + }, + ); + }, + ), + )); + + // Open the dialog. + await tester.tap(find.byType(RaisedButton)); + + expect(rootObserver.dialogCount, 1); + expect(nestedObserver.dialogCount, 0); + }); + + testWidgets('showDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async { + final DialogObserver rootObserver = DialogObserver(); + final DialogObserver nestedObserver = DialogObserver(); + + await tester.pumpWidget(MaterialApp( + navigatorObservers: [rootObserver], + home: Navigator( + observers: [nestedObserver], + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + builder: (BuildContext context) { + return RaisedButton( + onPressed: () { + showDialog( + context: context, + useRootNavigator: false, + builder: (BuildContext innerContext) { + return const AlertDialog(title: Text('Title')); + }, + ); + }, + child: const Text('Show Dialog'), + ); + }, + ); + }, + ), + )); + + // Open the dialog. + await tester.tap(find.byType(RaisedButton)); + + expect(rootObserver.dialogCount, 0); + expect(nestedObserver.dialogCount, 1); + }); + group('Scrollable title and content', () { testWidgets('Title is scrollable', (WidgetTester tester) async { final Key titleKey = UniqueKey(); @@ -723,3 +794,15 @@ void main() { }); }); } + +class DialogObserver extends NavigatorObserver { + int dialogCount = 0; + + @override + void didPush(Route route, Route previousRoute) { + if (route.toString().contains('_DialogRoute')) { + dialogCount++; + } + super.didPush(route, previousRoute); + } +} diff --git a/packages/flutter/test/material/popup_menu_test.dart b/packages/flutter/test/material/popup_menu_test.dart index 791cb52f3c..44ca6ea71c 100644 --- a/packages/flutter/test/material/popup_menu_test.dart +++ b/packages/flutter/test/material/popup_menu_test.dart @@ -1114,6 +1114,83 @@ void main() { expect(find.text('PopupMenuButton icon'), findsOneWidget); }); + + testWidgets('showMenu uses nested navigator by default', (WidgetTester tester) async { + final MenuObserver rootObserver = MenuObserver(); + final MenuObserver nestedObserver = MenuObserver(); + + await tester.pumpWidget(MaterialApp( + navigatorObservers: [rootObserver], + home: Navigator( + observers: [nestedObserver], + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + builder: (BuildContext context) { + return RaisedButton( + onPressed: () { + showMenu( + context: context, + position: const RelativeRect.fromLTRB(0, 0, 0, 0), + items: >[ + const PopupMenuItem( + value: 1, child: Text('1'), + ), + ], + ); + }, + child: const Text('Show Menu'), + ); + }, + ); + }, + ), + )); + + // Open the dialog. + await tester.tap(find.byType(RaisedButton)); + + expect(rootObserver.menuCount, 0); + expect(nestedObserver.menuCount, 1); + }); + + testWidgets('showMenu uses root navigator if useRootNavigator is true', (WidgetTester tester) async { + final MenuObserver rootObserver = MenuObserver(); + final MenuObserver nestedObserver = MenuObserver(); + + await tester.pumpWidget(MaterialApp( + navigatorObservers: [rootObserver], + home: Navigator( + observers: [nestedObserver], + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + builder: (BuildContext context) { + return RaisedButton( + onPressed: () { + showMenu( + context: context, + useRootNavigator: true, + position: const RelativeRect.fromLTRB(0, 0, 0, 0), + items: >[ + const PopupMenuItem( + value: 1, child: Text('1'), + ), + ], + ); + }, + child: const Text('Show Menu'), + ); + }, + ); + }, + ), + )); + + // Open the dialog. + await tester.tap(find.byType(RaisedButton)); + + expect(rootObserver.menuCount, 1); + expect(nestedObserver.menuCount, 0); + }); } class TestApp extends StatefulWidget { @@ -1153,3 +1230,15 @@ class _TestAppState extends State { ); } } + +class MenuObserver extends NavigatorObserver { + int menuCount = 0; + + @override + void didPush(Route route, Route previousRoute) { + if (route.toString().contains('_PopupMenuRoute')) { + menuCount++; + } + super.didPush(route, previousRoute); + } +} diff --git a/packages/flutter/test/material/time_picker_test.dart b/packages/flutter/test/material/time_picker_test.dart index 8cacc1e896..134ebf7d4e 100644 --- a/packages/flutter/test/material/time_picker_test.dart +++ b/packages/flutter/test/material/time_picker_test.dart @@ -622,6 +622,73 @@ void _tests() { // button and the right edge of the 800 wide window. expect(tester.getBottomLeft(find.text('OK')).dx, 800 - ltrOkRight); }); + + testWidgets('uses root navigator by default', (WidgetTester tester) async { + final PickerObserver rootObserver = PickerObserver(); + final PickerObserver nestedObserver = PickerObserver(); + + await tester.pumpWidget(MaterialApp( + navigatorObservers: [rootObserver], + home: Navigator( + observers: [nestedObserver], + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + builder: (BuildContext context) { + return RaisedButton( + onPressed: () { + showTimePicker( + context: context, + initialTime: const TimeOfDay(hour: 7, minute: 0), + ); + }, + child: const Text('Show Picker'), + ); + }, + ); + }, + ), + )); + + // Open the dialog. + await tester.tap(find.byType(RaisedButton)); + + expect(rootObserver.pickerCount, 1); + expect(nestedObserver.pickerCount, 0); + }); + + testWidgets('uses nested navigator if useRootNavigator is false', (WidgetTester tester) async { + final PickerObserver rootObserver = PickerObserver(); + final PickerObserver nestedObserver = PickerObserver(); + + await tester.pumpWidget(MaterialApp( + navigatorObservers: [rootObserver], + home: Navigator( + observers: [nestedObserver], + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + builder: (BuildContext context) { + return RaisedButton( + onPressed: () { + showTimePicker( + context: context, + useRootNavigator: false, + initialTime: const TimeOfDay(hour: 7, minute: 0), + ); + }, + child: const Text('Show Picker'), + ); + }, + ); + }, + ), + )); + + // Open the dialog. + await tester.tap(find.byType(RaisedButton)); + + expect(rootObserver.pickerCount, 0); + expect(nestedObserver.pickerCount, 1); + }); } final Finder findDialPaint = find.descendant( @@ -695,3 +762,15 @@ class _CustomPainterSemanticsTester { expect(tester.renderObject(findDialPaint), expectedLabels); } } + +class PickerObserver extends NavigatorObserver { + int pickerCount = 0; + + @override + void didPush(Route route, Route previousRoute) { + if (route.toString().contains('_DialogRoute')) { + pickerCount++; + } + super.didPush(route, previousRoute); + } +} diff --git a/packages/flutter/test/widgets/routes_test.dart b/packages/flutter/test/widgets/routes_test.dart index 2b0decf0dc..26b6542445 100644 --- a/packages/flutter/test/widgets/routes_test.dart +++ b/packages/flutter/test/widgets/routes_test.dart @@ -788,6 +788,81 @@ void main() { expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation); expect(trainHopper2.currentTrain, isNull); // Has been disposed. }); + + testWidgets('showGeneralDialog uses root navigator by default', (WidgetTester tester) async { + final DialogObserver rootObserver = DialogObserver(); + final DialogObserver nestedObserver = DialogObserver(); + + await tester.pumpWidget(MaterialApp( + navigatorObservers: [rootObserver], + home: Navigator( + observers: [nestedObserver], + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + builder: (BuildContext context) { + return RaisedButton( + onPressed: () { + showGeneralDialog( + context: context, + barrierDismissible: false, + transitionDuration: Duration.zero, + pageBuilder: (BuildContext innerContext, _, __) { + return const SizedBox(); + }, + ); + }, + child: const Text('Show Dialog'), + ); + }, + ); + }, + ), + )); + + // Open the dialog. + await tester.tap(find.byType(RaisedButton)); + + expect(rootObserver.dialogCount, 1); + expect(nestedObserver.dialogCount, 0); + }); + + testWidgets('showGeneralDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async { + final DialogObserver rootObserver = DialogObserver(); + final DialogObserver nestedObserver = DialogObserver(); + + await tester.pumpWidget(MaterialApp( + navigatorObservers: [rootObserver], + home: Navigator( + observers: [nestedObserver], + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + builder: (BuildContext context) { + return RaisedButton( + onPressed: () { + showGeneralDialog( + useRootNavigator: false, + context: context, + barrierDismissible: false, + transitionDuration: Duration.zero, + pageBuilder: (BuildContext innerContext, _, __) { + return const SizedBox(); + }, + ); + }, + child: const Text('Show Dialog'), + ); + }, + ); + }, + ), + )); + + // Open the dialog. + await tester.tap(find.byType(RaisedButton)); + + expect(rootObserver.dialogCount, 0); + expect(nestedObserver.dialogCount, 1); + }); }); } @@ -805,3 +880,15 @@ class TestPageRouteBuilder extends PageRouteBuilder { return CurvedAnimation(parent: super.createAnimation(), curve: Curves.easeOutExpo); } } + +class DialogObserver extends NavigatorObserver { + int dialogCount = 0; + + @override + void didPush(Route route, Route previousRoute) { + if (route.toString().contains('_DialogRoute')) { + dialogCount++; + } + super.didPush(route, previousRoute); + } +}