Skip to content

Commit bd112c2

Browse files
u7088495gnprice
authored andcommitted
dialog: Use Cupertino-flavored alert dialogs on iOS
Fixes: #996
1 parent 361ee16 commit bd112c2

File tree

3 files changed

+144
-55
lines changed

3 files changed

+144
-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: 60 additions & 27 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,41 @@ 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-
26-
// TODO check "Learn more" button?
20+
switch (defaultTargetPlatform) {
21+
case TargetPlatform.android:
22+
case TargetPlatform.fuchsia:
23+
case TargetPlatform.linux:
24+
case TargetPlatform.windows:
25+
final dialog = tester.widget<AlertDialog>(find.bySubtype<AlertDialog>());
26+
tester.widget(find.descendant(matchRoot: true,
27+
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
28+
if (expectedMessage != null) {
29+
tester.widget(find.descendant(matchRoot: true,
30+
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
31+
}
32+
return tester.widget(find.descendant(of: find.byWidget(dialog),
33+
matching: find.widgetWithText(TextButton, 'OK')));
2734

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

33-
// TODO(#996) update this to check for per-platform flavors of alert dialog
3449
/// Checks that there is no dialog.
3550
/// Fails if one is found.
3651
void checkNoDialog(WidgetTester tester) {
37-
check(find.byType(AlertDialog)).findsNothing();
52+
check(find.byType(Dialog)).findsNothing();
53+
check(find.bySubtype<AlertDialog>()).findsNothing();
54+
check(find.byType(CupertinoAlertDialog)).findsNothing();
3855
}
3956

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

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

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

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

test/widgets/dialog_test.dart

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,69 @@
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();
56+
checkErrorDialog(tester, expectedTitle: title);
57+
2358
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-
});
59+
final expectedMode = switch (defaultTargetPlatform) {
60+
TargetPlatform.android => LaunchMode.inAppBrowserView,
61+
TargetPlatform.iOS => LaunchMode.externalApplication,
62+
_ => throw StateError('attempted to test with $defaultTargetPlatform'),
63+
};
64+
check(testBinding.takeLaunchUrlCalls()).single
65+
.equals((url: learnMoreButtonUrl, mode: expectedMode));
66+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
2867
});
2968

3069
group('showSuggestedActionDialog', () {
@@ -41,7 +80,7 @@ void main() {
4180
await tester.pump();
4281
await tester.tap(find.text('Sure'));
4382
await check(dialog.result).completes((it) => it.equals(true));
44-
});
83+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
4584

4685
testWidgets('tap cancel', (tester) async {
4786
addTearDown(testBinding.reset);
@@ -56,7 +95,7 @@ void main() {
5695
await tester.pump();
5796
await tester.tap(find.text('Cancel'));
5897
await check(dialog.result).completes((it) => it.equals(null));
59-
});
98+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
6099

61100
testWidgets('tap outside dialog area', (tester) async {
62101
addTearDown(testBinding.reset);
@@ -71,7 +110,7 @@ void main() {
71110
await tester.pump();
72111
await tester.tapAt(tester.getTopLeft(find.byType(TestZulipApp)));
73112
await check(dialog.result).completes((it) => it.equals(null));
74-
});
113+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
75114
});
76115

77116
// TODO(#1594): test UpgradeWelcomeDialog

0 commit comments

Comments
 (0)