diff --git a/packages/flutter/lib/src/cupertino/route.dart b/packages/flutter/lib/src/cupertino/route.dart index 0206f7604c4..fdfe92b0742 100644 --- a/packages/flutter/lib/src/cupertino/route.dart +++ b/packages/flutter/lib/src/cupertino/route.dart @@ -1277,6 +1277,10 @@ class CupertinoModalPopupRoute extends PopupRoute { /// [StatefulBuilder] or a custom [StatefulWidget] if the widget needs to /// update dynamically. /// +/// The [requestFocus] parameter is used to specify whether the popup should +/// request focus when shown. +/// {@macro flutter.widgets.navigator.Route.requestFocus} +/// /// {@macro flutter.widgets.RawDialogRoute} /// /// Returns a `Future` that resolves to the value that was passed to @@ -1318,6 +1322,7 @@ Future showCupertinoModalPopup({ bool semanticsDismissible = false, RouteSettings? routeSettings, Offset? anchorPoint, + bool? requestFocus, }) { return Navigator.of(context, rootNavigator: useRootNavigator).push( CupertinoModalPopupRoute( @@ -1328,6 +1333,7 @@ Future showCupertinoModalPopup({ semanticsDismissible: semanticsDismissible, settings: routeSettings, anchorPoint: anchorPoint, + requestFocus: requestFocus, ), ); } @@ -1361,6 +1367,9 @@ Widget _buildCupertinoDialogTransitions( /// By default, `useRootNavigator` is `true` and the dialog route created by /// this method is pushed to the root navigator. /// +/// {@macro flutter.material.dialog.requestFocus} +/// {@macro flutter.widgets.navigator.Route.requestFocus} +/// /// {@macro flutter.widgets.RawDialogRoute} /// /// If the application has multiple [Navigator] objects, it may be necessary to @@ -1405,6 +1414,7 @@ Future showCupertinoDialog({ bool barrierDismissible = false, RouteSettings? routeSettings, Offset? anchorPoint, + bool? requestFocus, }) { return Navigator.of(context, rootNavigator: useRootNavigator).push( CupertinoDialogRoute( @@ -1415,6 +1425,7 @@ Future showCupertinoDialog({ barrierColor: CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context), settings: routeSettings, anchorPoint: anchorPoint, + requestFocus: requestFocus, ), ); } diff --git a/packages/flutter/lib/src/material/bottom_sheet.dart b/packages/flutter/lib/src/material/bottom_sheet.dart index 047478f806a..49253eec852 100644 --- a/packages/flutter/lib/src/material/bottom_sheet.dart +++ b/packages/flutter/lib/src/material/bottom_sheet.dart @@ -1202,6 +1202,10 @@ class ModalBottomSheetRoute extends PopupRoute { /// The [sheetAnimationStyle] parameter is used to override the modal bottom sheet /// animation duration and reverse animation duration. /// +/// The [requestFocus] parameter is used to specify whether the bottom sheet should +/// request focus when shown. +/// {@macro flutter.widgets.navigator.Route.requestFocus} +/// /// If [AnimationStyle.duration] is provided, it will be used to override /// the modal bottom sheet animation duration in the underlying /// [BottomSheet.createAnimationController]. @@ -1254,6 +1258,7 @@ Future showModalBottomSheet({ AnimationController? transitionAnimationController, Offset? anchorPoint, AnimationStyle? sheetAnimationStyle, + bool? requestFocus, }) { assert(debugCheckHasMediaQuery(context)); assert(debugCheckHasMaterialLocalizations(context)); @@ -1282,6 +1287,7 @@ Future showModalBottomSheet({ anchorPoint: anchorPoint, useSafeArea: useSafeArea, sheetAnimationStyle: sheetAnimationStyle, + requestFocus: requestFocus, ), ); } diff --git a/packages/flutter/lib/src/material/dialog.dart b/packages/flutter/lib/src/material/dialog.dart index abd63a58a25..0a36900c0e1 100644 --- a/packages/flutter/lib/src/material/dialog.dart +++ b/packages/flutter/lib/src/material/dialog.dart @@ -1395,6 +1395,12 @@ Widget _buildMaterialDialogTransitions( /// [TraversalEdgeBehavior.closedLoop] is used, because it's typical for dialogs /// to allow users to cycle through dialog widgets without leaving the dialog. /// +/// {@template flutter.material.dialog.requestFocus} +/// The `requestFocus` argument is used to specify whether the dialog should +/// request focus when shown. +/// {@endtemplate} +/// {@macro flutter.widgets.navigator.Route.requestFocus} +/// /// {@macro flutter.widgets.RawDialogRoute} /// /// If the application has multiple [Navigator] objects, it may be necessary to @@ -1459,6 +1465,7 @@ Future showDialog({ RouteSettings? routeSettings, Offset? anchorPoint, TraversalEdgeBehavior? traversalEdgeBehavior, + bool? requestFocus, }) { assert(_debugIsActive(context)); assert(debugCheckHasMaterialLocalizations(context)); @@ -1484,6 +1491,7 @@ Future showDialog({ themes: themes, anchorPoint: anchorPoint, traversalEdgeBehavior: traversalEdgeBehavior ?? TraversalEdgeBehavior.closedLoop, + requestFocus: requestFocus, ), ); } @@ -1507,6 +1515,7 @@ Future showAdaptiveDialog({ RouteSettings? routeSettings, Offset? anchorPoint, TraversalEdgeBehavior? traversalEdgeBehavior, + bool? requestFocus, }) { final ThemeData theme = Theme.of(context); switch (theme.platform) { @@ -1525,6 +1534,7 @@ Future showAdaptiveDialog({ routeSettings: routeSettings, anchorPoint: anchorPoint, traversalEdgeBehavior: traversalEdgeBehavior, + requestFocus: requestFocus, ); case TargetPlatform.iOS: case TargetPlatform.macOS: @@ -1536,6 +1546,7 @@ Future showAdaptiveDialog({ useRootNavigator: useRootNavigator, anchorPoint: anchorPoint, routeSettings: routeSettings, + requestFocus: requestFocus, ); } } diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index e6f63dea5e5..ad369870726 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -164,8 +164,10 @@ abstract class Route extends _RoutePlaceholder { /// If the [settings] are not provided, an empty [RouteSettings] object is /// used instead. /// + /// {@template flutter.widgets.navigator.Route.requestFocus} /// If [requestFocus] is not provided, the value of [Navigator.requestFocus] is /// used instead. + /// {@endtemplate} Route({RouteSettings? settings, bool? requestFocus}) : _settings = settings ?? const RouteSettings(), _requestFocus = requestFocus { diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index c676858f988..b48f25d4b3a 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -2631,6 +2631,9 @@ class RawDialogRoute extends PopupRoute { /// The `routeSettings` will be used in the construction of the dialog's route. /// See [RouteSettings] for more details. /// +/// {@macro flutter.material.dialog.requestFocus} +/// {@macro flutter.widgets.navigator.Route.requestFocus} +/// /// {@macro flutter.widgets.RawDialogRoute} /// /// Returns a [Future] that resolves to the value (if any) that was passed to @@ -2672,6 +2675,7 @@ Future showGeneralDialog({ bool useRootNavigator = true, RouteSettings? routeSettings, Offset? anchorPoint, + bool? requestFocus, }) { assert(!barrierDismissible || barrierLabel != null); return Navigator.of(context, rootNavigator: useRootNavigator).push( @@ -2684,6 +2688,7 @@ Future showGeneralDialog({ transitionBuilder: transitionBuilder, settings: routeSettings, anchorPoint: anchorPoint, + requestFocus: requestFocus, ), ); } diff --git a/packages/flutter/test/cupertino/route_test.dart b/packages/flutter/test/cupertino/route_test.dart index 07554e240c1..534fcd9da02 100644 --- a/packages/flutter/test/cupertino/route_test.dart +++ b/packages/flutter/test/cupertino/route_test.dart @@ -3111,6 +3111,76 @@ void main() { expect(focusScopeNode.hasFocus, isFalse); }, ); + + testWidgets('requestFocus works correctly in showCupertinoModalPopup.', ( + WidgetTester tester, + ) async { + final GlobalKey navigatorKey = GlobalKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( + CupertinoApp(navigatorKey: navigatorKey, home: CupertinoTextField(focusNode: focusNode)), + ); + focusNode.requestFocus(); + await tester.pump(); + expect(focusNode.hasFocus, true); + + showCupertinoModalPopup( + context: navigatorKey.currentContext!, + requestFocus: true, + builder: (BuildContext context) => const Text('popup'), + ); + await tester.pumpAndSettle(); + expect(FocusScope.of(tester.element(find.text('popup'))).hasFocus, true); + expect(focusNode.hasFocus, false); + + navigatorKey.currentState!.pop(); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, true); + + showCupertinoModalPopup( + context: navigatorKey.currentContext!, + requestFocus: false, + builder: (BuildContext context) => const Text('popup'), + ); + await tester.pumpAndSettle(); + expect(FocusScope.of(tester.element(find.text('popup'))).hasFocus, false); + expect(focusNode.hasFocus, true); + }); + + testWidgets('requestFocus works correctly in showCupertinoDialog.', (WidgetTester tester) async { + final GlobalKey navigatorKey = GlobalKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( + CupertinoApp(navigatorKey: navigatorKey, home: CupertinoTextField(focusNode: focusNode)), + ); + focusNode.requestFocus(); + await tester.pump(); + expect(focusNode.hasFocus, true); + + showCupertinoDialog( + context: navigatorKey.currentContext!, + requestFocus: true, + builder: (BuildContext context) => const Text('dialog'), + ); + await tester.pumpAndSettle(); + expect(FocusScope.of(tester.element(find.text('dialog'))).hasFocus, true); + expect(focusNode.hasFocus, false); + + navigatorKey.currentState!.pop(); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, true); + + showCupertinoDialog( + context: navigatorKey.currentContext!, + requestFocus: false, + builder: (BuildContext context) => const Text('dialog'), + ); + await tester.pumpAndSettle(); + expect(FocusScope.of(tester.element(find.text('dialog'))).hasFocus, false); + expect(focusNode.hasFocus, true); + }); } class MockNavigatorObserver extends NavigatorObserver { diff --git a/packages/flutter/test/material/bottom_sheet_test.dart b/packages/flutter/test/material/bottom_sheet_test.dart index b8a111d2d21..6003fd614f8 100644 --- a/packages/flutter/test/material/bottom_sheet_test.dart +++ b/packages/flutter/test/material/bottom_sheet_test.dart @@ -2750,6 +2750,43 @@ void main() { expect(getTextFieldFocusNode()?.hasFocus, true); }, ); + + testWidgets('requestFocus works correctly in showModalBottomSheet.', (WidgetTester tester) async { + final GlobalKey navigatorKey = GlobalKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( + MaterialApp( + navigatorKey: navigatorKey, + home: Scaffold(body: TextField(focusNode: focusNode)), + ), + ); + focusNode.requestFocus(); + await tester.pump(); + expect(focusNode.hasFocus, true); + + showModalBottomSheet( + context: navigatorKey.currentContext!, + requestFocus: true, + builder: (BuildContext context) => const Text('BottomSheet'), + ); + await tester.pumpAndSettle(); + expect(FocusScope.of(tester.element(find.text('BottomSheet'))).hasFocus, true); + expect(focusNode.hasFocus, false); + + navigatorKey.currentState!.pop(); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, true); + + showModalBottomSheet( + context: navigatorKey.currentContext!, + requestFocus: false, + builder: (BuildContext context) => const Text('BottomSheet'), + ); + await tester.pumpAndSettle(); + expect(FocusScope.of(tester.element(find.text('BottomSheet'))).hasFocus, false); + expect(focusNode.hasFocus, true); + }); } class _TestPage extends StatelessWidget { diff --git a/packages/flutter/test/material/dialog_test.dart b/packages/flutter/test/material/dialog_test.dart index 894f58cb412..c2f1baf0634 100644 --- a/packages/flutter/test/material/dialog_test.dart +++ b/packages/flutter/test/material/dialog_test.dart @@ -2820,6 +2820,43 @@ void main() { expect(find.text(dialogText), findsOneWidget); expect(getTextFieldFocusNode()?.hasFocus, true); }); + + testWidgets('requestFocus works correctly in showDialog.', (WidgetTester tester) async { + final GlobalKey navigatorKey = GlobalKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( + MaterialApp( + navigatorKey: navigatorKey, + home: Scaffold(body: TextField(focusNode: focusNode)), + ), + ); + focusNode.requestFocus(); + await tester.pump(); + expect(focusNode.hasFocus, true); + + showDialog( + context: navigatorKey.currentContext!, + requestFocus: true, + builder: (BuildContext context) => const Text('dialog'), + ); + await tester.pumpAndSettle(); + expect(FocusScope.of(tester.element(find.text('dialog'))).hasFocus, true); + expect(focusNode.hasFocus, false); + + navigatorKey.currentState!.pop(); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, true); + + showDialog( + context: navigatorKey.currentContext!, + requestFocus: false, + builder: (BuildContext context) => const Text('dialog'), + ); + await tester.pumpAndSettle(); + expect(FocusScope.of(tester.element(find.text('dialog'))).hasFocus, false); + expect(focusNode.hasFocus, true); + }); } @pragma('vm:entry-point') diff --git a/packages/flutter/test/widgets/routes_test.dart b/packages/flutter/test/widgets/routes_test.dart index b9a574dd55b..92ffb7a2da7 100644 --- a/packages/flutter/test/widgets/routes_test.dart +++ b/packages/flutter/test/widgets/routes_test.dart @@ -2656,6 +2656,53 @@ void main() { expect(focusScope.directionalTraversalEdgeBehavior, element); } }); + + testWidgets('requestFocus works correctly in showGeneralDialog.', (WidgetTester tester) async { + final GlobalKey navigatorKey = GlobalKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( + MaterialApp( + navigatorKey: navigatorKey, + home: Scaffold(body: TextField(focusNode: focusNode)), + ), + ); + focusNode.requestFocus(); + await tester.pump(); + expect(focusNode.hasFocus, true); + + showGeneralDialog( + context: navigatorKey.currentContext!, + requestFocus: true, + pageBuilder: + ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) => const Text('dialog'), + ); + await tester.pumpAndSettle(); + expect(FocusScope.of(tester.element(find.text('dialog'))).hasFocus, true); + expect(focusNode.hasFocus, false); + + navigatorKey.currentState!.pop(); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, true); + + showGeneralDialog( + context: navigatorKey.currentContext!, + requestFocus: false, + pageBuilder: + ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) => const Text('dialog'), + ); + await tester.pumpAndSettle(); + expect(FocusScope.of(tester.element(find.text('dialog'))).hasFocus, false); + expect(focusNode.hasFocus, true); + }); } double _getOpacity(GlobalKey key, WidgetTester tester) {