showDialogs adds a requestFocus parameter. (#162928)

Fixes: #162920

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
yim 2025-02-20 11:01:07 +08:00 committed by GitHub
parent 01fe4e262a
commit a6ff677120
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 226 additions and 0 deletions

View File

@ -1277,6 +1277,10 @@ class CupertinoModalPopupRoute<T> extends PopupRoute<T> {
/// [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<T?> showCupertinoModalPopup<T>({
bool semanticsDismissible = false,
RouteSettings? routeSettings,
Offset? anchorPoint,
bool? requestFocus,
}) {
return Navigator.of(context, rootNavigator: useRootNavigator).push(
CupertinoModalPopupRoute<T>(
@ -1328,6 +1333,7 @@ Future<T?> showCupertinoModalPopup<T>({
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<T?> showCupertinoDialog<T>({
bool barrierDismissible = false,
RouteSettings? routeSettings,
Offset? anchorPoint,
bool? requestFocus,
}) {
return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(
CupertinoDialogRoute<T>(
@ -1415,6 +1425,7 @@ Future<T?> showCupertinoDialog<T>({
barrierColor: CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context),
settings: routeSettings,
anchorPoint: anchorPoint,
requestFocus: requestFocus,
),
);
}

View File

@ -1202,6 +1202,10 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
/// 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<T?> showModalBottomSheet<T>({
AnimationController? transitionAnimationController,
Offset? anchorPoint,
AnimationStyle? sheetAnimationStyle,
bool? requestFocus,
}) {
assert(debugCheckHasMediaQuery(context));
assert(debugCheckHasMaterialLocalizations(context));
@ -1282,6 +1287,7 @@ Future<T?> showModalBottomSheet<T>({
anchorPoint: anchorPoint,
useSafeArea: useSafeArea,
sheetAnimationStyle: sheetAnimationStyle,
requestFocus: requestFocus,
),
);
}

View File

@ -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<T?> showDialog<T>({
RouteSettings? routeSettings,
Offset? anchorPoint,
TraversalEdgeBehavior? traversalEdgeBehavior,
bool? requestFocus,
}) {
assert(_debugIsActive(context));
assert(debugCheckHasMaterialLocalizations(context));
@ -1484,6 +1491,7 @@ Future<T?> showDialog<T>({
themes: themes,
anchorPoint: anchorPoint,
traversalEdgeBehavior: traversalEdgeBehavior ?? TraversalEdgeBehavior.closedLoop,
requestFocus: requestFocus,
),
);
}
@ -1507,6 +1515,7 @@ Future<T?> showAdaptiveDialog<T>({
RouteSettings? routeSettings,
Offset? anchorPoint,
TraversalEdgeBehavior? traversalEdgeBehavior,
bool? requestFocus,
}) {
final ThemeData theme = Theme.of(context);
switch (theme.platform) {
@ -1525,6 +1534,7 @@ Future<T?> showAdaptiveDialog<T>({
routeSettings: routeSettings,
anchorPoint: anchorPoint,
traversalEdgeBehavior: traversalEdgeBehavior,
requestFocus: requestFocus,
);
case TargetPlatform.iOS:
case TargetPlatform.macOS:
@ -1536,6 +1546,7 @@ Future<T?> showAdaptiveDialog<T>({
useRootNavigator: useRootNavigator,
anchorPoint: anchorPoint,
routeSettings: routeSettings,
requestFocus: requestFocus,
);
}
}

View File

@ -164,8 +164,10 @@ abstract class Route<T> 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 {

View File

@ -2631,6 +2631,9 @@ class RawDialogRoute<T> extends PopupRoute<T> {
/// 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<T?> showGeneralDialog<T extends Object?>({
bool useRootNavigator = true,
RouteSettings? routeSettings,
Offset? anchorPoint,
bool? requestFocus,
}) {
assert(!barrierDismissible || barrierLabel != null);
return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(
@ -2684,6 +2688,7 @@ Future<T?> showGeneralDialog<T extends Object?>({
transitionBuilder: transitionBuilder,
settings: routeSettings,
anchorPoint: anchorPoint,
requestFocus: requestFocus,
),
);
}

View File

@ -3111,6 +3111,76 @@ void main() {
expect(focusScopeNode.hasFocus, isFalse);
},
);
testWidgets('requestFocus works correctly in showCupertinoModalPopup.', (
WidgetTester tester,
) async {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
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<void>(
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<void>(
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<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
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<void>(
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<void>(
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 {

View File

@ -2750,6 +2750,43 @@ void main() {
expect(getTextFieldFocusNode()?.hasFocus, true);
},
);
testWidgets('requestFocus works correctly in showModalBottomSheet.', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
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<void>(
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<void>(
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 {

View File

@ -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<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
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<void>(
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<void>(
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')

View File

@ -2656,6 +2656,53 @@ void main() {
expect(focusScope.directionalTraversalEdgeBehavior, element);
}
});
testWidgets('requestFocus works correctly in showGeneralDialog.', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
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<void>(
context: navigatorKey.currentContext!,
requestFocus: true,
pageBuilder:
(
BuildContext context,
Animation<double> animation,
Animation<double> 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<void>(
context: navigatorKey.currentContext!,
requestFocus: false,
pageBuilder:
(
BuildContext context,
Animation<double> animation,
Animation<double> 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) {