Skip to content

Commit 42ea6b7

Browse files
committed
dialog: Use Cupertino-flavored alert dialogs on iOS
Fixes: zulip#996
1 parent 40165ef commit 42ea6b7

File tree

3 files changed

+180
-39
lines changed

3 files changed

+180
-39
lines changed

lib/widgets/dialog.dart

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import 'package:flutter/cupertino.dart';
2+
import 'package:flutter/foundation.dart';
13
import 'package:flutter/material.dart';
24

35
import '../generated/l10n/zulip_localizations.dart';
46
import 'actions.dart';
57

6-
Widget _dialogActionText(String text) {
8+
Widget _materialDialogActionText(String text) {
79
return Text(
810
text,
911

@@ -17,6 +19,20 @@ Widget _dialogActionText(String text) {
1719
);
1820
}
1921

22+
/// A platform-appropriate action for [AlertDialog.adaptive]'s [actions] param.
23+
Widget _adaptiveAction({required VoidCallback onPressed, required String text}) {
24+
switch (defaultTargetPlatform) {
25+
case TargetPlatform.android:
26+
case TargetPlatform.fuchsia:
27+
case TargetPlatform.linux:
28+
case TargetPlatform.windows:
29+
return TextButton(onPressed: onPressed, child: _materialDialogActionText(text));
30+
case TargetPlatform.iOS:
31+
case TargetPlatform.macOS:
32+
return CupertinoDialogAction(onPressed: onPressed, child: Text(text));
33+
}
34+
}
35+
2036
/// Tracks the status of a dialog, in being still open or already closed.
2137
///
2238
/// See also:
@@ -46,17 +62,18 @@ DialogStatus showErrorDialog({
4662
final zulipLocalizations = ZulipLocalizations.of(context);
4763
final future = showDialog<void>(
4864
context: context,
49-
builder: (BuildContext context) => AlertDialog(
65+
builder: (BuildContext context) => AlertDialog.adaptive(
5066
title: Text(title),
5167
content: message != null ? SingleChildScrollView(child: Text(message)) : null,
5268
actions: [
5369
if (learnMoreButtonUrl != null)
54-
TextButton(
70+
_adaptiveAction(
5571
onPressed: () => PlatformActions.launchUrl(context, learnMoreButtonUrl),
56-
child: _dialogActionText(zulipLocalizations.errorDialogLearnMore)),
57-
TextButton(
72+
text: zulipLocalizations.errorDialogContinue,
73+
),
74+
_adaptiveAction(
5875
onPressed: () => Navigator.pop(context),
59-
child: _dialogActionText(zulipLocalizations.errorDialogContinue)),
76+
text: zulipLocalizations.errorDialogContinue),
6077
]));
6178
return DialogStatus(future);
6279
}
@@ -71,18 +88,18 @@ void showSuggestedActionDialog({
7188
final zulipLocalizations = ZulipLocalizations.of(context);
7289
showDialog<void>(
7390
context: context,
74-
builder: (BuildContext context) => AlertDialog(
91+
builder: (BuildContext context) => AlertDialog.adaptive(
7592
title: Text(title),
7693
content: SingleChildScrollView(child: Text(message)),
7794
actions: [
78-
TextButton(
95+
_adaptiveAction(
7996
onPressed: () => Navigator.pop(context),
80-
child: _dialogActionText(zulipLocalizations.dialogCancel)),
81-
TextButton(
97+
text: zulipLocalizations.dialogCancel),
98+
_adaptiveAction(
8299
onPressed: () {
83100
onActionButtonPress();
84101
Navigator.pop(context);
85102
},
86-
child: _dialogActionText(actionButtonText ?? zulipLocalizations.dialogContinue)),
103+
text: actionButtonText ?? zulipLocalizations.dialogContinue),
87104
]));
88105
}

test/widgets/dialog_checks.dart

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import 'package:checks/checks.dart';
2+
import 'package:flutter/cupertino.dart';
3+
import 'package:flutter/foundation.dart';
24
import 'package:flutter/material.dart';
35
import 'package:flutter_checks/flutter_checks.dart';
46
import 'package:flutter_test/flutter_test.dart';
57
import 'package:zulip/widgets/dialog.dart';
68

7-
/// In a widget test, check that showErrorDialog was called with the right text.
9+
/// In a widget test, check that [showErrorDialog] was called with the right text.
810
///
911
/// Checks for an error dialog matching an expected title
1012
/// and, optionally, matching an expected message. Fails if none is found.
@@ -15,26 +17,44 @@ Widget checkErrorDialog(WidgetTester tester, {
1517
required String expectedTitle,
1618
String? expectedMessage,
1719
}) {
18-
final dialog = tester.widget<AlertDialog>(find.byType(AlertDialog));
19-
tester.widget(find.descendant(matchRoot: true,
20-
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
21-
if (expectedMessage != null) {
22-
tester.widget(find.descendant(matchRoot: true,
23-
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
24-
}
25-
2620
// TODO check "Learn more" button?
21+
switch (defaultTargetPlatform) {
22+
case TargetPlatform.android:
23+
case TargetPlatform.fuchsia:
24+
case TargetPlatform.linux:
25+
case TargetPlatform.windows:
26+
final dialog = tester.widget<AlertDialog>(find.bySubtype<AlertDialog>());
27+
tester.widget(find.descendant(matchRoot: true,
28+
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
29+
if (expectedMessage != null) {
30+
tester.widget(find.descendant(matchRoot: true,
31+
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
32+
}
33+
34+
return tester.widget(find.descendant(of: find.byWidget(dialog),
35+
matching: find.widgetWithText(TextButton, 'OK')));
2736

28-
return tester.widget(
29-
find.descendant(of: find.byWidget(dialog),
30-
matching: find.widgetWithText(TextButton, 'OK')));
37+
case TargetPlatform.iOS:
38+
case TargetPlatform.macOS:
39+
final dialog = tester.widget<CupertinoAlertDialog>(find.byType(CupertinoAlertDialog));
40+
tester.widget(find.descendant(matchRoot: true,
41+
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
42+
if (expectedMessage != null) {
43+
tester.widget(find.descendant(matchRoot: true,
44+
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
45+
}
46+
47+
return tester.widget(find.descendant(of: find.byWidget(dialog),
48+
matching: find.widgetWithText(CupertinoDialogAction, 'OK')));
49+
}
3150
}
3251

33-
// TODO(#996) update this to check for per-platform flavors of alert dialog
3452
/// Checks that there is no dialog.
3553
/// Fails if one is found.
3654
void checkNoDialog(WidgetTester tester) {
37-
check(find.byType(AlertDialog)).findsNothing();
55+
check(find.byType(Dialog)).findsNothing();
56+
check(find.bySubtype<AlertDialog>()).findsNothing();
57+
check(find.byType(CupertinoAlertDialog)).findsNothing();
3858
}
3959

4060
/// In a widget test, check that [showSuggestedActionDialog] was called
@@ -51,19 +71,35 @@ void checkNoDialog(WidgetTester tester) {
5171
required String expectedMessage,
5272
String? expectedActionButtonText,
5373
}) {
54-
final dialog = tester.widget<AlertDialog>(find.byType(AlertDialog));
55-
tester.widget(find.descendant(matchRoot: true,
56-
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
57-
tester.widget(find.descendant(matchRoot: true,
58-
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
74+
switch (defaultTargetPlatform) {
75+
case TargetPlatform.android:
76+
case TargetPlatform.fuchsia:
77+
case TargetPlatform.linux:
78+
case TargetPlatform.windows:
79+
final dialog = tester.widget<AlertDialog>(find.bySubtype<AlertDialog>());
80+
tester.widget(find.descendant(matchRoot: true,
81+
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
82+
tester.widget(find.descendant(matchRoot: true,
83+
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
5984

60-
final actionButton = tester.widget(
61-
find.descendant(of: find.byWidget(dialog),
62-
matching: find.widgetWithText(TextButton, expectedActionButtonText ?? 'Continue')));
85+
final actionButton = tester.widget(find.descendant(of: find.byWidget(dialog),
86+
matching: find.widgetWithText(TextButton, expectedActionButtonText ?? 'Continue')));
87+
final cancelButton = tester.widget(find.descendant(of: find.byWidget(dialog),
88+
matching: find.widgetWithText(TextButton, 'Cancel')));
89+
return (actionButton, cancelButton);
6390

64-
final cancelButton = tester.widget(
65-
find.descendant(of: find.byWidget(dialog),
66-
matching: find.widgetWithText(TextButton, 'Cancel')));
91+
case TargetPlatform.iOS:
92+
case TargetPlatform.macOS:
93+
final dialog = tester.widget<CupertinoAlertDialog>(find.byType(CupertinoAlertDialog));
94+
tester.widget(find.descendant(matchRoot: true,
95+
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
96+
tester.widget(find.descendant(matchRoot: true,
97+
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
6798

68-
return (actionButton, cancelButton);
99+
final actionButton = tester.widget(find.descendant(of: find.byWidget(dialog),
100+
matching: find.widgetWithText(CupertinoDialogAction, expectedActionButtonText ?? 'Continue')));
101+
final cancelButton = tester.widget(find.descendant(of: find.byWidget(dialog),
102+
matching: find.widgetWithText(CupertinoDialogAction, 'Cancel')));
103+
return (actionButton, cancelButton);
104+
}
69105
}

test/widgets/dialog_test.dart

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,51 @@
11
import 'package:checks/checks.dart';
2-
import 'package:flutter/widgets.dart';
2+
import 'package:flutter/material.dart';
33
import 'package:flutter_test/flutter_test.dart';
44
import 'package:url_launcher/url_launcher.dart';
55
import 'package:zulip/widgets/dialog.dart';
66

77
import '../model/binding.dart';
8+
import 'dialog_checks.dart';
89
import 'test_app.dart';
910

1011
void main() {
1112
TestZulipBinding.ensureInitialized();
1213

14+
late BuildContext context;
15+
16+
const title = "Dialog Title";
17+
const message = "Dialog message.";
18+
19+
Future<void> prepare(WidgetTester tester) async {
20+
addTearDown(testBinding.reset);
21+
22+
await tester.pumpWidget(const TestZulipApp(
23+
child: Scaffold(body: Placeholder())));
24+
await tester.pump();
25+
context = tester.element(find.byType(Placeholder));
26+
}
27+
1328
group('showErrorDialog', () {
29+
testWidgets('show error dialog', (tester) async {
30+
await prepare(tester);
31+
32+
showErrorDialog(context: context, title: title, message: message);
33+
await tester.pump();
34+
checkErrorDialog(tester, expectedTitle: title, expectedMessage: message);
35+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
36+
37+
testWidgets('user closes error dialog', (tester) async {
38+
await prepare(tester);
39+
40+
showErrorDialog(context: context, title: title, message: message);
41+
await tester.pump();
42+
43+
final button = checkErrorDialog(tester, expectedTitle: title);
44+
await tester.tap(find.byWidget(button));
45+
await tester.pump();
46+
checkNoDialog(tester);
47+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
48+
1449
testWidgets('tap "Learn more" button', (tester) async {
1550
addTearDown(testBinding.reset);
1651
await tester.pumpWidget(TestZulipApp());
@@ -24,6 +59,59 @@ void main() {
2459
check(testBinding.takeLaunchUrlCalls()).single.equals((
2560
url: Uri.parse('https://foo.example'),
2661
mode: LaunchMode.inAppBrowserView));
27-
});
62+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
63+
});
64+
65+
group('showSuggestedActionDialog', () {
66+
const actionButtonText = "Action";
67+
68+
testWidgets('show suggested action dialog', (tester) async {
69+
await prepare(tester);
70+
71+
showSuggestedActionDialog(context: context, title: title, message: message,
72+
actionButtonText: actionButtonText, onActionButtonPress: () {});
73+
await tester.pump();
74+
75+
checkSuggestedActionDialog(tester, expectedTitle: title, expectedMessage: message,
76+
expectedActionButtonText: actionButtonText);
77+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
78+
79+
testWidgets('user presses action button', (tester) async {
80+
await prepare(tester);
81+
82+
bool wasPressed = false;
83+
void onActionButtonPress() {
84+
wasPressed = true;
85+
}
86+
showSuggestedActionDialog(context: context, title: title, message: message,
87+
actionButtonText: actionButtonText, onActionButtonPress: onActionButtonPress);
88+
await tester.pump();
89+
90+
final (actionButton, _) = checkSuggestedActionDialog(tester, expectedTitle: title,
91+
expectedMessage: message, expectedActionButtonText: actionButtonText);
92+
await tester.tap(find.byWidget(actionButton));
93+
await tester.pump();
94+
checkNoDialog(tester);
95+
check(wasPressed).isTrue();
96+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
97+
98+
testWidgets('user cancels', (tester) async {
99+
await prepare(tester);
100+
101+
bool wasPressed = false;
102+
void onActionButtonPress() {
103+
wasPressed = true;
104+
}
105+
showSuggestedActionDialog(context: context, title: title, message: message,
106+
actionButtonText: actionButtonText, onActionButtonPress: onActionButtonPress);
107+
await tester.pump();
108+
109+
final (_, cancelButton) = checkSuggestedActionDialog(tester, expectedTitle: title,
110+
expectedMessage: message, expectedActionButtonText: actionButtonText);
111+
await tester.tap(find.byWidget(cancelButton));
112+
await tester.pump();
113+
checkNoDialog(tester);
114+
check(wasPressed).isFalse();
115+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
28116
});
29117
}

0 commit comments

Comments
 (0)