Skip to content

Commit 3d26d9f

Browse files
committed
dialog: Use Cupertino-flavored alert dialogs on iOS
Fixes: zulip#996
1 parent acb93ea commit 3d26d9f

File tree

3 files changed

+154
-55
lines changed

3 files changed

+154
-55
lines changed

lib/widgets/dialog.dart

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
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';
@@ -7,7 +9,7 @@ import 'app.dart';
79
import 'content.dart';
810
import 'store.dart';
911

10-
Widget _dialogActionText(String text) {
12+
Widget _materialDialogActionText(String text) {
1113
return Text(
1214
text,
1315

@@ -21,6 +23,20 @@ Widget _dialogActionText(String text) {
2123
);
2224
}
2325

26+
/// A platform-appropriate action for [AlertDialog.adaptive]'s [actions] param.
27+
Widget _adaptiveAction({required VoidCallback onPressed, required String text}) {
28+
switch (defaultTargetPlatform) {
29+
case TargetPlatform.android:
30+
case TargetPlatform.fuchsia:
31+
case TargetPlatform.linux:
32+
case TargetPlatform.windows:
33+
return TextButton(onPressed: onPressed, child: _materialDialogActionText(text));
34+
case TargetPlatform.iOS:
35+
case TargetPlatform.macOS:
36+
return CupertinoDialogAction(onPressed: onPressed, child: Text(text));
37+
}
38+
}
39+
2440
/// Tracks the status of a dialog, in being still open or already closed.
2541
///
2642
/// Use [T] to identify the outcome of the interaction:
@@ -71,17 +87,17 @@ DialogStatus<void> showErrorDialog({
7187
final zulipLocalizations = ZulipLocalizations.of(context);
7288
final future = showDialog<void>(
7389
context: context,
74-
builder: (BuildContext context) => AlertDialog(
90+
builder: (BuildContext context) => AlertDialog.adaptive(
7591
title: Text(title),
7692
content: message != null ? SingleChildScrollView(child: Text(message)) : null,
7793
actions: [
7894
if (learnMoreButtonUrl != null)
79-
TextButton(
95+
_adaptiveAction(
8096
onPressed: () => PlatformActions.launchUrl(context, learnMoreButtonUrl),
81-
child: _dialogActionText(zulipLocalizations.errorDialogLearnMore)),
82-
TextButton(
97+
text: zulipLocalizations.errorDialogLearnMore),
98+
_adaptiveAction(
8399
onPressed: () => Navigator.pop(context),
84-
child: _dialogActionText(zulipLocalizations.errorDialogContinue)),
100+
text: zulipLocalizations.errorDialogContinue),
85101
]));
86102
return DialogStatus(future);
87103
}
@@ -103,16 +119,16 @@ DialogStatus<bool> showSuggestedActionDialog({
103119
final zulipLocalizations = ZulipLocalizations.of(context);
104120
final future = showDialog<bool>(
105121
context: context,
106-
builder: (BuildContext context) => AlertDialog(
122+
builder: (BuildContext context) => AlertDialog.adaptive(
107123
title: Text(title),
108124
content: SingleChildScrollView(child: Text(message)),
109125
actions: [
110-
TextButton(
126+
_adaptiveAction(
111127
onPressed: () => Navigator.pop<bool>(context, null),
112-
child: _dialogActionText(zulipLocalizations.dialogCancel)),
113-
TextButton(
128+
text: zulipLocalizations.dialogCancel),
129+
_adaptiveAction(
114130
onPressed: () => Navigator.pop<bool>(context, true),
115-
child: _dialogActionText(actionButtonText ?? zulipLocalizations.dialogContinue)),
131+
text: actionButtonText ?? zulipLocalizations.dialogContinue),
116132
]));
117133
return DialogStatus(future);
118134
}
@@ -164,7 +180,7 @@ class UpgradeWelcomeDialog extends StatelessWidget {
164180
@override
165181
Widget build(BuildContext context) {
166182
final zulipLocalizations = ZulipLocalizations.of(context);
167-
return AlertDialog(
183+
return AlertDialog.adaptive(
168184
title: Text(zulipLocalizations.upgradeWelcomeDialogTitle),
169185
content: SingleChildScrollView(
170186
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
@@ -177,8 +193,9 @@ class UpgradeWelcomeDialog extends StatelessWidget {
177193
zulipLocalizations.upgradeWelcomeDialogLinkText)),
178194
])),
179195
actions: [
180-
TextButton(onPressed: () => Navigator.pop(context),
181-
child: Text(zulipLocalizations.upgradeWelcomeDialogDismiss)),
196+
_adaptiveAction(
197+
onPressed: () => Navigator.pop(context),
198+
text: zulipLocalizations.upgradeWelcomeDialogDismiss)
182199
]);
183200
}
184201
}

test/widgets/dialog_checks.dart

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
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+
import '../model/binding.dart';
10+
11+
/// In a widget test, check that [showErrorDialog] was called with the right text.
812
///
913
/// Checks for an error dialog matching an expected title
1014
/// and, optionally, matching an expected message. Fails if none is found.
@@ -15,26 +19,41 @@ Widget checkErrorDialog(WidgetTester tester, {
1519
required String expectedTitle,
1620
String? expectedMessage,
1721
}) {
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-
26-
// TODO check "Learn more" button?
22+
switch (defaultTargetPlatform) {
23+
case TargetPlatform.android:
24+
case TargetPlatform.fuchsia:
25+
case TargetPlatform.linux:
26+
case TargetPlatform.windows:
27+
final dialog = tester.widget<AlertDialog>(find.bySubtype<AlertDialog>());
28+
tester.widget(find.descendant(matchRoot: true,
29+
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
30+
if (expectedMessage != null) {
31+
tester.widget(find.descendant(matchRoot: true,
32+
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
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+
return tester.widget(find.descendant(of: find.byWidget(dialog),
47+
matching: find.widgetWithText(CupertinoDialogAction, 'OK')));
48+
}
3149
}
3250

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

4059
/// In a widget test, check that [showSuggestedActionDialog] was called
@@ -51,19 +70,35 @@ void checkNoDialog(WidgetTester tester) {
5170
required String expectedMessage,
5271
String? expectedActionButtonText,
5372
}) {
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)));
73+
switch (defaultTargetPlatform) {
74+
case TargetPlatform.android:
75+
case TargetPlatform.fuchsia:
76+
case TargetPlatform.linux:
77+
case TargetPlatform.windows:
78+
final dialog = tester.widget<AlertDialog>(find.bySubtype<AlertDialog>());
79+
tester.widget(find.descendant(matchRoot: true,
80+
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
81+
tester.widget(find.descendant(matchRoot: true,
82+
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
5983

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

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

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

test/widgets/dialog_test.dart

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,77 @@
11
import 'package:checks/checks.dart';
2-
import 'package:flutter/widgets.dart';
2+
import 'package:flutter/foundation.dart';
3+
import 'package:flutter/material.dart';
34
import 'package:flutter_test/flutter_test.dart';
45
import 'package:url_launcher/url_launcher.dart';
56
import 'package:zulip/widgets/dialog.dart';
67

78
import '../model/binding.dart';
9+
import 'dialog_checks.dart';
810
import 'test_app.dart';
911

1012
void main() {
1113
TestZulipBinding.ensureInitialized();
1214

15+
late BuildContext context;
16+
17+
const title = "Dialog Title";
18+
const message = "Dialog message.";
19+
20+
Future<void> prepare(WidgetTester tester) async {
21+
addTearDown(testBinding.reset);
22+
23+
await tester.pumpWidget(const TestZulipApp(
24+
child: Scaffold(body: Placeholder())));
25+
await tester.pump();
26+
context = tester.element(find.byType(Placeholder));
27+
}
28+
1329
group('showErrorDialog', () {
14-
testWidgets('tap "Learn more" button', (tester) async {
15-
addTearDown(testBinding.reset);
16-
await tester.pumpWidget(TestZulipApp());
30+
testWidgets('show error dialog', (tester) async {
31+
await prepare(tester);
32+
33+
showErrorDialog(context: context, title: title, message: message);
1734
await tester.pump();
18-
final element = tester.element(find.byType(Placeholder));
35+
checkErrorDialog(tester, expectedTitle: title, expectedMessage: message);
36+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
1937

20-
showErrorDialog(context: element, title: 'hello',
21-
learnMoreButtonUrl: Uri.parse('https://foo.example'));
38+
testWidgets('user closes error dialog', (tester) async {
39+
await prepare(tester);
40+
41+
showErrorDialog(context: context, title: title, message: message);
42+
await tester.pump();
43+
44+
final button = checkErrorDialog(tester, expectedTitle: title);
45+
await tester.tap(find.byWidget(button));
46+
await tester.pump();
47+
checkNoDialog(tester);
48+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
49+
50+
testWidgets('tap "Learn more" button', (tester) async {
51+
await prepare(tester);
52+
53+
final learnMoreButtonUrl = Uri.parse('https://foo.example');
54+
showErrorDialog(context: context, title: title, learnMoreButtonUrl: learnMoreButtonUrl);
2255
await tester.pump();
2356
await tester.tap(find.text('Learn more'));
24-
check(testBinding.takeLaunchUrlCalls()).single.equals((
25-
url: Uri.parse('https://foo.example'),
26-
mode: LaunchMode.inAppBrowserView));
27-
});
57+
58+
checkErrorDialog(tester, expectedTitle: title);
59+
60+
switch (defaultTargetPlatform) {
61+
case TargetPlatform.android:
62+
case TargetPlatform.fuchsia:
63+
case TargetPlatform.linux:
64+
case TargetPlatform.windows:
65+
check(testBinding.takeLaunchUrlCalls()).single.equals((
66+
url: learnMoreButtonUrl,
67+
mode: LaunchMode.inAppBrowserView));
68+
case TargetPlatform.iOS:
69+
case TargetPlatform.macOS:
70+
check(testBinding.takeLaunchUrlCalls()).single.equals((
71+
url: learnMoreButtonUrl,
72+
mode: LaunchMode.externalApplication));
73+
}
74+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
2875
});
2976

3077
group('showSuggestedActionDialog', () {
@@ -41,7 +88,7 @@ void main() {
4188
await tester.pump();
4289
await tester.tap(find.text('Sure'));
4390
await check(dialog.result).completes((it) => it.equals(true));
44-
});
91+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
4592

4693
testWidgets('tap cancel', (tester) async {
4794
addTearDown(testBinding.reset);
@@ -56,7 +103,7 @@ void main() {
56103
await tester.pump();
57104
await tester.tap(find.text('Cancel'));
58105
await check(dialog.result).completes((it) => it.equals(null));
59-
});
106+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
60107

61108
testWidgets('tap outside dialog area', (tester) async {
62109
addTearDown(testBinding.reset);
@@ -71,7 +118,7 @@ void main() {
71118
await tester.pump();
72119
await tester.tapAt(tester.getTopLeft(find.byType(TestZulipApp)));
73120
await check(dialog.result).completes((it) => it.equals(null));
74-
});
121+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
75122
});
76123

77124
// TODO(#1594): test UpgradeWelcomeDialog

0 commit comments

Comments
 (0)