diff --git a/packages/stream_chat_flutter/lib/src/poll/creator/poll_config_option.dart b/packages/stream_chat_flutter/lib/src/poll/creator/poll_config_option.dart new file mode 100644 index 0000000000..a808b0b632 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/poll/creator/poll_config_option.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template pollConfigOption} +/// A card-style toggle for poll configuration options. +/// +/// Renders a rounded card with a title, description, and toggle switch. +/// When the switch is enabled and [child] is provided, it is revealed +/// below the header with an animated size transition. +/// +/// Can be nested — since the header is a simple [Row] rather than a tappable +/// list tile, padding from an outer [PollConfigOption] does not interfere +/// with an inner one. +/// {@endtemplate} +class PollConfigOption extends StatelessWidget { + /// {@macro pollConfigOption} + const PollConfigOption({ + super.key, + this.value = false, + required this.title, + this.description, + this.child, + this.backgroundColor, + this.contentPadding, + this.childSpacing, + this.onChanged, + }); + + /// Whether the toggle switch is on. + final bool value; + + /// The primary label of the card. + final String title; + + /// An optional short description displayed below [title]. + final String? description; + + /// Optional widget displayed below the header when [value] is true. + final Widget? child; + + /// The background color of the card. + /// + /// Defaults to [StreamColorScheme.backgroundSurfaceCard]. + final Color? backgroundColor; + + /// The padding inside the card around the content. + /// + /// Defaults to `EdgeInsets.all(spacing.md)`. Pass [EdgeInsets.zero] for + /// nested cards that sit inside a parent card's content padding. + final EdgeInsetsGeometry? contentPadding; + + /// The vertical spacing between the header and [child]. + /// + /// Defaults to `spacing.md`. + final double? childSpacing; + + /// Called when the user toggles the switch on or off. + /// + /// The card passes the new value to the callback but does not actually + /// change state until the parent widget rebuilds with the new [value]. + final ValueChanged? onChanged; + + @override + Widget build(BuildContext context) { + final theme = StreamPollCreatorTheme.of(context); + final defaults = _PollConfigOptionDefaults(context); + + final configOptionStyle = theme.configOptionStyle; + + final radius = context.streamRadius; + + final effectiveBackgroundColor = backgroundColor ?? configOptionStyle?.backgroundColor ?? defaults.backgroundColor; + final effectiveContentPadding = contentPadding ?? configOptionStyle?.contentPadding ?? defaults.contentPadding; + final effectiveChildSpacing = childSpacing ?? configOptionStyle?.childSpacing ?? defaults.childSpacing; + + return AnimatedSize( + duration: kThemeAnimationDuration, + alignment: Alignment.topCenter, + child: DecoratedBox( + decoration: BoxDecoration( + color: effectiveBackgroundColor, + borderRadius: BorderRadius.all(radius.xl), + ), + child: Padding( + padding: effectiveContentPadding, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: effectiveChildSpacing, + children: [ + _PollConfigOptionHeader( + title: title, + description: description, + value: value, + onChanged: onChanged, + ), + if (child case final child? when value) child, + ], + ), + ), + ), + ); + } +} + +// The header row: title/description on the left, toggle switch on the right. +class _PollConfigOptionHeader extends StatelessWidget { + const _PollConfigOptionHeader({ + required this.value, + required this.title, + this.description, + this.onChanged, + }); + + final String title; + final String? description; + final bool value; + final ValueChanged? onChanged; + + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + + final theme = StreamPollCreatorTheme.of(context); + final defaults = _PollConfigOptionDefaults(context); + final configOptionStyle = theme.configOptionStyle; + + final effectiveTitleStyle = configOptionStyle?.titleTextStyle ?? defaults.titleTextStyle; + final effectiveDescriptionStyle = configOptionStyle?.descriptionTextStyle ?? defaults.descriptionTextStyle; + final effectiveSwitchStyle = configOptionStyle?.switchStyle ?? defaults.switchStyle; + + return Row( + spacing: spacing.md, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + spacing: spacing.xxs, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: effectiveTitleStyle), + if (description case final description?) Text(description, style: effectiveDescriptionStyle), + ], + ), + ), + StreamSwitch( + value: value, + onChanged: onChanged, + style: effectiveSwitchStyle, + ), + ], + ); + } +} + +class _PollConfigOptionDefaults extends StreamPollConfigOptionStyle { + _PollConfigOptionDefaults(this._context); + + final BuildContext _context; + + late final _colorScheme = _context.streamColorScheme; + late final _textTheme = _context.streamTextTheme; + late final _spacing = _context.streamSpacing; + + @override + double get childSpacing => _spacing.md; + + @override + Color get backgroundColor => _colorScheme.backgroundSurfaceCard; + + @override + EdgeInsetsGeometry get contentPadding => EdgeInsets.all(_spacing.md); + + @override + TextStyle get titleTextStyle => _textTheme.headingSm.copyWith(color: _colorScheme.textPrimary); + + @override + TextStyle get descriptionTextStyle => _textTheme.captionDefault.copyWith(color: _colorScheme.textTertiary); +} diff --git a/packages/stream_chat_flutter/lib/src/poll/creator/poll_option_reorderable_list_view.dart b/packages/stream_chat_flutter/lib/src/poll/creator/poll_option_reorderable_list_view.dart index 4527af1fbf..7382e3a4ce 100644 --- a/packages/stream_chat_flutter/lib/src/poll/creator/poll_option_reorderable_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/poll/creator/poll_option_reorderable_list_view.dart @@ -1,10 +1,9 @@ -import 'dart:ui'; - import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/misc/separated_reorderable_list_view.dart'; import 'package:stream_chat_flutter/src/poll/creator/stream_delete_option_dialog.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; class _NullConst { const _NullConst(); @@ -48,6 +47,8 @@ class PollOptionItem { } } +enum _PollOptionVariant { option, addOption } + /// {@template pollOptionListItem} /// A widget that represents a poll option list item. /// {@endtemplate} @@ -55,17 +56,34 @@ class PollOptionListItem extends StatelessWidget { /// {@macro pollOptionListItem} const PollOptionListItem({ super.key, - required this.option, + required PollOptionItem this.option, this.hintText, this.focusNode, this.onRemove, this.onChanged, - }); + }) : onAddOptionPressed = null, + _variant = .option; + + /// Creates an "add an option" button styled to match the option list. + const PollOptionListItem.addOption({ + super.key, + this.hintText, + VoidCallback? onPressed, + }) : option = null, + focusNode = null, + onRemove = null, + onChanged = null, + onAddOptionPressed = onPressed, + _variant = .addOption; + + final _PollOptionVariant _variant; /// The poll option item. - final PollOptionItem option; + /// + /// Required for the default constructor, `null` for [addOption]. + final PollOptionItem? option; - /// Hint to be displayed in the poll option list item. + /// Hint to be displayed in the poll option list item or as the button label. final String? hintText; /// The focus node for the text field. @@ -77,66 +95,88 @@ class PollOptionListItem extends StatelessWidget { /// Callback called when the poll option item is changed. final ValueSetter? onChanged; + /// Callback called when the "add an option" button is pressed. + /// + /// If `null`, the button is disabled. + /// Only used by the [addOption] constructor. + final VoidCallback? onAddOptionPressed; + @override Widget build(BuildContext context) { - final theme = StreamPollCreatorTheme.of(context); - final fillColor = theme.optionsTextFieldFillColor; - final borderRadius = theme.optionsTextFieldBorderRadius; - - final colorTheme = StreamChatTheme.of(context).colorTheme; + return switch (_variant) { + _PollOptionVariant.option => _buildOption(context), + _PollOptionVariant.addOption => _buildAddOption(context), + }; + } - return DecoratedBox( - decoration: BoxDecoration( - color: fillColor, - borderRadius: borderRadius, + Widget _buildOption(BuildContext context) { + assert(option != null, 'option must not be null'); + + final icons = context.streamIcons; + + return StreamTextInput( + initialValue: option!.text, + hintText: hintText, + focusNode: focusNode, + textCapitalization: TextCapitalization.sentences, + helperText: option!.error, + helperState: option!.error != null ? StreamHelperState.error : null, + leading: MouseRegion( + cursor: SystemMouseCursors.grab, + child: Icon(icons.reorder), ), - child: Row( - children: [ - Padding( - padding: const EdgeInsetsDirectional.all(16), - child: MouseRegion( - cursor: SystemMouseCursors.grab, - child: Icon( - size: 24, - Icons.drag_handle_rounded, - color: colorTheme.textLowEmphasis, - ), - ), - ), - Expanded( - child: StreamPollTextField( - initialValue: option.text, - hintText: hintText, - style: theme.optionsTextFieldStyle, - fillColor: fillColor, - borderRadius: borderRadius, - errorText: option.error, - errorStyle: theme.optionsTextFieldErrorStyle, - focusNode: focusNode, - contentPadding: const EdgeInsets.symmetric(vertical: 18), - onChanged: switch (onChanged) { - final onChanged? => (text) { - final updated = option.copyWith(text: text); - return onChanged.call(updated); - }, - _ => null, - }, - ), - ), - IconButton( - iconSize: 24, - icon: Icon(context.streamIcons.delete), - style: IconButton.styleFrom( - foregroundColor: colorTheme.textLowEmphasis, - ), - // TODO: Enable once we have min SDK set to 3.29.0 - // onLongPress: () {/* Consume long press */}, - onPressed: switch (onRemove) { - final onRemove? => () => onRemove.call(option), - _ => null, - }, - ), - ], + trailing: StreamButton.icon( + type: .ghost, + style: .secondary, + icon: icons.minusCircle, + themeStyle: .from( + fixedSize: const .square(20), + tapTargetSize: .shrinkWrap, + ), + onTap: switch (onRemove) { + final onRemove? => () => onRemove.call(option!), + _ => null, + }, + ), + onChanged: switch (onChanged) { + final onChanged? => (text) { + final updated = option!.copyWith(text: text); + return onChanged.call(updated); + }, + _ => null, + }, + ); + } + + Widget _buildAddOption(BuildContext context) { + final radius = context.streamRadius; + final spacing = context.streamSpacing; + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + + final inputStyle = StreamPollCreatorTheme.of(context).optionInputStyle; + + final effectiveTextStyle = inputStyle?.textStyle ?? textTheme.bodyDefault; + final effectivePadding = inputStyle?.contentPadding ?? .symmetric(vertical: spacing.sm, horizontal: spacing.md); + final effectiveBorderColor = inputStyle?.border?.color ?? colorScheme.borderDefault; + final effectiveBorderRadius = inputStyle?.borderRadius ?? .all(radius.lg); + + return SizedBox( + width: .infinity, + child: StreamButton( + type: .outline, + style: .secondary, + onTap: onAddOptionPressed, + themeStyle: .from( + fixedSize: .infinite, + textStyle: effectiveTextStyle, + padding: effectivePadding, + tapTargetSize: .shrinkWrap, + borderColor: effectiveBorderColor, + alignment: AlignmentDirectional.centerStart, + shape: RoundedRectangleBorder(borderRadius: effectiveBorderRadius), + ), + label: hintText ?? context.translations.addAnOptionLabel, ), ); } @@ -261,11 +301,12 @@ class _PollOptionReorderableListViewState extends State{}; - // Check if the option is empty. - if (option.text.isEmpty) return translations.pollOptionEmptyError; - - // Check for duplicate options if duplicates are not allowed. - if (widget.allowDuplicate case false) { - if (_options.values.any((it) { - // Skip if it's the same option - if (it.id == option.id) return false; - - return it.text == option.text; - })) { + String? _errorFor(String normalized) { + if (normalized.isEmpty) return translations.pollOptionEmptyError; + if (checkDuplicates) { + if (seen.add(normalized)) return null; return translations.pollOptionDuplicateError; } + + return null; } - return null; + // Pass 1 — validate every option except the one being edited. + _options.updateAll((key, option) { + if (key == changedOptionId) return option; + + final normalized = option.text.trim().toLowerCase(); + return option.copyWith(error: _errorFor(normalized)); + }); + + // Pass 2 — validate the edited option against the pre-existing texts. + if (changedOptionId case final id? when _options.containsKey(id)) { + final option = _options[id]!; + final normalized = option.text.trim().toLowerCase(); + _options[id] = option.copyWith(error: _errorFor(normalized)); + } } Future _onOptionRemoved(PollOptionItem option) async { @@ -301,6 +369,7 @@ class _PollOptionReorderableListViewState extends State{ for (final option in options) option.id: option, }; + + _revalidateOptions(); }); // Notify the parent widget about the change @@ -393,60 +455,64 @@ class _PollOptionReorderableListViewState extends State const SizedBox(height: 8), - onReorderStart: (_) => FocusScope.of(context).unfocus(), - onReorder: _onOptionReorder, - itemBuilder: (context, index) { - final option = _options.values.elementAt(index); - return PollOptionListItem( - key: Key(option.id), - option: option, - hintText: widget.itemHintText, - focusNode: _focusNodes[option.id], - onRemove: _onOptionRemoved, - onChanged: _onOptionChanged, - ); - }, - ), - ), - const SizedBox(height: 8), - SizedBox( - width: double.infinity, - child: FilledButton.tonal( - onPressed: _canAddMoreOptions ? _onAddOptionPressed : null, - style: TextButton.styleFrom( - alignment: Alignment.centerLeft, - textStyle: theme.optionsTextFieldStyle, - shape: RoundedRectangleBorder( - borderRadius: borderRadius ?? BorderRadius.zero, - ), - padding: const EdgeInsets.symmetric( - vertical: 18, - horizontal: 16, - ), - backgroundColor: theme.optionsTextFieldFillColor, - foregroundColor: theme.optionsTextFieldStyle?.color, + child: StreamTextInputTheme( + data: .new(style: effectiveInputStyle), + child: SeparatedReorderableListView( + shrinkWrap: true, + itemCount: _options.length, + physics: const NeverScrollableScrollPhysics(), + proxyDecorator: _proxyDecorator, + separatorBuilder: (_, __) => SizedBox(height: spacing.xs), + onReorderStart: (_) => FocusScope.of(context).unfocus(), + onReorder: _onOptionReorder, + itemBuilder: (context, index) { + final option = _options.values.elementAt(index); + return PollOptionListItem( + key: Key(option.id), + option: option, + hintText: widget.itemHintText, + focusNode: _focusNodes[option.id], + onRemove: _onOptionRemoved, + onChanged: _onOptionChanged, + ); + }, ), - child: Text(context.translations.addAnOptionLabel), ), ), + SizedBox(height: spacing.xs), + PollOptionListItem.addOption( + hintText: widget.itemHintText, + onPressed: _canAddMoreOptions ? _onAddOptionPressed : null, + ), ], ); } } + +class _PollOptionListViewDefaults extends StreamPollCreatorThemeData { + _PollOptionListViewDefaults(this._context); + + final BuildContext _context; + + late final _colorScheme = _context.streamColorScheme; + late final _textTheme = _context.streamTextTheme; + + @override + TextStyle get headerTextStyle => _textTheme.headingSm.copyWith(color: _colorScheme.textPrimary); +} diff --git a/packages/stream_chat_flutter/lib/src/poll/creator/poll_question_text_field.dart b/packages/stream_chat_flutter/lib/src/poll/creator/poll_question_text_field.dart index a5e8ea25be..b83fe6ad8a 100644 --- a/packages/stream_chat_flutter/lib/src/poll/creator/poll_question_text_field.dart +++ b/packages/stream_chat_flutter/lib/src/poll/creator/poll_question_text_field.dart @@ -1,9 +1,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/poll/stream_poll_text_field.dart'; -import 'package:stream_chat_flutter/src/theme/poll_creator_theme.dart'; -import 'package:stream_chat_flutter/src/utils/extensions.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; class _NullConst { const _NullConst(); @@ -116,41 +114,48 @@ class _PollQuestionTextFieldState extends State { @override Widget build(BuildContext context) { final theme = StreamPollCreatorTheme.of(context); - final fillColor = theme.questionTextFieldFillColor; - final borderRadius = theme.questionTextFieldBorderRadius; + final defaults = _PollQuestionTextFieldDefaults(context); + + final spacing = context.streamSpacing; + + final effectiveHeaderStyle = theme.headerTextStyle ?? defaults.headerTextStyle; + final effectiveInputStyle = theme.questionInputStyle ?? defaults.questionInputStyle; return Column( + spacing: spacing.xs, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (widget.title case final title?) ...[ - Text(title, style: theme.questionHeaderStyle), - const SizedBox(height: 8), - ], - DecoratedBox( - decoration: BoxDecoration( - color: fillColor, - borderRadius: borderRadius, - ), - child: StreamPollTextField( - initialValue: _question.text, - hintText: widget.hintText, - fillColor: fillColor, - style: theme.questionTextFieldStyle, - borderRadius: borderRadius, - errorText: _question.error, - errorStyle: theme.questionTextFieldErrorStyle, - onChanged: (text) { - _question = _question.copyWith( - text: text, - error: _validateQuestion(text), - ); - - widget.onChanged?.call(_question); - }, - ), + if (widget.title case final title?) Text(title, style: effectiveHeaderStyle), + StreamTextInput( + initialValue: _question.text, + hintText: widget.hintText, + helperText: _question.error, + helperState: _question.error != null ? .error : null, + textCapitalization: TextCapitalization.sentences, + style: effectiveInputStyle, + onChanged: (text) { + _question = _question.copyWith( + text: text, + error: _validateQuestion(text), + ); + + widget.onChanged?.call(_question); + }, ), ], ); } } + +class _PollQuestionTextFieldDefaults extends StreamPollCreatorThemeData { + _PollQuestionTextFieldDefaults(this._context); + + final BuildContext _context; + + late final _colorScheme = _context.streamColorScheme; + late final _textTheme = _context.streamTextTheme; + + @override + TextStyle get headerTextStyle => _textTheme.headingSm.copyWith(color: _colorScheme.textPrimary); +} diff --git a/packages/stream_chat_flutter/lib/src/poll/creator/poll_switch_list_tile.dart b/packages/stream_chat_flutter/lib/src/poll/creator/poll_switch_list_tile.dart deleted file mode 100644 index b1088d3a90..0000000000 --- a/packages/stream_chat_flutter/lib/src/poll/creator/poll_switch_list_tile.dart +++ /dev/null @@ -1,240 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -class _NullConst { - const _NullConst(); -} - -const _nullConst = _NullConst(); - -/// {@template pollSwitchListTile} -/// A widget that represents a switch list tile for poll input. -/// -/// The switch list tile contains a title and a switch. -/// -/// Optionally, it can contain a list of children widgets that are displayed -/// below the switch when the switch is enabled. -/// -/// see also: -/// - [PollSwitchTextField], a widget that represents a toggleable text field -/// for poll input. -/// {@endtemplate} -class PollSwitchListTile extends StatelessWidget { - /// {@macro pollSwitchListTile} - const PollSwitchListTile({ - super.key, - this.value = false, - required this.title, - this.children = const [], - this.onChanged, - }); - - /// The current value of the switch. - final bool value; - - /// The title of the switch list tile. - final String title; - - /// Optional list of children widgets to be displayed when the switch is - /// enabled. - /// - /// If `null`, no children will be displayed. - final List children; - - /// Callback called when the switch value is changed. - final ValueSetter? onChanged; - - @override - Widget build(BuildContext context) { - final theme = StreamPollCreatorTheme.of(context); - final fillColor = theme.switchListTileFillColor; - final borderRadius = theme.switchListTileBorderRadius; - - final listTile = SwitchListTile( - value: value, - onChanged: onChanged, - tileColor: fillColor, - title: Text(title, style: theme.switchListTileTitleStyle), - contentPadding: const EdgeInsets.only(left: 16, right: 8), - shape: RoundedRectangleBorder( - borderRadius: borderRadius ?? BorderRadius.zero, - ), - ); - - return DecoratedBox( - decoration: BoxDecoration( - color: fillColor, - borderRadius: borderRadius, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - listTile, - if (value) ...children, - ], - ), - ); - } -} - -/// {@template pollSwitchItem} -/// A data class that represents a poll boolean item. -/// {@endtemplate} -class PollSwitchItem { - /// {@macro pollSwitchItem} - PollSwitchItem({ - String? id, - this.value = false, - this.inputValue, - this.error, - }) : id = id ?? const Uuid().v4(); - - /// The unique id of the poll option item. - final String id; - - /// The boolean value of the poll switch item. - final bool value; - - /// Optional input value linked to the poll switch item. - final T? inputValue; - - /// Optional error message based on the validation of the poll switch item - /// and its input value. - /// - /// If the poll switch item is valid, this will be `null`. - final String? error; - - /// A copy of the current [PollSwitchItem] with the provided values. - PollSwitchItem copyWith({ - String? id, - bool? value, - Object? error = _nullConst, - Object? inputValue = _nullConst, - }) { - return PollSwitchItem( - id: id ?? this.id, - value: value ?? this.value, - error: error == _nullConst ? this.error : error as String?, - inputValue: inputValue == _nullConst ? this.inputValue : inputValue as T?, - ); - } -} - -/// {@template pollSwitchTextField} -/// A widget that represents a toggleable text field for poll input. -/// -/// Generally used as one of the children of [PollSwitchListTile]. -/// {@endtemplate} -class PollSwitchTextField extends StatefulWidget { - /// {@macro pollSwitchTextField} - const PollSwitchTextField({ - super.key, - required this.item, - this.hintText, - this.keyboardType, - this.onChanged, - this.validator, - }); - - /// The current value of the switch text field. - final PollSwitchItem item; - - /// The hint text to be displayed in the text field. - final String? hintText; - - /// The keyboard type of the text field. - final TextInputType? keyboardType; - - /// Callback called when the switch text field is changed. - final ValueChanged>? onChanged; - - /// The validator function to validate the input value. - final String? Function(PollSwitchItem)? validator; - - @override - State createState() => _PollSwitchTextFieldState(); -} - -class _PollSwitchTextFieldState extends State { - late var _item = widget.item.copyWith( - error: widget.validator?.call(widget.item), - ); - - @override - void didUpdateWidget(covariant PollSwitchTextField oldWidget) { - super.didUpdateWidget(oldWidget); - // Update the item if the updated item is different from the current item. - final currItem = _item; - final newItem = widget.item; - final itemEquality = EqualityBy, (bool, int?)>( - (it) => (it.value, it.inputValue), - ); - - if (itemEquality.equals(currItem, newItem) case false) { - _item = newItem; - } - } - - void _onSwitchToggled(bool value) { - setState(() { - // Update the switch value. - _item = _item.copyWith(value: value); - // Validate the switch value. - _item = _item.copyWith(error: widget.validator?.call(_item)); - - // Notify the parent widget about the change - widget.onChanged?.call(_item); - }); - } - - void _onFieldChanged(String text) { - setState(() { - // Update the input value. - _item = _item.copyWith(inputValue: int.tryParse(text)); - // Validate the input value. - _item = _item.copyWith(error: widget.validator?.call(_item)); - - // Notify the parent widget about the change - widget.onChanged?.call(_item); - }); - } - - @override - Widget build(BuildContext context) { - final theme = StreamPollCreatorTheme.of(context); - final fillColor = theme.switchListTileFillColor; - final borderRadius = theme.switchListTileBorderRadius; - - return DecoratedBox( - decoration: BoxDecoration( - color: fillColor, - borderRadius: borderRadius, - ), - child: Row( - children: [ - Expanded( - child: StreamPollTextField( - hintText: widget.hintText, - enabled: _item.value, - fillColor: fillColor, - style: theme.switchListTileTitleStyle, - keyboardType: widget.keyboardType, - borderRadius: borderRadius, - errorText: _item.value ? _item.error : null, - errorStyle: theme.switchListTileErrorStyle, - initialValue: _item.inputValue?.toString(), - onChanged: _onFieldChanged, - ), - ), - Switch( - value: _item.value, - onChanged: _onSwitchToggled, - ), - const SizedBox(width: 8), - ], - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/poll/creator/stream_delete_option_dialog.dart b/packages/stream_chat_flutter/lib/src/poll/creator/stream_delete_option_dialog.dart index 11c901bf39..36320d0950 100644 --- a/packages/stream_chat_flutter/lib/src/poll/creator/stream_delete_option_dialog.dart +++ b/packages/stream_chat_flutter/lib/src/poll/creator/stream_delete_option_dialog.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/theme/poll_creator_theme.dart'; import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/utils/extensions.dart'; @@ -25,7 +24,6 @@ class PollDeleteOptionDialog extends StatelessWidget { @override Widget build(BuildContext context) { final theme = StreamChatTheme.of(context); - final pollCreatorTheme = StreamPollCreatorTheme.of(context); final actions = [ TextButton( @@ -49,14 +47,8 @@ class PollDeleteOptionDialog extends StatelessWidget { ]; return AlertDialog( - title: Text( - context.translations.deletePollOptionLabel, - style: pollCreatorTheme.actionDialogTitleStyle, - ), - content: Text( - context.translations.deletePollOptionQuestion, - style: pollCreatorTheme.actionDialogContentStyle, - ), + title: Text(context.translations.deletePollOptionLabel), + content: Text(context.translations.deletePollOptionQuestion), actions: actions, titlePadding: const EdgeInsetsDirectional.fromSTEB(16, 24, 16, 4), contentPadding: const EdgeInsetsDirectional.fromSTEB(16, 4, 16, 24), diff --git a/packages/stream_chat_flutter/lib/src/poll/creator/stream_poll_creator_dialog.dart b/packages/stream_chat_flutter/lib/src/poll/creator/stream_poll_creator_dialog.dart index a8e2760458..985f96e177 100644 --- a/packages/stream_chat_flutter/lib/src/poll/creator/stream_poll_creator_dialog.dart +++ b/packages/stream_chat_flutter/lib/src/poll/creator/stream_poll_creator_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template showStreamPollCreatorDialog} /// Shows the poll creator dialog based on the screen size. @@ -110,7 +111,6 @@ class _StreamPollCreatorDialogState extends State { @override Widget build(BuildContext context) { final theme = StreamChatTheme.of(context); - final pollCreatorTheme = StreamPollCreatorTheme.of(context); final actions = [ TextButton( @@ -129,11 +129,6 @@ class _StreamPollCreatorDialogState extends State { return TextButton( onPressed: isValid ? () { - final errors = _controller.validateGranularly(); - if (errors.isNotEmpty) { - return; - } - final sanitizedPoll = _controller.sanitizedPoll; return Navigator.of(context).pop(sanitizedPoll); } @@ -150,16 +145,13 @@ class _StreamPollCreatorDialogState extends State { ]; return AlertDialog( - title: Text( - context.translations.createPollLabel(), - style: pollCreatorTheme.appBarTitleStyle, - ), + title: Text(context.translations.createPollLabel()), titlePadding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), actions: actions, contentPadding: EdgeInsets.zero, actionsPadding: const EdgeInsets.all(8), - backgroundColor: pollCreatorTheme.backgroundColor, + backgroundColor: theme.colorTheme.appBg, content: SizedBox( width: 640, // Similar to BottomSheet default width on M3 child: StreamPollCreatorWidget( @@ -219,40 +211,52 @@ class _StreamPollCreatorFullScreenDialogState extends State null, + true => () { + final sanitizedPoll = _controller.sanitizedPoll; + return Navigator.of(context).pop(sanitizedPoll); + }, + }, ); }, ), @@ -265,3 +269,20 @@ class _StreamPollCreatorFullScreenDialogState extends State .from(tapTargetSize: .shrinkWrap); + + @override + StreamButtonThemeStyle get secondaryActionStyle => .from( + tapTargetSize: .shrinkWrap, + borderColor: _colorScheme.borderDefault, + ); +} diff --git a/packages/stream_chat_flutter/lib/src/poll/creator/stream_poll_creator_widget.dart b/packages/stream_chat_flutter/lib/src/poll/creator/stream_poll_creator_widget.dart index 93e4872ce9..9e1414472d 100644 --- a/packages/stream_chat_flutter/lib/src/poll/creator/stream_poll_creator_widget.dart +++ b/packages/stream_chat_flutter/lib/src/poll/creator/stream_poll_creator_widget.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/poll/creator/poll_config_option.dart'; import 'package:stream_chat_flutter/src/poll/creator/poll_option_reorderable_list_view.dart'; import 'package:stream_chat_flutter/src/poll/creator/poll_question_text_field.dart'; -import 'package:stream_chat_flutter/src/poll/creator/poll_switch_list_tile.dart'; +import 'package:stream_chat_flutter/src/theme/poll_creator_theme.dart'; import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template streamPollCreator} /// A widget that allows users to create a poll. @@ -36,9 +38,13 @@ class StreamPollCreatorWidget extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = StreamPollCreatorTheme.of(context); + return ValueListenableBuilder( valueListenable: controller, builder: (context, poll, child) { + final spacing = context.streamSpacing; + final config = controller.config; final translations = context.translations; @@ -60,10 +66,10 @@ class StreamPollCreatorWidget extends StatelessWidget { initialQuestion: PollQuestion(id: poll.id, text: poll.name), onChanged: (question) => controller.question = question.text, ), - const SizedBox(height: 32), + SizedBox(height: spacing.xl), PollOptionReorderableListView( title: translations.optionLabel(isPlural: true), - itemHintText: translations.optionLabel(), + itemHintText: context.translations.addAnOptionLabel, allowDuplicate: config.allowDuplicateOptions, optionsRange: config.optionsRange, initialOptions: [ @@ -73,63 +79,54 @@ class StreamPollCreatorWidget extends StatelessWidget { for (final option in options) PollOption(id: option.id, text: option.text), ], ), - const SizedBox(height: 32), - PollSwitchListTile( + SizedBox(height: spacing.xxl), + PollConfigOption( title: translations.multipleAnswersLabel, + description: 'Select more than one option', value: poll.enforceUniqueVote == false, onChanged: (value) { controller.enforceUniqueVote = !value; // We also need to reset maxVotesAllowed if disabled. if (value case false) controller.maxVotesAllowed = null; }, - children: [ - PollSwitchTextField( - hintText: translations.maximumVotesPerPersonLabel, - item: PollSwitchItem( - value: poll.maxVotesAllowed != null, - inputValue: poll.maxVotesAllowed, - ), - keyboardType: TextInputType.number, - validator: (item) { - if (config.allowedVotesRange case final allowedRange?) { - final votes = item.inputValue; - if (votes == null) return null; - - return translations.maxVotesPerPersonValidationError( - votes, - allowedRange, - ); - } - - return null; - }, - onChanged: (option) { - final enabled = option.value; - final maxVotes = option.inputValue; - - controller.maxVotesAllowed = enabled ? maxVotes : null; - }, + child: PollConfigOption( + contentPadding: EdgeInsets.zero, + title: translations.maximumVotesPerPersonLabel, + description: switch (config.allowedVotesRange) { + final range? => 'Choose between ${range.min ?? 2}\u2013${range.max ?? 10} options', + _ => 'Choose between 2\u201310 options', + }, + value: poll.maxVotesAllowed != null, + onChanged: (enabled) { + controller.maxVotesAllowed = enabled ? config.allowedVotesRange?.min ?? 2 : null; + }, + child: StreamStepper( + min: config.allowedVotesRange?.min ?? 2, + max: config.allowedVotesRange?.max ?? 10, + value: poll.maxVotesAllowed ?? config.allowedVotesRange?.min ?? 2, + onChanged: (value) => controller.maxVotesAllowed = value, + style: theme.configOptionStyle?.stepperStyle, ), - ], + ), ), - const SizedBox(height: 8), - PollSwitchListTile( + SizedBox(height: spacing.md), + PollConfigOption( title: translations.anonymousPollLabel, - value: poll.votingVisibility == VotingVisibility.anonymous, - onChanged: (anon) => controller.votingVisibility = - anon // - ? VotingVisibility.anonymous - : VotingVisibility.public, + description: 'Hide who voted', + value: poll.votingVisibility == .anonymous, + onChanged: (anon) => controller.votingVisibility = anon ? .anonymous : .public, ), - const SizedBox(height: 8), - PollSwitchListTile( + SizedBox(height: spacing.md), + PollConfigOption( title: translations.suggestAnOptionLabel, + description: 'Let others add options', value: poll.allowUserSuggestedOptions, onChanged: (allow) => controller.allowSuggestions = allow, ), - const SizedBox(height: 8), - PollSwitchListTile( + SizedBox(height: spacing.md), + PollConfigOption( title: translations.addACommentLabel, + description: 'Allow others to add comments', value: poll.allowAnswers, onChanged: (allow) => controller.allowComments = allow, ), diff --git a/packages/stream_chat_flutter/lib/src/poll/interactor/poll_add_comment_dialog.dart b/packages/stream_chat_flutter/lib/src/poll/interactor/poll_add_comment_dialog.dart index 4f5fe67596..2bc8366018 100644 --- a/packages/stream_chat_flutter/lib/src/poll/interactor/poll_add_comment_dialog.dart +++ b/packages/stream_chat_flutter/lib/src/poll/interactor/poll_add_comment_dialog.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/poll/stream_poll_text_field.dart'; +import 'package:flutter/services.dart'; import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template showPollAddCommentDialog} /// Shows a dialog that allows the user to add a poll comment. @@ -94,18 +95,12 @@ class _PollAddCommentDialogState extends State { contentPadding: const EdgeInsets.all(16), actionsPadding: const EdgeInsets.all(8), backgroundColor: theme.colorTheme.appBg, - content: StreamPollTextField( - autoFocus: true, + content: StreamTextInput( + autofocus: true, initialValue: _comment, hintText: context.translations.enterYourCommentLabel, - contentPadding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 16, - ), - // TODO: Fix when working on poll create screen - // style: pollInteractorTheme.pollActionDialogTextFieldStyle, - // fillColor: pollInteractorTheme.pollActionDialogTextFieldFillColor, - // borderRadius: pollInteractorTheme.pollActionDialogTextFieldBorderRadius, + inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'^\s'))], + style: const .new(contentPadding: .symmetric(vertical: 12, horizontal: 16)), onChanged: (value) => setState(() => _comment = value), ), ); diff --git a/packages/stream_chat_flutter/lib/src/poll/interactor/poll_suggest_option_dialog.dart b/packages/stream_chat_flutter/lib/src/poll/interactor/poll_suggest_option_dialog.dart index 44ab300753..93aa168f60 100644 --- a/packages/stream_chat_flutter/lib/src/poll/interactor/poll_suggest_option_dialog.dart +++ b/packages/stream_chat_flutter/lib/src/poll/interactor/poll_suggest_option_dialog.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/poll/stream_poll_text_field.dart'; +import 'package:flutter/services.dart'; import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template showPollSuggestOptionDialog} /// Shows a dialog that allows the user to suggest an option for a poll. @@ -94,18 +95,12 @@ class _PollSuggestOptionDialogState extends State { contentPadding: const EdgeInsets.all(16), actionsPadding: const EdgeInsets.all(8), backgroundColor: theme.colorTheme.appBg, - content: StreamPollTextField( - autoFocus: true, + content: StreamTextInput( + autofocus: true, initialValue: _option, hintText: context.translations.enterANewOptionLabel, - contentPadding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 16, - ), - // TODO: Fix when working on poll create screen - // style: pollInteractorTheme.pollActionDialogTextFieldStyle, - // fillColor: pollInteractorTheme.pollActionDialogTextFieldFillColor, - // borderRadius: pollInteractorTheme.pollActionDialogTextFieldBorderRadius, + inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'^\s'))], + style: const .new(contentPadding: .symmetric(vertical: 12, horizontal: 16)), onChanged: (value) => setState(() => _option = value), ), ); diff --git a/packages/stream_chat_flutter/lib/src/poll/stream_poll_text_field.dart b/packages/stream_chat_flutter/lib/src/poll/stream_poll_text_field.dart deleted file mode 100644 index 9a28423978..0000000000 --- a/packages/stream_chat_flutter/lib/src/poll/stream_poll_text_field.dart +++ /dev/null @@ -1,271 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -const _kTransitionDuration = Duration(milliseconds: 167); - -/// {@template streamPollTextField} -/// A widget that represents a text field for poll input. -/// {@endtemplate} -class StreamPollTextField extends StatefulWidget { - /// {@macro streamPollTextField} - const StreamPollTextField({ - super.key, - this.initialValue, - this.style, - this.enabled = true, - this.hintText, - this.fillColor, - this.errorText, - this.errorStyle, - this.contentPadding = const EdgeInsets.symmetric( - vertical: 18, - horizontal: 16, - ), - this.borderRadius, - this.focusNode, - this.keyboardType, - this.autoFocus = false, - this.onChanged, - }); - - /// The initial value of the text field. - /// - /// If `null`, the text field will be empty. - final String? initialValue; - - /// The style to use for the text field. - final TextStyle? style; - - /// Whether the text field is enabled. - final bool enabled; - - /// The hint text to be displayed in the text field. - final String? hintText; - - /// The fill color of the text field. - final Color? fillColor; - - /// The error text to be displayed below the text field. - /// - /// If `null`, no error text will be displayed. - final String? errorText; - - /// The style to use for the error text. - final TextStyle? errorStyle; - - /// The padding around the text field content. - final EdgeInsetsGeometry contentPadding; - - /// The border radius of the text field. - final BorderRadius? borderRadius; - - /// The keyboard type of the text field. - final TextInputType? keyboardType; - - /// Whether the text field should autofocus. - final bool autoFocus; - - /// The focus node of the text field. - final FocusNode? focusNode; - - /// Callback called when the text field value is changed. - final ValueChanged? onChanged; - - @override - State createState() => _StreamPollTextFieldState(); -} - -class _StreamPollTextFieldState extends State { - late final _controller = TextEditingController(text: widget.initialValue); - - @override - void didUpdateWidget(covariant StreamPollTextField oldWidget) { - super.didUpdateWidget(oldWidget); - // Update the controller value if the updated initial value is different - // from the current value. - final currValue = _controller.text; - final newValue = widget.initialValue; - if (currValue != newValue) { - _controller.value = switch (newValue) { - final value? => TextEditingValue( - text: value, - selection: TextSelection.collapsed(offset: value.length), - ), - _ => TextEditingValue.empty, - }; - } - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = StreamChatTheme.of(context); - - // Reduce vertical padding if there is an error text. - var contentPadding = widget.contentPadding; - final verticalPadding = contentPadding.vertical; - final horizontalPadding = contentPadding.horizontal; - if (widget.errorText != null) { - contentPadding = contentPadding.subtract( - EdgeInsets.symmetric(vertical: verticalPadding / 4), - ); - } - - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - PollTextFieldError( - padding: EdgeInsets.only( - top: verticalPadding / 4, - left: horizontalPadding / 2, - right: horizontalPadding / 2, - ), - errorText: widget.errorText, - errorStyle: - widget.errorStyle ?? - theme.textTheme.footnote.copyWith( - color: theme.colorTheme.accentError, - ), - ), - TextField( - autocorrect: false, - controller: _controller, - focusNode: widget.focusNode, - onChanged: widget.onChanged, - style: widget.style ?? theme.textTheme.headline, - keyboardType: widget.keyboardType, - autofocus: widget.autoFocus, - inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'^\s'))], - decoration: InputDecoration( - filled: true, - isCollapsed: true, - enabled: widget.enabled, - fillColor: widget.fillColor, - hintText: widget.hintText, - hintStyle: (widget.style ?? theme.textTheme.headline).copyWith( - color: theme.colorTheme.textLowEmphasis, - ), - contentPadding: contentPadding, - border: OutlineInputBorder( - borderRadius: widget.borderRadius ?? BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - ), - ), - ], - ); - } -} - -/// {@template pollTextFieldError} -/// A widget that displays an error text around a text field with a fade -/// transition. -/// -/// Usually used with [StreamPollTextField]. -/// {@endtemplate} -class PollTextFieldError extends StatefulWidget { - /// {@macro pollTextFieldError} - const PollTextFieldError({ - super.key, - this.errorText, - this.errorStyle, - this.errorMaxLines, - this.textAlign, - this.padding, - }); - - /// The error text to be displayed. - final String? errorText; - - /// The maximum number of lines for the error text. - final int? errorMaxLines; - - /// The alignment of the error text. - final TextAlign? textAlign; - - /// The style of the error text. - final TextStyle? errorStyle; - - /// The padding around the error text. - final EdgeInsetsGeometry? padding; - - @override - State createState() => _PollTextFieldErrorState(); -} - -class _PollTextFieldErrorState extends State - with SingleTickerProviderStateMixin { - late AnimationController _controller; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - duration: _kTransitionDuration, - vsync: this, - )..addListener(() => setState(() {})); - - if (widget.errorText != null) { - _controller.value = 1.0; - } - } - - @override - void didUpdateWidget(covariant PollTextFieldError oldWidget) { - super.didUpdateWidget(oldWidget); - // Animate the error text if the error text state has changed. - final newError = widget.errorText; - final currError = oldWidget.errorText; - final errorTextStateChanged = (newError != null) != (currError != null); - if (errorTextStateChanged) { - if (newError != null) { - _controller.forward(); - } else { - _controller.reverse(); - } - } - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final errorText = widget.errorText; - if (errorText == null) return const Empty(); - - return Container( - padding: widget.padding, - child: Semantics( - container: true, - child: FadeTransition( - opacity: _controller, - child: FractionalTranslation( - translation: Tween( - begin: const Offset(0, 0.25), - end: Offset.zero, - ).evaluate(_controller.view), - child: Text( - errorText, - style: widget.errorStyle, - textAlign: widget.textAlign, - overflow: TextOverflow.ellipsis, - maxLines: widget.errorMaxLines, - ), - ), - ), - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/theme/poll_creator_theme.dart b/packages/stream_chat_flutter/lib/src/theme/poll_creator_theme.dart index 940fdbb4e2..ffafbaf508 100644 --- a/packages/stream_chat_flutter/lib/src/theme/poll_creator_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/poll_creator_theme.dart @@ -1,42 +1,62 @@ -import 'dart:ui'; - -import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; +import 'package:theme_extensions_builder_annotation/theme_extensions_builder_annotation.dart'; + +part 'poll_creator_theme.g.theme.dart'; -/// {@template streamPollCreatorTheme} -/// Overrides the default style of [StreamPollCreatorWidget] descendants. +/// Applies a poll creator theme to descendant [StreamPollCreatorWidget] +/// widgets. +/// +/// Wrap a subtree with [StreamPollCreatorTheme] to override poll creator +/// styling. Access the merged theme using [StreamPollCreatorTheme.of]. +/// +/// {@tool snippet} +/// +/// Override poll creator styling for a specific section: +/// +/// ```dart +/// StreamPollCreatorTheme( +/// data: StreamPollCreatorThemeData( +/// headerTextStyle: TextStyle(fontWeight: FontWeight.w700), +/// configOptionStyle: StreamPollConfigOptionStyle( +/// switchStyle: StreamSwitchStyle.from( +/// selectedTrackColor: Colors.green, +/// ), +/// ), +/// ), +/// child: StreamPollCreatorWidget(controller: controller), +/// ) +/// ``` +/// {@end-tool} /// /// See also: /// -/// * [StreamPollCreatorThemeData], which is used to configure this theme. -/// {@endtemplate} +/// * [StreamPollCreatorThemeData], which describes the poll creator theme. +/// * [StreamPollCreatorWidget], the widget affected by this theme. class StreamPollCreatorTheme extends InheritedTheme { - /// Creates a [StreamPollCreatorTheme]. - /// - /// The [data] parameter must not be null. + /// Creates a poll creator theme that controls descendant widgets. const StreamPollCreatorTheme({ super.key, required this.data, required super.child, }); - /// The configuration of this theme. + /// The poll creator theme data for descendant widgets. final StreamPollCreatorThemeData data; - /// The closest instance of this class that encloses the given context. + /// Returns the [StreamPollCreatorThemeData] merged from local and global + /// themes. /// - /// If there is no enclosing [StreamPollCreatorTheme] widget, then - /// [StreamChatThemeData.pollCreatorTheme] is used. + /// Local values from the nearest [StreamPollCreatorTheme] ancestor take + /// precedence over global values from [StreamChatTheme.of]. /// - /// Typical usage is as follows: - /// - /// ```dart - /// StreamPollCreatorTheme theme = StreamPollCreatorTheme.of(context); - /// ``` + /// This allows partial overrides — for example, overriding only + /// [StreamPollCreatorThemeData.headerTextStyle] while inheriting other + /// properties from the global theme. static StreamPollCreatorThemeData of(BuildContext context) { - final pollCreatorTheme = context.dependOnInheritedWidgetOfExactType(); - return pollCreatorTheme?.data ?? StreamChatTheme.of(context).pollCreatorTheme; + final localTheme = context.dependOnInheritedWidgetOfExactType(); + return StreamChatTheme.of(context).pollCreatorTheme.merge(localTheme?.data); } @override @@ -46,264 +66,136 @@ class StreamPollCreatorTheme extends InheritedTheme { bool updateShouldNotify(StreamPollCreatorTheme oldWidget) => data != oldWidget.data; } -/// {@template streamPollCreatorThemeData} -/// A style that overrides the default appearance of [StreamPollCreator] widget -/// when used with [StreamPollCreatorTheme] or with the overall -/// [StreamChatTheme]'s [StreamChatThemeData.pollCreatorTheme]. -/// {@endtemplate} -class StreamPollCreatorThemeData with Diagnosticable { - /// {@macro streamPollCreatorThemeData} +/// Theme data for customizing [StreamPollCreatorWidget] widgets. +/// +/// {@tool snippet} +/// +/// Customize poll creator appearance globally: +/// +/// ```dart +/// StreamChatThemeData( +/// pollCreatorTheme: StreamPollCreatorThemeData( +/// headerTextStyle: TextStyle(fontWeight: FontWeight.w700), +/// ), +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamPollCreatorWidget], the widget that uses this theme data. +/// * [StreamPollCreatorTheme], for overriding theme in a widget subtree. +@themeGen +@immutable +class StreamPollCreatorThemeData with _$StreamPollCreatorThemeData { + /// Creates poll creator theme data with optional style overrides. const StreamPollCreatorThemeData({ - this.backgroundColor, - this.appBarTitleStyle, - this.appBarElevation, - this.appBarBackgroundColor, - this.appBarForegroundColor, - this.questionTextFieldFillColor, - this.questionHeaderStyle, - this.questionTextFieldStyle, - this.questionTextFieldErrorStyle, - this.questionTextFieldBorderRadius, - this.optionsHeaderStyle, - this.optionsTextFieldStyle, - this.optionsTextFieldFillColor, - this.optionsTextFieldErrorStyle, - this.optionsTextFieldBorderRadius, - this.switchListTileFillColor, - this.switchListTileTitleStyle, - this.switchListTileErrorStyle, - this.switchListTileBorderRadius, - this.actionDialogTitleStyle, - this.actionDialogContentStyle, + this.headerTextStyle, + this.questionInputStyle, + this.optionInputStyle, + this.configOptionStyle, + this.primaryActionStyle, + this.secondaryActionStyle, }); - /// The background color of the poll creator. - final Color? backgroundColor; - - /// The text style of the appBar title. - final TextStyle? appBarTitleStyle; - - /// The elevation of the appBar. - final double? appBarElevation; - - /// The background color of the appBar. - final Color? appBarBackgroundColor; - - /// The foreground color of the appBar (icon and text color). - final Color? appBarForegroundColor; - - /// The fill color of the question text field. - final Color? questionTextFieldFillColor; - - /// The style of the question header text. - final TextStyle? questionHeaderStyle; - - /// The text style of the question text field. - final TextStyle? questionTextFieldStyle; - - /// The text style of the question error text when the question is invalid. - final TextStyle? questionTextFieldErrorStyle; - - /// The border radius of the question text field. - final BorderRadius? questionTextFieldBorderRadius; + /// The text style for section header labels (e.g. "Questions", "Options"). + /// + /// If null, defaults to [StreamTextTheme.headingSm] with primary color. + final TextStyle? headerTextStyle; - /// The fill color of the options text field. - final Color? optionsTextFieldFillColor; + /// The visual styling for the question text input. + /// + /// If null, the text input uses its own inherited theme defaults. + final StreamTextInputStyle? questionInputStyle; - /// The style of the options header text. - final TextStyle? optionsHeaderStyle; + /// The visual styling for the option text inputs. + /// + /// If null, the text input uses its own inherited theme defaults. + final StreamTextInputStyle? optionInputStyle; - /// The text style of the options text field. - final TextStyle? optionsTextFieldStyle; + /// The visual styling for option toggle cards (e.g. "Multiple answers", + /// "Anonymous poll"). + final StreamPollConfigOptionStyle? configOptionStyle; - /// The text style of the options error text when the options are invalid. - final TextStyle? optionsTextFieldErrorStyle; + /// The visual styling for the primary action button (e.g. create/confirm). + final StreamButtonThemeStyle? primaryActionStyle; - /// The border radius of the options text field. - final BorderRadius? optionsTextFieldBorderRadius; + /// The visual styling for secondary action buttons (e.g. close/cancel). + final StreamButtonThemeStyle? secondaryActionStyle; - /// The fill color of the switch list tile. - final Color? switchListTileFillColor; + /// Linearly interpolate between two [StreamPollCreatorThemeData] objects. + static StreamPollCreatorThemeData? lerp( + StreamPollCreatorThemeData? a, + StreamPollCreatorThemeData? b, + double t, + ) => _$StreamPollCreatorThemeData.lerp(a, b, t); +} - /// The text style of the switch list tile title. - final TextStyle? switchListTileTitleStyle; +/// Visual styling properties for poll creator option toggle cards. +/// +/// Defines the appearance of the toggle-switch cards used for poll +/// configuration options like "Multiple answers" and "Anonymous poll", +/// including sub-component styles for the toggle switch and stepper. +/// +/// See also: +/// +/// * [StreamPollCreatorThemeData], which wraps this style for theming. +/// * [StreamPollCreatorWidget], which uses this styling. +@themeGen +@immutable +class StreamPollConfigOptionStyle with _$StreamPollConfigOptionStyle { + /// Creates poll creator option card style properties. + const StreamPollConfigOptionStyle({ + this.backgroundColor, + this.contentPadding, + this.childSpacing, + this.titleTextStyle, + this.descriptionTextStyle, + this.switchStyle, + this.stepperStyle, + }); - /// The text style of the switch list tile error text when the switch list - /// tile is invalid. - final TextStyle? switchListTileErrorStyle; + /// The background color of the option card. + /// + /// If null, defaults to [StreamColorScheme.backgroundSurfaceCard]. + final Color? backgroundColor; - /// The border radius of the switch list tile. - final BorderRadius? switchListTileBorderRadius; + /// The padding inside the card around the content. + /// + /// If null, defaults to `EdgeInsets.all(spacing.md)`. + final EdgeInsetsGeometry? contentPadding; - /// The text style of the action dialog title. - final TextStyle? actionDialogTitleStyle; + /// The vertical spacing between the header and the child widget. + /// + /// If null, defaults to `spacing.md`. + final double? childSpacing; - /// The text style of the action dialog content. - final TextStyle? actionDialogContentStyle; + /// The text style for the option card title. + /// + /// If null, defaults to [StreamTextTheme.headingSm] with primary color. + final TextStyle? titleTextStyle; - /// Copies this [StreamPollCreatorThemeData] with some new values. - StreamPollCreatorThemeData copyWith({ - Color? backgroundColor, - TextStyle? appBarTitleStyle, - double? appBarElevation, - Color? appBarBackgroundColor, - Color? appBarForegroundColor, - Color? questionTextFieldFillColor, - TextStyle? questionHeaderStyle, - TextStyle? questionTextFieldStyle, - TextStyle? questionTextFieldErrorStyle, - BorderRadius? questionTextFieldBorderRadius, - Color? optionsTextFieldFillColor, - TextStyle? optionsHeaderStyle, - TextStyle? optionsTextFieldStyle, - TextStyle? optionsTextFieldErrorStyle, - BorderRadius? optionsTextFieldBorderRadius, - Color? switchListTileFillColor, - TextStyle? switchListTileTitleStyle, - TextStyle? switchListTileErrorStyle, - BorderRadius? switchListTileBorderRadius, - TextStyle? actionDialogTitleStyle, - TextStyle? actionDialogContentStyle, - }) { - return StreamPollCreatorThemeData( - backgroundColor: backgroundColor ?? this.backgroundColor, - appBarTitleStyle: appBarTitleStyle ?? this.appBarTitleStyle, - appBarElevation: appBarElevation ?? this.appBarElevation, - appBarBackgroundColor: appBarBackgroundColor ?? this.appBarBackgroundColor, - appBarForegroundColor: appBarForegroundColor ?? this.appBarForegroundColor, - questionTextFieldFillColor: questionTextFieldFillColor ?? this.questionTextFieldFillColor, - questionHeaderStyle: questionHeaderStyle ?? this.questionHeaderStyle, - questionTextFieldStyle: questionTextFieldStyle ?? this.questionTextFieldStyle, - questionTextFieldErrorStyle: questionTextFieldErrorStyle ?? this.questionTextFieldErrorStyle, - questionTextFieldBorderRadius: questionTextFieldBorderRadius ?? this.questionTextFieldBorderRadius, - optionsTextFieldFillColor: optionsTextFieldFillColor ?? this.optionsTextFieldFillColor, - optionsHeaderStyle: optionsHeaderStyle ?? this.optionsHeaderStyle, - optionsTextFieldStyle: optionsTextFieldStyle ?? this.optionsTextFieldStyle, - optionsTextFieldErrorStyle: optionsTextFieldErrorStyle ?? this.optionsTextFieldErrorStyle, - optionsTextFieldBorderRadius: optionsTextFieldBorderRadius ?? this.optionsTextFieldBorderRadius, - switchListTileFillColor: switchListTileFillColor ?? this.switchListTileFillColor, - switchListTileTitleStyle: switchListTileTitleStyle ?? this.switchListTileTitleStyle, - switchListTileErrorStyle: switchListTileErrorStyle ?? this.switchListTileErrorStyle, - switchListTileBorderRadius: switchListTileBorderRadius ?? this.switchListTileBorderRadius, - actionDialogTitleStyle: actionDialogTitleStyle ?? this.actionDialogTitleStyle, - actionDialogContentStyle: actionDialogContentStyle ?? this.actionDialogContentStyle, - ); - } + /// The text style for the option card description. + /// + /// If null, defaults to [StreamTextTheme.captionDefault] with tertiary + /// color. + final TextStyle? descriptionTextStyle; - /// Merges [this] [StreamPollCreatorThemeData] with the [other] - StreamPollCreatorThemeData merge(StreamPollCreatorThemeData? other) { - if (other == null) return this; - return copyWith( - backgroundColor: other.backgroundColor ?? backgroundColor, - appBarTitleStyle: other.appBarTitleStyle ?? appBarTitleStyle, - appBarElevation: other.appBarElevation ?? appBarElevation, - appBarBackgroundColor: other.appBarBackgroundColor ?? appBarBackgroundColor, - appBarForegroundColor: other.appBarForegroundColor ?? appBarForegroundColor, - questionTextFieldFillColor: other.questionTextFieldFillColor ?? questionTextFieldFillColor, - questionHeaderStyle: other.questionHeaderStyle ?? questionHeaderStyle, - questionTextFieldStyle: other.questionTextFieldStyle ?? questionTextFieldStyle, - questionTextFieldErrorStyle: other.questionTextFieldErrorStyle ?? questionTextFieldErrorStyle, - questionTextFieldBorderRadius: other.questionTextFieldBorderRadius ?? questionTextFieldBorderRadius, - optionsTextFieldFillColor: other.optionsTextFieldFillColor ?? optionsTextFieldFillColor, - optionsHeaderStyle: other.optionsHeaderStyle ?? optionsHeaderStyle, - optionsTextFieldStyle: other.optionsTextFieldStyle ?? optionsTextFieldStyle, - optionsTextFieldErrorStyle: other.optionsTextFieldErrorStyle ?? optionsTextFieldErrorStyle, - optionsTextFieldBorderRadius: other.optionsTextFieldBorderRadius ?? optionsTextFieldBorderRadius, - switchListTileFillColor: other.switchListTileFillColor ?? switchListTileFillColor, - switchListTileTitleStyle: other.switchListTileTitleStyle ?? switchListTileTitleStyle, - switchListTileErrorStyle: other.switchListTileErrorStyle ?? switchListTileErrorStyle, - switchListTileBorderRadius: other.switchListTileBorderRadius ?? switchListTileBorderRadius, - actionDialogTitleStyle: other.actionDialogTitleStyle ?? actionDialogTitleStyle, - actionDialogContentStyle: other.actionDialogContentStyle ?? actionDialogContentStyle, - ); - } + /// The visual styling for the toggle switch in the card. + /// + /// If null, the toggle switch uses its own inherited theme defaults. + final StreamSwitchStyle? switchStyle; - /// Linearly interpolate between two [StreamPollCreatorThemeData]. - StreamPollCreatorThemeData lerp( - StreamPollCreatorThemeData a, - StreamPollCreatorThemeData b, + /// The visual styling for the stepper control in the card. + /// + /// If null, the stepper uses its own inherited theme defaults. + final StreamStepperStyle? stepperStyle; + + /// Linearly interpolate between two [StreamPollConfigOptionStyle] + /// objects. + static StreamPollConfigOptionStyle? lerp( + StreamPollConfigOptionStyle? a, + StreamPollConfigOptionStyle? b, double t, - ) { - return StreamPollCreatorThemeData( - backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - appBarTitleStyle: TextStyle.lerp(a.appBarTitleStyle, b.appBarTitleStyle, t), - appBarElevation: lerpDouble(a.appBarElevation, b.appBarElevation, t), - appBarBackgroundColor: Color.lerp(a.appBarBackgroundColor, b.appBarBackgroundColor, t), - appBarForegroundColor: Color.lerp(a.appBarForegroundColor, b.appBarForegroundColor, t), - questionTextFieldFillColor: Color.lerp(a.questionTextFieldFillColor, b.questionTextFieldFillColor, t), - questionHeaderStyle: TextStyle.lerp(a.questionHeaderStyle, b.questionHeaderStyle, t), - questionTextFieldStyle: TextStyle.lerp(a.questionTextFieldStyle, b.questionTextFieldStyle, t), - questionTextFieldErrorStyle: TextStyle.lerp(a.questionTextFieldErrorStyle, b.questionTextFieldErrorStyle, t), - questionTextFieldBorderRadius: BorderRadius.lerp( - a.questionTextFieldBorderRadius, - b.questionTextFieldBorderRadius, - t, - ), - optionsTextFieldFillColor: Color.lerp(a.optionsTextFieldFillColor, b.optionsTextFieldFillColor, t), - optionsHeaderStyle: TextStyle.lerp(a.optionsHeaderStyle, b.optionsHeaderStyle, t), - optionsTextFieldStyle: TextStyle.lerp(a.optionsTextFieldStyle, b.optionsTextFieldStyle, t), - optionsTextFieldErrorStyle: TextStyle.lerp(a.optionsTextFieldErrorStyle, b.optionsTextFieldErrorStyle, t), - optionsTextFieldBorderRadius: BorderRadius.lerp( - a.optionsTextFieldBorderRadius, - b.optionsTextFieldBorderRadius, - t, - ), - switchListTileFillColor: Color.lerp(a.switchListTileFillColor, b.switchListTileFillColor, t), - switchListTileTitleStyle: TextStyle.lerp(a.switchListTileTitleStyle, b.switchListTileTitleStyle, t), - switchListTileErrorStyle: TextStyle.lerp(a.switchListTileErrorStyle, b.switchListTileErrorStyle, t), - switchListTileBorderRadius: BorderRadius.lerp(a.switchListTileBorderRadius, b.switchListTileBorderRadius, t), - actionDialogTitleStyle: TextStyle.lerp(a.actionDialogTitleStyle, b.actionDialogTitleStyle, t), - actionDialogContentStyle: TextStyle.lerp(a.actionDialogContentStyle, b.actionDialogContentStyle, t), - ); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is StreamPollCreatorThemeData && - other.backgroundColor == backgroundColor && - other.appBarTitleStyle == appBarTitleStyle && - other.appBarElevation == appBarElevation && - other.appBarBackgroundColor == appBarBackgroundColor && - other.appBarForegroundColor == appBarForegroundColor && - other.questionTextFieldFillColor == questionTextFieldFillColor && - other.questionHeaderStyle == questionHeaderStyle && - other.questionTextFieldStyle == questionTextFieldStyle && - other.questionTextFieldErrorStyle == questionTextFieldErrorStyle && - other.questionTextFieldBorderRadius == questionTextFieldBorderRadius && - other.optionsTextFieldFillColor == optionsTextFieldFillColor && - other.optionsHeaderStyle == optionsHeaderStyle && - other.optionsTextFieldStyle == optionsTextFieldStyle && - other.optionsTextFieldErrorStyle == optionsTextFieldErrorStyle && - other.optionsTextFieldBorderRadius == optionsTextFieldBorderRadius && - other.switchListTileFillColor == switchListTileFillColor && - other.switchListTileTitleStyle == switchListTileTitleStyle && - other.switchListTileErrorStyle == switchListTileErrorStyle && - other.switchListTileBorderRadius == switchListTileBorderRadius && - other.actionDialogTitleStyle == actionDialogTitleStyle && - other.actionDialogContentStyle == actionDialogContentStyle; - - @override - int get hashCode => - backgroundColor.hashCode ^ - appBarTitleStyle.hashCode ^ - appBarElevation.hashCode ^ - appBarBackgroundColor.hashCode ^ - appBarForegroundColor.hashCode ^ - questionTextFieldFillColor.hashCode ^ - questionHeaderStyle.hashCode ^ - questionTextFieldStyle.hashCode ^ - questionTextFieldErrorStyle.hashCode ^ - questionTextFieldBorderRadius.hashCode ^ - optionsTextFieldFillColor.hashCode ^ - optionsHeaderStyle.hashCode ^ - optionsTextFieldStyle.hashCode ^ - optionsTextFieldErrorStyle.hashCode ^ - optionsTextFieldBorderRadius.hashCode ^ - switchListTileFillColor.hashCode ^ - switchListTileTitleStyle.hashCode ^ - switchListTileErrorStyle.hashCode ^ - switchListTileBorderRadius.hashCode ^ - actionDialogTitleStyle.hashCode ^ - actionDialogContentStyle.hashCode; + ) => _$StreamPollConfigOptionStyle.lerp(a, b, t); } diff --git a/packages/stream_chat_flutter/lib/src/theme/poll_creator_theme.g.theme.dart b/packages/stream_chat_flutter/lib/src/theme/poll_creator_theme.g.theme.dart new file mode 100644 index 0000000000..c704e4731b --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/poll_creator_theme.g.theme.dart @@ -0,0 +1,278 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_element + +part of 'poll_creator_theme.dart'; + +// ************************************************************************** +// ThemeGenGenerator +// ************************************************************************** + +mixin _$StreamPollCreatorThemeData { + bool get canMerge => true; + + static StreamPollCreatorThemeData? lerp( + StreamPollCreatorThemeData? a, + StreamPollCreatorThemeData? b, + double t, + ) { + if (identical(a, b)) { + return a; + } + + if (a == null) { + return t == 1.0 ? b : null; + } + + if (b == null) { + return t == 0.0 ? a : null; + } + + return StreamPollCreatorThemeData( + headerTextStyle: TextStyle.lerp(a.headerTextStyle, b.headerTextStyle, t), + questionInputStyle: StreamTextInputStyle.lerp( + a.questionInputStyle, + b.questionInputStyle, + t, + ), + optionInputStyle: StreamTextInputStyle.lerp( + a.optionInputStyle, + b.optionInputStyle, + t, + ), + configOptionStyle: StreamPollConfigOptionStyle.lerp( + a.configOptionStyle, + b.configOptionStyle, + t, + ), + primaryActionStyle: StreamButtonThemeStyle.lerp( + a.primaryActionStyle, + b.primaryActionStyle, + t, + ), + secondaryActionStyle: StreamButtonThemeStyle.lerp( + a.secondaryActionStyle, + b.secondaryActionStyle, + t, + ), + ); + } + + StreamPollCreatorThemeData copyWith({ + TextStyle? headerTextStyle, + StreamTextInputStyle? questionInputStyle, + StreamTextInputStyle? optionInputStyle, + StreamPollConfigOptionStyle? configOptionStyle, + StreamButtonThemeStyle? primaryActionStyle, + StreamButtonThemeStyle? secondaryActionStyle, + }) { + final _this = (this as StreamPollCreatorThemeData); + + return StreamPollCreatorThemeData( + headerTextStyle: headerTextStyle ?? _this.headerTextStyle, + questionInputStyle: questionInputStyle ?? _this.questionInputStyle, + optionInputStyle: optionInputStyle ?? _this.optionInputStyle, + configOptionStyle: configOptionStyle ?? _this.configOptionStyle, + primaryActionStyle: primaryActionStyle ?? _this.primaryActionStyle, + secondaryActionStyle: secondaryActionStyle ?? _this.secondaryActionStyle, + ); + } + + StreamPollCreatorThemeData merge(StreamPollCreatorThemeData? other) { + final _this = (this as StreamPollCreatorThemeData); + + if (other == null || identical(_this, other)) { + return _this; + } + + if (!other.canMerge) { + return other; + } + + return copyWith( + headerTextStyle: + _this.headerTextStyle?.merge(other.headerTextStyle) ?? + other.headerTextStyle, + questionInputStyle: + _this.questionInputStyle?.merge(other.questionInputStyle) ?? + other.questionInputStyle, + optionInputStyle: + _this.optionInputStyle?.merge(other.optionInputStyle) ?? + other.optionInputStyle, + configOptionStyle: + _this.configOptionStyle?.merge(other.configOptionStyle) ?? + other.configOptionStyle, + primaryActionStyle: + _this.primaryActionStyle?.merge(other.primaryActionStyle) ?? + other.primaryActionStyle, + secondaryActionStyle: + _this.secondaryActionStyle?.merge(other.secondaryActionStyle) ?? + other.secondaryActionStyle, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + if (other.runtimeType != runtimeType) { + return false; + } + + final _this = (this as StreamPollCreatorThemeData); + final _other = (other as StreamPollCreatorThemeData); + + return _other.headerTextStyle == _this.headerTextStyle && + _other.questionInputStyle == _this.questionInputStyle && + _other.optionInputStyle == _this.optionInputStyle && + _other.configOptionStyle == _this.configOptionStyle && + _other.primaryActionStyle == _this.primaryActionStyle && + _other.secondaryActionStyle == _this.secondaryActionStyle; + } + + @override + int get hashCode { + final _this = (this as StreamPollCreatorThemeData); + + return Object.hash( + runtimeType, + _this.headerTextStyle, + _this.questionInputStyle, + _this.optionInputStyle, + _this.configOptionStyle, + _this.primaryActionStyle, + _this.secondaryActionStyle, + ); + } +} + +mixin _$StreamPollConfigOptionStyle { + bool get canMerge => true; + + static StreamPollConfigOptionStyle? lerp( + StreamPollConfigOptionStyle? a, + StreamPollConfigOptionStyle? b, + double t, + ) { + if (identical(a, b)) { + return a; + } + + if (a == null) { + return t == 1.0 ? b : null; + } + + if (b == null) { + return t == 0.0 ? a : null; + } + + return StreamPollConfigOptionStyle( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + contentPadding: EdgeInsetsGeometry.lerp( + a.contentPadding, + b.contentPadding, + t, + ), + childSpacing: lerpDouble$(a.childSpacing, b.childSpacing, t), + titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), + descriptionTextStyle: TextStyle.lerp( + a.descriptionTextStyle, + b.descriptionTextStyle, + t, + ), + switchStyle: StreamSwitchStyle.lerp(a.switchStyle, b.switchStyle, t), + stepperStyle: StreamStepperStyle.lerp(a.stepperStyle, b.stepperStyle, t), + ); + } + + StreamPollConfigOptionStyle copyWith({ + Color? backgroundColor, + EdgeInsetsGeometry? contentPadding, + double? childSpacing, + TextStyle? titleTextStyle, + TextStyle? descriptionTextStyle, + StreamSwitchStyle? switchStyle, + StreamStepperStyle? stepperStyle, + }) { + final _this = (this as StreamPollConfigOptionStyle); + + return StreamPollConfigOptionStyle( + backgroundColor: backgroundColor ?? _this.backgroundColor, + contentPadding: contentPadding ?? _this.contentPadding, + childSpacing: childSpacing ?? _this.childSpacing, + titleTextStyle: titleTextStyle ?? _this.titleTextStyle, + descriptionTextStyle: descriptionTextStyle ?? _this.descriptionTextStyle, + switchStyle: switchStyle ?? _this.switchStyle, + stepperStyle: stepperStyle ?? _this.stepperStyle, + ); + } + + StreamPollConfigOptionStyle merge(StreamPollConfigOptionStyle? other) { + final _this = (this as StreamPollConfigOptionStyle); + + if (other == null || identical(_this, other)) { + return _this; + } + + if (!other.canMerge) { + return other; + } + + return copyWith( + backgroundColor: other.backgroundColor, + contentPadding: other.contentPadding, + childSpacing: other.childSpacing, + titleTextStyle: + _this.titleTextStyle?.merge(other.titleTextStyle) ?? + other.titleTextStyle, + descriptionTextStyle: + _this.descriptionTextStyle?.merge(other.descriptionTextStyle) ?? + other.descriptionTextStyle, + switchStyle: + _this.switchStyle?.merge(other.switchStyle) ?? other.switchStyle, + stepperStyle: + _this.stepperStyle?.merge(other.stepperStyle) ?? other.stepperStyle, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + if (other.runtimeType != runtimeType) { + return false; + } + + final _this = (this as StreamPollConfigOptionStyle); + final _other = (other as StreamPollConfigOptionStyle); + + return _other.backgroundColor == _this.backgroundColor && + _other.contentPadding == _this.contentPadding && + _other.childSpacing == _this.childSpacing && + _other.titleTextStyle == _this.titleTextStyle && + _other.descriptionTextStyle == _this.descriptionTextStyle && + _other.switchStyle == _this.switchStyle && + _other.stepperStyle == _this.stepperStyle; + } + + @override + int get hashCode { + final _this = (this as StreamPollConfigOptionStyle); + + return Object.hash( + runtimeType, + _this.backgroundColor, + _this.contentPadding, + _this.childSpacing, + _this.titleTextStyle, + _this.descriptionTextStyle, + _this.switchStyle, + _this.stepperStyle, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart index a769458286..45dea27c00 100644 --- a/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart @@ -302,51 +302,7 @@ class StreamChatThemeData { messageListViewTheme: StreamMessageListViewThemeData( backgroundColor: colorTheme.appBg, ), - pollCreatorTheme: StreamPollCreatorThemeData( - backgroundColor: colorTheme.appBg, - appBarBackgroundColor: colorTheme.barsBg, - appBarForegroundColor: colorTheme.textHighEmphasis, - appBarElevation: 1, - appBarTitleStyle: textTheme.headlineBold.copyWith( - color: colorTheme.textHighEmphasis, - ), - questionTextFieldFillColor: colorTheme.inputBg, - questionHeaderStyle: textTheme.headline.copyWith( - color: colorTheme.textHighEmphasis, - ), - questionTextFieldStyle: textTheme.headline.copyWith( - color: colorTheme.textHighEmphasis, - ), - questionTextFieldErrorStyle: textTheme.footnote.copyWith( - color: colorTheme.accentError, - ), - questionTextFieldBorderRadius: BorderRadius.circular(12), - optionsTextFieldFillColor: colorTheme.inputBg, - optionsHeaderStyle: textTheme.headline.copyWith( - color: colorTheme.textHighEmphasis, - ), - optionsTextFieldStyle: textTheme.headline.copyWith( - color: colorTheme.textHighEmphasis, - ), - optionsTextFieldErrorStyle: textTheme.footnote.copyWith( - color: colorTheme.accentError, - ), - optionsTextFieldBorderRadius: BorderRadius.circular(12), - switchListTileFillColor: colorTheme.inputBg, - switchListTileTitleStyle: textTheme.headline.copyWith( - color: colorTheme.textHighEmphasis, - ), - switchListTileErrorStyle: textTheme.footnote.copyWith( - color: colorTheme.accentError, - ), - switchListTileBorderRadius: BorderRadius.circular(12), - actionDialogTitleStyle: textTheme.headlineBold.copyWith( - color: colorTheme.textHighEmphasis, - ), - actionDialogContentStyle: textTheme.body.copyWith( - color: colorTheme.textHighEmphasis, - ), - ), + pollCreatorTheme: const StreamPollCreatorThemeData(), pollInteractorTheme: const StreamPollInteractorThemeData(), pollResultsDialogTheme: StreamPollResultsDialogThemeData( backgroundColor: colorTheme.appBg, diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 8eacfead00..dd39f18b4c 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -53,6 +53,17 @@ export 'package:stream_core_flutter/stream_core_flutter.dart' StreamListTile, StreamListTileContainer, StreamSwitch, + StreamStepper, + StreamStepperProps, + StreamStepperStyle, + StreamStepperTheme, + StreamStepperThemeData, + StreamTextInputStyle, + StreamTextInputTheme, + StreamTextInputThemeData, + StreamSwitchStyle, + StreamSwitchTheme, + StreamSwitchThemeData, StreamUnicodeEmoji, streamSupportedEmojis; @@ -158,7 +169,6 @@ export 'src/poll/stream_poll_comments_dialog.dart'; export 'src/poll/stream_poll_option_votes_dialog.dart'; export 'src/poll/stream_poll_options_dialog.dart'; export 'src/poll/stream_poll_results_dialog.dart'; -export 'src/poll/stream_poll_text_field.dart'; export 'src/reactions/detail/reaction_detail_sheet.dart'; export 'src/reactions/picker/reaction_picker.dart'; export 'src/reactions/user_reactions.dart'; diff --git a/packages/stream_chat_flutter/test/src/bottom_sheets/goldens/ci/edit_message_sheet_0.png b/packages/stream_chat_flutter/test/src/bottom_sheets/goldens/ci/edit_message_sheet_0.png index 2cdd7ae4f0..46819594a7 100644 Binary files a/packages/stream_chat_flutter/test/src/bottom_sheets/goldens/ci/edit_message_sheet_0.png and b/packages/stream_chat_flutter/test/src/bottom_sheets/goldens/ci/edit_message_sheet_0.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_dark.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_dark.png index ba72ea1654..575be478cb 100644 Binary files a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_dark.png and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_light.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_light.png index cba5644a08..aab51b08c1 100644 Binary files a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_light.png and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_light.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_dark.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_dark.png index e2772f5c06..cfdd200ee1 100644 Binary files a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_dark.png and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_light.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_light.png index 37f33dc53c..fac3a83cae 100644 Binary files a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_light.png and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_light.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_with_reactions_dark.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_with_reactions_dark.png index 3493dabe51..38524bd115 100644 Binary files a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_with_reactions_dark.png and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_with_reactions_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_with_reactions_light.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_with_reactions_light.png index b760b7d0b9..e9ff74d09c 100644 Binary files a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_with_reactions_light.png and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_with_reactions_light.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_with_reactions_dark.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_with_reactions_dark.png index 855bc03e2b..eb88364db3 100644 Binary files a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_with_reactions_dark.png and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_with_reactions_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_with_reactions_light.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_with_reactions_light.png index 16414bb424..660f447e36 100644 Binary files a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_with_reactions_light.png and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_with_reactions_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_delete_option_dialog_dark.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_delete_option_dialog_dark.png index 3147e1e01d..152400c7f5 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_delete_option_dialog_dark.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_delete_option_dialog_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_delete_option_dialog_light.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_delete_option_dialog_light.png index 72d412692d..a62dffbb23 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_delete_option_dialog_light.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_delete_option_dialog_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_dark.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_dark.png index 7d50ee1a0b..35931c645a 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_dark.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_error_dark.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_error_dark.png index 21e5bd213f..b7ca2748e6 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_error_dark.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_error_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_error_light.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_error_light.png index 629bb9ce23..4c03a484e6 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_error_light.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_error_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_light.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_light.png index 24c03f908d..c67e372b05 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_light.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_dark.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_dark.png index fdffc5c05a..cda2e5fd94 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_dark.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_error_dark.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_error_dark.png index 325781cf82..450d9b2637 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_error_dark.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_error_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_error_light.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_error_light.png index 64ac751f02..19a4fe4dca 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_error_light.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_error_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_light.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_light.png index ae1e3618ed..ca6abbbc00 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_light.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_dialog_dark.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_dialog_dark.png index 52ca2ca4c9..31fec3d701 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_dialog_dark.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_dialog_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_dialog_light.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_dialog_light.png index 180d152692..165d5e06ce 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_dialog_light.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_dialog_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_full_screen_dialog_dark.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_full_screen_dialog_dark.png index 73462eb438..050b1fa196 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_full_screen_dialog_dark.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_full_screen_dialog_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_full_screen_dialog_light.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_full_screen_dialog_light.png index 97ca70c7e8..050b1fa196 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_full_screen_dialog_light.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_full_screen_dialog_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/poll_option_reorderable_list_view_test.dart b/packages/stream_chat_flutter/test/src/poll/creator/poll_option_reorderable_list_view_test.dart index 383ba526e1..718cbec662 100644 --- a/packages/stream_chat_flutter/test/src/poll/creator/poll_option_reorderable_list_view_test.dart +++ b/packages/stream_chat_flutter/test/src/poll/creator/poll_option_reorderable_list_view_test.dart @@ -88,12 +88,12 @@ void main() { ); // Find the add button - final addButton = find.byType(FilledButton); + final addButton = find.addOptionButton(); expect(addButton, findsOneWidget); // The button should be disabled since we're at max options - final button = tester.widget(addButton); - expect(button.onPressed, isNull); + final button = tester.widget(addButton); + expect(button.props.onTap, isNull); }); testWidgets('should respect both min and max options', (tester) async { @@ -114,8 +114,7 @@ void main() { expect(textFields, findsNWidgets(2)); // Add two more options to reach maximum - final addButton = find.byType(FilledButton); - await tester.tap(addButton); + await tester.tap(find.addOptionButton()); await tester.pumpAndSettle(); // Fill the newly added option so we can add another @@ -124,15 +123,16 @@ void main() { await tester.pumpAndSettle(); // Add one more option - await tester.tap(addButton); + await tester.tap(find.addOptionButton()); await tester.pumpAndSettle(); // Should now have 4 options (max reached) expect(find.byType(TextField), findsNWidgets(4)); // Add button should now be disabled since we reached max - final button = tester.widget(addButton); - expect(button.onPressed, isNull); + final addButton = find.addOptionButton(); + final button = tester.widget(addButton); + expect(button.props.onTap, isNull); }); testWidgets( @@ -151,9 +151,9 @@ void main() { ); // Add button should be enabled for unlimited options - final addButton = find.byType(FilledButton); - final button = tester.widget(addButton); - expect(button.onPressed, isNotNull); + final addButton = find.addOptionButton(); + final button = tester.widget(addButton); + expect(button.props.onTap, isNotNull); }, ); }); @@ -172,8 +172,7 @@ void main() { ); // Find the add button and tap it - final addButton = find.byType(FilledButton); - await tester.tap(addButton); + await tester.tap(find.addOptionButton()); await tester.pumpAndSettle(); // Verify that there are now 2 text fields @@ -205,12 +204,12 @@ void main() { ); // Find the add button - final addButton = find.byType(FilledButton); + final addButton = find.addOptionButton(); expect(addButton, findsOneWidget); // The button should be disabled since there's already an empty option - final button = tester.widget(addButton); - expect(button.onPressed, isNull); + final button = tester.widget(addButton); + expect(button.props.onTap, isNull); }, ); @@ -230,12 +229,12 @@ void main() { ); // Find the add button - final addButton = find.byType(FilledButton); + final addButton = find.addOptionButton(); expect(addButton, findsOneWidget); // The button should be enabled since no empty options exist - final button = tester.widget(addButton); - expect(button.onPressed, isNotNull); + final button = tester.widget(addButton); + expect(button.props.onTap, isNotNull); }, ); @@ -255,9 +254,9 @@ void main() { ); // Initially, add button should be disabled - var addButton = find.byType(FilledButton); - var button = tester.widget(addButton); - expect(button.onPressed, isNull); + var addButton = find.addOptionButton(); + var button = tester.widget(addButton); + expect(button.props.onTap, isNull); // Fill the empty option final textFields = find.byType(TextField); @@ -265,9 +264,9 @@ void main() { await tester.pumpAndSettle(); // Now add button should be enabled - addButton = find.byType(FilledButton); - button = tester.widget(addButton); - expect(button.onPressed, isNotNull); + addButton = find.addOptionButton(); + button = tester.widget(addButton); + expect(button.props.onTap, isNotNull); }, ); }); @@ -292,8 +291,7 @@ void main() { ); // Find the add button and tap it - final addButton = find.byType(FilledButton); - await tester.tap(addButton); + await tester.tap(find.addOptionButton()); await tester.pumpAndSettle(); // Verify a new option was added @@ -373,7 +371,7 @@ void main() { ); // Find the delete buttons - final deleteButtons = find.byIcon(StreamIconData.delete); + final deleteButtons = find.byIcon(StreamIconData.minusCircle); expect(deleteButtons, findsNWidgets(3)); // Tap the first delete button @@ -411,7 +409,7 @@ void main() { expect(find.byType(TextField), findsNWidgets(3)); // Find and tap the delete button for the first option - final deleteButtons = find.byIcon(StreamIconData.delete); + final deleteButtons = find.byIcon(StreamIconData.minusCircle); await tester.tap(deleteButtons.first); await tester.pumpAndSettle(); @@ -445,7 +443,7 @@ void main() { expect(find.byType(TextField), findsNWidgets(3)); // Find and tap the delete button for the first option - final deleteButtons = find.byIcon(StreamIconData.delete); + final deleteButtons = find.byIcon(StreamIconData.minusCircle); await tester.tap(deleteButtons.first); await tester.pumpAndSettle(); @@ -479,7 +477,7 @@ void main() { expect(find.byType(TextField), findsNWidgets(2)); // Try to delete the first option - final deleteButtons = find.byIcon(StreamIconData.delete); + final deleteButtons = find.byIcon(StreamIconData.minusCircle); await tester.tap(deleteButtons.first); await tester.pumpAndSettle(); @@ -507,6 +505,11 @@ void main() { }); } +extension on CommonFinders { + /// Finds the "Add an option" [StreamButton] at the bottom of the list. + Finder addOptionButton() => find.widgetWithText(StreamButton, 'Add an option'); +} + Widget _wrapWithMaterialApp( Widget widget, { Brightness? brightness, diff --git a/packages/stream_chat_flutter/test/src/poll/creator/stream_poll_creator_widget_test.dart b/packages/stream_chat_flutter/test/src/poll/creator/stream_poll_creator_widget_test.dart index 78eb1b99ae..b1c74d2f89 100644 --- a/packages/stream_chat_flutter/test/src/poll/creator/stream_poll_creator_widget_test.dart +++ b/packages/stream_chat_flutter/test/src/poll/creator/stream_poll_creator_widget_test.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/src/poll/creator/poll_config_option.dart'; import 'package:stream_chat_flutter/src/poll/creator/poll_option_reorderable_list_view.dart'; import 'package:stream_chat_flutter/src/poll/creator/poll_question_text_field.dart'; -import 'package:stream_chat_flutter/src/poll/creator/poll_switch_list_tile.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { @@ -25,7 +25,7 @@ void main() { // Verify that the widget is rendered correctly expect(find.byType(PollQuestionTextField), findsOneWidget); expect(find.byType(PollOptionReorderableListView), findsOneWidget); - expect(find.byType(PollSwitchListTile), findsNWidgets(4)); + expect(find.byType(PollConfigOption), findsNWidgets(4)); }); testWidgets('StreamPollCreatorWidget updates poll state correctly', (tester) async { @@ -52,62 +52,63 @@ void main() { await tester.pumpAndSettle(); expect(controller.value.name, 'What is your favorite color?'); - await tester.tap(find.switchListTileText('Multiple answers')); + await tester.tap(find.switchTileText('Multiple answers')); await tester.pumpAndSettle(); expect(controller.value.enforceUniqueVote, false); - await tester.tap( - find.descendant( - of: find.byType(PollSwitchTextField), - matching: find.byType(Switch), - ), - ); + // The nested "Maximum votes per person" tile is now visible. + await tester.tap(find.switchTileText('Maximum votes per person')); await tester.pumpAndSettle(); - expect(controller.value.maxVotesAllowed, null); + expect(controller.value.maxVotesAllowed, 1); - await tester.enterText( - find.descendant( - of: find.byType(PollSwitchTextField), - matching: find.byType(TextField), - ), - '3', - ); - await tester.pumpAndSettle(); - expect(controller.value.maxVotesAllowed, 3); - - await tester.tap(find.switchListTileText('Anonymous poll')); + await tester.tap(find.switchTileText('Anonymous poll')); await tester.pumpAndSettle(); expect(controller.value.votingVisibility, VotingVisibility.anonymous); - await tester.tap(find.switchListTileText('Suggest an option')); + await tester.dragUntilVisible( + find.switchTileText('Suggest an option'), + find.byType(SingleChildScrollView), + const Offset(0, -100), + ); + + await tester.tap(find.switchTileText('Suggest an option')); await tester.pumpAndSettle(); expect(controller.value.allowUserSuggestedOptions, true); await tester.dragUntilVisible( - find.switchListTileText('Add a comment'), + find.switchTileText('Add a comment'), find.byType(SingleChildScrollView), - const Offset(0, 500), + const Offset(0, -100), ); - await tester.tap(find.switchListTileText('Add a comment')); + await tester.tap(find.switchTileText('Add a comment')); await tester.pumpAndSettle(); expect(controller.value.allowAnswers, true); }); } extension on CommonFinders { - Finder switchListTileText(String title) { - return ancestor( + Finder switchTileText(String title) { + final card = find.ancestor( of: find.text(title), - matching: find.byType(SwitchListTile), + matching: find.byType(PollConfigOption), ); + return find + .descendant( + of: card.first, + matching: find.byType(StreamSwitch), + ) + .first; } } -Widget _wrapWithMaterialApp(Widget widget) { +Widget _wrapWithMaterialApp( + Widget widget, { + Brightness? brightness, +}) { return MaterialApp( home: StreamChatTheme( - data: StreamChatThemeData(), + data: StreamChatThemeData(brightness: brightness), child: Scaffold( body: widget, ), diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_dark.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_dark.png index 7d50ee1a0b..35931c645a 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_dark.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_error.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_error.png index 629bb9ce23..4c03a484e6 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_error.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_error.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_light.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_light.png index 24c03f908d..c67e372b05 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_light.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_dark.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_dark.png index fdffc5c05a..cda2e5fd94 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_dark.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_error.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_error.png index 64ac751f02..19a4fe4dca 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_error.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_error.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_light.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_light.png index ae1e3618ed..ca6abbbc00 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_light.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_dialog_dark.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_dialog_dark.png index 52ca2ca4c9..31fec3d701 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_dialog_dark.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_dialog_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_dialog_light.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_dialog_light.png index 180d152692..165d5e06ce 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_dialog_light.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_dialog_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_full_screen_dialog_dark.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_full_screen_dialog_dark.png index 73462eb438..050b1fa196 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_full_screen_dialog_dark.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_full_screen_dialog_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_full_screen_dialog_light.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_full_screen_dialog_light.png index 97ca70c7e8..050b1fa196 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_full_screen_dialog_light.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_full_screen_dialog_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_dark.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_dark.png index d39a882fe2..6e0042d904 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_dark.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_light.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_light.png index 87c0315c35..9b670cec50 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_light.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_with_initial_value_dark.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_with_initial_value_dark.png index 9d1c3dd68f..a7da99ffc8 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_with_initial_value_dark.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_with_initial_value_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_with_initial_value_light.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_with_initial_value_light.png index d236e1ed1a..cc8be672df 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_with_initial_value_light.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_with_initial_value_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_dark.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_dark.png index 066c6c333e..d3f266cc36 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_dark.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_light.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_light.png index 3166377ddd..f65aebb6c9 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_light.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_with_initial_option_dark.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_with_initial_option_dark.png index ef0f74b8b5..d07a595ee3 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_with_initial_option_dark.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_with_initial_option_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_with_initial_option_light.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_with_initial_option_light.png index dd2c06920b..1661f4f937 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_with_initial_option_light.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_with_initial_option_light.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_dark.png b/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_dark.png index cd992667dc..ec086e87f0 100644 Binary files a/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_dark.png and b/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_filtered_dark.png b/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_filtered_dark.png index bd98b5f377..f376b296fa 100644 Binary files a/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_filtered_dark.png and b/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_filtered_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_filtered_light.png b/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_filtered_light.png index aa307c61a2..a117cde882 100644 Binary files a/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_filtered_light.png and b/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_filtered_light.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_light.png b/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_light.png index 006010aa45..c264c3eec5 100644 Binary files a/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_light.png and b/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_light.png differ