CupertinoActionSheet (#19232)

Adding CupertinoActionSheet, showCupertinoModalPopup
This commit is contained in:
Natalie Sampsell 2018-08-09 21:55:41 -07:00 committed by GitHub
parent 21f22ed3ba
commit 96326d4743
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 2392 additions and 4 deletions

View File

@ -7,6 +7,7 @@
/// To use, import `package:flutter/cupertino.dart`.
library cupertino;
export 'src/cupertino/action_sheet.dart';
export 'src/cupertino/activity_indicator.dart';
export 'src/cupertino/app.dart';
export 'src/cupertino/bottom_tab_bar.dart';

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,9 @@ const double _kMinFlingVelocity = 1.0; // Screen widths per second.
// Barrier color for a Cupertino modal barrier.
const Color _kModalBarrierColor = Color(0x6604040F);
// The duration of the transition used when a modal popup is shown.
const Duration _kModalPopupTransitionDuration = Duration(milliseconds: 335);
// Offset from offscreen to the right to fully on screen.
final Tween<Offset> _kRightMiddleTween = new Tween<Offset>(
begin: const Offset(1.0, 0.0),
@ -715,6 +718,102 @@ class _CupertinoEdgeShadowPainter extends BoxPainter {
}
}
class _CupertinoModalPopupRoute<T> extends PopupRoute<T> {
_CupertinoModalPopupRoute({
this.builder,
this.barrierLabel,
RouteSettings settings,
}) : super(settings: settings);
final WidgetBuilder builder;
@override
final String barrierLabel;
@override
Color get barrierColor => _kModalBarrierColor;
@override
bool get barrierDismissible => true;
@override
bool get semanticsDismissible => false;
@override
Duration get transitionDuration => _kModalPopupTransitionDuration;
Animation<double> _animation;
Tween<Offset> _offsetTween;
@override
Animation<double> createAnimation() {
assert(_animation == null);
_animation = new CurvedAnimation(
parent: super.createAnimation(),
curve: Curves.ease,
reverseCurve: Curves.ease.flipped,
);
_offsetTween = new Tween<Offset>(
begin: const Offset(0.0, 1.0),
end: const Offset(0.0, 0.0),
);
return _animation;
}
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return builder(context);
}
@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return new Align(
alignment: Alignment.bottomCenter,
child: new FractionalTranslation(
translation: _offsetTween.evaluate(_animation),
child: child,
),
);
}
}
/// Shows a modal iOS-style popup that slides up from the bottom of the screen.
///
/// Such a popup is an alternative to a menu or a dialog and prevents the user
/// from interacting with the rest of the app.
///
/// The `context` argument is used to look up the [Navigator] for the popup.
/// It is only used when the method is called. Its corresponding widget can be
/// safely removed from the tree before the popup is closed.
///
/// The `builder` argument typically builds a [CupertinoActionSheet] widget.
/// Content below the widget is dimmed with a [ModalBarrier]. The widget built
/// by the `builder` does not share a context with the location that
/// `showCupertinoModalPopup` is originally called from. Use a
/// [StatefulBuilder] or a custom [StatefulWidget] if the widget needs to
/// update dynamically.
///
/// Returns a `Future` that resolves to the value that was passed to
/// [Navigator.pop] when the popup was closed.
///
/// See also:
///
/// * [ActionSheet], which is the widget usually returned by the `builder`
/// argument to [showCupertinoModalPopup].
/// * <https://developer.apple.com/design/human-interface-guidelines/ios/views/action-sheets/>
Future<T> showCupertinoModalPopup<T>({
@required BuildContext context,
@required WidgetBuilder builder,
}) {
return Navigator.of(context, rootNavigator: true).push(
new _CupertinoModalPopupRoute<T>(
builder: builder,
barrierLabel: 'Dismiss',
),
);
}
Widget _buildCupertinoDialogTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
final CurvedAnimation fadeAnimation = new CurvedAnimation(
parent: animation,

View File

@ -70,6 +70,7 @@ class BoxDecoration extends Decoration {
/// [BoxShape.circle].
/// * If [boxShadow] is null, this decoration does not paint a shadow.
/// * If [gradient] is null, this decoration does not paint gradients.
/// * If [backgroundBlendMode] is null, this decoration paints with [BlendMode.srcOver]
///
/// The [shape] argument must not be null.
const BoxDecoration({
@ -79,13 +80,20 @@ class BoxDecoration extends Decoration {
this.borderRadius,
this.boxShadow,
this.gradient,
this.backgroundBlendMode,
this.shape = BoxShape.rectangle,
}) : assert(shape != null);
}) : assert(shape != null),
// TODO(mattcarroll): Use "backgroundBlendMode == null" when Dart #31140 is in.
assert(
identical(backgroundBlendMode, null) || color != null || gradient != null,
'backgroundBlendMode applies to BoxDecoration\'s background color or'
'gradient, but no color or gradient were provided.'
);
@override
bool debugAssertIsValid() {
assert(shape != BoxShape.circle ||
borderRadius == null); // Can't have a border radius if you're a circle.
borderRadius == null); // Can't have a border radius if you're a circle.
return super.debugAssertIsValid();
}
@ -136,6 +144,14 @@ class BoxDecoration extends Decoration {
/// The [gradient] is drawn under the [image].
final Gradient gradient;
/// The blend mode applied to the [color] or [gradient] background of the box.
///
/// If no [backgroundBlendMode] is provided, then the default painting blend
/// mode is used.
///
/// If no [color] or [gradient] is provided, then blend mode has no impact.
final BlendMode backgroundBlendMode;
/// The shape to fill the background [color], [gradient], and [image] into and
/// to cast as the [boxShadow].
///
@ -332,6 +348,8 @@ class _BoxDecorationPainter extends BoxPainter {
if (_cachedBackgroundPaint == null ||
(_decoration.gradient != null && _rectForCachedBackgroundPaint != rect)) {
final Paint paint = new Paint();
if (_decoration.backgroundBlendMode != null)
paint.blendMode = _decoration.backgroundBlendMode;
if (_decoration.color != null)
paint.color = _decoration.color;
if (_decoration.gradient != null) {

View File

@ -33,6 +33,7 @@ class ModalBarrier extends StatelessWidget {
this.color,
this.dismissible = true,
this.semanticsLabel,
this.barrierSemanticsDismissible = true,
}) : super(key: key);
/// If non-null, fill the barrier with this color.
@ -51,6 +52,13 @@ class ModalBarrier extends StatelessWidget {
/// [ModalBarrier] built by [ModalRoute] pages.
final bool dismissible;
/// Whether the modal barrier semantics are included in the semantics tree.
///
/// See also:
/// * [ModalRoute.semanticsDismissible], which controls this property for
/// the [ModalBarrier] built by [ModalRoute] pages.
final bool barrierSemanticsDismissible;
/// Semantics label used for the barrier if it is [dismissable].
///
/// The semantics label is read out by accessibility tools (e.g. TalkBack
@ -66,10 +74,12 @@ class ModalBarrier extends StatelessWidget {
Widget build(BuildContext context) {
assert(!dismissible || semanticsLabel == null || debugCheckHasDirectionality(context));
final bool semanticsDismissible = dismissible && defaultTargetPlatform != TargetPlatform.android;
final bool modalBarrierSemanticsDismissible = barrierSemanticsDismissible ?? semanticsDismissible;
return new BlockSemantics(
child: new ExcludeSemantics(
// On Android, the back button is used to dismiss a modal.
excluding: !semanticsDismissible,
// On Android, the back button is used to dismiss a modal. On iOS, some
// modal barriers are not dismissible in accessibility mode.
excluding: !semanticsDismissible || !modalBarrierSemanticsDismissible,
child: new GestureDetector(
onTapDown: (TapDownDetails details) {
if (dismissible)
@ -117,6 +127,7 @@ class AnimatedModalBarrier extends AnimatedWidget {
Animation<Color> color,
this.dismissible = true,
this.semanticsLabel,
this.barrierSemanticsDismissible,
}) : super(key: key, listenable: color);
/// If non-null, fill the barrier with this color.
@ -145,12 +156,20 @@ class AnimatedModalBarrier extends AnimatedWidget {
/// [ModalBarrier] built by [ModalRoute] pages.
final String semanticsLabel;
/// Whether the modal barrier semantics are included in the semantics tree.
///
/// See also:
/// * [ModalRoute.semanticsDismissible], which controls this property for
/// the [ModalBarrier] built by [ModalRoute] pages.
final bool barrierSemanticsDismissible;
@override
Widget build(BuildContext context) {
return new ModalBarrier(
color: color?.value,
dismissible: dismissible,
semanticsLabel: semanticsLabel,
barrierSemanticsDismissible: barrierSemanticsDismissible,
);
}
}

View File

@ -898,6 +898,21 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// * [ModalBarrier], the widget that implements this feature.
bool get barrierDismissible;
/// Whether the semantics of the modal barrier are included in the
/// semantics tree.
///
/// The modal barrier is the scrim that is rendered behind each route, which
/// generally prevents the user from interacting with the route below the
/// current route, and normally partially obscures such routes.
///
/// If [semanticsDismissible] is true, then modal barrier semantics are
/// included in the semantics tree.
///
/// If [semanticsDismissible] is false, then modal barrier semantics are
/// excluded from the the semantics tree and tapping on the modal barrier
/// has no effect.
bool get semanticsDismissible => true;
/// The color to use for the modal barrier. If this is null, the barrier will
/// be transparent.
///
@ -1173,11 +1188,13 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
color: color,
dismissible: barrierDismissible, // changedInternalState is called if this updates
semanticsLabel: barrierLabel, // changedInternalState is called if this updates
barrierSemanticsDismissible: semanticsDismissible,
);
} else {
barrier = new ModalBarrier(
dismissible: barrierDismissible, // changedInternalState is called if this updates
semanticsLabel: barrierLabel, // changedInternalState is called if this updates
barrierSemanticsDismissible: semanticsDismissible,
);
}
return new IgnorePointer(

View File

@ -0,0 +1,967 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.dart';
void main() {
testWidgets('Verify that a tap on modal barrier dismisses an action sheet',
(WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
const CupertinoActionSheet(
title: Text('Action Sheet'),
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
expect(find.text('Action Sheet'), findsOneWidget);
await tester.tapAt(const Offset(20.0, 20.0));
await tester.pump();
expect(find.text('Action Sheet'), findsNothing);
});
testWidgets('Verify that a tap on title section (not buttons) does not dismiss an action sheet',
(WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
const CupertinoActionSheet(
title: Text('Action Sheet'),
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
expect(find.text('Action Sheet'), findsOneWidget);
await tester.tap(find.text('Action Sheet'));
await tester.pump();
expect(find.text('Action Sheet'), findsOneWidget);
});
testWidgets('Action sheet destructive text style', (WidgetTester tester) async {
await tester.pumpWidget(
boilerplate(
new CupertinoActionSheetAction(
isDestructiveAction: true,
child: const Text('Ok'),
onPressed: () {},
),
),
);
final DefaultTextStyle widget = tester.widget(find.widgetWithText(DefaultTextStyle, 'Ok'));
expect(widget.style.color, CupertinoColors.destructiveRed);
});
testWidgets('Action sheet default text style', (WidgetTester tester) async {
await tester.pumpWidget(
boilerplate(
new CupertinoActionSheetAction(
isDefaultAction: true,
child: const Text('Ok'),
onPressed: () {},
),
),
);
final DefaultTextStyle widget = tester.widget(find.widgetWithText(DefaultTextStyle, 'Ok'));
expect(widget.style.fontWeight, equals(FontWeight.w600));
});
testWidgets('Action sheet text styles are correct when both title and message are included',
(WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
const CupertinoActionSheet(
title: Text('Action Sheet'),
message: Text('An action sheet')
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
final DefaultTextStyle titleStyle = tester.firstWidget(find.widgetWithText(DefaultTextStyle,
'Action Sheet'));
final DefaultTextStyle messageStyle = tester.firstWidget(find.widgetWithText(DefaultTextStyle,
'An action sheet'));
expect(titleStyle.style.fontWeight, FontWeight.w600);
expect(messageStyle.style.fontWeight, FontWeight.w400);
});
testWidgets('Action sheet text styles are correct when title but no message is included',
(WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
const CupertinoActionSheet(
title: Text('Action Sheet'),
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
final DefaultTextStyle titleStyle = tester.firstWidget(find.widgetWithText(DefaultTextStyle,
'Action Sheet'));
expect(titleStyle.style.fontWeight, FontWeight.w400);
});
testWidgets('Action sheet text styles are correct when message but no title is included',
(WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
const CupertinoActionSheet(
message: Text('An action sheet'),
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
final DefaultTextStyle messageStyle = tester.firstWidget(find.widgetWithText(DefaultTextStyle,
'An action sheet'));
expect(messageStyle.style.fontWeight, FontWeight.w600);
});
testWidgets('Content section but no actions', (WidgetTester tester) async {
final ScrollController scrollController = new ScrollController();
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
new CupertinoActionSheet(
title: const Text('The title'),
message: const Text('The message.'),
messageScrollController: scrollController,
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
// Content section should be at the bottom left of action sheet
// (minus padding).
expect(tester.getBottomLeft(find.byType(ClipRRect)),
tester.getBottomLeft(find.byType(CupertinoActionSheet)) - const Offset(-8.0, 10.0));
// Check that the dialog size is the same as the content section size
// (minus padding).
expect(
tester.getSize(find.byType(ClipRRect)).height,
tester.getSize(find.byType(CupertinoActionSheet)).height - 20.0,
);
expect(
tester.getSize(find.byType(ClipRRect)).width,
tester.getSize(find.byType(CupertinoActionSheet)).width - 16.0,
);
});
testWidgets('Actions but no content section', (WidgetTester tester) async {
final ScrollController actionScrollController = new ScrollController();
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
new CupertinoActionSheet(
actions: <Widget>[
new CupertinoActionSheetAction(
child: const Text('One'),
onPressed: () {},
),
new CupertinoActionSheetAction(
child: const Text('Two'),
onPressed: () {},
),
],
actionScrollController: actionScrollController,
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
final Finder finder = find.byElementPredicate(
(Element element) {
return element.widget.runtimeType.toString() == '_CupertinoAlertActionSection';
},
);
// Check that the title/message section is not displayed (action section is
// at the top of the action sheet + padding).
expect(tester.getTopLeft(finder),
tester.getTopLeft(find.byType(CupertinoActionSheet)) + const Offset(8.0, 10.0));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)) + const Offset(8.0, 10.0),
tester.getTopLeft(find.widgetWithText(CupertinoActionSheetAction, 'One')));
expect(tester.getBottomLeft(find.byType(CupertinoActionSheet)) + const Offset(8.0, -10.0),
tester.getBottomLeft(find.widgetWithText(CupertinoActionSheetAction, 'Two')));
});
testWidgets('Action section is scrollable', (WidgetTester tester) async {
final ScrollController actionScrollController = new ScrollController();
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
new Builder(builder: (BuildContext context) {
return new MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 3.0),
child: new CupertinoActionSheet(
title: const Text('The title'),
message: const Text('The message.'),
actions: <Widget>[
new CupertinoActionSheetAction(
child: const Text('One'),
onPressed: () {},
),
new CupertinoActionSheetAction(
child: const Text('Two'),
onPressed: () {},
),
new CupertinoActionSheetAction(
child: const Text('Three'),
onPressed: () {},
),
new CupertinoActionSheetAction(
child: const Text('Four'),
onPressed: () {},
),
new CupertinoActionSheetAction(
child: const Text('Five'),
onPressed: () {},
),
],
actionScrollController: actionScrollController,
),
);
}),
)
);
await tester.tap(find.text('Go'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
// Check that the action buttons list is scrollable.
expect(actionScrollController.offset, 0.0);
actionScrollController.jumpTo(100.0);
expect(actionScrollController.offset, 100.0);
actionScrollController.jumpTo(0.0);
// Check that the action buttons are aligned vertically.
expect(tester.getCenter(find.widgetWithText(CupertinoActionSheetAction, 'One')).dx, equals(400.0));
expect(tester.getCenter(find.widgetWithText(CupertinoActionSheetAction, 'Two')).dx, equals(400.0));
expect(tester.getCenter(find.widgetWithText(CupertinoActionSheetAction, 'Three')).dx, equals(400.0));
expect(tester.getCenter(find.widgetWithText(CupertinoActionSheetAction, 'Four')).dx, equals(400.0));
expect(tester.getCenter(find.widgetWithText(CupertinoActionSheetAction, 'Five')).dx, equals(400.0));
// Check that the action buttons are the correct heights.
expect(tester.getSize(find.widgetWithText(CupertinoActionSheetAction, 'One')).height, equals(92.0));
expect(tester.getSize(find.widgetWithText(CupertinoActionSheetAction, 'Two')).height, equals(92.0));
expect(tester.getSize(find.widgetWithText(CupertinoActionSheetAction, 'Three')).height, equals(92.0));
expect(tester.getSize(find.widgetWithText(CupertinoActionSheetAction, 'Four')).height, equals(92.0));
expect(tester.getSize(find.widgetWithText(CupertinoActionSheetAction, 'Five')).height, equals(92.0));
});
testWidgets('Content section is scrollable', (WidgetTester tester) async {
final ScrollController messageScrollController = new ScrollController();
double screenHeight;
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
new Builder(builder: (BuildContext context) {
screenHeight = MediaQuery.of(context).size.height;
return new MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 3.0),
child: new CupertinoActionSheet(
title: const Text('The title'),
message: new Text('Very long content' * 200),
actions: <Widget>[
new CupertinoActionSheetAction(
child: const Text('One'),
onPressed: () {},
),
new CupertinoActionSheetAction(
child: const Text('Two'),
onPressed: () {},
),
],
messageScrollController: messageScrollController,
),
);
}),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
expect(messageScrollController.offset, 0.0);
messageScrollController.jumpTo(100.0);
expect(messageScrollController.offset, 100.0);
// Set the scroll position back to zero.
messageScrollController.jumpTo(0.0);
// Expect the action sheet to take all available height.
expect(tester.getSize(find.byType(CupertinoActionSheet)).height, screenHeight);
});
testWidgets('Tap on button calls onPressed', (WidgetTester tester) async {
bool wasPressed = false;
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
new Builder(builder: (BuildContext context) {
return new CupertinoActionSheet(
actions: <Widget>[
new CupertinoActionSheetAction(
child: const Text('One'),
onPressed: () {
wasPressed = true;
Navigator.pop(context);
},
),
],
);
}),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(wasPressed, isFalse);
await tester.tap(find.text('One'));
expect(wasPressed, isTrue);
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('One'), findsNothing);
});
testWidgets('Action sheet width is correct when given infinite horizontal space',
(WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
new Row(
children: <Widget>[
new CupertinoActionSheet(
actions: <Widget>[
new CupertinoActionSheetAction(
child: const Text('One'),
onPressed: () {},
),
new CupertinoActionSheetAction(
child: const Text('Two'),
onPressed: () {},
),
],
),
],
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
expect(tester.getSize(find.byType(CupertinoActionSheet)).width, 600.0);
});
testWidgets('Action sheet height is correct when given infinite vertical space',
(WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
new Column(
children: <Widget>[
new CupertinoActionSheet(
actions: <Widget>[
new CupertinoActionSheetAction(
child: const Text('One'),
onPressed: () {},
),
new CupertinoActionSheetAction(
child: const Text('Two'),
onPressed: () {},
),
],
),
],
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
expect(tester.getSize(find.byType(CupertinoActionSheet)).height,
moreOrLessEquals(132.33333333333334));
});
testWidgets('1 action button with cancel button', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
new CupertinoActionSheet(
title: const Text('The title'),
message: new Text('Very long content' * 200),
actions: <Widget>[
new CupertinoActionSheetAction(
child: const Text('One'),
onPressed: () {},
),
],
cancelButton: new CupertinoActionSheetAction(
child: const Text('Cancel'),
onPressed: () {},
),
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
// Action section is size of one action button.
expect(findScrollableActionsSectionRenderBox(tester).size.height, 56.0);
});
testWidgets('2 action buttons with cancel button', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
new CupertinoActionSheet(
title: const Text('The title'),
message: new Text('Very long content' * 200),
actions: <Widget>[
new CupertinoActionSheetAction(
child: const Text('One'),
onPressed: () {},
),
new CupertinoActionSheetAction(
child: const Text('Two'),
onPressed: () {},
),
],
cancelButton: new CupertinoActionSheetAction(
child: const Text('Cancel'),
onPressed: () {},
),
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
expect(findScrollableActionsSectionRenderBox(tester).size.height,
moreOrLessEquals(112.33333333333331));
});
testWidgets('3 action buttons with cancel button', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
new CupertinoActionSheet(
title: const Text('The title'),
message: new Text('Very long content' * 200),
actions: <Widget>[
new CupertinoActionSheetAction(
child: const Text('One'),
onPressed: () {},
),
new CupertinoActionSheetAction(
child: const Text('Two'),
onPressed: () {},
),
new CupertinoActionSheetAction(
child: const Text('Three'),
onPressed: () {},
),
],
cancelButton: new CupertinoActionSheetAction(
child: const Text('Cancel'),
onPressed: () {},
),
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
expect(findScrollableActionsSectionRenderBox(tester).size.height,
moreOrLessEquals(168.66666666666669));
});
testWidgets('4+ action buttons with cancel button', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
new CupertinoActionSheet(
title: const Text('The title'),
message: new Text('Very long content' * 200),
actions: <Widget>[
new CupertinoActionSheetAction(
child: const Text('One'),
onPressed: () {},
),
new CupertinoActionSheetAction(
child: const Text('Two'),
onPressed: () {},
),
new CupertinoActionSheetAction(
child: const Text('Three'),
onPressed: () {},
),
new CupertinoActionSheetAction(
child: const Text('Four'),
onPressed: () {},
),
],
cancelButton: new CupertinoActionSheetAction(
child: const Text('Cancel'),
onPressed: () {},
),
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
expect(findScrollableActionsSectionRenderBox(tester).size.height,
moreOrLessEquals(84.33333333333337));
});
testWidgets('1 action button without cancel button', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
new CupertinoActionSheet(
title: const Text('The title'),
message: new Text('Very long content' * 200),
actions: <Widget>[
new CupertinoActionSheetAction(
child: const Text('One'),
onPressed: () {},
),
],
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
expect(findScrollableActionsSectionRenderBox(tester).size.height, 56.0);
});
testWidgets('2+ action buttons without cancel button', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
new CupertinoActionSheet(
title: const Text('The title'),
message: new Text('Very long content' * 200),
actions: <Widget>[
new CupertinoActionSheetAction(
child: const Text('One'),
onPressed: () {},
),
new CupertinoActionSheetAction(
child: const Text('Two'),
onPressed: () {},
),
],
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
expect(findScrollableActionsSectionRenderBox(tester).size.height,
moreOrLessEquals(84.33333333333337));
});
testWidgets('Action sheet with just cancel button is correct', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
new CupertinoActionSheet(
cancelButton: new CupertinoActionSheetAction(
child: const Text('Cancel'),
onPressed: (){},
),
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
// Height should be cancel button height + padding
expect(tester.getSize(find.byType(CupertinoActionSheet)).height, 76.0);
expect(tester.getSize(find.byType(CupertinoActionSheet)).width, 600.0);
});
testWidgets('Cancel button tap calls onPressed', (WidgetTester tester) async {
bool wasPressed = false;
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
new Builder(builder: (BuildContext context) {
return new CupertinoActionSheet(
cancelButton: new CupertinoActionSheetAction(
child: const Text('Cancel'),
onPressed: () {
wasPressed = true;
Navigator.pop(context);
},
),
);
}),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(wasPressed, isFalse);
await tester.tap(find.text('Cancel'));
expect(wasPressed, isTrue);
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Cancel'), findsNothing);
});
testWidgets('Layout is correct when cancel button is present', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
new CupertinoActionSheet(
title: const Text('The title'),
message: const Text('The message'),
actions: <Widget>[
new CupertinoActionSheetAction(
child: const Text('One'),
onPressed: () {},
),
new CupertinoActionSheetAction(
child: const Text('Two'),
onPressed: () {},
),
],
cancelButton: new CupertinoActionSheetAction(
child: const Text('Cancel'),
onPressed: () {},
),
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(tester.getBottomLeft(find.widgetWithText(CupertinoActionSheetAction, 'Cancel')).dy, 590.0);
expect(tester.getBottomLeft(find.widgetWithText(CupertinoActionSheetAction, 'One')).dy,
moreOrLessEquals(469.66666666666663));
expect(tester.getBottomLeft(find.widgetWithText(CupertinoActionSheetAction, 'Two')).dy, 526.0);
});
testWidgets('Enter/exit animation is correct', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
new CupertinoActionSheet(
title: const Text('The title'),
message: const Text('The message'),
actions: <Widget>[
new CupertinoActionSheetAction(
child: const Text('One'),
onPressed: () {},
),
new CupertinoActionSheetAction(
child: const Text('Two'),
onPressed: () {},
),
],
cancelButton: new CupertinoActionSheetAction(
child: const Text('Cancel'),
onPressed: () {},
),
),
),
);
// Enter animation
await tester.tap(find.text('Go'));
await tester.pump();
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, 600.0);
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(530.9, 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(426.7, 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(365.0, 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(334.0, 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(321.0, 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(319.3, 0.1));
// Action sheet has reached final height
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(319.3, 0.1));
// Exit animation
await tester.tapAt(const Offset(20.0, 20.0));
await tester.pump();
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(319.3, 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(388.4, 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(492.6, 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(554.2, 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(585.2, 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(598.2, 0.1));
// Action sheet has disappeared
await tester.pump(const Duration(milliseconds: 60));
expect(find.byType(CupertinoActionSheet), findsNothing);
});
testWidgets('Modal barrier is pressed during transition', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
new CupertinoActionSheet(
title: const Text('The title'),
message: const Text('The message'),
actions: <Widget>[
new CupertinoActionSheetAction(
child: const Text('One'),
onPressed: () {},
),
new CupertinoActionSheetAction(
child: const Text('Two'),
onPressed: () {},
),
],
cancelButton: new CupertinoActionSheetAction(
child: const Text('Cancel'),
onPressed: () {},
),
),
),
);
// Enter animation
await tester.tap(find.text('Go'));
await tester.pump();
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, 600.0);
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(530.9, 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(426.7, 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(365.0, 0.1));
// Exit animation
await tester.tapAt(const Offset(20.0, 20.0));
await tester.pump(const Duration(milliseconds: 60));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(426.7, 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, closeTo(530.9, 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, 600.0);
// Action sheet has disappeared
await tester.pump(const Duration(milliseconds: 60));
expect(find.byType(CupertinoActionSheet), findsNothing);
});
testWidgets('Action sheet semantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
new CupertinoActionSheet(
title: const Text('The title'),
message: const Text('The message'),
actions: <Widget>[
new CupertinoActionSheetAction(
child: const Text('One'),
onPressed: () {},
),
new CupertinoActionSheetAction(
child: const Text('Two'),
onPressed: () {},
),
],
cancelButton: new CupertinoActionSheetAction(
child: const Text('Cancel'),
onPressed: () {},
),
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
expect(
semantics,
hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.scopesRoute,
SemanticsFlag.namesRoute,
],
label: 'Alert',
children: <TestSemantics>[
new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
label: 'The title',
),
new TestSemantics(
label: 'The message',
),
],
),
new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
],
actions: <SemanticsAction>[
SemanticsAction.tap,
],
label: 'One',
),
new TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
],
actions: <SemanticsAction>[
SemanticsAction.tap,
],
label: 'Two',
),
],
),
new TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
],
actions: <SemanticsAction>[
SemanticsAction.tap,
],
label: 'Cancel',
)
],
),
],
),
],
),
ignoreId: true,
ignoreRect: true,
ignoreTransform: true,
),
);
semantics.dispose();
});
}
RenderBox findScrollableActionsSectionRenderBox(WidgetTester tester) {
final RenderObject actionsSection = tester.renderObject(find.byElementPredicate(
(Element element) {
return element.widget.runtimeType.toString() == '_CupertinoAlertActionSection';
}),
);
assert(actionsSection is RenderBox);
return actionsSection;
}
Widget createAppWithButtonThatLaunchesActionSheet(Widget actionSheet) {
return new CupertinoApp(
home: new Center(
child: new Builder(builder: (BuildContext context) {
return new CupertinoButton(
onPressed: () {
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) {
return actionSheet;
},
);
},
child: const Text('Go'),
);
}),
),
);
}
Widget boilerplate(Widget child) {
return new Directionality(
textDirection: TextDirection.ltr,
child: child,
);
}