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();