Skip to content

Commit 5368fde

Browse files
committed
dialog: Use Cupertino-flavored alert dialogs on iOS
Fixes: #996
1 parent 27983b7 commit 5368fde

File tree

3 files changed

+188
-34
lines changed

3 files changed

+188
-34
lines changed

lib/widgets/dialog.dart

+25-9
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
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

5-
Widget _dialogActionText(String text) {
7+
Widget _materialDialogActionText(String text) {
68
return Text(
79
text,
810

@@ -16,6 +18,20 @@ Widget _dialogActionText(String text) {
1618
);
1719
}
1820

21+
/// A platform-appropriate action for [AlertDialog.adaptive]'s [actions] param.
22+
Widget _adaptiveAction({required VoidCallback onPressed, required String text}) {
23+
switch (defaultTargetPlatform) {
24+
case TargetPlatform.android:
25+
case TargetPlatform.fuchsia:
26+
case TargetPlatform.linux:
27+
case TargetPlatform.windows:
28+
return TextButton(onPressed: onPressed, child: _materialDialogActionText(text));
29+
case TargetPlatform.iOS:
30+
case TargetPlatform.macOS:
31+
return CupertinoDialogAction(onPressed: onPressed, child: Text(text));
32+
}
33+
}
34+
1935
/// Tracks the status of a dialog, in being still open or already closed.
2036
///
2137
/// See also:
@@ -43,13 +59,13 @@ DialogStatus showErrorDialog({
4359
final zulipLocalizations = ZulipLocalizations.of(context);
4460
final future = showDialog<void>(
4561
context: context,
46-
builder: (BuildContext context) => AlertDialog(
62+
builder: (BuildContext context) => AlertDialog.adaptive(
4763
title: Text(title),
4864
content: message != null ? SingleChildScrollView(child: Text(message)) : null,
4965
actions: [
50-
TextButton(
66+
_adaptiveAction(
5167
onPressed: () => Navigator.pop(context),
52-
child: _dialogActionText(zulipLocalizations.errorDialogContinue)),
68+
text: zulipLocalizations.errorDialogContinue),
5369
]));
5470
return DialogStatus(future);
5571
}
@@ -64,18 +80,18 @@ void showSuggestedActionDialog({
6480
final zulipLocalizations = ZulipLocalizations.of(context);
6581
showDialog<void>(
6682
context: context,
67-
builder: (BuildContext context) => AlertDialog(
83+
builder: (BuildContext context) => AlertDialog.adaptive(
6884
title: Text(title),
6985
content: SingleChildScrollView(child: Text(message)),
7086
actions: [
71-
TextButton(
87+
_adaptiveAction(
7288
onPressed: () => Navigator.pop(context),
73-
child: _dialogActionText(zulipLocalizations.dialogCancel)),
74-
TextButton(
89+
text: zulipLocalizations.dialogCancel),
90+
_adaptiveAction(
7591
onPressed: () {
7692
onActionButtonPress();
7793
Navigator.pop(context);
7894
},
79-
child: _dialogActionText(actionButtonText ?? zulipLocalizations.dialogContinue)),
95+
text: actionButtonText ?? zulipLocalizations.dialogContinue),
8096
]));
8197
}

test/widgets/dialog_checks.dart

+62-25
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,24 +17,43 @@ 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-
}
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+
33+
return tester.widget(find.descendant(of: find.byWidget(dialog),
34+
matching: find.widgetWithText(TextButton, 'OK')));
35+
36+
case TargetPlatform.iOS:
37+
case TargetPlatform.macOS:
38+
final dialog = tester.widget<CupertinoAlertDialog>(find.byType(CupertinoAlertDialog));
39+
tester.widget(find.descendant(matchRoot: true,
40+
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
41+
if (expectedMessage != null) {
42+
tester.widget(find.descendant(matchRoot: true,
43+
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
44+
}
2545

26-
return tester.widget(
27-
find.descendant(of: find.byWidget(dialog),
28-
matching: find.widgetWithText(TextButton, 'OK')));
46+
return tester.widget(find.descendant(of: find.byWidget(dialog),
47+
matching: find.widgetWithText(CupertinoDialogAction, 'OK')));
48+
}
2949
}
3050

31-
// TODO(#996) update this to check for per-platform flavors of alert dialog
3251
/// Checks that there is no dialog.
3352
/// Fails if one is found.
3453
void checkNoDialog(WidgetTester tester) {
35-
check(find.byType(AlertDialog)).findsNothing();
54+
check(find.byType(Dialog)).findsNothing();
55+
check(find.bySubtype<AlertDialog>()).findsNothing();
56+
check(find.byType(CupertinoAlertDialog)).findsNothing();
3657
}
3758

3859
/// In a widget test, check that [showSuggestedActionDialog] was called
@@ -49,19 +70,35 @@ void checkNoDialog(WidgetTester tester) {
4970
required String expectedMessage,
5071
String? expectedActionButtonText,
5172
}) {
52-
final dialog = tester.widget<AlertDialog>(find.byType(AlertDialog));
53-
tester.widget(find.descendant(matchRoot: true,
54-
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
55-
tester.widget(find.descendant(matchRoot: true,
56-
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)));
5783

58-
final actionButton = tester.widget(
59-
find.descendant(of: find.byWidget(dialog),
60-
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);
6189

62-
final cancelButton = tester.widget(
63-
find.descendant(of: find.byWidget(dialog),
64-
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)));
6597

66-
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+
}
67104
}

test/widgets/dialog_test.dart

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import 'package:checks/checks.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter_test/flutter_test.dart';
4+
import 'package:zulip/widgets/dialog.dart';
5+
6+
import '../model/binding.dart';
7+
import 'dialog_checks.dart';
8+
import 'test_app.dart';
9+
10+
void main() {
11+
TestZulipBinding.ensureInitialized();
12+
13+
late BuildContext context;
14+
15+
const title = "Dialog Title";
16+
const message = "Dialog message.";
17+
18+
Future<void> prepare(WidgetTester tester) async {
19+
addTearDown(testBinding.reset);
20+
21+
await tester.pumpWidget(const TestZulipApp(
22+
child: Scaffold(body: Placeholder())));
23+
await tester.pump();
24+
context = tester.element(find.byType(Placeholder));
25+
}
26+
27+
group('showErrorDialog', () {
28+
testWidgets('show error dialog', (tester) async {
29+
await prepare(tester);
30+
31+
showErrorDialog(context: context, title: title, message: message);
32+
await tester.pump();
33+
checkErrorDialog(tester, expectedTitle: title, expectedMessage: message);
34+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
35+
36+
testWidgets('user closes error dialog', (tester) async {
37+
await prepare(tester);
38+
39+
showErrorDialog(context: context, title: title, message: message);
40+
await tester.pump();
41+
42+
final button = checkErrorDialog(tester, expectedTitle: title);
43+
await tester.tap(find.byWidget(button));
44+
await tester.pump();
45+
checkNoDialog(tester);
46+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
47+
});
48+
49+
group('showSuggestedActionDialog', () {
50+
const actionButtonText = "Action";
51+
52+
testWidgets('show suggested action dialog', (tester) async {
53+
await prepare(tester);
54+
55+
showSuggestedActionDialog(context: context, title: title, message: message,
56+
actionButtonText: actionButtonText, onActionButtonPress: () {});
57+
await tester.pump();
58+
59+
checkSuggestedActionDialog(tester, expectedTitle: title, expectedMessage: message,
60+
expectedActionButtonText: actionButtonText);
61+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
62+
63+
testWidgets('user presses action button', (tester) async {
64+
await prepare(tester);
65+
66+
bool wasPressed = false;
67+
void onActionButtonPress() {
68+
wasPressed = true;
69+
}
70+
showSuggestedActionDialog(context: context, title: title, message: message,
71+
actionButtonText: actionButtonText, onActionButtonPress: onActionButtonPress);
72+
await tester.pump();
73+
74+
final (actionButton, _) = checkSuggestedActionDialog(tester, expectedTitle: title,
75+
expectedMessage: message, expectedActionButtonText: actionButtonText);
76+
await tester.tap(find.byWidget(actionButton));
77+
await tester.pump();
78+
checkNoDialog(tester);
79+
check(wasPressed).isTrue();
80+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
81+
82+
testWidgets('user cancels', (tester) async {
83+
await prepare(tester);
84+
85+
bool wasPressed = false;
86+
void onActionButtonPress() {
87+
wasPressed = true;
88+
}
89+
showSuggestedActionDialog(context: context, title: title, message: message,
90+
actionButtonText: actionButtonText, onActionButtonPress: onActionButtonPress);
91+
await tester.pump();
92+
93+
final (_, cancelButton) = checkSuggestedActionDialog(tester, expectedTitle: title,
94+
expectedMessage: message, expectedActionButtonText: actionButtonText);
95+
await tester.tap(find.byWidget(cancelButton));
96+
await tester.pump();
97+
checkNoDialog(tester);
98+
check(wasPressed).isFalse();
99+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
100+
});
101+
}

0 commit comments

Comments
 (0)