diff --git a/packages/flutter/lib/src/cupertino/route.dart b/packages/flutter/lib/src/cupertino/route.dart
index 983fe61c91..554b43d981 100644
--- a/packages/flutter/lib/src/cupertino/route.dart
+++ b/packages/flutter/lib/src/cupertino/route.dart
@@ -999,8 +999,12 @@ class _CupertinoEdgeShadowPainter extends BoxPainter {
/// The `routeSettings` argument is used to provide [RouteSettings] to the
/// created Route.
///
+/// {@macro flutter.widgets.RawDialogRoute}
+///
/// See also:
///
+/// * [DisplayFeatureSubScreen], which documents the specifics of how
+/// [DisplayFeature]s can split the screen into sub-screens.
/// * [CupertinoActionSheet], which is the widget usually returned by the
/// `builder` argument.
/// *
@@ -1015,6 +1019,7 @@ class CupertinoModalPopupRoute extends PopupRoute {
bool? semanticsDismissible,
ImageFilter? filter,
RouteSettings? settings,
+ this.anchorPoint,
}) : super(
filter: filter,
settings: settings,
@@ -1056,6 +1061,9 @@ class CupertinoModalPopupRoute extends PopupRoute {
late Tween _offsetTween;
+ /// {@macro flutter.widgets.DisplayFeatureSubScreen.anchorPoint}
+ final Offset? anchorPoint;
+
@override
Animation createAnimation() {
assert(_animation == null);
@@ -1078,7 +1086,10 @@ class CupertinoModalPopupRoute extends PopupRoute {
Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) {
return CupertinoUserInterfaceLevel(
data: CupertinoUserInterfaceLevelData.elevated,
- child: Builder(builder: builder),
+ child: DisplayFeatureSubScreen(
+ anchorPoint: anchorPoint,
+ child: Builder(builder: builder),
+ ),
);
}
@@ -1127,6 +1138,8 @@ class CupertinoModalPopupRoute extends PopupRoute {
/// [StatefulBuilder] or a custom [StatefulWidget] if the widget needs to
/// update dynamically.
///
+/// {@macro flutter.widgets.RawDialogRoute}
+///
/// Returns a `Future` that resolves to the value that was passed to
/// [Navigator.pop] when the popup was closed.
///
@@ -1151,6 +1164,8 @@ class CupertinoModalPopupRoute extends PopupRoute {
///
/// See also:
///
+/// * [DisplayFeatureSubScreen], which documents the specifics of how
+/// [DisplayFeature]s can split the screen into sub-screens.
/// * [CupertinoActionSheet], which is the widget usually returned by the
/// `builder` argument to [showCupertinoModalPopup].
/// *
@@ -1163,6 +1178,7 @@ Future showCupertinoModalPopup({
bool useRootNavigator = true,
bool? semanticsDismissible,
RouteSettings? routeSettings,
+ Offset? anchorPoint,
}) {
assert(useRootNavigator != null);
return Navigator.of(context, rootNavigator: useRootNavigator).push(
@@ -1173,6 +1189,7 @@ Future showCupertinoModalPopup({
barrierDismissible: barrierDismissible,
semanticsDismissible: semanticsDismissible,
settings: routeSettings,
+ anchorPoint: anchorPoint,
),
);
}
@@ -1223,6 +1240,8 @@ Widget _buildCupertinoDialogTransitions(BuildContext context, Animation
/// By default, `useRootNavigator` is `true` and the dialog route created by
/// this method is pushed to the root navigator.
///
+/// {@macro flutter.widgets.RawDialogRoute}
+///
/// 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)`.
@@ -1254,6 +1273,8 @@ Widget _buildCupertinoDialogTransitions(BuildContext context, Animation
/// * [CupertinoAlertDialog], an iOS-style alert dialog.
/// * [showDialog], which displays a Material-style dialog.
/// * [showGeneralDialog], which allows for customization of the dialog popup.
+/// * [DisplayFeatureSubScreen], which documents the specifics of how
+/// [DisplayFeature]s can split the screen into sub-screens.
/// *
Future showCupertinoDialog({
required BuildContext context,
@@ -1262,6 +1283,7 @@ Future showCupertinoDialog({
bool useRootNavigator = true,
bool barrierDismissible = false,
RouteSettings? routeSettings,
+ Offset? anchorPoint,
}) {
assert(builder != null);
assert(useRootNavigator != null);
@@ -1273,6 +1295,7 @@ Future showCupertinoDialog({
barrierLabel: barrierLabel,
barrierColor: CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context),
settings: routeSettings,
+ anchorPoint: anchorPoint,
));
}
@@ -1303,12 +1326,16 @@ Future showCupertinoDialog({
/// The `settings` argument define the settings for this route. See
/// [RouteSettings] for details.
///
+/// {@macro flutter.widgets.RawDialogRoute}
+///
/// See also:
///
/// * [showCupertinoDialog], which is a way to display
/// an iOS-style dialog.
/// * [showGeneralDialog], which allows for customization of the dialog popup.
/// * [showDialog], which displays a Material dialog.
+/// * [DisplayFeatureSubScreen], which documents the specifics of how
+/// [DisplayFeature]s can split the screen into sub-screens.
class CupertinoDialogRoute extends RawDialogRoute {
/// A dialog route that shows an iOS-style dialog.
CupertinoDialogRoute({
@@ -1321,6 +1348,7 @@ class CupertinoDialogRoute extends RawDialogRoute {
Duration transitionDuration = const Duration(milliseconds: 250),
RouteTransitionsBuilder? transitionBuilder = _buildCupertinoDialogTransitions,
RouteSettings? settings,
+ Offset? anchorPoint,
}) : assert(barrierDismissible != null),
super(
pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) {
@@ -1332,5 +1360,6 @@ class CupertinoDialogRoute extends RawDialogRoute {
transitionDuration: transitionDuration,
transitionBuilder: transitionBuilder,
settings: settings,
+ anchorPoint: anchorPoint,
);
}
diff --git a/packages/flutter/lib/src/material/about.dart b/packages/flutter/lib/src/material/about.dart
index 5800accc74..a6c692399d 100644
--- a/packages/flutter/lib/src/material/about.dart
+++ b/packages/flutter/lib/src/material/about.dart
@@ -167,8 +167,9 @@ 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], [useRootNavigator] and [routeSettings] arguments are passed to
-/// [showDialog], the documentation for which discusses how it is used.
+/// The [context], [useRootNavigator], [routeSettings] and [anchorPoint]
+/// arguments are passed to [showDialog], the documentation for which discusses
+/// how it is used.
void showAboutDialog({
required BuildContext context,
String? applicationName,
@@ -178,6 +179,7 @@ void showAboutDialog({
List? children,
bool useRootNavigator = true,
RouteSettings? routeSettings,
+ Offset? anchorPoint,
}) {
assert(context != null);
assert(useRootNavigator != null);
@@ -194,6 +196,7 @@ void showAboutDialog({
);
},
routeSettings: routeSettings,
+ anchorPoint: anchorPoint,
);
}
diff --git a/packages/flutter/lib/src/material/bottom_sheet.dart b/packages/flutter/lib/src/material/bottom_sheet.dart
index 6607b751d5..fdc6f61db2 100644
--- a/packages/flutter/lib/src/material/bottom_sheet.dart
+++ b/packages/flutter/lib/src/material/bottom_sheet.dart
@@ -466,6 +466,7 @@ class _ModalBottomSheetRoute extends PopupRoute {
required this.isScrollControlled,
RouteSettings? settings,
this.transitionAnimationController,
+ this.anchorPoint,
}) : assert(isScrollControlled != null),
assert(isDismissible != null),
assert(enableDrag != null),
@@ -483,6 +484,7 @@ class _ModalBottomSheetRoute extends PopupRoute {
final bool isDismissible;
final bool enableDrag;
final AnimationController? transitionAnimationController;
+ final Offset? anchorPoint;
@override
Duration get transitionDuration => _bottomSheetEnterDuration;
@@ -520,20 +522,23 @@ class _ModalBottomSheetRoute extends PopupRoute {
final Widget bottomSheet = MediaQuery.removePadding(
context: context,
removeTop: true,
- child: Builder(
- builder: (BuildContext context) {
- final BottomSheetThemeData sheetTheme = Theme.of(context).bottomSheetTheme;
- return _ModalBottomSheet(
- route: this,
- backgroundColor: backgroundColor ?? sheetTheme.modalBackgroundColor ?? sheetTheme.backgroundColor,
- elevation: elevation ?? sheetTheme.modalElevation ?? sheetTheme.elevation,
- shape: shape,
- clipBehavior: clipBehavior,
- constraints: constraints,
- isScrollControlled: isScrollControlled,
- enableDrag: enableDrag,
- );
- },
+ child: DisplayFeatureSubScreen(
+ anchorPoint: anchorPoint,
+ child: Builder(
+ builder: (BuildContext context) {
+ final BottomSheetThemeData sheetTheme = Theme.of(context).bottomSheetTheme;
+ return _ModalBottomSheet(
+ route: this,
+ backgroundColor: backgroundColor ?? sheetTheme.modalBackgroundColor ?? sheetTheme.backgroundColor,
+ elevation: elevation ?? sheetTheme.modalElevation ?? sheetTheme.elevation,
+ shape: shape,
+ clipBehavior: clipBehavior,
+ constraints: constraints,
+ isScrollControlled: isScrollControlled,
+ enableDrag: enableDrag,
+ );
+ },
+ ),
),
);
return capturedThemes.wrap(bottomSheet);
@@ -645,6 +650,8 @@ class _BottomSheetSuspendedCurve extends ParametricCurve {
/// sheet. This is particularly useful in the case that a user wants to observe
/// [PopupRoute]s within a [NavigatorObserver].
///
+/// {@macro flutter.widgets.RawDialogRoute}
+///
/// Returns a `Future` that resolves to the value (if any) that was passed to
/// [Navigator.pop] when the modal bottom sheet was closed.
///
@@ -665,6 +672,8 @@ class _BottomSheetSuspendedCurve extends ParametricCurve {
/// non-modal bottom sheets.
/// * [DraggableScrollableSheet], which allows you to create a bottom sheet
/// that grows and then becomes scrollable once it reaches its maximum size.
+/// * [DisplayFeatureSubScreen], which documents the specifics of how
+/// [DisplayFeature]s can split the screen into sub-screens.
/// *
Future showModalBottomSheet({
required BuildContext context,
@@ -681,6 +690,7 @@ Future showModalBottomSheet({
bool enableDrag = true,
RouteSettings? routeSettings,
AnimationController? transitionAnimationController,
+ Offset? anchorPoint,
}) {
assert(context != null);
assert(builder != null);
@@ -707,6 +717,7 @@ Future showModalBottomSheet({
enableDrag: enableDrag,
settings: routeSettings,
transitionAnimationController: transitionAnimationController,
+ anchorPoint: anchorPoint,
));
}
diff --git a/packages/flutter/lib/src/material/date_picker.dart b/packages/flutter/lib/src/material/date_picker.dart
index e0d82d8195..cba7e461ad 100644
--- a/packages/flutter/lib/src/material/date_picker.dart
+++ b/packages/flutter/lib/src/material/date_picker.dart
@@ -102,6 +102,8 @@ const double _inputFormLandscapeHeight = 108.0;
/// [DatePickerMode.day] mode. It defaults to [DatePickerMode.day], and
/// must be non-null.
///
+/// {@macro flutter.widgets.RawDialogRoute}
+///
/// ### State Restoration
///
/// Using this method will not enable state restoration for the date picker.
@@ -128,6 +130,8 @@ const double _inputFormLandscapeHeight = 108.0;
/// used to select a range of dates.
/// * [CalendarDatePicker], which provides the calendar grid used by the date picker dialog.
/// * [InputDatePickerFormField], which provides a text input field for entering dates.
+/// * [DisplayFeatureSubScreen], which documents the specifics of how
+/// [DisplayFeature]s can split the screen into sub-screens.
/// * [showTimePicker], which shows a dialog that contains a material design time picker.
///
Future showDatePicker({
@@ -152,6 +156,7 @@ Future showDatePicker({
String? fieldHintText,
String? fieldLabelText,
TextInputType? keyboardType,
+ Offset? anchorPoint,
}) async {
assert(context != null);
assert(initialDate != null);
@@ -221,6 +226,7 @@ Future showDatePicker({
builder: (BuildContext context) {
return builder == null ? dialog : builder(context, dialog);
},
+ anchorPoint: anchorPoint,
);
}
@@ -898,6 +904,8 @@ class _DatePickerHeader extends StatelessWidget {
/// The [builder] parameter can be used to wrap the dialog widget
/// to add inherited widgets like [Theme].
///
+/// {@macro flutter.widgets.RawDialogRoute}
+///
/// ### State Restoration
///
/// Using this method will not enable state restoration for the date range picker.
@@ -923,7 +931,8 @@ class _DatePickerHeader extends StatelessWidget {
/// * [showDatePicker], which shows a material design date picker used to
/// select a single date.
/// * [DateTimeRange], which is used to describe a date range.
-///
+/// * [DisplayFeatureSubScreen], which documents the specifics of how
+/// [DisplayFeature]s can split the screen into sub-screens.
Future showDateRangePicker({
required BuildContext context,
DateTimeRange? initialDateRange,
@@ -947,6 +956,7 @@ Future showDateRangePicker({
RouteSettings? routeSettings,
TextDirection? textDirection,
TransitionBuilder? builder,
+ Offset? anchorPoint,
}) async {
assert(context != null);
assert(
@@ -1029,6 +1039,7 @@ Future showDateRangePicker({
builder: (BuildContext context) {
return builder == null ? dialog : builder(context, dialog);
},
+ anchorPoint: anchorPoint,
);
}
diff --git a/packages/flutter/lib/src/material/dialog.dart b/packages/flutter/lib/src/material/dialog.dart
index 4127baa462..d405ac4f1f 100644
--- a/packages/flutter/lib/src/material/dialog.dart
+++ b/packages/flutter/lib/src/material/dialog.dart
@@ -1007,6 +1007,8 @@ Widget _buildMaterialDialogTransitions(BuildContext context, Animation a
/// The `routeSettings` argument is passed to [showGeneralDialog],
/// see [RouteSettings] for details.
///
+/// {@macro flutter.widgets.RawDialogRoute}
+///
/// 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)`.
@@ -1041,6 +1043,8 @@ Widget _buildMaterialDialogTransitions(BuildContext context, Animation a
/// * [Dialog], on which [SimpleDialog] and [AlertDialog] are based.
/// * [showCupertinoDialog], which displays an iOS-style dialog.
/// * [showGeneralDialog], which allows for customization of the dialog popup.
+/// * [DisplayFeatureSubScreen], which documents the specifics of how
+/// [DisplayFeature]s can split the screen into sub-screens.
/// *
Future showDialog({
required BuildContext context,
@@ -1051,6 +1055,7 @@ Future showDialog({
bool useSafeArea = true,
bool useRootNavigator = true,
RouteSettings? routeSettings,
+ Offset? anchorPoint,
}) {
assert(builder != null);
assert(barrierDismissible != null);
@@ -1075,6 +1080,7 @@ Future showDialog({
useSafeArea: useSafeArea,
settings: routeSettings,
themes: themes,
+ anchorPoint: anchorPoint,
));
}
@@ -1113,11 +1119,15 @@ Future showDialog({
/// The `settings` argument define the settings for this route. See
/// [RouteSettings] for details.
///
+/// {@macro flutter.widgets.RawDialogRoute}
+///
/// See also:
///
/// * [showDialog], which is a way to display a DialogRoute.
/// * [showGeneralDialog], which allows for customization of the dialog popup.
/// * [showCupertinoDialog], which displays an iOS-style dialog.
+/// * [DisplayFeatureSubScreen], which documents the specifics of how
+/// [DisplayFeature]s can split the screen into sub-screens.
class DialogRoute extends RawDialogRoute {
/// A dialog route with Material entrance and exit animations,
/// modal barrier color, and modal barrier behavior (dialog is dismissible
@@ -1131,6 +1141,7 @@ class DialogRoute extends RawDialogRoute {
String? barrierLabel,
bool useSafeArea = true,
RouteSettings? settings,
+ Offset? anchorPoint,
}) : assert(barrierDismissible != null),
super(
pageBuilder: (BuildContext buildContext, Animation animation, Animation secondaryAnimation) {
@@ -1147,6 +1158,7 @@ class DialogRoute extends RawDialogRoute {
transitionDuration: const Duration(milliseconds: 150),
transitionBuilder: _buildMaterialDialogTransitions,
settings: settings,
+ anchorPoint: anchorPoint,
);
}
diff --git a/packages/flutter/lib/src/material/time_picker.dart b/packages/flutter/lib/src/material/time_picker.dart
index 423b98bdf3..b1e3d3ad9c 100644
--- a/packages/flutter/lib/src/material/time_picker.dart
+++ b/packages/flutter/lib/src/material/time_picker.dart
@@ -2365,6 +2365,8 @@ class _TimePickerDialogState extends State with RestorationMix
/// [hourLabelText], [minuteLabelText] and [confirmText] can be provided to
/// override the default values.
///
+/// {@macro flutter.widgets.RawDialogRoute}
+///
/// By default, the time picker gets its colors from the overall theme's
/// [ColorScheme]. The time picker can be further customized by providing a
/// [TimePickerThemeData] to the overall theme.
@@ -2409,6 +2411,8 @@ class _TimePickerDialogState extends State with RestorationMix
/// date picker.
/// * [TimePickerThemeData], which allows you to customize the colors,
/// typography, and shape of the time picker.
+/// * [DisplayFeatureSubScreen], which documents the specifics of how
+/// [DisplayFeature]s can split the screen into sub-screens.
Future showTimePicker({
required BuildContext context,
required TimeOfDay initialTime,
@@ -2423,6 +2427,7 @@ Future showTimePicker({
String? minuteLabelText,
RouteSettings? routeSettings,
EntryModeChangeCallback? onEntryModeChanged,
+ Offset? anchorPoint,
}) async {
assert(context != null);
assert(initialTime != null);
@@ -2448,6 +2453,7 @@ Future showTimePicker({
return builder == null ? dialog : builder(context, dialog);
},
routeSettings: routeSettings,
+ anchorPoint: anchorPoint,
);
}
diff --git a/packages/flutter/lib/src/widgets/display_feature_sub_screen.dart b/packages/flutter/lib/src/widgets/display_feature_sub_screen.dart
index 1acee0aa82..f2f5c01992 100644
--- a/packages/flutter/lib/src/widgets/display_feature_sub_screen.dart
+++ b/packages/flutter/lib/src/widgets/display_feature_sub_screen.dart
@@ -27,14 +27,14 @@ import 'media_query.dart';
/// bottom sub-screen
///
/// After determining the sub-screens, the closest one to [anchorPoint] is used
-/// to render the [child].
+/// to render the content.
///
/// If no [anchorPoint] is provided, then [Directionality] is used:
///
/// * for [TextDirection.ltr], [anchorPoint] is `Offset.zero`, which will
-/// cause the [child] to appear in the top-left sub-screen.
+/// cause the content to appear in the top-left sub-screen.
/// * for [TextDirection.rtl], [anchorPoint] is `Offset(double.maxFinite, 0)`,
-/// which will cause the [child] to appear in the top-right sub-screen.
+/// which will cause the content to appear in the top-right sub-screen.
///
/// If no [anchorPoint] is provided, and there is no [Directionality] ancestor
/// widget in the tree, then the widget asserts during build in debug mode.
@@ -58,6 +58,7 @@ class DisplayFeatureSubScreen extends StatelessWidget {
required this.child,
}) : super(key: key);
+ /// {@template flutter.widgets.DisplayFeatureSubScreen.anchorPoint}
/// The anchor point used to pick the closest sub-screen.
///
/// If the anchor point sits inside one of these sub-screens, then that
@@ -75,6 +76,7 @@ class DisplayFeatureSubScreen extends StatelessWidget {
/// * for [TextDirection.rtl], [anchorPoint] is
/// `Offset(double.maxFinite, 0)`, which will cause the top-right
/// sub-screen to be picked.
+ /// {@endtemplate}
final Offset? anchorPoint;
/// The widget below this widget in the tree.
diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart
index a239096643..368cb6a5a0 100644
--- a/packages/flutter/lib/src/widgets/routes.dart
+++ b/packages/flutter/lib/src/widgets/routes.dart
@@ -13,6 +13,7 @@ import 'package:flutter/services.dart';
import 'actions.dart';
import 'basic.dart';
+import 'display_feature_sub_screen.dart';
import 'focus_manager.dart';
import 'focus_scope.dart';
import 'framework.dart';
@@ -1864,8 +1865,25 @@ abstract class RouteAware {
/// The `settings` argument define the settings for this route. See
/// [RouteSettings] for details.
///
+/// {@template flutter.widgets.RawDialogRoute}
+/// A [DisplayFeature] can split the screen into sub-screens. The closest one to
+/// [anchorPoint] is used to render the content.
+///
+/// If no [anchorPoint] is provided, then [Directionality] is used:
+///
+/// * for [TextDirection.ltr], [anchorPoint] is `Offset.zero`, which will
+/// cause the content to appear in the top-left sub-screen.
+/// * for [TextDirection.rtl], [anchorPoint] is `Offset(double.maxFinite, 0)`,
+/// which will cause the content to appear in the top-right sub-screen.
+///
+/// If no [anchorPoint] is provided, and there is no [Directionality] ancestor
+/// widget in the tree, then the widget asserts during build in debug mode.
+/// {@endtemplate}
+///
/// See also:
///
+/// * [DisplayFeatureSubScreen], which documents the specifics of how
+/// [DisplayFeature]s can split the screen into sub-screens.
/// * [showGeneralDialog], which is a way to display a RawDialogRoute.
/// * [showDialog], which is a way to display a DialogRoute.
/// * [showCupertinoDialog], which displays an iOS-style dialog.
@@ -1879,6 +1897,7 @@ class RawDialogRoute extends PopupRoute {
Duration transitionDuration = const Duration(milliseconds: 200),
RouteTransitionsBuilder? transitionBuilder,
RouteSettings? settings,
+ this.anchorPoint,
}) : assert(barrierDismissible != null),
_pageBuilder = pageBuilder,
_barrierDismissible = barrierDismissible,
@@ -1908,12 +1927,18 @@ class RawDialogRoute extends PopupRoute {
final RouteTransitionsBuilder? _transitionBuilder;
+ /// {@macro flutter.widgets.DisplayFeatureSubScreen.anchorPoint}
+ final Offset? anchorPoint;
+
@override
Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) {
return Semantics(
scopesRoute: true,
explicitChildNodes: true,
- child: _pageBuilder(context, animation, secondaryAnimation),
+ child: DisplayFeatureSubScreen(
+ anchorPoint: anchorPoint,
+ child: _pageBuilder(context, animation, secondaryAnimation),
+ ),
);
}
@@ -1979,6 +2004,8 @@ class RawDialogRoute extends PopupRoute {
/// The `routeSettings` will be used in the construction of the dialog's route.
/// See [RouteSettings] for more details.
///
+/// {@macro flutter.widgets.RawDialogRoute}
+///
/// Returns a [Future] that resolves to the value (if any) that was passed to
/// [Navigator.pop] when the dialog was closed.
///
@@ -2003,6 +2030,8 @@ class RawDialogRoute extends PopupRoute {
///
/// See also:
///
+/// * [DisplayFeatureSubScreen], which documents the specifics of how
+/// [DisplayFeature]s can split the screen into sub-screens.
/// * [showDialog], which displays a Material-style dialog.
/// * [showCupertinoDialog], which displays an iOS-style dialog.
Future showGeneralDialog({
@@ -2015,6 +2044,7 @@ Future showGeneralDialog({
RouteTransitionsBuilder? transitionBuilder,
bool useRootNavigator = true,
RouteSettings? routeSettings,
+ Offset? anchorPoint,
}) {
assert(pageBuilder != null);
assert(useRootNavigator != null);
@@ -2027,6 +2057,7 @@ Future showGeneralDialog({
transitionDuration: transitionDuration,
transitionBuilder: transitionBuilder,
settings: routeSettings,
+ anchorPoint: anchorPoint,
));
}
diff --git a/packages/flutter/test/cupertino/dialog_test.dart b/packages/flutter/test/cupertino/dialog_test.dart
index da5c538227..b07823b43e 100644
--- a/packages/flutter/test/cupertino/dialog_test.dart
+++ b/packages/flutter/test/cupertino/dialog_test.dart
@@ -7,6 +7,7 @@
@Tags(['reduced-test-set'])
import 'dart:math';
+import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@@ -1317,6 +1318,123 @@ void main() {
expect(scrollbars.length, 2);
expect(scrollbars[0].controller != scrollbars[1].controller, isTrue);
});
+
+ group('showCupertinoDialog avoids overlapping display features', () {
+ testWidgets('positioning using anchorPoint', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ CupertinoApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: child!,
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+
+ final BuildContext context = tester.element(find.text('Test'));
+ showCupertinoDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return const Placeholder();
+ },
+ anchorPoint: const Offset(1000, 0),
+ );
+ await tester.pumpAndSettle();
+
+ // Should take the right side of the screen
+ expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(410.0, 0.0));
+ expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0));
+ });
+
+ testWidgets('positioning using Directionality', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ CupertinoApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: Directionality(
+ textDirection: TextDirection.rtl,
+ child: child!,
+ ),
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+
+ final BuildContext context = tester.element(find.text('Test'));
+ showCupertinoDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return const Placeholder();
+ },
+ );
+ await tester.pumpAndSettle();
+
+ // Should take the right side of the screen
+ expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(410.0, 0.0));
+ expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0));
+ });
+
+ testWidgets('default positioning', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ CupertinoApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: child!,
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+
+ final BuildContext context = tester.element(find.text('Test'));
+ showCupertinoDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return const Placeholder();
+ },
+ );
+ await tester.pumpAndSettle();
+
+ // By default it should place the dialog on the left screen
+ expect(tester.getTopLeft(find.byType(Placeholder)), Offset.zero);
+ expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(390.0, 600.0));
+ });
+ });
}
RenderBox findActionButtonRenderBoxByTitle(WidgetTester tester, String title) {
diff --git a/packages/flutter/test/cupertino/route_test.dart b/packages/flutter/test/cupertino/route_test.dart
index 902afacbb9..a48bbe453d 100644
--- a/packages/flutter/test/cupertino/route_test.dart
+++ b/packages/flutter/test/cupertino/route_test.dart
@@ -3,6 +3,8 @@
// found in the LICENSE file.
@TestOn('!chrome')
+import 'dart:ui';
+
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@@ -1881,6 +1883,240 @@ void main() {
await tester.restoreFrom(restorationData);
expect(find.byType(CupertinoActionSheet), findsOneWidget);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615
+
+ group('showCupertinoDialog avoids overlapping display features', () {
+ testWidgets('positioning with anchorPoint', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ CupertinoApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: child!,
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+ final BuildContext context = tester.element(find.text('Test'));
+
+ showCupertinoDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return const Placeholder();
+ },
+ anchorPoint: const Offset(1000, 0),
+ );
+ await tester.pumpAndSettle();
+
+ // Should take the right side of the screen
+ expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(410.0, 0.0));
+ expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0));
+ });
+
+ testWidgets('positioning with Directionality', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ CupertinoApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: Directionality(
+ textDirection: TextDirection.rtl,
+ child: child!,
+ ),
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+ final BuildContext context = tester.element(find.text('Test'));
+
+ showCupertinoDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return const Placeholder();
+ },
+ );
+ await tester.pumpAndSettle();
+
+ // Since this is RTL, it should place the dialog on the right screen
+ expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(410.0, 0.0));
+ expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0));
+ });
+
+ testWidgets('positioning by default', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ CupertinoApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: child!,
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+ final BuildContext context = tester.element(find.text('Test'));
+
+ showCupertinoDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return const Placeholder();
+ },
+ );
+ await tester.pumpAndSettle();
+
+ // By default it should place the dialog on the left screen
+ expect(tester.getTopLeft(find.byType(Placeholder)), Offset.zero);
+ expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(390.0, 600.0));
+ });
+ });
+
+ group('showCupertinoModalPopup avoids overlapping display features', () {
+ testWidgets('positioning using anchorPoint', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ CupertinoApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: child!,
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+
+ final BuildContext context = tester.element(find.text('Test'));
+ showCupertinoModalPopup(
+ context: context,
+ builder: (BuildContext context) {
+ return const Placeholder();
+ },
+ anchorPoint: const Offset(1000, 0),
+ );
+ await tester.pumpAndSettle();
+
+ // Should take the right side of the screen
+ expect(tester.getTopLeft(find.byType(Placeholder)).dx, 410);
+ expect(tester.getBottomRight(find.byType(Placeholder)).dx, 800);
+ });
+
+ testWidgets('positioning using Directionality', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ CupertinoApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: Directionality(
+ textDirection: TextDirection.rtl,
+ child: child!,
+ ),
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+
+ final BuildContext context = tester.element(find.text('Test'));
+ showCupertinoModalPopup(
+ context: context,
+ builder: (BuildContext context) {
+ return const Placeholder();
+ },
+ );
+ await tester.pumpAndSettle();
+
+ // This is RTL, so it should place the dialog on the right screen
+ expect(tester.getTopLeft(find.byType(Placeholder)).dx, 410);
+ expect(tester.getBottomRight(find.byType(Placeholder)).dx, 800);
+ });
+
+ testWidgets('default positioning', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ CupertinoApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: child!,
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+
+ final BuildContext context = tester.element(find.text('Test'));
+ showCupertinoModalPopup(
+ context: context,
+ builder: (BuildContext context) {
+ return const Placeholder();
+ },
+ );
+ await tester.pumpAndSettle();
+
+ // By default it should place the dialog on the left screen
+ expect(tester.getTopLeft(find.byType(Placeholder)).dx, 0.0);
+ expect(tester.getBottomRight(find.byType(Placeholder)).dx, 390.0);
+ });
+ });
}
class MockNavigatorObserver extends NavigatorObserver {
diff --git a/packages/flutter/test/material/about_test.dart b/packages/flutter/test/material/about_test.dart
index 31167cbbde..9ed25d7012 100644
--- a/packages/flutter/test/material/about_test.dart
+++ b/packages/flutter/test/material/about_test.dart
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
+import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
@@ -593,6 +594,135 @@ void main() {
expect(nestedObserver.dialogCount, 1);
});
+ group('showAboutDialog avoids overlapping display features', () {
+ testWidgets('default positioning', (WidgetTester tester) async {
+ await tester.pumpWidget(MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: child!,
+ );
+ },
+ home: Builder(
+ builder: (BuildContext context) => ElevatedButton(
+ onPressed: () {
+ showAboutDialog(
+ context: context,
+ useRootNavigator: false,
+ applicationName: 'A',
+ );
+ },
+ child: const Text('Show About Dialog'),
+ ),
+ ),
+ ));
+
+ // Open the dialog.
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pumpAndSettle();
+
+ // By default it should place the dialog on the left screen
+ expect(tester.getTopLeft(find.byType(AboutDialog)), Offset.zero);
+ expect(tester.getBottomRight(find.byType(AboutDialog)), const Offset(390.0, 600.0));
+ });
+
+ testWidgets('positioning using anchorPoint', (WidgetTester tester) async {
+ await tester.pumpWidget(MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: child!,
+ );
+ },
+ home: Builder(
+ builder: (BuildContext context) => ElevatedButton(
+ onPressed: () {
+ showAboutDialog(
+ context: context,
+ useRootNavigator: false,
+ applicationName: 'A',
+ anchorPoint: const Offset(1000, 0),
+ );
+ },
+ child: const Text('Show About Dialog'),
+ ),
+ ),
+ ));
+
+ // Open the dialog.
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pumpAndSettle();
+
+ // The anchorPoint hits the right side of the display
+ expect(tester.getTopLeft(find.byType(AboutDialog)), const Offset(410.0, 0.0));
+ expect(tester.getBottomRight(find.byType(AboutDialog)), const Offset(800.0, 600.0));
+ });
+
+ testWidgets('positioning using Directionality', (WidgetTester tester) async {
+ await tester.pumpWidget(MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: Directionality(
+ textDirection: TextDirection.rtl,
+ child: child!,
+ ),
+ );
+ },
+ home: Builder(
+ builder: (BuildContext context) => ElevatedButton(
+ onPressed: () {
+ showAboutDialog(
+ context: context,
+ useRootNavigator: false,
+ applicationName: 'A',
+ );
+ },
+ child: const Text('Show About Dialog'),
+ ),
+ ),
+ ));
+
+ // Open the dialog.
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pumpAndSettle();
+
+ // Since this is rtl, the first screen is the on the right
+ expect(tester.getTopLeft(find.byType(AboutDialog)), const Offset(410.0, 0.0));
+ expect(tester.getBottomRight(find.byType(AboutDialog)), const Offset(800.0, 600.0));
+ });
+ });
+
testWidgets("AboutListTile's child should not be offset when the icon is not specified.", (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
diff --git a/packages/flutter/test/material/bottom_sheet_test.dart b/packages/flutter/test/material/bottom_sheet_test.dart
index c70848ea4a..f2dcd2b6d4 100644
--- a/packages/flutter/test/material/bottom_sheet_test.dart
+++ b/packages/flutter/test/material/bottom_sheet_test.dart
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'dart:ui';
+
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -1254,6 +1256,123 @@ void main() {
await tester.tap(find.text('close 1'));
await tester.pumpAndSettle();
expect(find.text('BottomSheet 2'), findsOneWidget);
+ });
+
+ group('Modal BottomSheet avoids overlapping display features', () {
+ testWidgets('positioning using anchorPoint', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: child!,
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+
+ final BuildContext context = tester.element(find.text('Test'));
+ showModalBottomSheet(
+ context: context,
+ builder: (BuildContext context) {
+ return const Placeholder();
+ },
+ anchorPoint: const Offset(1000, 0),
+ );
+ await tester.pumpAndSettle();
+
+ // Should take the right side of the screen
+ expect(tester.getTopLeft(find.byType(Placeholder)).dx, 410);
+ expect(tester.getBottomRight(find.byType(Placeholder)).dx, 800);
+ });
+
+ testWidgets('positioning using Directionality', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: Directionality(
+ textDirection: TextDirection.rtl,
+ child: child!,
+ ),
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+
+ final BuildContext context = tester.element(find.text('Test'));
+ showModalBottomSheet(
+ context: context,
+ builder: (BuildContext context) {
+ return const Placeholder();
+ },
+ );
+ await tester.pumpAndSettle();
+
+ // This is RTL, so it should place the dialog on the right screen
+ expect(tester.getTopLeft(find.byType(Placeholder)).dx, 410);
+ expect(tester.getBottomRight(find.byType(Placeholder)).dx, 800);
+ });
+
+ testWidgets('default positioning', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: child!,
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+
+ final BuildContext context = tester.element(find.text('Test'));
+ showModalBottomSheet(
+ context: context,
+ builder: (BuildContext context) {
+ return const Placeholder();
+ },
+ );
+ await tester.pumpAndSettle();
+
+ // By default it should place the dialog on the left screen
+ expect(tester.getTopLeft(find.byType(Placeholder)).dx, 0.0);
+ expect(tester.getBottomRight(find.byType(Placeholder)).dx, 390.0);
+ });
});
group('constraints', () {
diff --git a/packages/flutter/test/material/date_picker_test.dart b/packages/flutter/test/material/date_picker_test.dart
index 70cf8119b1..b4a7ac3170 100644
--- a/packages/flutter/test/material/date_picker_test.dart
+++ b/packages/flutter/test/material/date_picker_test.dart
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'dart:ui';
+
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -1136,6 +1138,123 @@ void main() {
});
});
+ group('showDatePicker avoids overlapping display features', () {
+ testWidgets('positioning with anchorPoint', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: child!,
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+
+ final BuildContext context = tester.element(find.text('Test'));
+ showDatePicker(
+ context: context,
+ initialDate: DateTime.now(),
+ firstDate: DateTime(2018),
+ lastDate: DateTime(2030),
+ anchorPoint: const Offset(1000, 0),
+ );
+ await tester.pumpAndSettle();
+
+ // Should take the right side of the screen
+ expect(tester.getTopLeft(find.byType(DatePickerDialog)), const Offset(410.0, 0.0));
+ expect(tester.getBottomRight(find.byType(DatePickerDialog)), const Offset(800.0, 600.0));
+ });
+
+ testWidgets('positioning with Directionality', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: Directionality(
+ textDirection: TextDirection.rtl,
+ child: child!,
+ ),
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+
+ final BuildContext context = tester.element(find.text('Test'));
+ showDatePicker(
+ context: context,
+ initialDate: DateTime.now(),
+ firstDate: DateTime(2018),
+ lastDate: DateTime(2030),
+ );
+ await tester.pumpAndSettle();
+
+ // By default it should place the dialog on the right screen
+ expect(tester.getTopLeft(find.byType(DatePickerDialog)), const Offset(410.0, 0.0));
+ expect(tester.getBottomRight(find.byType(DatePickerDialog)), const Offset(800.0, 600.0));
+ });
+
+ testWidgets('positioning with defaults', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: child!,
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+
+ final BuildContext context = tester.element(find.text('Test'));
+ showDatePicker(
+ context: context,
+ initialDate: DateTime.now(),
+ firstDate: DateTime(2018),
+ lastDate: DateTime(2030),
+ );
+ await tester.pumpAndSettle();
+
+ // By default it should place the dialog on the left screen
+ expect(tester.getTopLeft(find.byType(DatePickerDialog)), Offset.zero);
+ expect(tester.getBottomRight(find.byType(DatePickerDialog)), const Offset(390.0, 600.0));
+ });
+ });
+
testWidgets('DatePickerDialog is state restorable', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
diff --git a/packages/flutter/test/material/date_range_picker_test.dart b/packages/flutter/test/material/date_range_picker_test.dart
index 9c6f5b98a8..3b0bb58dc4 100644
--- a/packages/flutter/test/material/date_range_picker_test.dart
+++ b/packages/flutter/test/material/date_range_picker_test.dart
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'dart:ui';
+
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -959,6 +961,121 @@ void main() {
expect(find.byType(TextField), findsNothing);
expect(find.byIcon(Icons.edit), findsNothing);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615
+
+ group('showDateRangePicker avoids overlapping display features', () {
+ testWidgets('positioning with anchorPoint', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: child!,
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+
+ final BuildContext context = tester.element(find.text('Test'));
+ showDateRangePicker(
+ context: context,
+ firstDate: DateTime(2018),
+ lastDate: DateTime(2030),
+ anchorPoint: const Offset(1000, 0),
+ );
+ await tester.pumpAndSettle();
+
+ // Should take the right side of the screen
+ expect(tester.getTopLeft(find.byType(DateRangePickerDialog)), const Offset(410.0, 0.0));
+ expect(tester.getBottomRight(find.byType(DateRangePickerDialog)), const Offset(800.0, 600.0));
+ });
+
+ testWidgets('positioning with Directionality', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: Directionality(
+ textDirection: TextDirection.rtl,
+ child: child!,
+ ),
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+
+ final BuildContext context = tester.element(find.text('Test'));
+ showDateRangePicker(
+ context: context,
+ firstDate: DateTime(2018),
+ lastDate: DateTime(2030),
+ anchorPoint: const Offset(1000, 0),
+ );
+ await tester.pumpAndSettle();
+
+ // By default it should place the dialog on the right screen
+ expect(tester.getTopLeft(find.byType(DateRangePickerDialog)), const Offset(410.0, 0.0));
+ expect(tester.getBottomRight(find.byType(DateRangePickerDialog)), const Offset(800.0, 600.0));
+ });
+
+ testWidgets('positioning with defaults', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: child!,
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+
+ final BuildContext context = tester.element(find.text('Test'));
+ showDateRangePicker(
+ context: context,
+ firstDate: DateTime(2018),
+ lastDate: DateTime(2030),
+ );
+ await tester.pumpAndSettle();
+
+ // By default it should place the dialog on the left screen
+ expect(tester.getTopLeft(find.byType(DateRangePickerDialog)), Offset.zero);
+ expect(tester.getBottomRight(find.byType(DateRangePickerDialog)), const Offset(390.0, 600.0));
+ });
+ });
}
class _RestorableDateRangePickerDialogTestWidget extends StatefulWidget {
diff --git a/packages/flutter/test/material/dialog_test.dart b/packages/flutter/test/material/dialog_test.dart
index c19bc90994..f1b958490a 100644
--- a/packages/flutter/test/material/dialog_test.dart
+++ b/packages/flutter/test/material/dialog_test.dart
@@ -1921,6 +1921,123 @@ void main() {
expect(nestedObserver.dialogCount, 1);
});
+ group('showDialog avoids overlapping display features', () {
+ testWidgets('positioning with anchorPoint', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: child!,
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+ final BuildContext context = tester.element(find.text('Test'));
+
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return const Placeholder();
+ },
+ anchorPoint: const Offset(1000, 0),
+ );
+ await tester.pumpAndSettle();
+
+ // Should take the right side of the screen
+ expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(410.0, 0.0));
+ expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0));
+ });
+
+ testWidgets('positioning with Directionality', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: Directionality(
+ textDirection: TextDirection.rtl,
+ child: child!,
+ ),
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+ final BuildContext context = tester.element(find.text('Test'));
+
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return const Placeholder();
+ },
+ );
+ await tester.pumpAndSettle();
+
+ // Since this is RTL, it should place the dialog on the right screen
+ expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(410.0, 0.0));
+ expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0));
+ });
+
+ testWidgets('positioning by default', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: child!,
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+ final BuildContext context = tester.element(find.text('Test'));
+
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return const Placeholder();
+ },
+ );
+ await tester.pumpAndSettle();
+
+ // By default it should place the dialog on the left screen
+ expect(tester.getTopLeft(find.byType(Placeholder)), Offset.zero);
+ expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(390.0, 600.0));
+ });
+ });
+
group('AlertDialog.scrollable: ', () {
testWidgets('Title is scrollable', (WidgetTester tester) async {
final Key titleKey = UniqueKey();
diff --git a/packages/flutter/test/material/time_picker_test.dart b/packages/flutter/test/material/time_picker_test.dart
index b2e7a36441..a04081463a 100644
--- a/packages/flutter/test/material/time_picker_test.dart
+++ b/packages/flutter/test/material/time_picker_test.dart
@@ -3,6 +3,8 @@
// found in the LICENSE file.
@TestOn('!chrome')
+import 'dart:ui';
+
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -849,6 +851,117 @@ void _tests() {
expect(tester.getSize(find.text('41')).height, equals(minutesDisplayHeight));
expect(tester.getSize(find.text('AM')).height, equals(amHeight2x));
});
+
+ group('showTimePicker avoids overlapping display features', () {
+ testWidgets('positioning with anchorPoint', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: child!,
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+ final BuildContext context = tester.element(find.text('Test'));
+
+ showTimePicker(
+ context: context,
+ initialTime: const TimeOfDay(hour: 7, minute: 0),
+ anchorPoint: const Offset(1000, 0),
+ );
+
+ await tester.pumpAndSettle();
+ // Should take the right side of the screen
+ expect(tester.getTopLeft(find.byType(TimePickerDialog)), const Offset(410.0, 0.0));
+ expect(tester.getBottomRight(find.byType(TimePickerDialog)), const Offset(800.0, 600.0));
+ });
+
+ testWidgets('positioning with Directionality', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: Directionality(
+ textDirection: TextDirection.rtl,
+ child: child!,
+ ),
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+ final BuildContext context = tester.element(find.text('Test'));
+
+ // By default it should place the dialog on the right screen
+ showTimePicker(
+ context: context,
+ initialTime: const TimeOfDay(hour: 7, minute: 0),
+ );
+
+ await tester.pumpAndSettle();
+ expect(tester.getTopLeft(find.byType(TimePickerDialog)), const Offset(410.0, 0.0));
+ expect(tester.getBottomRight(find.byType(TimePickerDialog)), const Offset(800.0, 600.0));
+ });
+
+ testWidgets('positioning with defaults', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: child!,
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+ final BuildContext context = tester.element(find.text('Test'));
+
+ // By default it should place the dialog on the left screen
+ showTimePicker(
+ context: context,
+ initialTime: const TimeOfDay(hour: 7, minute: 0),
+ );
+
+ await tester.pumpAndSettle();
+ expect(tester.getTopLeft(find.byType(TimePickerDialog)), Offset.zero);
+ expect(tester.getBottomRight(find.byType(TimePickerDialog)), const Offset(390.0, 600.0));
+ });
+ });
}
void _testsInput() {
diff --git a/packages/flutter/test/widgets/routes_test.dart b/packages/flutter/test/widgets/routes_test.dart
index 24f86c5f22..82340e832d 100644
--- a/packages/flutter/test/widgets/routes_test.dart
+++ b/packages/flutter/test/widgets/routes_test.dart
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:collection';
+import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -1190,6 +1191,123 @@ void main() {
expect(route.transitionDuration, isNotNull);
});
+ group('showGeneralDialog avoids overlapping display features', () {
+ testWidgets('positioning with anchorPoint', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: child!,
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+ final BuildContext context = tester.element(find.text('Test'));
+
+ showGeneralDialog(
+ context: context,
+ pageBuilder: (BuildContext context, _, __) {
+ return const Placeholder();
+ },
+ anchorPoint: const Offset(1000, 0),
+ );
+ await tester.pumpAndSettle();
+
+ // Should take the right side of the screen
+ expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(410.0, 0.0));
+ expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0));
+ });
+
+ testWidgets('positioning with Directionality', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: Directionality(
+ textDirection: TextDirection.rtl,
+ child: child!,
+ ),
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+ final BuildContext context = tester.element(find.text('Test'));
+
+ showGeneralDialog(
+ context: context,
+ pageBuilder: (BuildContext context, _, __) {
+ return const Placeholder();
+ },
+ );
+ await tester.pumpAndSettle();
+
+ // Since this is RTL, it should place the dialog on the right screen
+ expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(410.0, 0.0));
+ expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0));
+ });
+
+ testWidgets('positioning by default', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ // Display has a vertical hinge down the middle
+ data: const MediaQueryData(
+ size: Size(800, 600),
+ displayFeatures: [
+ DisplayFeature(
+ bounds: Rect.fromLTRB(390, 0, 410, 600),
+ type: DisplayFeatureType.hinge,
+ state: DisplayFeatureState.unknown,
+ ),
+ ],
+ ),
+ child: child!,
+ );
+ },
+ home: const Center(child: Text('Test')),
+ ),
+ );
+ final BuildContext context = tester.element(find.text('Test'));
+
+ showGeneralDialog(
+ context: context,
+ pageBuilder: (BuildContext context, _, __) {
+ return const Placeholder();
+ },
+ );
+ await tester.pumpAndSettle();
+
+ // By default it should place the dialog on the left screen
+ expect(tester.getTopLeft(find.byType(Placeholder)), Offset.zero);
+ expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(390.0, 600.0));
+ });
+ });
+
testWidgets('reverseTransitionDuration defaults to transitionDuration', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey();