Skip to content

Commit 3bb7ed6

Browse files
committed
feat(ui, localization): add delete poll option functionality
This introduces the ability for users to delete options when creating or editing a poll. A delete icon is now present on each poll option item. Tapping it shows a confirmation dialog. If confirmed, the option is removed from the list. The list enforces the minimum number of required options. This change includes: - A new `PollDeleteOptionDialog` and `showPollDeleteOptionDialog` function. - An `onRemove` callback to the `_PollOptionItem` widget. - New theming options for the dialog (`actionDialogTitleStyle`, `actionDialogContentStyle`). - New translations for the delete confirmation dialog across all supported languages. - Comprehensive widget and golden tests for the new functionality and dialog.
1 parent 6b6de2e commit 3bb7ed6

26 files changed

+546
-26
lines changed

packages/stream_chat_flutter/lib/src/localization/translations.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,12 @@ abstract class Translations {
445445
/// The confirmation text shown when the user tries to end a poll.
446446
String get endVoteConfirmationText;
447447

448+
/// The label for "delete poll option"
449+
String get deletePollOptionLabel;
450+
451+
/// The question asked while showing delete poll option dialog
452+
String get deletePollOptionQuestion;
453+
448454
/// The label for "Create".
449455
String get createLabel;
450456

@@ -1105,6 +1111,13 @@ Attachment limit exceeded: it's not possible to add more than $limit attachments
11051111
String get endVoteConfirmationText =>
11061112
'Are you sure you want to end the vote?';
11071113

1114+
@override
1115+
String get deletePollOptionLabel => 'Delete option?';
1116+
1117+
@override
1118+
String get deletePollOptionQuestion =>
1119+
'Are you sure you want to delete this option?';
1120+
11081121
@override
11091122
String get createLabel => 'Create';
11101123

packages/stream_chat_flutter/lib/src/misc/separated_reorderable_list_view.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// ignore_for_file: use_is_even_rather_than_modulo
1+
// ignore_for_file: parameter_assignments, use_is_even_rather_than_modulo
22
import 'dart:math' as math;
33
import 'package:flutter/material.dart';
44

@@ -79,7 +79,7 @@ class SeparatedReorderableListView extends ReorderableListView {
7979
? (newIndex + 1) ~/ 2
8080
: newIndex ~/ 2;
8181

82-
onReorder(updatedOldIndex, updatedNewIndex);
82+
return onReorder(updatedOldIndex, updatedNewIndex);
8383
},
8484
);
8585
}

packages/stream_chat_flutter/lib/src/poll/creator/poll_option_reorderable_list_view.dart

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:ui';
33
import 'package:collection/collection.dart';
44
import 'package:flutter/material.dart';
55
import 'package:stream_chat_flutter/src/misc/separated_reorderable_list_view.dart';
6+
import 'package:stream_chat_flutter/src/poll/creator/stream_delete_option_dialog.dart';
67
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
78

89
class _NullConst {
@@ -57,6 +58,7 @@ class PollOptionListItem extends StatelessWidget {
5758
required this.option,
5859
this.hintText,
5960
this.focusNode,
61+
this.onRemove,
6062
this.onChanged,
6163
});
6264

@@ -69,6 +71,9 @@ class PollOptionListItem extends StatelessWidget {
6971
/// The focus node for the text field.
7072
final FocusNode? focusNode;
7173

74+
/// Callback called when the poll option item is removed.
75+
final ValueSetter<PollOptionItem>? onRemove;
76+
7277
/// Callback called when the poll option item is changed.
7378
final ValueSetter<PollOptionItem>? onChanged;
7479

@@ -78,13 +83,26 @@ class PollOptionListItem extends StatelessWidget {
7883
final fillColor = theme.optionsTextFieldFillColor;
7984
final borderRadius = theme.optionsTextFieldBorderRadius;
8085

86+
final colorTheme = StreamChatTheme.of(context).colorTheme;
87+
8188
return DecoratedBox(
8289
decoration: BoxDecoration(
8390
color: fillColor,
8491
borderRadius: borderRadius,
8592
),
8693
child: Row(
8794
children: [
95+
Padding(
96+
padding: const EdgeInsetsDirectional.all(16),
97+
child: MouseRegion(
98+
cursor: SystemMouseCursors.grab,
99+
child: Icon(
100+
size: 24,
101+
Icons.drag_handle_rounded,
102+
color: colorTheme.textLowEmphasis,
103+
),
104+
),
105+
),
88106
Expanded(
89107
child: StreamPollTextField(
90108
initialValue: option.text,
@@ -95,13 +113,27 @@ class PollOptionListItem extends StatelessWidget {
95113
errorText: option.error,
96114
errorStyle: theme.optionsTextFieldErrorStyle,
97115
focusNode: focusNode,
98-
onChanged: (text) => onChanged?.call(option.copyWith(text: text)),
116+
contentPadding: const EdgeInsets.symmetric(vertical: 18),
117+
onChanged: switch (onChanged) {
118+
final onChanged? => (text) {
119+
final updated = option.copyWith(text: text);
120+
return onChanged.call(updated);
121+
},
122+
_ => null,
123+
},
99124
),
100125
),
101-
const SizedBox(
102-
width: 48,
103-
height: 48,
104-
child: Icon(Icons.drag_handle_rounded),
126+
IconButton(
127+
iconSize: 24,
128+
icon: const StreamSvgIcon(icon: StreamSvgIcons.delete),
129+
style: IconButton.styleFrom(
130+
foregroundColor: colorTheme.textLowEmphasis,
131+
),
132+
onLongPress: () {/* Consume long press */},
133+
onPressed: switch (onRemove) {
134+
final onRemove? => () => onRemove.call(option),
135+
_ => null,
136+
},
105137
),
106138
],
107139
),
@@ -217,7 +249,7 @@ class _PollOptionReorderableListViewState
217249
final newOptions = widget.initialOptions;
218250

219251
final optionItemEquality = ListEquality<PollOptionItem>(
220-
EqualityBy((it) => (it.id, it.text)),
252+
EqualityBy((it) => it.id),
221253
);
222254

223255
if (optionItemEquality.equals(currOptions, newOptions) case false) {
@@ -263,6 +295,24 @@ class _PollOptionReorderableListViewState
263295
return null;
264296
}
265297

298+
Future<void> _onOptionRemoved(PollOptionItem option) async {
299+
final confirm = await showPollDeleteOptionDialog(context: context);
300+
if (confirm == null || !confirm) return;
301+
302+
setState(() {
303+
_options.remove(option.id);
304+
_focusNodes.remove(option.id)?.dispose();
305+
});
306+
307+
// Ensure we have at least the minimum number of options
308+
_ensureMinimumOptions();
309+
310+
// Notify the parent widget about the change
311+
WidgetsBinding.instance.addPostFrameCallback((_) {
312+
widget.onOptionsChanged?.call([..._options.values]);
313+
});
314+
}
315+
266316
void _onOptionChanged(PollOptionItem option) {
267317
setState(() {
268318
// Update the changed option.
@@ -370,6 +420,7 @@ class _PollOptionReorderableListViewState
370420
option: option,
371421
hintText: widget.itemHintText,
372422
focusNode: _focusNodes[option.id],
423+
onRemove: _onOptionRemoved,
373424
onChanged: _onOptionChanged,
374425
);
375426
},
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:stream_chat_flutter/src/theme/poll_creator_theme.dart';
3+
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
4+
import 'package:stream_chat_flutter/src/utils/extensions.dart';
5+
6+
/// {@template showPollDeleteOptionDialog}
7+
/// Shows a dialog that allows the user to confirm deletion of a poll option.
8+
/// {@endtemplate}
9+
Future<bool?> showPollDeleteOptionDialog({
10+
required BuildContext context,
11+
}) {
12+
return showDialog<bool?>(
13+
context: context,
14+
builder: (_) => const PollDeleteOptionDialog(),
15+
);
16+
}
17+
18+
/// {@template pollDeleteOptionDialog}
19+
/// A dialog that allows the user to confirm deletion of a poll option.
20+
/// {@endtemplate}
21+
class PollDeleteOptionDialog extends StatelessWidget {
22+
/// {@macro pollDeleteOptionDialog}
23+
const PollDeleteOptionDialog({super.key});
24+
25+
@override
26+
Widget build(BuildContext context) {
27+
final theme = StreamChatTheme.of(context);
28+
final pollCreatorTheme = StreamPollCreatorTheme.of(context);
29+
30+
final actions = [
31+
TextButton(
32+
onPressed: () => Navigator.of(context).maybePop(false),
33+
style: TextButton.styleFrom(
34+
textStyle: theme.textTheme.headlineBold,
35+
foregroundColor: theme.colorTheme.accentPrimary,
36+
disabledForegroundColor: theme.colorTheme.disabled,
37+
),
38+
child: Text(context.translations.cancelLabel.toUpperCase()),
39+
),
40+
TextButton(
41+
onPressed: () => Navigator.of(context).maybePop(true),
42+
style: TextButton.styleFrom(
43+
textStyle: theme.textTheme.headlineBold,
44+
foregroundColor: theme.colorTheme.accentPrimary,
45+
disabledForegroundColor: theme.colorTheme.disabled,
46+
),
47+
child: Text(context.translations.deleteLabel.toUpperCase()),
48+
),
49+
];
50+
51+
return AlertDialog(
52+
title: Text(
53+
context.translations.deletePollOptionLabel,
54+
style: pollCreatorTheme.actionDialogTitleStyle,
55+
),
56+
content: Text(
57+
context.translations.deletePollOptionQuestion,
58+
style: pollCreatorTheme.actionDialogContentStyle,
59+
),
60+
actions: actions,
61+
titlePadding: const EdgeInsetsDirectional.fromSTEB(16, 24, 16, 4),
62+
contentPadding: const EdgeInsetsDirectional.fromSTEB(16, 4, 16, 24),
63+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
64+
actionsPadding: const EdgeInsets.all(8),
65+
backgroundColor: theme.colorTheme.appBg,
66+
);
67+
}
68+
}

packages/stream_chat_flutter/lib/src/theme/poll_creator_theme.dart

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ class StreamPollCreatorThemeData with Diagnosticable {
7676
this.switchListTileTitleStyle,
7777
this.switchListTileErrorStyle,
7878
this.switchListTileBorderRadius,
79+
this.actionDialogTitleStyle,
80+
this.actionDialogContentStyle,
7981
});
8082

8183
/// The background color of the poll creator.
@@ -133,6 +135,12 @@ class StreamPollCreatorThemeData with Diagnosticable {
133135
/// The border radius of the switch list tile.
134136
final BorderRadius? switchListTileBorderRadius;
135137

138+
/// The text style of the action dialog title.
139+
final TextStyle? actionDialogTitleStyle;
140+
141+
/// The text style of the action dialog content.
142+
final TextStyle? actionDialogContentStyle;
143+
136144
/// Copies this [StreamPollCreatorThemeData] with some new values.
137145
StreamPollCreatorThemeData copyWith({
138146
Color? backgroundColor,
@@ -153,6 +161,8 @@ class StreamPollCreatorThemeData with Diagnosticable {
153161
TextStyle? switchListTileTitleStyle,
154162
TextStyle? switchListTileErrorStyle,
155163
BorderRadius? switchListTileBorderRadius,
164+
TextStyle? actionDialogTitleStyle,
165+
TextStyle? actionDialogContentStyle,
156166
}) {
157167
return StreamPollCreatorThemeData(
158168
backgroundColor: backgroundColor ?? this.backgroundColor,
@@ -186,6 +196,10 @@ class StreamPollCreatorThemeData with Diagnosticable {
186196
switchListTileErrorStyle ?? this.switchListTileErrorStyle,
187197
switchListTileBorderRadius:
188198
switchListTileBorderRadius ?? this.switchListTileBorderRadius,
199+
actionDialogTitleStyle:
200+
actionDialogTitleStyle ?? this.actionDialogTitleStyle,
201+
actionDialogContentStyle:
202+
actionDialogContentStyle ?? this.actionDialogContentStyle,
189203
);
190204
}
191205

@@ -224,6 +238,10 @@ class StreamPollCreatorThemeData with Diagnosticable {
224238
other.switchListTileErrorStyle ?? switchListTileErrorStyle,
225239
switchListTileBorderRadius:
226240
other.switchListTileBorderRadius ?? switchListTileBorderRadius,
241+
actionDialogTitleStyle:
242+
other.actionDialogTitleStyle ?? actionDialogTitleStyle,
243+
actionDialogContentStyle:
244+
other.actionDialogContentStyle ?? actionDialogContentStyle,
227245
);
228246
}
229247

@@ -268,6 +286,10 @@ class StreamPollCreatorThemeData with Diagnosticable {
268286
a.switchListTileErrorStyle, b.switchListTileErrorStyle, t),
269287
switchListTileBorderRadius: BorderRadius.lerp(
270288
a.switchListTileBorderRadius, b.switchListTileBorderRadius, t),
289+
actionDialogTitleStyle: TextStyle.lerp(
290+
a.actionDialogTitleStyle, b.actionDialogTitleStyle, t),
291+
actionDialogContentStyle: TextStyle.lerp(
292+
a.actionDialogContentStyle, b.actionDialogContentStyle, t),
271293
);
272294
}
273295

@@ -293,7 +315,9 @@ class StreamPollCreatorThemeData with Diagnosticable {
293315
other.switchListTileFillColor == switchListTileFillColor &&
294316
other.switchListTileTitleStyle == switchListTileTitleStyle &&
295317
other.switchListTileErrorStyle == switchListTileErrorStyle &&
296-
other.switchListTileBorderRadius == switchListTileBorderRadius;
318+
other.switchListTileBorderRadius == switchListTileBorderRadius &&
319+
other.actionDialogTitleStyle == actionDialogTitleStyle &&
320+
other.actionDialogContentStyle == actionDialogContentStyle;
297321

298322
@override
299323
int get hashCode =>
@@ -314,5 +338,7 @@ class StreamPollCreatorThemeData with Diagnosticable {
314338
switchListTileFillColor.hashCode ^
315339
switchListTileTitleStyle.hashCode ^
316340
switchListTileErrorStyle.hashCode ^
317-
switchListTileBorderRadius.hashCode;
341+
switchListTileBorderRadius.hashCode ^
342+
actionDialogTitleStyle.hashCode ^
343+
actionDialogContentStyle.hashCode;
318344
}

packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,12 @@ class StreamChatThemeData {
379379
color: colorTheme.accentError,
380380
),
381381
switchListTileBorderRadius: BorderRadius.circular(12),
382+
actionDialogTitleStyle: textTheme.headlineBold.copyWith(
383+
color: colorTheme.textHighEmphasis,
384+
),
385+
actionDialogContentStyle: textTheme.body.copyWith(
386+
color: colorTheme.textHighEmphasis,
387+
),
382388
),
383389
pollInteractorTheme: StreamPollInteractorThemeData(
384390
pollTitleStyle: textTheme.headlineBold.copyWith(

packages/stream_chat_flutter/test/src/localization/default_translations_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ void main() {
7676
expect(translations.deleteLabel, isNotNull);
7777
expect(translations.deleteMessageLabel, isNotNull);
7878
expect(translations.deleteMessageQuestion, isNotNull);
79+
expect(translations.deletePollOptionLabel, isNotNull);
80+
expect(translations.deletePollOptionQuestion, isNotNull);
7981
expect(translations.operationCouldNotBeCompletedText, isNotNull);
8082
expect(translations.replyLabel, isNotNull);
8183
// pinned
1.4 KB
Loading
1.41 KB
Loading
1.4 KB
Loading

0 commit comments

Comments
 (0)