Skip to content

Commit 777bb1b

Browse files
committed
dialog: Use Cupertino-flavored alert dialogs on iOS
Fixes: zulip#996
1 parent 0417c87 commit 777bb1b

File tree

3 files changed

+156
-40
lines changed

3 files changed

+156
-40
lines changed

lib/widgets/dialog.dart

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter/cupertino.dart';
3+
import 'package:flutter/foundation.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,15 @@ 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(
75-
onPressed: () {
76-
onActionButtonPress();
77-
Navigator.pop(context);
78-
},
79-
child: _dialogActionText(actionButtonText ?? zulipLocalizations.dialogContinue)),
89+
text: zulipLocalizations.dialogCancel),
90+
_adaptiveAction(
91+
onPressed: onActionButtonPress,
92+
text: actionButtonText ?? zulipLocalizations.dialogContinue),
8093
]));
81-
}
94+
}

test/widgets/dialog_checks.dart

Lines changed: 76 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import 'package:flutter/cupertino.dart';
2+
import 'package:flutter/foundation.dart';
13
import 'package:checks/checks.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,22 +17,50 @@ 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')));
2535

26-
return tester.widget(
27-
find.descendant(of: find.byWidget(dialog),
28-
matching: find.widgetWithText(TextButton, 'OK')));
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+
}
45+
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
51+
/// Checks that there is no error dialog.
52+
/// Fails if one is found.
3253
void checkNoErrorDialog(WidgetTester tester) {
33-
check(find.byType(AlertDialog)).findsNothing();
54+
switch (defaultTargetPlatform) {
55+
case TargetPlatform.android:
56+
case TargetPlatform.fuchsia:
57+
case TargetPlatform.linux:
58+
case TargetPlatform.windows:
59+
check(find.bySubtype<AlertDialog>()).findsNothing();
60+
case TargetPlatform.iOS:
61+
case TargetPlatform.macOS:
62+
check(find.byType(CupertinoAlertDialog)).findsNothing();
63+
}
3464
}
3565

3666
/// In a widget test, check that [showSuggestedActionDialog] was called
@@ -47,19 +77,39 @@ void checkNoErrorDialog(WidgetTester tester) {
4777
required String expectedMessage,
4878
String? expectedActionButtonText,
4979
}) {
50-
final dialog = tester.widget<AlertDialog>(find.byType(AlertDialog));
51-
tester.widget(find.descendant(matchRoot: true,
52-
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
53-
tester.widget(find.descendant(matchRoot: true,
54-
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
80+
switch (defaultTargetPlatform) {
81+
case TargetPlatform.android:
82+
case TargetPlatform.fuchsia:
83+
case TargetPlatform.linux:
84+
case TargetPlatform.windows:
85+
final dialog = tester.widget<AlertDialog>(find.bySubtype<AlertDialog>());
86+
tester.widget(find.descendant(matchRoot: true,
87+
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
88+
tester.widget(find.descendant(matchRoot: true,
89+
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
5590

56-
final actionButton = tester.widget(
57-
find.descendant(of: find.byWidget(dialog),
58-
matching: find.widgetWithText(TextButton, expectedActionButtonText ?? 'Continue')));
91+
final actionButton = tester.widget(find.descendant(of: find.byWidget(dialog),
92+
matching: find.widgetWithText(TextButton, expectedActionButtonText ?? 'Continue')));
5993

60-
final cancelButton = tester.widget(
61-
find.descendant(of: find.byWidget(dialog),
62-
matching: find.widgetWithText(TextButton, 'Cancel')));
94+
final cancelButton = tester.widget(find.descendant(of: find.byWidget(dialog),
95+
matching: find.widgetWithText(TextButton, 'Cancel')));
6396

64-
return (actionButton, cancelButton);
65-
}
97+
return (actionButton, cancelButton);
98+
99+
case TargetPlatform.iOS:
100+
case TargetPlatform.macOS:
101+
final dialog = tester.widget<CupertinoAlertDialog>(find.byType(CupertinoAlertDialog));
102+
tester.widget(find.descendant(matchRoot: true,
103+
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
104+
tester.widget(find.descendant(matchRoot: true,
105+
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
106+
107+
final actionButton = tester.widget(find.descendant(of: find.byWidget(dialog),
108+
matching: find.widgetWithText(CupertinoDialogAction, expectedActionButtonText ?? 'Continue')));
109+
110+
final cancelButton = tester.widget(find.descendant(of: find.byWidget(dialog),
111+
matching: find.widgetWithText(CupertinoDialogAction, 'Cancel')));
112+
113+
return (actionButton, cancelButton);
114+
}
115+
}

test/widgets/dialog_test.dart

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:zulip/widgets/dialog.dart';
3+
import 'package:flutter_test/flutter_test.dart';
4+
5+
import '../model/binding.dart';
6+
import 'dialog_checks.dart';
7+
import 'test_app.dart';
8+
9+
void main() {
10+
TestZulipBinding.ensureInitialized();
11+
12+
late BuildContext context;
13+
14+
Future<void> prepare(WidgetTester tester) async {
15+
addTearDown(testBinding.reset);
16+
17+
await tester.pumpWidget(const TestZulipApp(
18+
child: Scaffold(body: Placeholder())));
19+
await tester.pump();
20+
context = tester.element(find.byType(Placeholder));
21+
}
22+
23+
group('showErrorDialog', () {
24+
testWidgets('show error dialog', (tester) async {
25+
await prepare(tester);
26+
27+
String title = "Dialog Title";
28+
String message = "Dialog message.";
29+
30+
showErrorDialog(context: context, title: title, message: message);
31+
await tester.pump();
32+
checkErrorDialog(tester, expectedTitle: title, expectedMessage: message);
33+
34+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
35+
});
36+
37+
group('showSuggestedActionDialog', () {
38+
testWidgets('show suggested action dialog', (tester) async {
39+
await prepare(tester);
40+
41+
String title = "Dialog Title";
42+
String message = "Dialog message.";
43+
String actionButtonText = "Action";
44+
45+
showSuggestedActionDialog(context: context, title: title, message: message,
46+
actionButtonText: actionButtonText, onActionButtonPress: () {});
47+
await tester.pump();
48+
checkSuggestedActionDialog(tester, expectedTitle: title, expectedMessage: message,
49+
expectedActionButtonText: actionButtonText);
50+
51+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
52+
});
53+
}

0 commit comments

Comments
 (0)