From 065a0f1795672594bfb66b95e262e7cfcb6d3e8d Mon Sep 17 00:00:00 2001 From: mikecoomber <58986130+mikecoomber@users.noreply.github.com> Date: Tue, 30 Jul 2024 10:38:59 +0100 Subject: [PATCH] refactor: Form fields now correctly implement FormField (#139) --- .../pages/components/date_input_example.dart | 7 +- .../components/password_input_example.dart | 23 +- .../pages/components/phone_input_example.dart | 102 ++--- .../pages/components/search_bar_example.dart | 4 +- .../components/select_input_example.dart | 34 +- .../pages/components/text_input_example.dart | 123 +++--- .../pages/components/time_input_example.dart | 32 +- .../components/password_input_widgetbook.dart | 1 - .../components/search_bar_widgetbook.dart | 9 +- lib/src/components/date_input/date_input.dart | 238 +++++----- .../components/password/password_input.dart | 108 +++-- .../components/phone_input/phone_input.dart | 316 +++++++------- lib/src/components/search_bar/search_bar.dart | 408 ++++++++---------- .../components/select_input/select_input.dart | 211 +++++---- .../text_input/internal_text_input.dart | 380 ++++++++++++++++ lib/src/components/text_input/text_input.dart | 402 ++--------------- lib/src/components/time_input/time_input.dart | 273 ++++++------ .../components/top_app_bar/top_app_bar.dart | 2 +- lib/src/interfaces/form_field.dart | 142 ++++-- .../password/golden/password_default.png | Bin 3637 -> 3637 bytes .../password/golden/password_error.png | Bin 3850 -> 3850 bytes .../password/password_input_test.dart | 23 +- .../search_bar/golden/search_bar_default.png | Bin 3924 -> 3971 bytes .../search_bar/golden/search_bar_full.png | Bin 5022 -> 5058 bytes .../search_bar/golden/search_bar_medium.png | Bin 3959 -> 3971 bytes .../search_bar/golden/search_bar_sharp.png | Bin 3719 -> 3779 bytes .../search_bar/golden/search_bar_small.png | Bin 3920 -> 3903 bytes .../search_bar/search_bar_test.dart | 87 ++-- 28 files changed, 1535 insertions(+), 1390 deletions(-) create mode 100644 lib/src/components/text_input/internal_text_input.dart diff --git a/example/lib/pages/components/date_input_example.dart b/example/lib/pages/components/date_input_example.dart index a1e3218a..e7dd616e 100644 --- a/example/lib/pages/components/date_input_example.dart +++ b/example/lib/pages/components/date_input_example.dart @@ -12,8 +12,6 @@ class DateInputExample extends StatefulWidget { } class _DateInputExampleState extends State { - String? _errorText; - @override Widget build(BuildContext context) { return ExampleScaffold( @@ -29,8 +27,10 @@ class _DateInputExampleState extends State { padding: const EdgeInsets.all(20), child: ZetaDateInput( label: 'Birthdate', + onChange: (DateTime? value) { + print(value); + }, hintText: 'Enter birthdate', - errorText: _errorText ?? 'Invalid date', initialValue: DateTime.now(), size: ZetaWidgetSize.large, ), @@ -45,7 +45,6 @@ class _DateInputExampleState extends State { child: ZetaDateInput( label: 'Label', hintText: 'Default hint text', - errorText: 'Oops! Error hint text', size: ZetaWidgetSize.medium, ), ), diff --git a/example/lib/pages/components/password_input_example.dart b/example/lib/pages/components/password_input_example.dart index 5fccc848..4e9b5cf6 100644 --- a/example/lib/pages/components/password_input_example.dart +++ b/example/lib/pages/components/password_input_example.dart @@ -40,18 +40,25 @@ class _PasswordInputExampleState extends State { hintText: 'Password', controller: _passwordController, validator: (value) { - if (value != null) { - final regExp = RegExp(r'\d'); - if (regExp.hasMatch(value)) return 'Password is incorrect'; + print('validating'); + if (value == null || value.isEmpty) { + return 'Please enter a password'; } return null; }, ), - ZetaButton.primary( - label: 'Validate', - onPressed: () { - _formKey.currentState?.validate(); - }, + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ZetaButton( + label: 'Reset', + onPressed: () => _formKey.currentState?.reset(), + ), + ZetaButton( + label: 'Validate', + onPressed: () => _formKey.currentState?.validate(), + ), + ], ), SizedBox(height: ZetaSpacing.xl_6), ...passwordInputExampleRow(ZetaWidgetSize.large), diff --git a/example/lib/pages/components/phone_input_example.dart b/example/lib/pages/components/phone_input_example.dart index 469aff7b..81468aff 100644 --- a/example/lib/pages/components/phone_input_example.dart +++ b/example/lib/pages/components/phone_input_example.dart @@ -12,67 +12,67 @@ class PhoneInputExample extends StatefulWidget { } class _PhoneInputExampleState extends State { - String? _errorText; + final key = GlobalKey(); @override Widget build(BuildContext context) { return ExampleScaffold( name: 'Phone Input', child: SingleChildScrollView( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(20), - child: ZetaPhoneInput( - label: 'Phone number', - hintText: 'Enter your phone number', - hasError: _errorText != null, - errorText: _errorText, - onChange: (value) { - if (value?.isEmpty ?? true) setState(() => _errorText = null); - print(value); - }, - initialCountry: 'GB', - countries: ['US', 'GB', 'DE', 'AT', 'FR', 'IT', 'BG'], + child: Form( + key: key, + child: Column( + children: [ + ZetaButton(label: 'Reset', onPressed: () => key.currentState?.reset()), + Padding( + padding: const EdgeInsets.all(20), + child: ZetaPhoneInput( + label: 'Phone number', + hintText: 'Enter your phone number', + initialValue: const PhoneNumber(dialCode: '+44', number: '1234567890'), + onChange: (value) { + print(value?.dialCode); + print(value?.number); + }, + countries: ['US', 'GB', 'DE', 'AT', 'FR', 'IT', 'BG'], + ), ), - ), - Padding( - padding: const EdgeInsets.all(20), - child: ZetaPhoneInput( - label: 'Phone number', - hintText: 'Enter your phone number', - hasError: _errorText != null, - size: ZetaWidgetSize.large, - errorText: 'Error', - onChange: (value) { - if (value?.isEmpty ?? true) setState(() => _errorText = null); - print(value); - }, - countries: ['US', 'GB', 'DE', 'AT', 'FR', 'IT', 'BG'], - selectCountrySemanticLabel: 'Choose country code', + Padding( + padding: const EdgeInsets.all(20), + child: ZetaPhoneInput( + label: 'Phone number', + hintText: 'Enter your phone number', + size: ZetaWidgetSize.large, + errorText: 'Error', + onChange: (value) { + print(value); + }, + countries: ['US', 'GB', 'DE', 'AT', 'FR', 'IT', 'BG'], + selectCountrySemanticLabel: 'Choose country code', + ), ), - ), - Divider(color: Colors.grey[200]), - Padding( - padding: const EdgeInsets.only(top: 20), - child: Text('Disabled', style: ZetaTextStyles.titleMedium), - ), - Padding( - padding: const EdgeInsets.all(20), - child: ZetaPhoneInput( - label: 'Phone number', - hintText: 'Enter your phone number', - disabled: true, + Divider(color: Colors.grey[200]), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Text('Disabled', style: ZetaTextStyles.titleMedium), ), - ), - Padding( - padding: const EdgeInsets.all(20), - child: ZetaPhoneInput( - label: 'Phone number', - hintText: 'Enter your phone number', + Padding( + padding: const EdgeInsets.all(20), + child: ZetaPhoneInput( + label: 'Phone number', + hintText: 'Enter your phone number', + disabled: true, + ), ), - ), - ], + Padding( + padding: const EdgeInsets.all(20), + child: ZetaPhoneInput( + label: 'Phone number', + hintText: 'Enter your phone number', + ), + ), + ], + ), ), ), ); diff --git a/example/lib/pages/components/search_bar_example.dart b/example/lib/pages/components/search_bar_example.dart index 691bcaab..b44ee768 100644 --- a/example/lib/pages/components/search_bar_example.dart +++ b/example/lib/pages/components/search_bar_example.dart @@ -26,9 +26,9 @@ class _SearchBarExampleState extends State { Padding( padding: const EdgeInsets.all(20), child: ZetaSearchBar( - onChanged: (value) {}, + onChange: (value) {}, textInputAction: TextInputAction.search, - onSubmit: (text) { + onFieldSubmitted: (text) { print(text); }, ), diff --git a/example/lib/pages/components/select_input_example.dart b/example/lib/pages/components/select_input_example.dart index 9b7f850a..9fede035 100644 --- a/example/lib/pages/components/select_input_example.dart +++ b/example/lib/pages/components/select_input_example.dart @@ -46,12 +46,25 @@ class _SelectInputExampleState extends State { placeholder: 'Placeholder', initialValue: "Item 1", items: items, + validator: (value) { + if (value == null) { + return 'Please select an item'; + } + return null; + }, dropdownSemantics: 'Open dropdown', ), ZetaSelectInput( label: 'Medium', hintText: 'Default hint text', placeholder: 'Placeholder', + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (value) { + if (value != 'Item 2') { + return 'Please select Item 2'; + } + return null; + }, items: items, ), ZetaSelectInput( @@ -68,11 +81,22 @@ class _SelectInputExampleState extends State { disabled: true, items: items, ), - ZetaButton( - label: 'Validate', - onPressed: () { - formKey.currentState?.validate(); - }, + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ZetaButton( + label: 'Validate', + onPressed: () { + formKey.currentState?.validate(); + }, + ), + ZetaButton( + label: 'Reset', + onPressed: () { + formKey.currentState?.reset(); + }, + ), + ], ) ].divide(const SizedBox(height: 8)).toList(), ), diff --git a/example/lib/pages/components/text_input_example.dart b/example/lib/pages/components/text_input_example.dart index 66b5cd1e..c945e23f 100644 --- a/example/lib/pages/components/text_input_example.dart +++ b/example/lib/pages/components/text_input_example.dart @@ -9,68 +9,89 @@ class TextInputExample extends StatelessWidget { @override Widget build(BuildContext context) { - GlobalKey key = GlobalKey(); + final controller = TextEditingController(text: 'Initial value'); + final key = GlobalKey(); return ExampleScaffold( name: 'Text Input', child: Center( child: SingleChildScrollView( child: Padding( padding: EdgeInsets.all(16), - child: Column( - children: [ - ZetaTextInput( - size: ZetaWidgetSize.large, - key: key, - placeholder: 'Placeholder', - prefixText: '£', - label: 'Label', - requirementLevel: ZetaFormFieldRequirement.mandatory, - errorText: 'Error text', - disabled: false, - hintText: 'hint text', - suffix: IconButton( - icon: ZetaIcon(ZetaIcons.add_alert), - onPressed: () {}, + child: Form( + key: key, + child: Column( + children: [ + ZetaTextInput( + size: ZetaWidgetSize.large, + placeholder: 'Placeholder', + prefixText: '£', + controller: controller, + onChange: print, + label: 'Label', + requirementLevel: ZetaFormFieldRequirement.mandatory, + errorText: 'Error text', + disabled: false, + hintText: 'hint text', + suffix: IconButton( + icon: ZetaIcon(ZetaIcons.add_alert), + onPressed: () {}, + ), + ), + ZetaTextInput( + placeholder: 'Placeholder', + prefixText: '£', + initialValue: 'Initial value', ), - ), - ZetaButton( - label: 'Clear', - onPressed: () => key.currentState?.reset(), - ), - ZetaTextInput( - placeholder: 'Placeholder', - prefixText: '£', - ), - ZetaTextInput( - size: ZetaWidgetSize.small, - placeholder: 'Placeholder', - prefix: SizedBox( - height: 8, - child: IconButton( - iconSize: 12, - splashRadius: 1, - icon: ZetaIcon( - ZetaIcons.add_alert, + ZetaTextInput( + size: ZetaWidgetSize.small, + placeholder: 'Placeholder', + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a value'; + } + return null; + }, + prefix: SizedBox( + height: 8, + child: IconButton( + iconSize: 12, + splashRadius: 1, + icon: ZetaIcon( + ZetaIcons.add_alert, + ), + onPressed: () {}, ), - onPressed: () {}, ), ), - ), - const SizedBox(height: 8), - ZetaTextInput( - placeholder: 'Placeholder', - prefix: ZetaIcon( - ZetaIcons.star, - size: 20, + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ZetaButton( + label: 'Reset', + onPressed: () => key.currentState?.reset(), + ), + ZetaButton( + label: 'Validate', + onPressed: () => key.currentState?.validate(), + ), + ], + ), + const SizedBox(height: 8), + ZetaTextInput( + placeholder: 'Placeholder', + prefix: ZetaIcon( + ZetaIcons.star, + size: 20, + ), + ), + ZetaTextInput( + size: ZetaWidgetSize.small, + placeholder: 'Placeholder', + suffixText: 'kg', + prefixText: '£', ), - ), - ZetaTextInput( - size: ZetaWidgetSize.small, - placeholder: 'Placeholder', - suffixText: 'kg', - prefixText: '£', - ), - ].divide(const SizedBox(height: 12)).toList(), + ].divide(const SizedBox(height: 12)).toList(), + ), ), ), ), diff --git a/example/lib/pages/components/time_input_example.dart b/example/lib/pages/components/time_input_example.dart index 6c55299f..a14d28f7 100644 --- a/example/lib/pages/components/time_input_example.dart +++ b/example/lib/pages/components/time_input_example.dart @@ -23,31 +23,47 @@ class TimeInputExample extends StatelessWidget { key: formKey, child: Column( children: [ - ZetaButton( - label: 'Validate inputs', - onPressed: () => print(formKey.currentState?.validate()), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ZetaButton( + label: 'Validate form', + onPressed: () => formKey.currentState?.validate(), + ), + ZetaButton( + label: 'Reset form', + onPressed: () => formKey.currentState?.reset(), + ), + ], ), ZetaTimeInput( label: 'Large', hintText: 'Default hint text', onChange: (value) => print(value), - errorText: 'Oops! Error hint text', + onSaved: (value) => print(value), size: ZetaWidgetSize.large, initialValue: TimeOfDay.now(), clearSemanticLabel: 'Clear', + validator: (value) { + if (value == null) { + return 'Time is required'; + } + return null; + }, timePickerSemanticLabel: 'Open time picker', + autovalidateMode: AutovalidateMode.onUserInteraction, ), ZetaTimeInput( label: 'Medium', hintText: 'Default hint text', requirementLevel: ZetaFormFieldRequirement.optional, onChange: (value) => print(value), - errorText: 'Oops! Error hint text', size: ZetaWidgetSize.medium, ), ZetaTimeInput( label: 'Small', hintText: 'Default hint text', + requirementLevel: ZetaFormFieldRequirement.mandatory, onChange: (value) => print(value), errorText: 'Oops! Error hint text', size: ZetaWidgetSize.small, @@ -55,10 +71,8 @@ class TimeInputExample extends StatelessWidget { ].divide(const SizedBox(height: 12)).toList(), ), ), - const SizedBox( - height: 48, - ), - ZetaTimeInput(label: '12 Hr Time Picker', use12Hr: true), + const SizedBox(height: 48), + ZetaTimeInput(label: '12 Hr Time Picker', use24HourFormat: true), ZetaTimeInput(label: 'Disabled Time Picker', disabled: true, hintText: 'Disabled time picker'), ].divide(const SizedBox(height: 12)).toList(), ), diff --git a/example/widgetbook/pages/components/password_input_widgetbook.dart b/example/widgetbook/pages/components/password_input_widgetbook.dart index 9a409790..151a181f 100644 --- a/example/widgetbook/pages/components/password_input_widgetbook.dart +++ b/example/widgetbook/pages/components/password_input_widgetbook.dart @@ -32,7 +32,6 @@ class _PasswordState extends State<_Password> { constraints: BoxConstraints(maxWidth: 328), child: ZetaPasswordInput( disabled: disabledKnob(context), - obscureText: context.knobs.boolean(label: 'Obscure text', initialValue: true), size: context.knobs.list( label: 'Size', options: ZetaWidgetSize.values, diff --git a/example/widgetbook/pages/components/search_bar_widgetbook.dart b/example/widgetbook/pages/components/search_bar_widgetbook.dart index d9704d92..d0ec70b8 100644 --- a/example/widgetbook/pages/components/search_bar_widgetbook.dart +++ b/example/widgetbook/pages/components/search_bar_widgetbook.dart @@ -35,10 +35,6 @@ Widget searchBarUseCase(BuildContext context) { options: ZetaWidgetBorder.values, labelBuilder: (shape) => shape.name, ); - final showLeadingIcon = context.knobs.boolean( - label: 'Show leading icon', - initialValue: true, - ); final showSpeechToText = context.knobs.boolean( label: 'Show Speech-To-Text button', initialValue: true, @@ -53,10 +49,9 @@ Widget searchBarUseCase(BuildContext context) { size: size, shape: shape, disabled: disabled, - hint: hint, - showLeadingIcon: showLeadingIcon, + hintText: hint, showSpeechToText: showSpeechToText, - onChanged: (value) { + onChange: (value) { if (value == null) return; setState( () => items = _items diff --git a/lib/src/components/date_input/date_input.dart b/lib/src/components/date_input/date_input.dart index 9915d785..8993b246 100644 --- a/lib/src/components/date_input/date_input.dart +++ b/lib/src/components/date_input/date_input.dart @@ -7,6 +7,7 @@ import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; import '../../../zeta_flutter.dart'; import '../../interfaces/form_field.dart'; import '../buttons/input_icon_button.dart'; +import '../text_input/internal_text_input.dart'; /// A form field used to input dates. /// @@ -20,19 +21,66 @@ class ZetaDateInput extends ZetaFormField { super.initialValue, super.onChange, super.requirementLevel = ZetaFormFieldRequirement.none, - super.rounded, + super.validator, + super.onFieldSubmitted, + super.onSaved, + super.autovalidateMode, + bool? rounded, this.label, this.hintText, this.errorText, - this.validator, this.size = ZetaWidgetSize.medium, this.dateFormat = 'MM/dd/yyyy', - this.minDate, - this.maxDate, this.pickerInitialEntryMode, this.datePickerSemanticLabel, + this.minDate, + this.maxDate, this.clearSemanticLabel, - }) : assert((minDate == null || maxDate == null) || minDate.isBefore(maxDate), 'minDate cannot be after maxDate'); + }) : super( + builder: (field) { + final _ZetaDateInputState state = field as _ZetaDateInputState; + final colors = Zeta.of(field.context).colors; + + return InternalTextInput( + label: label, + hintText: hintText, + errorText: field.errorText ?? errorText, + size: size, + placeholder: dateFormat, + controller: state.controller, + onSubmit: onFieldSubmitted != null ? (_) => onFieldSubmitted(field.value) : null, + requirementLevel: requirementLevel, + rounded: rounded, + disabled: disabled, + inputFormatters: [ + state._dateFormatter, + ], + suffix: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + if (state.controller.text.isNotEmpty) + InputIconButton( + icon: ZetaIcons.cancel, + onTap: state.clear, + disabled: disabled, + size: size, + color: colors.iconSubtle, + semanticLabel: clearSemanticLabel, + ), + InputIconButton( + icon: ZetaIcons.calendar, + onTap: state.pickDate, + disabled: disabled, + size: size, + color: colors.iconDefault, + semanticLabel: datePickerSemanticLabel, + ), + ], + ), + ); + }, + ); /// The label for the input. final String? label; @@ -41,8 +89,6 @@ class ZetaDateInput extends ZetaFormField { final String? hintText; /// The error displayed below the input. - /// - /// If you want to have custom error messages for different types of error, use [validator]. final String? errorText; /// The format the given date should be in @@ -61,16 +107,6 @@ class ZetaDateInput extends ZetaFormField { /// The initial entry mode of the date picker. final DatePickerEntryMode? pickerInitialEntryMode; - /// The validator passed to the text input. - /// Returns a string containing an error message. - /// - /// By default, the form field checks for if the date is within [minDate] and [maxDate] (if given). - /// It also checks for null values unless [requirementLevel] is set to [ZetaFormFieldRequirement.optional] - /// - /// If the default validation fails, [errorText] will be shown. - /// However, if [validator] catches any of these conditions, the return value of [validator] will be shown. - final String? Function(DateTime? value)? validator; - /// The semantic label for the clear button. /// /// {@macro zeta-widget-semantic-label} @@ -82,48 +118,36 @@ class ZetaDateInput extends ZetaFormField { final String? datePickerSemanticLabel; @override - State createState() => ZetaDateInputState(); + FormFieldState createState() => _ZetaDateInputState(); + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(DiagnosticsProperty('rounded', rounded)) ..add(ObjectFlagProperty?>.has('onChange', onChange)) ..add(DiagnosticsProperty('initialValue', initialValue)) - ..add(DiagnosticsProperty('disabled', disabled)) ..add(StringProperty('label', label)) ..add(StringProperty('hintText', hintText)) ..add(StringProperty('errorText', errorText)) ..add(EnumProperty('size', size)) ..add(ObjectFlagProperty.has('validator', validator)) ..add(StringProperty('dateFormat', dateFormat)) - ..add(DiagnosticsProperty('minDate', minDate)) - ..add(DiagnosticsProperty('maxDate', maxDate)) ..add(EnumProperty('pickerInitialEntryMode', pickerInitialEntryMode)) ..add(StringProperty('semanticCalendar', datePickerSemanticLabel)) - ..add(StringProperty('semanticClear', clearSemanticLabel)); + ..add(StringProperty('semanticClear', clearSemanticLabel)) + ..add(DiagnosticsProperty('minDate', minDate)) + ..add(DiagnosticsProperty('maxDate', maxDate)); } } /// State for [ZetaDateInput] -class ZetaDateInputState extends State implements ZetaFormFieldState { - ZetaColors get _colors => Zeta.of(context).colors; - +class _ZetaDateInputState extends FormFieldState { late final MaskTextInputFormatter _dateFormatter; - final _controller = TextEditingController(); - final GlobalKey _key = GlobalKey(); - - String? _errorText; + @override + ZetaDateInput get widget => super.widget as ZetaDateInput; - bool get _showClearButton => _controller.text.isNotEmpty; - - DateTime? get _value { - final value = _dateFormatter.getMaskedText().trim(); - final date = DateFormat(widget.dateFormat).tryParseStrict(value); - - return date; - } + final TextEditingController controller = TextEditingController(); @override void initState() { @@ -133,34 +157,56 @@ class ZetaDateInputState extends State implements ZetaFormFieldSt type: MaskAutoCompletionType.eager, ); - if (widget.initialValue != null) { - _setText(widget.initialValue!); - WidgetsBinding.instance.addPostFrameCallback((_) { - _key.currentState?.validate(); - }); - } + _setValue(widget.initialValue); + controller.addListener(_onChange); super.initState(); } @override void dispose() { - _controller.dispose(); + controller.dispose(); super.dispose(); } - void _onChange() { - if (_dateFormatter.getMaskedText().length == widget.dateFormat.length && (_key.currentState?.validate() ?? false)) { - widget.onChange?.call(_value); - } - setState(() {}); + @override + void reset() { + _setValue(widget.initialValue); + super.reset(); } - void _setText(DateTime value) { - _controller.text = DateFormat(widget.dateFormat).format(value); - _dateFormatter.formatEditUpdate(TextEditingValue.empty, _controller.value); + void clear() { + _setValue(null); } - Future _pickDate() async { + void _setValue(DateTime? value) { + final dateTimeStr = _dateTimeToString(value); + _dateFormatter.formatEditUpdate( + TextEditingValue.empty, + TextEditingValue(text: dateTimeStr), + ); + controller.text = dateTimeStr; + } + + DateTime? _parseValue() { + final value = _dateFormatter.getMaskedText().trim(); + final date = DateFormat(widget.dateFormat).tryParseStrict(value); + + return date; + } + + String _dateTimeToString(DateTime? value) { + return value != null ? DateFormat(widget.dateFormat).format(value) : ''; + } + + void _onChange() { + final newValue = _parseValue(); + widget.onChange?.call(newValue); + super.didChange(newValue); + } + + Future pickDate() async { + final colors = Zeta.of(context).colors; + final firstDate = widget.minDate ?? DateTime(0000); final lastDate = widget.maxDate ?? DateTime(3000); DateTime fallbackDate = DateTime.now(); @@ -171,10 +217,10 @@ class ZetaDateInputState extends State implements ZetaFormFieldSt late final DateTime initialDate; - if (_value == null || (_value != null && _value!.isBefore(firstDate) || _value!.isAfter(lastDate))) { + if (value == null || (value != null && value!.isBefore(firstDate) || value!.isAfter(lastDate))) { initialDate = fallbackDate; } else { - initialDate = _value!; + initialDate = value!; } final rounded = context.rounded; @@ -187,14 +233,14 @@ class ZetaDateInputState extends State implements ZetaFormFieldSt builder: (BuildContext context, Widget? child) { return Theme( data: Theme.of(context).copyWith( - dividerTheme: DividerThemeData(color: _colors.borderSubtle), + dividerTheme: DividerThemeData(color: colors.borderSubtle), datePickerTheme: DatePickerThemeData( shape: RoundedRectangleBorder( borderRadius: rounded ? ZetaRadius.rounded : ZetaRadius.none, ), headerHeadlineStyle: ZetaTextStyles.titleLarge, headerHelpStyle: ZetaTextStyles.labelLarge, - dividerColor: _colors.borderSubtle, + dividerColor: colors.borderSubtle, dayStyle: ZetaTextStyles.bodyMedium, ), ), @@ -203,81 +249,13 @@ class ZetaDateInputState extends State implements ZetaFormFieldSt }, ); if (result != null) { - _setText(result); + _setValue(result); } } @override - void reset() { - _dateFormatter.clear(); - _key.currentState?.reset(); - setState(() { - _errorText = null; - }); - _controller.clear(); - widget.onChange?.call(null); - } - - @override - bool validate() => _key.currentState?.validate() ?? false; - - @override - Widget build(BuildContext context) { - return ZetaTextInput( - disabled: widget.disabled, - key: _key, - rounded: widget.rounded, - size: widget.size, - errorText: _errorText, - label: widget.label, - hintText: widget.hintText, - placeholder: widget.dateFormat, - controller: _controller, - inputFormatters: [ - _dateFormatter, - ], - validator: (_) { - final customValidation = widget.validator?.call(_value); - if (_value == null || - (widget.minDate != null && _value!.isBefore(widget.minDate!)) || - (widget.maxDate != null && _value!.isAfter(widget.maxDate!)) || - customValidation != null) { - setState(() { - _errorText = customValidation ?? widget.errorText ?? ''; - }); - return ''; - } - - setState(() { - _errorText = null; - }); - return null; - }, - onChange: (_) => _onChange(), - requirementLevel: widget.requirementLevel, - suffix: Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - if (_showClearButton) - InputIconButton( - icon: ZetaIcons.cancel, - onTap: reset, - disabled: widget.disabled, - size: widget.size, - color: _colors.iconSubtle, - semanticLabel: widget.clearSemanticLabel, - ), - InputIconButton( - icon: ZetaIcons.calendar, - onTap: _pickDate, - disabled: widget.disabled, - size: widget.size, - color: _colors.iconDefault, - semanticLabel: widget.datePickerSemanticLabel, - ), - ], - ), - ); + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('controller', controller)); } } diff --git a/lib/src/components/password/password_input.dart b/lib/src/components/password/password_input.dart index e2505740..2fa85a9f 100644 --- a/lib/src/components/password/password_input.dart +++ b/lib/src/components/password/password_input.dart @@ -3,24 +3,27 @@ import 'package:flutter/material.dart'; import '../../../zeta_flutter.dart'; import '../../interfaces/form_field.dart'; +import '../text_input/internal_text_input.dart'; /// Zeta Password Input /// {@category Components} -class ZetaPasswordInput extends ZetaFormField { +class ZetaPasswordInput extends ZetaTextFormField { ///Constructs [ZetaPasswordInput] - const ZetaPasswordInput({ + ZetaPasswordInput({ super.key, - super.rounded, + bool? rounded, super.initialValue, + super.autovalidateMode, super.requirementLevel = ZetaFormFieldRequirement.none, super.onChange, + super.onSaved, + super.onFieldSubmitted, super.disabled = false, - this.size = ZetaWidgetSize.large, - this.validator, + this.size = ZetaWidgetSize.medium, + super.validator, this.onSubmit, - this.obscureText = true, @Deprecated('Use disabled instead. ' 'This property has been renamed as of 0.11.2') bool enabled = true, - this.controller, + super.controller, this.hintText, this.errorText, this.label, @@ -28,13 +31,36 @@ class ZetaPasswordInput extends ZetaFormField { this.semanticLabel, this.obscureSemanticLabel, this.showSemanticLabel, - }); - - /// {@macro text-input-controller} - final TextEditingController? controller; - - /// {@macro text-input-obscure-text} - final bool obscureText; + }) : super( + builder: (field) { + final _ZetaPasswordInputState state = field as _ZetaPasswordInputState; + + return InternalTextInput( + size: size, + rounded: rounded, + controller: state.effectiveController, + hintText: hintText, + placeholder: placeholder, + label: label, + onChange: state.didChange, + requirementLevel: requirementLevel, + errorText: field.errorText ?? errorText, + onSubmit: onSubmit, + disabled: disabled, + obscureText: state._obscureText, + semanticLabel: semanticLabel, + suffix: MergeSemantics( + child: Semantics( + label: state._obscureText ? showSemanticLabel : obscureSemanticLabel, + child: IconButton( + icon: ZetaIcon(state._obscureText ? ZetaIcons.visibility_off : ZetaIcons.visibility), + onPressed: state.toggleVisibility, + ), + ), + ), + ); + }, + ); /// {@macro text-input-placeholder} final String? placeholder; @@ -51,13 +77,10 @@ class ZetaPasswordInput extends ZetaFormField { /// {@macro text-input-size} final ZetaWidgetSize size; - /// {@macro text-input-validator} - final String? Function(String?)? validator; - /// {@macro text-input-on-submit} final void Function(String? val)? onSubmit; - /// Value passed to the wrapping [Semantics] widget. + /// Value passed to the wrapping [Semantics] /// /// {@macro zeta-widget-semantic-label} final String? semanticLabel; @@ -73,14 +96,13 @@ class ZetaPasswordInput extends ZetaFormField { final String? showSemanticLabel; @override - State createState() => _ZetaPasswordInputState(); + FormFieldState createState() => _ZetaPasswordInputState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('controller', controller)) - ..add(DiagnosticsProperty('obscureText', obscureText)) ..add(StringProperty('hintText', hintText)) ..add(StringProperty('label', label)) ..add(StringProperty('footerText', hintText)) @@ -95,46 +117,12 @@ class ZetaPasswordInput extends ZetaFormField { } } -class _ZetaPasswordInputState extends State { - late bool _obscureText; +class _ZetaPasswordInputState extends ZetaTextFormFieldState { + bool _obscureText = true; - @override - void initState() { - super.initState(); - _obscureText = widget.obscureText; - } - - @override - Widget build(BuildContext context) { - final rounded = context.rounded; - - return ZetaTextInput( - size: widget.size, - rounded: rounded, - controller: widget.controller, - validator: widget.validator, - hintText: widget.hintText, - placeholder: widget.placeholder, - label: widget.label, - onChange: widget.onChange, - errorText: widget.errorText, - onSubmit: widget.onSubmit, - disabled: widget.disabled, - obscureText: _obscureText, - semanticLabel: widget.semanticLabel, - suffix: MergeSemantics( - child: Semantics( - label: _obscureText ? widget.showSemanticLabel : widget.obscureSemanticLabel, - child: IconButton( - icon: ZetaIcon(_obscureText ? ZetaIcons.visibility_off : ZetaIcons.visibility), - onPressed: () { - setState(() { - _obscureText = !_obscureText; - }); - }, - ), - ), - ), - ); + void toggleVisibility() { + setState(() { + _obscureText = !_obscureText; + }); } } diff --git a/lib/src/components/phone_input/phone_input.dart b/lib/src/components/phone_input/phone_input.dart index 4d183000..b842e31c 100644 --- a/lib/src/components/phone_input/phone_input.dart +++ b/lib/src/components/phone_input/phone_input.dart @@ -6,30 +6,140 @@ import 'package:flutter/services.dart'; import '../../../zeta_flutter.dart'; import '../../interfaces/form_field.dart'; -import '../text_input/hint_text.dart'; -import '../text_input/input_label.dart'; +import '../text_input/internal_text_input.dart'; import 'countries.dart'; +/// A phone number. +class PhoneNumber { + /// Creates a new [PhoneNumber]. + const PhoneNumber({ + required this.dialCode, + required this.number, + }); + + /// The dial code of the phone number. + final String dialCode; + + /// The number of the phone number. + final String number; +} + /// ZetaPhoneInput allows entering phone numbers. /// {@category Components} -class ZetaPhoneInput extends ZetaFormField { +class ZetaPhoneInput extends ZetaFormField { /// Constructor for [ZetaPhoneInput]. - const ZetaPhoneInput({ + ZetaPhoneInput({ super.key, - super.rounded, + bool? rounded, + super.onFieldSubmitted, + super.onSaved, super.initialValue, + super.validator, + @Deprecated('Use onChange instead. ' 'Deprecated as of 0.15.0') ValueChanged? onChanged, super.onChange, super.requirementLevel = ZetaFormFieldRequirement.none, this.label, + @Deprecated('Use hintText instead. ' 'Deprecated as of 0.15.0') String? hint, this.hintText, + @Deprecated('Use disabled instead. ' 'Deprecated as of 0.15.0') bool enabled = true, super.disabled = false, - this.hasError = false, + @Deprecated('Use errorText instead. ' 'Deprecated as of 0.15.0') bool hasError = false, this.errorText, - this.initialCountry, + @Deprecated('Use initialValue instead. ' 'Deprecated as of 0.15.0') String? initialCountry, this.countries, this.size = ZetaWidgetSize.medium, + @Deprecated('Set this as part of the initial value instead. ' 'Deprecated as of 0.15.0') String? countryDialCode, + @Deprecated('Set this as part of the initial value instead. ' 'enabled is deprecated as of 0.15.0') + String? phoneNumber, + @Deprecated('Country search hint is deprecated as of 0.15.0') String? countrySearchHint, + @Deprecated('Deprecated as of 0.15.0') bool? useRootNavigator, this.selectCountrySemanticLabel, - }); + super.autovalidateMode, + }) : super( + builder: (field) { + final _ZetaPhoneInputState state = field as _ZetaPhoneInputState; + + final colors = Zeta.of(field.context).colors; + final newRounded = rounded ?? field.context.rounded; + + return InternalTextInput( + label: label, + hintText: hintText, + errorText: field.errorText ?? errorText, + size: size, + controller: state.controller, + requirementLevel: requirementLevel, + rounded: rounded, + disabled: disabled, + focusNode: state._inputFocusNode, + onSubmit: onFieldSubmitted != null ? (_) => onFieldSubmitted(field.value) : null, + inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[\d\s\-]'))], + keyboardType: TextInputType.phone, + prefixText: state._selectedCountry.dialCode, + borderRadius: BorderRadius.only( + topRight: newRounded ? const Radius.circular(ZetaSpacing.minimum) : Radius.zero, + bottomRight: newRounded ? const Radius.circular(ZetaSpacing.minimum) : Radius.zero, + ), + externalPrefix: ZetaDropdown( + offset: const Offset(0, ZetaSpacing.medium), + onChange: !disabled ? state.onDropdownChanged : null, + value: state._selectedCountry.dialCode, + onDismissed: state.onDropdownDismissed, + items: state._dropdownItems, + builder: (context, selectedItem, dropdowncontroller) { + final borderSide = BorderSide( + color: disabled ? colors.borderDefault : colors.borderSubtle, + ); + + return GestureDetector( + onTap: !disabled ? dropdowncontroller.toggle : null, + child: Container( + constraints: BoxConstraints( + maxHeight: size == ZetaWidgetSize.large ? ZetaSpacing.xl_8 : ZetaSpacing.xl_6, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: newRounded ? const Radius.circular(ZetaSpacing.minimum) : Radius.zero, + bottomLeft: newRounded ? const Radius.circular(ZetaSpacing.minimum) : Radius.zero, + ), + border: Border( + left: borderSide, + top: borderSide, + bottom: borderSide, + ), + color: disabled ? colors.surfaceDisabled : colors.surfaceDefault, + ), + child: Column( + children: [ + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: ZetaSpacing.medium, + right: ZetaSpacing.small, + ), + child: selectedItem?.icon, + ), + ZetaIcon( + !dropdowncontroller.isOpen ? ZetaIcons.expand_more : ZetaIcons.expand_less, + color: !disabled ? colors.iconDefault : colors.iconDisabled, + size: ZetaSpacing.xl_1, + ), + ], + ), + ), + ], + ), + ), + ); + }, + ), + ); + }, + ); /// If provided, displays a label above the input field. final String? label; @@ -37,18 +147,10 @@ class ZetaPhoneInput extends ZetaFormField { /// If provided, displays a hint below the input field. final String? hintText; - /// Determines if the input field should be displayed in error style. - /// Default is `false`. - /// If `enabled` is `false`, this has no effect. - final bool hasError; - /// In combination with `hasError: true`, provides the error message /// to be displayed below the input field. final String? errorText; - /// The initial value for the selected country. - final String? initialCountry; - /// List of countries ISO 3166-1 alpha-2 codes final List? countries; @@ -63,33 +165,31 @@ class ZetaPhoneInput extends ZetaFormField { final String? selectCountrySemanticLabel; @override - State createState() => _ZetaPhoneInputState(); + FormFieldState createState() => _ZetaPhoneInputState(); + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(StringProperty('label', label)) ..add(StringProperty('hint', hintText)) - ..add(DiagnosticsProperty('enabled', disabled)) - ..add(DiagnosticsProperty('rounded', rounded)) - ..add(DiagnosticsProperty('hasError', hasError)) ..add(StringProperty('errorText', errorText)) - ..add(StringProperty('countryDialCode', initialCountry)) ..add(IterableProperty('countries', countries)) ..add(EnumProperty('size', size)) ..add(StringProperty('selectCountrySemanticLabel', selectCountrySemanticLabel)); } } -class _ZetaPhoneInputState extends State { +class _ZetaPhoneInputState extends FormFieldState { late List _countries; late List> _dropdownItems; late Country _selectedCountry; - late String _phoneNumber; - final FocusNode _inputFocusNode = FocusNode(); - ZetaWidgetSize get _size => widget.size == ZetaWidgetSize.small ? ZetaWidgetSize.medium : widget.size; + final TextEditingController controller = TextEditingController(); + + @override + ZetaPhoneInput get widget => super.widget as ZetaPhoneInput; @override void initState() { @@ -97,6 +197,17 @@ class _ZetaPhoneInputState extends State { _setCountries(); _setInitialCountry(); _setDropdownItems(); + + controller + ..text = widget.initialValue != null ? widget.initialValue!.number : '' + ..addListener(_onChanged); + } + + @override + void dispose() { + _inputFocusNode.dispose(); + controller.dispose(); + super.dispose(); } @override @@ -105,12 +216,36 @@ class _ZetaPhoneInputState extends State { _setCountries(); setState(_setDropdownItems); } - if (oldWidget.initialCountry != widget.initialCountry) { + if (oldWidget.initialValue != widget.initialValue) { setState(_setInitialCountry); + controller + ..removeListener(_onChanged) + ..text = widget.initialValue != null ? widget.initialValue!.number : '' + ..addListener(_onChanged); } super.didUpdateWidget(oldWidget); } + @override + void reset() { + setState(_setInitialCountry); + controller.text = widget.initialValue != null ? widget.initialValue!.number : ''; + + super.reset(); + } + + void onDropdownChanged(ZetaDropdownItem value) { + setState(() { + _selectedCountry = _countries.firstWhere((country) => country.dialCode == value.value); + }); + _inputFocusNode.requestFocus(); + _onChanged(); + } + + void onDropdownDismissed() { + setState(() {}); + } + void _setCountries() { _countries = widget.countries?.isEmpty ?? true ? Countries.list @@ -127,10 +262,9 @@ class _ZetaPhoneInputState extends State { void _setInitialCountry() { _selectedCountry = _countries.firstWhereOrNull( - (country) => country.isoCode == widget.initialCountry, + (country) => country.dialCode == widget.initialValue?.dialCode, ) ?? _countries.first; - _phoneNumber = widget.initialValue ?? ''; } void _setDropdownItems() { @@ -151,127 +285,15 @@ class _ZetaPhoneInputState extends State { .toList(); } - void _onChanged({Country? selectedCountry, String? phoneNumber}) { - setState(() { - if (selectedCountry != null) _selectedCountry = selectedCountry; - if (phoneNumber != null) _phoneNumber = phoneNumber; - }); - widget.onChange?.call('${_selectedCountry.dialCode}$_phoneNumber'); + void _onChanged() { + final newValue = PhoneNumber(dialCode: _selectedCountry.dialCode, number: controller.text); + widget.onChange?.call(newValue); + super.didChange(newValue); } @override - Widget build(BuildContext context) { - final zeta = Zeta.of(context); - final rounded = context.rounded; - - return Semantics( - enabled: !widget.disabled, - excludeSemantics: widget.disabled, - child: Column( - children: [ - if (widget.label != null) ...[ - ZetaInputLabel( - label: widget.label!, - requirementLevel: widget.requirementLevel, - disabled: widget.disabled, - ), - const SizedBox(height: ZetaSpacing.minimum), - ], - Row( - children: [ - ZetaDropdown( - offset: const Offset(0, ZetaSpacing.medium), - onChange: !widget.disabled - ? (value) { - setState(() { - _selectedCountry = _countries.firstWhere((country) => country.dialCode == value.value); - }); - _inputFocusNode.requestFocus(); - } - : null, - value: _selectedCountry.dialCode, - onDismissed: () => setState(() {}), - items: _dropdownItems, - builder: (context, selectedItem, controller) { - final borderSide = BorderSide( - color: widget.disabled ? zeta.colors.borderDefault : zeta.colors.borderSubtle, - ); - - return GestureDetector( - onTap: !widget.disabled ? controller.toggle : null, - child: Container( - constraints: BoxConstraints( - maxHeight: widget.size == ZetaWidgetSize.large ? ZetaSpacing.xl_8 : ZetaSpacing.xl_6, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topLeft: rounded ? const Radius.circular(ZetaSpacing.minimum) : Radius.zero, - bottomLeft: rounded ? const Radius.circular(ZetaSpacing.minimum) : Radius.zero, - ), - border: Border( - left: borderSide, - top: borderSide, - bottom: borderSide, - ), - color: widget.disabled ? zeta.colors.surfaceDisabled : zeta.colors.surfaceDefault, - ), - child: Column( - children: [ - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only( - left: ZetaSpacing.medium, - right: ZetaSpacing.small, - ), - child: selectedItem?.icon, - ), - ZetaIcon( - !controller.isOpen ? ZetaIcons.expand_more : ZetaIcons.expand_less, - color: !widget.disabled ? zeta.colors.iconDefault : zeta.colors.iconDisabled, - size: ZetaSpacing.xl_1, - ), - ], - ), - ), - ], - ), - ), - ); - }, - ), - Expanded( - child: textInputWithBorder( - initialValue: widget.initialValue, - disabled: widget.disabled, - size: _size, - requirementLevel: widget.requirementLevel, - rounded: rounded, - focusNode: _inputFocusNode, - errorText: widget.errorText != null ? '' : null, - inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[\d\s\-]'))], - keyboardType: TextInputType.phone, - onChange: (value) => _onChanged(phoneNumber: value), - prefixText: _selectedCountry.dialCode, - borderRadius: BorderRadius.only( - topRight: rounded ? const Radius.circular(ZetaSpacing.minimum) : Radius.zero, - bottomRight: rounded ? const Radius.circular(ZetaSpacing.minimum) : Radius.zero, - ), - ), - ), - ], - ), - ZetaHintText( - disabled: widget.disabled, - rounded: rounded, - hintText: widget.hintText, - errorText: widget.errorText, - ), - ], - ), - ); + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('controller', controller)); } } diff --git a/lib/src/components/search_bar/search_bar.dart b/lib/src/components/search_bar/search_bar.dart index 2eaaf8df..cdba9f7e 100644 --- a/lib/src/components/search_bar/search_bar.dart +++ b/lib/src/components/search_bar/search_bar.dart @@ -1,50 +1,192 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../../../zeta_flutter.dart'; +import '../../interfaces/form_field.dart'; +import '../buttons/input_icon_button.dart'; /// ZetaSearchBar provides input field for searching. /// {@category Components} -class ZetaSearchBar extends ZetaStatefulWidget { +class ZetaSearchBar extends ZetaTextFormField { /// Constructor for [ZetaSearchBar]. - const ZetaSearchBar({ - super.key, - this.size, - this.shape, - this.hint, - this.initialValue, - this.onChanged, - this.onSubmit, + ZetaSearchBar({ + super.autovalidateMode, + super.validator, + super.onSaved, + super.onChange, + @Deprecated('Use onFieldSubmitted instead. ' 'deprecated as of 0.15.0') ValueChanged? onSubmit, + super.onFieldSubmitted, + super.requirementLevel, + super.controller, + super.disabled = false, + super.initialValue, + this.size = ZetaWidgetSize.medium, + this.shape = ZetaWidgetBorder.rounded, + @Deprecated('Use hintText instead. ' 'deprecated as of 0.15.0') String? hint, + this.hintText, this.onSpeechToText, - this.disabled = false, - this.showLeadingIcon = true, this.showSpeechToText = true, @Deprecated('Use disabled instead. ' 'enabled is deprecated as of 0.11.0') bool enabled = true, this.focusNode, this.textInputAction, this.microphoneSemanticLabel, this.clearSemanticLabel, - }); + super.key, + @Deprecated('Show leading icon is deprecated as of 0.14.2') bool showLeadingIcon = true, + @Deprecated('Use onChange instead') ValueChanged? onChanged, + }) : super( + builder: (field) { + final zeta = Zeta.of(field.context); + + final _ZetaSearchBarState state = field as _ZetaSearchBarState; + + final BorderRadius borderRadius = switch (shape) { + ZetaWidgetBorder.rounded => ZetaRadius.minimal, + ZetaWidgetBorder.full => ZetaRadius.full, + _ => ZetaRadius.none, + }; + + final defaultInputBorder = OutlineInputBorder( + borderRadius: borderRadius, + borderSide: BorderSide(color: zeta.colors.cool.shade40), + ); + + final focusedBorder = defaultInputBorder.copyWith( + borderSide: BorderSide( + color: zeta.colors.blue.shade50, + width: ZetaSpacing.minimum, + ), + ); + + final disabledborder = defaultInputBorder.copyWith( + borderSide: BorderSide(color: zeta.colors.borderDisabled), + ); + + late final double iconSize; + late final double padding; + + switch (size) { + case ZetaWidgetSize.large: + iconSize = ZetaSpacing.xl_2; + padding = ZetaSpacing.medium; + case ZetaWidgetSize.medium: + iconSize = ZetaSpacing.xl_1; + padding = ZetaSpacing.small; + case ZetaWidgetSize.small: + iconSize = ZetaSpacing.large; + padding = ZetaSpacing.minimum; + } + + return ZetaRoundedScope( + rounded: shape != ZetaWidgetBorder.sharp, + child: Semantics( + excludeSemantics: disabled, + label: disabled ? hintText ?? 'Search' : null, // TODO(UX-1003): Localize + enabled: disabled ? false : null, + child: TextFormField( + focusNode: focusNode, + enabled: !disabled, + controller: state.effectiveController, + keyboardType: TextInputType.text, + textInputAction: textInputAction, + onFieldSubmitted: onFieldSubmitted, + onChanged: state.onChange, + style: ZetaTextStyles.bodyMedium, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric( + horizontal: 10, + vertical: padding, + ), + hintText: hintText ?? 'Search', // TODO(UX-1003): Localize + hintStyle: ZetaTextStyles.bodyMedium.copyWith( + color: !disabled ? zeta.colors.textSubtle : zeta.colors.textDisabled, + ), + prefixIcon: Padding( + padding: const EdgeInsets.only(left: ZetaSpacingBase.x2_5, right: ZetaSpacing.small), + child: ZetaIcon( + ZetaIcons.search, + color: !disabled ? zeta.colors.cool.shade70 : zeta.colors.cool.shade50, + size: iconSize, + ), + ), + prefixIconConstraints: const BoxConstraints( + minHeight: ZetaSpacing.xl_2, + minWidth: ZetaSpacing.xl_2, + ), + suffixIcon: IntrinsicHeight( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (state.effectiveController.text.isNotEmpty && !disabled) ...[ + Semantics( + container: true, + button: true, + excludeSemantics: true, + label: clearSemanticLabel, + child: InputIconButton( + icon: ZetaIcons.cancel, + onTap: () => state.onChange(''), + disabled: disabled, + size: size, + color: zeta.colors.iconSubtle, + key: const ValueKey('search-clear-btn'), + ), + ), + if (showSpeechToText) + SizedBox( + height: iconSize, + child: VerticalDivider( + color: zeta.colors.cool.shade40, + width: 5, + thickness: 1, + ), + ), + ], + if (showSpeechToText) + Semantics( + label: microphoneSemanticLabel, + container: true, + button: true, + excludeSemantics: true, + child: InputIconButton( + icon: ZetaIcons.microphone, + onTap: state.onSpeechToText, + key: const ValueKey('speech-to-text-btn'), + disabled: disabled, + size: size, + color: zeta.colors.iconDefault, + ), + ), + ], + ), + ), + suffixIconConstraints: const BoxConstraints( + minHeight: ZetaSpacing.xl_2, + minWidth: ZetaSpacing.xl_2, + ), + filled: !disabled ? null : true, + fillColor: !disabled ? null : zeta.colors.cool.shade30, + enabledBorder: defaultInputBorder, + focusedBorder: focusedBorder, + disabledBorder: disabledborder, + ), + ), + ), + ); + }, + ); /// Determines the size of the input field. - /// Default is [ZetaWidgetSize.large] - final ZetaWidgetSize? size; + /// Default is [ZetaWidgetSize.medium] + final ZetaWidgetSize size; /// Determines the shape of the input field. /// Default is [ZetaWidgetBorder.rounded] - final ZetaWidgetBorder? shape; + final ZetaWidgetBorder shape; /// If provided, displays a hint inside the input field. /// Default is `Search`. - final String? hint; - - /// The initial value. - final String? initialValue; - - /// A callback, which provides the entered text. - final void Function(String? text)? onChanged; - - /// A callback, called when [textInputAction] is performed. - final void Function(String text)? onSubmit; + final String? hintText; /// The type of action button to use for the keyboard. final TextInputAction? textInputAction; @@ -52,13 +194,6 @@ class ZetaSearchBar extends ZetaStatefulWidget { /// A callback, which is invoked when the microphone button is pressed. final Future Function()? onSpeechToText; - /// {@macro zeta-widget-disabled} - final bool disabled; - - /// Determines if there should be a leading icon. - /// Default is `true`. - final bool showLeadingIcon; - /// Determines if there should be a Speech-To-Text button. /// Default is `true`. final bool showSpeechToText; @@ -77,7 +212,7 @@ class ZetaSearchBar extends ZetaStatefulWidget { final String? clearSemanticLabel; @override - State createState() => _ZetaSearchBarState(); + FormFieldState createState() => _ZetaSearchBarState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -85,213 +220,28 @@ class ZetaSearchBar extends ZetaStatefulWidget { properties ..add(EnumProperty('size', size)) ..add(EnumProperty('shape', shape)) - ..add(StringProperty('hint', hint)) - ..add(DiagnosticsProperty('enabled', disabled)) - ..add(ObjectFlagProperty.has('onChanged', onChanged)) + ..add(StringProperty('hintText', hintText)) ..add(StringProperty('initialValue', initialValue)) ..add(ObjectFlagProperty.has('onSpeechToText', onSpeechToText)) - ..add(DiagnosticsProperty('showLeadingIcon', showLeadingIcon)) ..add(DiagnosticsProperty('showSpeechToText', showSpeechToText)) ..add(DiagnosticsProperty('focusNode', focusNode)) - ..add(ObjectFlagProperty.has('onSubmit', onSubmit)) ..add(EnumProperty('textInputAction', textInputAction)) ..add(StringProperty('microphoneSemanticLabel', microphoneSemanticLabel)) ..add(StringProperty('clearSemanticLabel', clearSemanticLabel)); } } -class _ZetaSearchBarState extends State { - late final TextEditingController _controller; - late ZetaWidgetSize _size; - late ZetaWidgetBorder _shape; - +class _ZetaSearchBarState extends ZetaTextFormFieldState { @override - void initState() { - super.initState(); - _controller = TextEditingController(text: widget.initialValue ?? ''); - _size = widget.size ?? ZetaWidgetSize.large; - _shape = widget.shape ?? ZetaWidgetBorder.rounded; - } - - @override - void didUpdateWidget(ZetaSearchBar oldWidget) { - super.didUpdateWidget(oldWidget); - _size = widget.size ?? ZetaWidgetSize.large; - _shape = widget.shape ?? ZetaWidgetBorder.rounded; - if (oldWidget.initialValue != widget.initialValue) { - _controller.text = widget.initialValue ?? ''; + ZetaSearchBar get widget => super.widget as ZetaSearchBar; + + Future onSpeechToText() async { + if (widget.onSpeechToText != null) { + final text = await widget.onSpeechToText!(); + if (text != null) { + effectiveController.text = text; + super.onChange(value); + } } } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final zeta = Zeta.of(context); - final iconSize = _iconSize(_size); - - return ZetaRoundedScope( - rounded: widget.shape != ZetaWidgetBorder.sharp, - child: Semantics( - excludeSemantics: widget.disabled, - label: widget.disabled ? widget.hint ?? 'Search' : null, // TODO(UX-1003): Localize - enabled: widget.disabled ? false : null, - child: TextFormField( - focusNode: widget.focusNode, - enabled: !widget.disabled, - controller: _controller, - keyboardType: TextInputType.text, - textInputAction: widget.textInputAction, - onFieldSubmitted: widget.onSubmit, - onChanged: (value) => setState(() => widget.onChanged?.call(value)), - style: ZetaTextStyles.bodyMedium, - decoration: InputDecoration( - isDense: true, - contentPadding: EdgeInsets.symmetric( - horizontal: 10, - vertical: _inputVerticalPadding(_size), - ), - hintText: widget.hint ?? 'Search', // TODO(UX-1003): Localize - hintStyle: ZetaTextStyles.bodyMedium.copyWith( - color: !widget.disabled ? zeta.colors.textDefault : zeta.colors.cool.shade50, - ), - prefixIcon: widget.showLeadingIcon - ? Padding( - padding: const EdgeInsets.only(left: ZetaSpacingBase.x2_5, right: ZetaSpacing.small), - child: ZetaIcon( - ZetaIcons.search, - color: !widget.disabled ? zeta.colors.cool.shade70 : zeta.colors.cool.shade50, - size: iconSize, - ), - ) - : null, - prefixIconConstraints: const BoxConstraints( - minHeight: ZetaSpacing.xl_2, - minWidth: ZetaSpacing.xl_2, - ), - suffixIcon: IntrinsicHeight( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (_controller.text.isNotEmpty && !widget.disabled) ...[ - Semantics( - container: true, - button: true, - excludeSemantics: true, - label: widget.clearSemanticLabel, - child: IconButton( - key: const ValueKey('search-clear-btn'), - visualDensity: const VisualDensity( - horizontal: -4, - vertical: -4, - ), - onPressed: () { - setState(_controller.clear); - widget.onChanged?.call(''); - }, - icon: ZetaIcon( - ZetaIcons.cancel, - color: zeta.colors.cool.shade70, - size: iconSize, - ), - ), - ), - if (widget.showSpeechToText) - SizedBox( - height: iconSize, - child: VerticalDivider( - color: zeta.colors.cool.shade40, - width: 5, - thickness: 1, - ), - ), - ], - if (widget.showSpeechToText) - Padding( - padding: const EdgeInsets.only(right: ZetaSpacing.minimum), - child: Semantics( - container: true, - label: widget.microphoneSemanticLabel, - excludeSemantics: true, - button: true, - child: IconButton( - tooltip: widget.microphoneSemanticLabel, - key: const ValueKey('speech-to-text-btn'), - visualDensity: const VisualDensity( - horizontal: -4, - vertical: -4, - ), - onPressed: widget.onSpeechToText == null - ? null - : () async { - final text = await widget.onSpeechToText!.call(); - if (text != null) { - setState(() => _controller.text = text); - widget.onChanged?.call(text); - } - }, - icon: ZetaIcon( - ZetaIcons.microphone, - size: iconSize, - ), - ), - ), - ), - ], - ), - ), - suffixIconConstraints: const BoxConstraints( - minHeight: ZetaSpacing.xl_2, - minWidth: ZetaSpacing.xl_2, - ), - filled: !widget.disabled ? null : true, - fillColor: !widget.disabled ? null : zeta.colors.cool.shade30, - enabledBorder: _defaultInputBorder(zeta, shape: _shape), - focusedBorder: _focusedInputBorder(zeta, shape: _shape), - disabledBorder: _defaultInputBorder(zeta, shape: _shape), - ), - ), - ), - ); - } - - double _inputVerticalPadding(ZetaWidgetSize size) => switch (size) { - ZetaWidgetSize.large => ZetaSpacing.medium, - ZetaWidgetSize.medium => ZetaSpacing.small, - ZetaWidgetSize.small => ZetaSpacing.minimum, - }; - - double _iconSize(ZetaWidgetSize size) => switch (size) { - ZetaWidgetSize.large => ZetaSpacing.xl_2, - ZetaWidgetSize.medium => ZetaSpacing.xl_1, - ZetaWidgetSize.small => ZetaSpacing.large, - }; - - OutlineInputBorder _defaultInputBorder( - Zeta zeta, { - required ZetaWidgetBorder shape, - }) => - OutlineInputBorder( - borderRadius: _borderRadius(shape), - borderSide: BorderSide(color: zeta.colors.cool.shade40), - ); - - OutlineInputBorder _focusedInputBorder( - Zeta zeta, { - required ZetaWidgetBorder shape, - }) => - OutlineInputBorder( - borderRadius: _borderRadius(shape), - borderSide: BorderSide(color: zeta.colors.blue.shade50), - ); - - BorderRadius _borderRadius(ZetaWidgetBorder shape) => switch (shape) { - ZetaWidgetBorder.rounded => ZetaRadius.minimal, - ZetaWidgetBorder.full => ZetaRadius.full, - _ => ZetaRadius.none, - }; } diff --git a/lib/src/components/select_input/select_input.dart b/lib/src/components/select_input/select_input.dart index 8f6b93aa..ec49e0d8 100644 --- a/lib/src/components/select_input/select_input.dart +++ b/lib/src/components/select_input/select_input.dart @@ -6,18 +6,23 @@ import '../../../zeta_flutter.dart'; import '../../interfaces/form_field.dart'; import '../buttons/input_icon_button.dart'; import '../dropdown/dropdown_controller.dart'; +import '../text_input/internal_text_input.dart'; /// Class for [ZetaSelectInput]. /// {@category Components} class ZetaSelectInput extends ZetaFormField { ///Constructor of [ZetaSelectInput] - const ZetaSelectInput({ + ZetaSelectInput({ super.key, - super.rounded, + bool? rounded, + super.validator, + super.onFieldSubmitted, + super.autovalidateMode, + super.onSaved, super.disabled = false, super.initialValue, super.onChange, - super.requirementLevel = ZetaFormFieldRequirement.none, + super.requirementLevel, required this.items, this.onTextChanged, this.size = ZetaWidgetSize.medium, @@ -25,10 +30,54 @@ class ZetaSelectInput extends ZetaFormField { this.hintText, this.prefix, this.placeholder, - this.validator, this.errorText, this.dropdownSemantics, - }); + }) : super( + builder: (field) { + final _ZetaSelectInputState state = field as _ZetaSelectInputState; + final colors = Zeta.of(field.context).colors; + + return ZetaRoundedScope( + rounded: rounded ?? field.context.rounded, + child: ZetaDropdown( + disableButtonSemantics: true, + items: state.currentItems, + onChange: !disabled ? state.setItem : null, + key: state.dropdownKey, + value: state._selectedItem?.value, + offset: const Offset(0, ZetaSpacing.xl_1 * -1), + onDismissed: state.onDropdownDismissed, + builder: (context, _, controller) { + return InternalTextInput( + size: size, + requirementLevel: requirementLevel, + disabled: disabled, + controller: state.inputController, + focusNode: state.inputFocusNode, + prefix: state._selectedItem?.icon ?? prefix, + label: label, + onSubmit: (_) { + state.onInputSubmitted(controller); + onFieldSubmitted?.call(state._selectedItem?.value); + }, + errorText: field.errorText ?? errorText, + placeholder: placeholder, + hintText: hintText, + onChange: (val) => state.onInputChanged(controller), + suffix: InputIconButton( + semanticLabel: dropdownSemantics, + icon: controller.isOpen ? ZetaIcons.expand_less : ZetaIcons.expand_more, + disabled: disabled, + size: size, + color: colors.iconSubtle, + onTap: () => state.onIconTapped(controller), + ), + ); + }, + ), + ); + }, + ); /// Input items as list of [ZetaDropdownItem] final List> items; @@ -44,9 +93,6 @@ class ZetaSelectInput extends ZetaFormField { /// The error text shown beneath the input when the validator fails. final String? errorText; - /// The validator for the input. - final String? Function(T? value)? validator; - /// Determines the size of the input field. /// Defaults to [ZetaWidgetSize.medium] final ZetaWidgetSize size; @@ -66,12 +112,12 @@ class ZetaSelectInput extends ZetaFormField { final String? dropdownSemantics; @override - State> createState() => _ZetaSelectInputState(); + FormFieldState createState() => _ZetaSelectInputState(); + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(DiagnosticsProperty('rounded', rounded)) ..add(EnumProperty('size', size)) ..add(StringProperty('hint', hintText)) ..add(ObjectFlagProperty?>.has('onTextChanged', onTextChanged)) @@ -84,19 +130,22 @@ class ZetaSelectInput extends ZetaFormField { } } -class _ZetaSelectInputState extends State> { - final GlobalKey> _dropdownKey = GlobalKey(); - final TextEditingController _inputController = TextEditingController(); +class _ZetaSelectInputState extends FormFieldState { + @override + ZetaSelectInput get widget => super.widget as ZetaSelectInput; + + final GlobalKey> dropdownKey = GlobalKey(); - ZetaDropdownItem? _selectedItem; + final TextEditingController inputController = TextEditingController(); + final FocusNode inputFocusNode = FocusNode(); - bool get _dropdownOpen => _dropdownKey.currentState?.isOpen ?? false; + late List> currentItems; + + ZetaDropdownItem? _selectedItem; @override void initState() { - _inputController.addListener( - () => setState(() {}), - ); + currentItems = widget.items; _setInitialItem(); super.initState(); } @@ -109,90 +158,88 @@ class _ZetaSelectInputState extends State> { } } + @override + void reset() { + super.reset(); + _setInitialItem(); + super.didChange(_selectedItem?.value); + widget.onChange?.call(_selectedItem?.value); + } + void _setInitialItem() { _selectedItem = widget.items.firstWhereOrNull((item) => item.value == widget.initialValue); - _inputController.text = _selectedItem?.label ?? ''; + inputController.text = _selectedItem?.label ?? ''; } - void _onInputChanged(ZetaDropdownController dropdownController) { - dropdownController.open(); + void onDropdownDismissed() { setState(() { - _selectedItem = null; + currentItems = widget.items; + _onLoseFocus(); }); - widget.onChange?.call(null); } - void _onIconTapped(ZetaDropdownController dropdownController) { - dropdownController.toggle(); - setState(() {}); + void onInputSubmitted(ZetaDropdownController dropdownController) { + if (dropdownController.isOpen && currentItems.isNotEmpty) { + setItem(currentItems.first); + } + dropdownController.close(); } - void _onDropdownChanged(ZetaDropdownItem item) { - _inputController.text = item.label; - setState(() { - _selectedItem = item; - }); - widget.onChange?.call(item.value); + void _onLoseFocus() { + if (_selectedItem == null) { + inputController.text = ''; + } else { + setItem(_selectedItem!); + } } - @override - Widget build(BuildContext context) { - final colors = Zeta.of(context).colors; - + void _filterItems() { late List> filteredItems; - if (_inputController.text.isNotEmpty) { + if (inputController.text.isNotEmpty) { filteredItems = widget.items.where( (item) { - return item.label.toLowerCase().startsWith(_inputController.text.toLowerCase()); + return item.label.toLowerCase().startsWith(inputController.text.toLowerCase()); }, ).toList(); } else { filteredItems = widget.items; } + setState(() { + currentItems = filteredItems; + }); + } - return ZetaRoundedScope( - rounded: context.rounded, - child: ZetaDropdown( - disableButtonSemantics: true, - items: filteredItems, - onChange: !widget.disabled ? _onDropdownChanged : null, - key: _dropdownKey, - value: _selectedItem?.value, - offset: const Offset(0, ZetaSpacing.xl_1 * -1), - onDismissed: () => setState(() {}), - builder: (context, _, controller) { - return ZetaTextInput( - size: widget.size, - requirementLevel: widget.requirementLevel, - disabled: widget.disabled, - validator: (_) { - final currentValue = _selectedItem?.value; - String? errorText; - final customValidation = widget.validator?.call(currentValue); - if ((currentValue == null && widget.requirementLevel != ZetaFormFieldRequirement.optional) || - customValidation != null) { - errorText = customValidation ?? widget.errorText ?? ''; - } - - return errorText; - }, - controller: _inputController, - prefix: _selectedItem?.icon ?? widget.prefix, - label: widget.label, - placeholder: widget.placeholder, - hintText: widget.hintText, - onChange: (val) => _onInputChanged(controller), - suffix: InputIconButton( - semanticLabel: widget.dropdownSemantics, - icon: _dropdownOpen ? ZetaIcons.expand_less : ZetaIcons.expand_more, - disabled: widget.disabled, - size: widget.size, - color: colors.iconSubtle, - onTap: () => _onIconTapped(controller), - ), - ); - }, - ), - ); + void onInputChanged(ZetaDropdownController dropdownController) { + dropdownController.open(); + _filterItems(); + } + + void onIconTapped(ZetaDropdownController dropdownController) { + dropdownController.toggle(); + if (dropdownController.isOpen) { + inputFocusNode.requestFocus(); + setState(() { + currentItems = widget.items; + }); + } + } + + void setItem(ZetaDropdownItem item) { + inputController.text = item.label; + setState(() { + _selectedItem = item; + }); + super.didChange(item.value); + widget.onChange?.call(item.value); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty>>('dropdownKey', dropdownKey)) + ..add(DiagnosticsProperty('inputController', inputController)) + ..add(DiagnosticsProperty('inputFocusNode', inputFocusNode)) + ..add(IterableProperty>('currentItems', currentItems)); } } diff --git a/lib/src/components/text_input/internal_text_input.dart b/lib/src/components/text_input/internal_text_input.dart new file mode 100644 index 00000000..d9d99e3e --- /dev/null +++ b/lib/src/components/text_input/internal_text_input.dart @@ -0,0 +1,380 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../../zeta_flutter.dart'; +import 'hint_text.dart'; +import 'input_label.dart'; + +/// Text inputs allow the user to enter text. +/// Not intended for external use. +class InternalTextInput extends ZetaStatefulWidget { + /// Creates a new [InternalTextInput] + const InternalTextInput({ + super.key, + this.onChange, + this.disabled = false, + ZetaFormFieldRequirement? requirementLevel, + super.rounded, + this.label, + this.hintText, + this.placeholder, + this.errorText, + this.controller, + this.suffix, + this.prefix, + this.size = ZetaWidgetSize.medium, + this.inputFormatters, + this.prefixText, + this.prefixTextStyle, + this.suffixText, + this.suffixTextStyle, + this.onSubmit, + this.obscureText = false, + this.keyboardType, + this.focusNode, + this.externalPrefix, + this.semanticLabel, + this.borderRadius, + }) : requirementLevel = requirementLevel ?? ZetaFormFieldRequirement.none, + assert(prefix == null || prefixText == null, 'Only one of prefix or prefixText can be accepted.'), + assert(suffix == null || suffixText == null, 'Only one of suffix or suffixText can be accepted.'); + + /// {@template text-input-label} + /// The label displayed above the input. + /// {@endtemplate} + final String? label; + + /// {@template text-input-on-submit} + /// Called when the input is submitted. + /// {@endtemplate} + final void Function(String? val)? onSubmit; + + /// {@template text-input-hint-text} + /// The hint text displayed below the input. + /// {@endtemplate} + final String? hintText; + + /// {@template text-input-placeholder} + /// The placeholder text displayed in the input. + /// {@endtemplate} + final String? placeholder; + + /// {@template text-input-error-text} + /// The error text shown beneath the input. Replaces [hintText]. + /// {@endtemplate} + final String? errorText; + + /// {@template text-input-controller} + /// The controller given to the input. + /// {@endtemplate} + final TextEditingController? controller; + + /// The widget displayed at the end of the input. Cannot be given in addition to [suffixText]. + final Widget? suffix; + + /// The text displayed at the end of the input. Cannot be given in addition to [suffix]. + final String? suffixText; + + /// The style applied to [suffixText]. + final TextStyle? suffixTextStyle; + + /// The widget displayed at the start of the input. Cannot be given in addition to [prefixText]. + final Widget? prefix; + + /// The text displayed at the end of the input. Cannot be given in addition to [prefix]. + final String? prefixText; + + /// The style applied to [prefixText]. + final TextStyle? prefixTextStyle; + + ///{@template text-input-size} + /// The size of the input. + /// {@endtemplate} + final ZetaWidgetSize size; + + /// The input formatters given to the text input. + final List? inputFormatters; + + /// {@template text-input-obscure-text} + /// Obscures the text within the input. + /// {@endtemplate} + final bool obscureText; + + /// The keyboard type of the input. + final TextInputType? keyboardType; + + /// The focus node of the input. + final FocusNode? focusNode; + + /// The border radius of the input. + final BorderRadius? borderRadius; + + /// Value passed to the wrapping [Semantics] widget. + /// + /// If null, the label will be used. + /// + /// {@macro zeta-widget-semantic-label} + final String? semanticLabel; + + /// Called when the input changes. + final ValueChanged? onChange; + + /// Disables the input. + final bool disabled; + + /// The requirement level of the input. + /// Defaults to [ZetaFormFieldRequirement.none] + final ZetaFormFieldRequirement requirementLevel; + + /// The widget displayed before the input. + final Widget? externalPrefix; + + @override + State createState() => InternalTextInputState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('label', label)) + ..add(StringProperty('hintText', hintText)) + ..add(StringProperty('placeholder', placeholder)) + ..add(StringProperty('errorText', errorText)) + ..add(DiagnosticsProperty('controller', controller)) + ..add(ObjectFlagProperty?>.has('onChanged', onChange)) + ..add(StringProperty('suffixText', suffixText)) + ..add(DiagnosticsProperty('suffixTextStyle', suffixTextStyle)) + ..add(StringProperty('prefixText', prefixText)) + ..add(DiagnosticsProperty('prefixTextStyle', prefixTextStyle)) + ..add(EnumProperty('size', size)) + ..add(DiagnosticsProperty('rounded', rounded)) + ..add(DiagnosticsProperty('disabled', disabled)) + ..add(IterableProperty('inputFormatters', inputFormatters)) + ..add(EnumProperty('requirementLevel', requirementLevel)) + ..add(ObjectFlagProperty.has('onSubmit', onSubmit)) + ..add(DiagnosticsProperty('obscureText', obscureText)) + ..add(DiagnosticsProperty('keyboardType', keyboardType)) + ..add(DiagnosticsProperty('focusNode', focusNode)) + ..add(DiagnosticsProperty('borderRadius', borderRadius)) + ..add(StringProperty('semanticLabel', semanticLabel)); + } +} + +/// The current state of a [InternalTextInput] +class InternalTextInputState extends State { + late final TextEditingController _controller; + ZetaColors get _colors => Zeta.of(context).colors; + + // TODO(UX-1143): refactor to use WidgetStateController + bool _hovered = false; + + Color get _backgroundColor { + if (widget.disabled) { + return _colors.surfaceDisabled; + } + if (widget.errorText != null) { + return _colors.error.shade10; + } + return _colors.surfacePrimary; + } + + TextStyle get _baseTextStyle { + TextStyle style = ZetaTextStyles.bodyMedium; + if (widget.size == ZetaWidgetSize.small) { + style = ZetaTextStyles.bodyXSmall; + } + return style; + } + + EdgeInsets get _contentPadding { + switch (widget.size) { + case ZetaWidgetSize.large: + return const EdgeInsets.symmetric( + horizontal: ZetaSpacing.medium, + vertical: ZetaSpacing.large, + ); + case ZetaWidgetSize.small: + case ZetaWidgetSize.medium: + return const EdgeInsets.symmetric( + horizontal: ZetaSpacing.medium, + vertical: ZetaSpacing.small, + ); + } + } + + TextStyle get _affixStyle { + Color color = _colors.textSubtle; + if (widget.disabled) { + color = _colors.textDisabled; + } + return _baseTextStyle.copyWith(color: color); + } + + BoxConstraints get _affixConstraints { + late final double width; + late final double height; + switch (widget.size) { + case ZetaWidgetSize.large: + width = ZetaSpacing.xl_6; + height = ZetaSpacing.xl_8; + case ZetaWidgetSize.medium: + width = ZetaSpacing.xl_6; + height = ZetaSpacing.xl_6; + case ZetaWidgetSize.small: + width = ZetaSpacing.xl_6; + height = ZetaSpacing.xl_4; + } + return BoxConstraints( + minWidth: width, + maxHeight: height, + ); + } + + Widget? get _prefix => _getAffix( + widget: widget.prefix, + text: widget.prefixText, + textStyle: widget.prefixTextStyle, + ); + + Widget? get _suffix => _getAffix( + widget: widget.suffix, + text: widget.suffixText, + textStyle: widget.suffixTextStyle, + ); + + Widget? _getAffix({ + required Widget? widget, + required String? text, + required TextStyle? textStyle, + }) { + if (widget == null && text == null) return null; + + late final Widget child; + if (widget != null) child = widget; + if (text != null) { + child = Center( + widthFactor: 1, + child: Text( + text, + ), + ).paddingHorizontal(ZetaSpacing.small); + } + final style = textStyle ?? _affixStyle; + return DefaultTextStyle( + style: style.copyWith(height: 1.5), + textHeightBehavior: const TextHeightBehavior( + applyHeightToFirstAscent: false, + ), + child: child, + ); + } + + OutlineInputBorder _baseBorder(bool rounded) => OutlineInputBorder( + borderRadius: widget.borderRadius ?? (rounded ? ZetaRadius.minimal : ZetaRadius.none), + borderSide: BorderSide( + color: !widget.disabled ? (_hovered ? _colors.borderSelected : _colors.borderSubtle) : _colors.borderDefault, + ), + ); + + OutlineInputBorder _focusedBorder(bool rounded) => _baseBorder(rounded).copyWith( + borderSide: BorderSide(color: _colors.primary.shade50, width: ZetaSpacingBase.x0_5), + ); // TODO(mikecoomber): change to colors.borderPrimary when added + + OutlineInputBorder _errorBorder(bool rounded) => _baseBorder(rounded).copyWith( + borderSide: BorderSide(color: _colors.error, width: ZetaSpacingBase.x0_5), + ); + + @override + void initState() { + _controller = widget.controller ?? TextEditingController(); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final rounded = context.rounded; + + return ZetaRoundedScope( + rounded: rounded, + child: Semantics( + label: widget.semanticLabel ?? widget.hintText, + enabled: !widget.disabled, + excludeSemantics: widget.disabled, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.label != null) ...[ + ZetaInputLabel( + label: widget.label!, + requirementLevel: widget.requirementLevel, + disabled: widget.disabled, + ), + const SizedBox(height: ZetaSpacing.minimum), + ], + Row( + children: [ + if (widget.externalPrefix != null) widget.externalPrefix!, + Expanded( + child: MouseRegion( + onEnter: !widget.disabled + ? (_) => setState(() { + _hovered = true; + }) + : null, + onExit: !widget.disabled + ? (_) => setState(() { + _hovered = false; + }) + : null, + child: TextField( + enabled: !widget.disabled, + controller: _controller, + keyboardType: widget.keyboardType, + inputFormatters: widget.inputFormatters, + textAlignVertical: TextAlignVertical.center, + onChanged: widget.onChange, + onSubmitted: widget.onSubmit, + style: _baseTextStyle, + cursorErrorColor: _colors.error, + obscureText: widget.obscureText, + focusNode: widget.focusNode, + decoration: InputDecoration( + isDense: true, + contentPadding: _contentPadding, + filled: true, + prefixIcon: _prefix, + prefixIconConstraints: widget.prefixText != null ? _affixConstraints : null, + suffixIcon: _suffix, + suffixIconConstraints: widget.suffixText != null ? _affixConstraints : null, + focusColor: _backgroundColor, + hoverColor: _backgroundColor, + fillColor: _backgroundColor, + enabledBorder: _baseBorder(rounded), + disabledBorder: _baseBorder(rounded), + focusedBorder: _focusedBorder(rounded), + focusedErrorBorder: _errorBorder(rounded), + errorBorder: widget.disabled ? _baseBorder(rounded) : _errorBorder(rounded), + hintText: widget.placeholder, + errorText: widget.errorText, + hintStyle: _baseTextStyle, + errorStyle: const TextStyle(height: 0.001, color: Colors.transparent), + ), + ), + ), + ), + ], + ), + ZetaHintText( + disabled: widget.disabled, + rounded: rounded, + hintText: widget.hintText, + errorText: widget.errorText, + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/components/text_input/text_input.dart b/lib/src/components/text_input/text_input.dart index 14677de6..2cd537da 100644 --- a/lib/src/components/text_input/text_input.dart +++ b/lib/src/components/text_input/text_input.dart @@ -4,90 +4,31 @@ import 'package:flutter/services.dart'; import '../../../zeta_flutter.dart'; import '../../interfaces/form_field.dart'; -import 'hint_text.dart'; -import 'input_label.dart'; - -/// Helper function to create a text input with a custom border. -/// Not intended for external use. -Widget textInputWithBorder({ - Key? key, - ValueChanged? onChange, - bool disabled = false, - ZetaFormFieldRequirement requirementLevel = ZetaFormFieldRequirement.none, - String? initialValue, - bool rounded = false, - String? label, - String? hintText, - String? placeholder, - String? errorText, - TextEditingController? controller, - String? Function(String?)? validator, - Widget? suffix, - Widget? prefix, - ZetaWidgetSize size = ZetaWidgetSize.medium, - List? inputFormatters, - String? prefixText, - TextStyle? prefixTextStyle, - String? suffixText, - TextStyle? suffixTextStyle, - void Function(String? val)? onSubmit, - bool obscureText = false, - TextInputType? keyboardType, - FocusNode? focusNode, - BorderRadius? borderRadius, -}) { - return ZetaTextInput._border( - key: key, - onChange: onChange, - disabled: disabled, - requirementLevel: requirementLevel, - initialValue: initialValue, - rounded: rounded, - label: label, - hintText: hintText, - placeholder: placeholder, - errorText: errorText, - controller: controller, - validator: validator, - suffix: suffix, - prefix: prefix, - size: size, - inputFormatters: inputFormatters, - prefixText: prefixText, - prefixTextStyle: prefixTextStyle, - suffixText: suffixText, - suffixTextStyle: suffixTextStyle, - onSubmit: onSubmit, - obscureText: obscureText, - keyboardType: keyboardType, - focusNode: focusNode, - borderRadius: borderRadius, - ); -} +import 'internal_text_input.dart'; /// Text inputs allow the user to enter text. /// /// To show error messages on the text input, use the [validator]. The string returned from this function will be displayed as the error message. /// Error messages can also be managed outside the text input by setting [errorText]. -/// -/// The input can be reset and validated by Creating a key of type [ZetaTextInputState] and calling either `reset` or `validate`. -/// However, it is recommended that the input is used and validated as part of a form. /// {@category Components} -class ZetaTextInput extends ZetaFormField { +class ZetaTextInput extends ZetaTextFormField { /// Creates a new [ZetaTextInput] - const ZetaTextInput({ + ZetaTextInput({ super.key, super.onChange, super.disabled = false, super.requirementLevel = ZetaFormFieldRequirement.none, super.initialValue, - super.rounded, + super.autovalidateMode, + super.validator, + super.onSaved, + super.onFieldSubmitted, + bool? rounded, this.label, this.hintText, this.placeholder, this.errorText, - this.controller, - this.validator, + super.controller, this.suffix, this.prefix, this.size = ZetaWidgetSize.medium, @@ -101,38 +42,37 @@ class ZetaTextInput extends ZetaFormField { this.keyboardType, this.focusNode, this.semanticLabel, - }) : borderRadius = null, - assert(initialValue == null || controller == null, 'Only one of initial value and controller can be accepted.'), + }) : assert(initialValue == null || controller == null, 'Only one of initial value and controller can be accepted.'), assert(prefix == null || prefixText == null, 'Only one of prefix or prefixText can be accepted.'), - assert(suffix == null || suffixText == null, 'Only one of suffix or suffixText can be accepted.'); + assert(suffix == null || suffixText == null, 'Only one of suffix or suffixText can be accepted.'), + super( + builder: (field) { + final ZetaTextFormFieldState state = field as ZetaTextFormFieldState; - const ZetaTextInput._border({ - super.key, - super.onChange, - super.disabled = false, - super.requirementLevel = ZetaFormFieldRequirement.none, - super.initialValue, - super.rounded, - this.label, - this.hintText, - this.placeholder, - this.errorText, - this.controller, - this.validator, - this.suffix, - this.prefix, - this.size = ZetaWidgetSize.medium, - this.inputFormatters, - this.prefixText, - this.prefixTextStyle, - this.suffixText, - this.suffixTextStyle, - this.onSubmit, - this.obscureText = false, - this.keyboardType, - this.focusNode, - this.borderRadius, - }) : semanticLabel = null; + return InternalTextInput( + label: label, + rounded: rounded, + hintText: hintText, + placeholder: placeholder, + errorText: field.errorText ?? errorText, + controller: state.effectiveController, + suffix: suffix, + suffixText: suffixText, + suffixTextStyle: suffixTextStyle, + prefix: prefix, + prefixText: prefixText, + prefixTextStyle: prefixTextStyle, + size: size, + onChange: state.onChange, + onSubmit: onSubmit, + inputFormatters: inputFormatters, + obscureText: obscureText, + keyboardType: keyboardType, + focusNode: focusNode, + semanticLabel: semanticLabel, + ); + }, + ); /// {@template text-input-label} /// The label displayed above the input. @@ -159,17 +99,6 @@ class ZetaTextInput extends ZetaFormField { /// {@endtemplate} final String? errorText; - /// {@template text-input-controller} - /// The controller given to the input. Cannot be given in addition to [initialValue]. - /// {@endtemplate} - final TextEditingController? controller; - - /// {@template text-input-validator} - /// The validator passed to the input. Should return the error message to be displayed below the input. - /// Should return null if there is no error. - /// {@endtemplate} - final String? Function(String?)? validator; - /// The widget displayed at the end of the input. Cannot be given in addition to [suffixText]. final Widget? suffix; @@ -207,9 +136,6 @@ class ZetaTextInput extends ZetaFormField { /// The focus node of the input. final FocusNode? focusNode; - /// The border radius of the input. - final BorderRadius? borderRadius; - /// Value passed to the wrapping [Semantics] widget. /// /// If null, the label will be used. @@ -218,7 +144,8 @@ class ZetaTextInput extends ZetaFormField { final String? semanticLabel; @override - State createState() => ZetaTextInputState(); + FormFieldState createState() => ZetaTextFormFieldState(); + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -236,261 +163,12 @@ class ZetaTextInput extends ZetaFormField { ..add(StringProperty('prefixText', prefixText)) ..add(DiagnosticsProperty('prefixTextStyle', prefixTextStyle)) ..add(EnumProperty('size', size)) - ..add(DiagnosticsProperty('rounded', rounded)) - ..add(DiagnosticsProperty('disabled', disabled)) ..add(IterableProperty('inputFormatters', inputFormatters)) ..add(EnumProperty('requirementLevel', requirementLevel)) ..add(ObjectFlagProperty.has('onSubmit', onSubmit)) ..add(DiagnosticsProperty('obscureText', obscureText)) ..add(DiagnosticsProperty('keyboardType', keyboardType)) ..add(DiagnosticsProperty('focusNode', focusNode)) - ..add(DiagnosticsProperty('borderRadius', borderRadius)) ..add(StringProperty('semanticLabel', semanticLabel)); } } - -/// The current state of a [ZetaTextInput] -class ZetaTextInputState extends State implements ZetaFormFieldState { - late final TextEditingController _controller; - final GlobalKey> _key = GlobalKey(); - ZetaColors get _colors => Zeta.of(context).colors; - - // TODO(mikecoomber): refactor to use WidgetStateController - bool _hovered = false; - - String? _errorText; - - Color get _backgroundColor { - if (widget.disabled) { - return _colors.surfaceDisabled; - } - if (_errorText != null) { - return _colors.error.shade10; - } - return _colors.surfacePrimary; - } - - TextStyle get _baseTextStyle { - TextStyle style = ZetaTextStyles.bodyMedium; - if (widget.size == ZetaWidgetSize.small) { - style = ZetaTextStyles.bodyXSmall; - } - return style; - } - - EdgeInsets get _contentPadding { - switch (widget.size) { - case ZetaWidgetSize.large: - return const EdgeInsets.symmetric( - horizontal: ZetaSpacing.medium, - vertical: ZetaSpacing.large, - ); - case ZetaWidgetSize.small: - case ZetaWidgetSize.medium: - return const EdgeInsets.symmetric( - horizontal: ZetaSpacing.medium, - vertical: ZetaSpacing.small, - ); - } - } - - TextStyle get _affixStyle { - Color color = _colors.textSubtle; - if (widget.disabled) { - color = _colors.textDisabled; - } - return _baseTextStyle.copyWith(color: color); - } - - BoxConstraints get _affixConstraints { - late final double width; - late final double height; - switch (widget.size) { - case ZetaWidgetSize.large: - width = ZetaSpacing.xl_6; - height = ZetaSpacing.xl_8; - case ZetaWidgetSize.medium: - width = ZetaSpacing.xl_6; - height = ZetaSpacing.xl_6; - case ZetaWidgetSize.small: - width = ZetaSpacing.xl_6; - height = ZetaSpacing.xl_4; - } - return BoxConstraints( - minWidth: width, - maxHeight: height, - ); - } - - Widget? get _prefix => _getAffix( - widget: widget.prefix, - text: widget.prefixText, - textStyle: widget.prefixTextStyle, - ); - - Widget? get _suffix => _getAffix( - widget: widget.suffix, - text: widget.suffixText, - textStyle: widget.suffixTextStyle, - ); - - Widget? _getAffix({ - required Widget? widget, - required String? text, - required TextStyle? textStyle, - }) { - if (widget == null && text == null) return null; - - late final Widget child; - if (widget != null) child = widget; - if (text != null) { - child = Center( - widthFactor: 1, - child: Text( - text, - ), - ).paddingHorizontal(ZetaSpacing.small); - } - final style = textStyle ?? _affixStyle; - return DefaultTextStyle( - style: style.copyWith(height: 1.5), - textHeightBehavior: const TextHeightBehavior( - applyHeightToFirstAscent: false, - ), - child: child, - ); - } - - OutlineInputBorder _baseBorder(bool rounded) => OutlineInputBorder( - borderRadius: widget.borderRadius ?? (rounded ? ZetaRadius.minimal : ZetaRadius.none), - borderSide: BorderSide( - color: !widget.disabled ? (_hovered ? _colors.borderSelected : _colors.borderSubtle) : _colors.borderDefault, - ), - ); - - OutlineInputBorder _focusedBorder(bool rounded) => _baseBorder(rounded).copyWith( - borderSide: BorderSide(color: _colors.primary.shade50, width: ZetaSpacingBase.x0_5), - ); // TODO(mikecoomber): change to colors.borderPrimary when added - - OutlineInputBorder _errorBorder(bool rounded) => _baseBorder(rounded).copyWith( - borderSide: BorderSide(color: _colors.error, width: ZetaSpacingBase.x0_5), - ); - - @override - void initState() { - _controller = widget.controller ?? TextEditingController(); - _errorText = widget.errorText; - - if (widget.initialValue != null) { - _controller.text = widget.initialValue!; - } - super.initState(); - } - - @override - void didUpdateWidget(covariant ZetaTextInput oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.errorText != widget.errorText) { - _errorText = widget.errorText; - } - if (oldWidget.initialValue != widget.initialValue && widget.initialValue != null) { - _controller.text = widget.initialValue!; - } - } - - @override - bool validate() => _key.currentState?.validate() ?? false; - - @override - void reset() { - _key.currentState?.reset(); - _controller.clear(); - } - - @override - Widget build(BuildContext context) { - final rounded = context.rounded; - - return ZetaRoundedScope( - rounded: rounded, - child: Semantics( - label: widget.semanticLabel ?? widget.hintText, - enabled: !widget.disabled, - excludeSemantics: widget.disabled, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.label != null) ...[ - ZetaInputLabel( - label: widget.label!, - requirementLevel: widget.requirementLevel, - disabled: widget.disabled, - ), - const SizedBox(height: ZetaSpacing.minimum), - ], - MouseRegion( - onEnter: !widget.disabled - ? (_) => setState(() { - _hovered = true; - }) - : null, - onExit: !widget.disabled - ? (_) => setState(() { - _hovered = false; - }) - : null, - child: TextFormField( - enabled: !widget.disabled, - key: _key, - controller: _controller, - keyboardType: widget.keyboardType, - inputFormatters: widget.inputFormatters, - validator: (val) { - setState(() { - _errorText = widget.validator?.call(val); - }); - return _errorText; - }, - onFieldSubmitted: widget.onSubmit, - textAlignVertical: TextAlignVertical.center, - onChanged: widget.onChange, - style: _baseTextStyle, - cursorErrorColor: _colors.error, - obscureText: widget.obscureText, - focusNode: widget.focusNode, - decoration: InputDecoration( - isDense: true, - contentPadding: _contentPadding, - filled: true, - prefixIcon: _prefix, - prefixIconConstraints: widget.prefixText != null ? _affixConstraints : null, - suffixIcon: _suffix, - suffixIconConstraints: widget.suffixText != null ? _affixConstraints : null, - focusColor: _backgroundColor, - hoverColor: _backgroundColor, - fillColor: _backgroundColor, - enabledBorder: _baseBorder(rounded), - disabledBorder: _baseBorder(rounded), - focusedBorder: _focusedBorder(rounded), - focusedErrorBorder: _errorBorder(rounded), - errorBorder: widget.disabled ? _baseBorder(rounded) : _errorBorder(rounded), - hintText: widget.placeholder, - errorText: _errorText, - hintStyle: _baseTextStyle, - errorStyle: const TextStyle(height: 0.001, color: Colors.transparent), - ), - ), - ), - ZetaHintText( - disabled: widget.disabled, - rounded: rounded, - hintText: widget.hintText, - errorText: _errorText, - ), - ], - ), - ), - ); - } -} diff --git a/lib/src/components/time_input/time_input.dart b/lib/src/components/time_input/time_input.dart index 9ff63fba..03ebab25 100644 --- a/lib/src/components/time_input/time_input.dart +++ b/lib/src/components/time_input/time_input.dart @@ -6,10 +6,10 @@ import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; import '../../../zeta_flutter.dart'; import '../../interfaces/form_field.dart'; import '../buttons/input_icon_button.dart'; +import '../text_input/internal_text_input.dart'; const _maxHrValue = 23; const _max12HrValue = 12; -const _maxMinsValue = 59; /// A form field used to input time. /// @@ -17,27 +17,77 @@ const _maxMinsValue = 59; /// {@category Components} class ZetaTimeInput extends ZetaFormField { /// Creates a new [ZetaTimeInput] - const ZetaTimeInput({ + ZetaTimeInput({ super.key, super.disabled = false, super.initialValue, super.onChange, super.requirementLevel = ZetaFormFieldRequirement.none, - super.rounded, - this.use12Hr, + super.validator, + @Deprecated('Use use24HourFormat instead') bool use12Hr = true, + this.use24HourFormat = true, this.label, this.hintText, this.errorText, - this.validator, this.size = ZetaWidgetSize.medium, this.pickerInitialEntryMode, this.clearSemanticLabel, this.timePickerSemanticLabel, - }); + super.autovalidateMode, + super.onFieldSubmitted, + super.onSaved, + bool? rounded, + }) : super( + builder: (field) { + final _ZetaTimeInputState state = field as _ZetaTimeInputState; + final colors = Zeta.of(field.context).colors; + + return InternalTextInput( + label: label, + hintText: hintText, + errorText: field.errorText ?? errorText, + size: size, + placeholder: state.timeFormat, + controller: state.controller, + onSubmit: onFieldSubmitted != null ? (_) => onFieldSubmitted(state.value) : null, + requirementLevel: requirementLevel, + rounded: rounded, + disabled: disabled, + inputFormatters: [ + state.timeFormatter, + ], + suffix: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + if (state.controller.text.isNotEmpty) + InputIconButton( + icon: ZetaIcons.cancel, + semanticLabel: clearSemanticLabel, + onTap: state.clear, + disabled: disabled, + size: size, + color: colors.iconSubtle, + ), + InputIconButton( + icon: ZetaIcons.clock_outline, + semanticLabel: timePickerSemanticLabel, + onTap: state.pickTime, + disabled: disabled, + size: size, + color: colors.iconDefault, + ), + ], + ), + ); + }, + ); /// Changes the time input to 12 hour time. - /// Uses the device default if not set. - final bool? use12Hr; + /// Defaults to true. + /// + /// If you want to set this to the device's default use `MediaQuery.of(context).alwaysUse24HourFormat` + final bool use24HourFormat; /// The label for the input. final String? label; @@ -54,16 +104,6 @@ class ZetaTimeInput extends ZetaFormField { /// The initial entry mode of the time picker. final TimePickerEntryMode? pickerInitialEntryMode; - /// The validator passed to the text input. - /// Returns a string containing an error message. - /// - /// By default, the input checks for invalid hour or minute values. - /// It also checks for null values unless [requirementLevel] is set to [ZetaFormFieldRequirement.optional] - /// - /// If the default validation fails, [errorText] will be shown. - /// However, if [validator] catches any of these conditions, the return value of [validator] will be shown. - final String? Function(TimeOfDay? value)? validator; - /// Semantic label for the clear button. /// /// {@macro zeta-widget-semantic-label} @@ -75,16 +115,15 @@ class ZetaTimeInput extends ZetaFormField { final String? timePickerSemanticLabel; @override - State createState() => ZetaTimeInputState(); + FormFieldState createState() => _ZetaTimeInputState(); + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(DiagnosticsProperty('rounded', rounded)) - ..add(DiagnosticsProperty('use12Hr', use12Hr)) + ..add(DiagnosticsProperty('use12Hr', use24HourFormat)) ..add(ObjectFlagProperty?>.has('onChange', onChange)) ..add(DiagnosticsProperty('initialValue', initialValue)) - ..add(DiagnosticsProperty('disabled', disabled)) ..add(StringProperty('label', label)) ..add(StringProperty('hintText', hintText)) ..add(StringProperty('errorText', errorText)) @@ -97,93 +136,105 @@ class ZetaTimeInput extends ZetaFormField { } /// State for [ZetaTimeInput] -class ZetaTimeInputState extends State implements ZetaFormFieldState { +class _ZetaTimeInputState extends FormFieldState { // TODO(UX-1032): add AM/PM selector inline. - ZetaColors get _colors => Zeta.of(context).colors; - - final _timeFormat = 'hh:mm'; // TODO(UX-1003): needs localizing. - late final MaskTextInputFormatter _timeFormatter; - - bool _firstBuildComplete = false; - bool get _use12Hr => widget.use12Hr ?? !MediaQuery.of(context).alwaysUse24HourFormat; + @override + ZetaTimeInput get widget => super.widget as ZetaTimeInput; - final _controller = TextEditingController(); - final GlobalKey _key = GlobalKey(); + final String timeFormat = 'hh:mm'; // TODO(UX-1003): needs localizing. - String? _errorText; + bool get _use24HourFormat => widget.use24HourFormat; - bool get _showClearButton => _controller.text.isNotEmpty; + late final MaskTextInputFormatter timeFormatter; + final TextEditingController controller = TextEditingController(); - int get _hrsLimit => _use12Hr ? _max12HrValue : _maxHrValue; - final int _minsLimit = _maxMinsValue; - - TimeOfDay? get _value { - final splitValue = _timeFormatter.getMaskedText().trim().split(':'); - if (splitValue.length > 1) { - final hrsValue = int.tryParse(splitValue[0]); - final minsValue = int.tryParse(splitValue[1]); - if (hrsValue != null && minsValue != null) { - return TimeOfDay(hour: hrsValue, minute: minsValue); - } - } - return null; - } + int get _hrsLimit => !_use24HourFormat ? _max12HrValue : _maxHrValue; @override void initState() { - _timeFormatter = MaskTextInputFormatter( - mask: _timeFormat.replaceAll(RegExp('[a-z]'), '#'), + timeFormatter = MaskTextInputFormatter( + mask: timeFormat.replaceAll(RegExp('[a-z]'), '#'), filter: {'#': RegExp('[0-9]')}, type: MaskAutoCompletionType.eager, ); - if (widget.initialValue != null) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _key.currentState?.validate(); - }); - } + _setValue(widget.initialValue); + controller.addListener(_onChange); super.initState(); } @override void dispose() { - _controller.dispose(); - + controller.dispose(); super.dispose(); } - void _onChange() { - if (_timeFormatter.getUnmaskedText().length > (_timeFormat.length - 2) && - (_key.currentState?.validate() ?? false)) { - widget.onChange?.call(_value); + @override + void reset() { + _setValue(widget.initialValue); + super.reset(); + } + + void clear() { + _setValue(null); + } + + void _setValue(TimeOfDay? value) { + final timeOfDayStr = _timeOfDayToString(value); + timeFormatter.formatEditUpdate( + TextEditingValue.empty, + TextEditingValue(text: timeOfDayStr), + ); + controller.text = timeOfDayStr; + } + + TimeOfDay? _parseValue() { + final splitValue = timeFormatter.getMaskedText().trim().split(':'); + if (splitValue.length > 1 && splitValue[1].length > 1) { + final hrsValue = int.tryParse(splitValue[0]); + final minsValue = int.tryParse(splitValue[1]); + if (hrsValue != null && minsValue != null) { + return TimeOfDay(hour: hrsValue, minute: minsValue); + } } - setState(() {}); + + return null; } - void _setText(TimeOfDay value) { - final hrsValue = _use12Hr && value.hour > _hrsLimit ? value.hour - _hrsLimit : value.hour; + String _timeOfDayToString(TimeOfDay? value) { + if (value == null) return ''; + + final hrsValue = !_use24HourFormat && value.hour > _hrsLimit ? value.hour - _hrsLimit : value.hour; final hrText = hrsValue.toString().padLeft(2, '0'); final minText = value.minute.toString().padLeft(2, '0'); - _controller.text = _timeFormatter.maskText(hrText + minText); - _timeFormatter.formatEditUpdate(TextEditingValue.empty, _controller.value); + return timeFormatter.maskText(hrText + minText); + } + + void _onChange() { + final newValue = _parseValue(); + super.didChange(newValue); + if (newValue != value) { + widget.onChange?.call(newValue); + } } - Future _pickTime() async { + Future pickTime() async { final rounded = context.rounded; + final colors = Zeta.of(context).colors; final result = await showTimePicker( context: context, initialEntryMode: widget.pickerInitialEntryMode ?? TimePickerEntryMode.dial, - initialTime: _value ?? TimeOfDay.now(), + initialTime: value ?? TimeOfDay.now(), builder: (BuildContext context, Widget? child) { return Theme( data: Theme.of(context).copyWith( timePickerTheme: TimePickerThemeData( - dialBackgroundColor: _colors.warm.shade30, - dayPeriodColor: _colors.primary, + dialBackgroundColor: colors.warm.shade30, + dayPeriodColor: colors.primary, shape: RoundedRectangleBorder( borderRadius: rounded ? ZetaRadius.rounded : ZetaRadius.none, ), @@ -198,89 +249,23 @@ class ZetaTimeInputState extends State implements ZetaFormFieldSt ), ), child: MediaQuery( - data: MediaQuery.of(context).copyWith(alwaysUse24HourFormat: !_use12Hr), + data: MediaQuery.of(context).copyWith(alwaysUse24HourFormat: _use24HourFormat), child: child!, ), ); }, ); if (result != null) { - _setText(result); + _setValue(result); } } @override - void reset() { - _timeFormatter.clear(); - _key.currentState?.reset(); - setState(() { - _errorText = null; - }); - _controller.clear(); - widget.onChange?.call(null); - } - - @override - bool validate() => _key.currentState?.validate() ?? false; - - @override - Widget build(BuildContext context) { - if (!_firstBuildComplete && widget.initialValue != null) { - _setText(widget.initialValue!); - _firstBuildComplete = true; - } - return ZetaTextInput( - disabled: widget.disabled, - key: _key, - size: widget.size, - errorText: _errorText, - rounded: widget.rounded, - label: widget.label, - hintText: widget.hintText, - placeholder: _timeFormat, - controller: _controller, - inputFormatters: [ - _timeFormatter, - ], - validator: (_) { - String? errorText; - final customValidation = widget.validator?.call(_value); - if ((_value == null && widget.requirementLevel != ZetaFormFieldRequirement.optional) || - (_value != null && (_value!.hour > _hrsLimit || _value!.minute > _minsLimit)) || - customValidation != null) { - errorText = customValidation ?? widget.errorText ?? ''; - } - - setState(() { - _errorText = errorText; - }); - return errorText; - }, - onChange: (_) => _onChange(), - requirementLevel: widget.requirementLevel, - suffix: Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - if (_showClearButton) - InputIconButton( - icon: ZetaIcons.cancel, - semanticLabel: widget.clearSemanticLabel, - onTap: reset, - disabled: widget.disabled, - size: widget.size, - color: _colors.iconSubtle, - ), - InputIconButton( - icon: ZetaIcons.clock_outline, - semanticLabel: widget.timePickerSemanticLabel, - onTap: _pickTime, - disabled: widget.disabled, - size: widget.size, - color: _colors.iconDefault, - ), - ], - ), - ); + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('timeFormat', timeFormat)) + ..add(DiagnosticsProperty('timeFormatter', timeFormatter)) + ..add(DiagnosticsProperty('controller', controller)); } } diff --git a/lib/src/components/top_app_bar/top_app_bar.dart b/lib/src/components/top_app_bar/top_app_bar.dart index a02eb5da..e70d90cd 100644 --- a/lib/src/components/top_app_bar/top_app_bar.dart +++ b/lib/src/components/top_app_bar/top_app_bar.dart @@ -5,7 +5,7 @@ import '../../../zeta_flutter.dart'; import 'extended_top_app_bar.dart'; import 'search_top_app_bar.dart'; -export 'search_top_app_bar.dart' show ZetaSearchController; +export 'search_top_app_bar.dart' hide ZetaTopAppBarSearchField; /// Top app bars provide content and actions related to the current screen. /// {@category Components} diff --git a/lib/src/interfaces/form_field.dart b/lib/src/interfaces/form_field.dart index 8f5152f7..353468e1 100644 --- a/lib/src/interfaces/form_field.dart +++ b/lib/src/interfaces/form_field.dart @@ -1,46 +1,136 @@ import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import '../../zeta_flutter.dart'; /// An interface for all form fields used in Zeta -abstract class ZetaFormFieldState { - /// Validates the form field. Returns true if there are no errors. - bool validate(); - - /// Resets the form field to its initial state. - void reset(); -} - -/// A common interface shared with all Zeta form elements. -abstract class ZetaFormField extends ZetaStatefulWidget { +abstract class ZetaFormField extends FormField { /// Creates a new [ZetaFormField] const ZetaFormField({ - required this.disabled, - required this.initialValue, + required super.builder, + required super.autovalidateMode, + required super.initialValue, + required super.validator, + required super.onSaved, required this.onChange, - required this.requirementLevel, - super.rounded, + required this.onFieldSubmitted, + ZetaFormFieldRequirement? requirementLevel, + bool disabled = false, super.key, - }); + }) : requirementLevel = requirementLevel ?? ZetaFormFieldRequirement.none, + super( + enabled: !disabled, + ); - /// {@macro zeta-widget-disabled} - final bool disabled; + /// Called whenever the form field changes. + final ValueChanged? onChange; - /// The initial value of the form field. - final T? initialValue; + /// Called whenever the form field is submitted. + final ValueChanged? onFieldSubmitted; - /// Called with the current value of the field whenever it is changed. - final ValueChanged? onChange; + /// The requirement level of the form field. + final ZetaFormFieldRequirement? requirementLevel; - /// The requirement level of the form field, e.g. mandatory or optional. - final ZetaFormFieldRequirement requirementLevel; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(DiagnosticsProperty('disabled', disabled)) - ..add(DiagnosticsProperty('initialValue', initialValue)) ..add(ObjectFlagProperty?>.has('onChange', onChange)) - ..add(EnumProperty('requirementLevel', requirementLevel)); + ..add(ObjectFlagProperty?>.has('onFieldSubmitted', onFieldSubmitted)) + ..add(EnumProperty('requirementLevel', requirementLevel)); + } +} + +/// A text form field used in Zeta +abstract class ZetaTextFormField extends ZetaFormField { + /// Creates a new [ZetaTextFormField] + ZetaTextFormField({ + required super.builder, + required super.autovalidateMode, + required super.validator, + required super.onSaved, + required super.onChange, + required super.onFieldSubmitted, + required super.disabled, + required super.requirementLevel, + required this.controller, + required String? initialValue, + super.key, + }) : super( + initialValue: controller != null ? controller.text : (initialValue ?? ''), + ); + + /// The controller for the text form field. + final TextEditingController? controller; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('controller', controller)); + } +} + +/// The state for a [ZetaTextFormField] +class ZetaTextFormFieldState extends FormFieldState { + @override + ZetaTextFormField get widget => super.widget as ZetaTextFormField; + + /// The effective controller for the form field. + /// This is either the controller passed in or a new controller. + late final TextEditingController effectiveController; + + @override + void initState() { + effectiveController = widget.controller ?? TextEditingController(); + + if (widget.initialValue != null) { + effectiveController.text = widget.initialValue!; + } + effectiveController.addListener(_handleControllerChange); + super.initState(); + } + + @override + void didUpdateWidget(covariant ZetaTextFormField oldWidget) { + if (oldWidget.initialValue != widget.initialValue && widget.initialValue != null) { + effectiveController.text = widget.initialValue!; + } + super.didUpdateWidget(oldWidget); + } + + @override + void reset() { + effectiveController.text = widget.initialValue ?? ''; + super.reset(); + widget.onChange?.call(effectiveController.text); + } + + /// Called whenever the form field changes. + void onChange(String? value) { + didChange(value); + widget.onChange?.call(value); + } + + @override + void didChange(String? value) { + super.didChange(value); + + if (effectiveController.text != value) { + effectiveController.text = value ?? ''; + } + } + + void _handleControllerChange() { + if (effectiveController.text != value) { + didChange(effectiveController.text); + } + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add( + DiagnosticsProperty('effectiveController', effectiveController), + ); } } diff --git a/test/src/components/password/golden/password_default.png b/test/src/components/password/golden/password_default.png index 87a49c788148fb7341411493cc2d89ca117dc9ef..8d9d57775350c1ebec7a33059e31b2fbbaad5d3a 100644 GIT binary patch delta 44 zcmdlgvsGq;KjY>UMn1;P?MyScCnqp#i9Peb|7nu7IZG)s0}yz+`njxgN@xNAQtA#@ delta 48 zcmdlgvsGq;KjUOOCeh9QjC_of^%+Gs$1=^}7JKHu%X*TvWb5jk3_#%N>gTe~DWM4f Dak~#I diff --git a/test/src/components/password/golden/password_error.png b/test/src/components/password/golden/password_error.png index f2288ab0d76c7f0fb7650483aa70694c672484e5..09070cbfcbac1f35c830961576086cea82a7e8a2 100644 GIT binary patch delta 44 zcmeB@>yq2R#kg6XaV_KKhfKmelM7h2#6HKLtGQ~x!=1mJ0SG)@{an^LB{Ts5Mb-{V delta 47 zcmeB@>yq2R#W?YS=w>d)wTzQ{nM5~lXARM9Eh4V95nBh7B9wx56nW{3ysY#Ex+D9+WZC=+zL#7s_vG$A zpL_24+*6sHl;G#P#}@$n5N?RkAq`}$Ic|GrLTwTJ|fK@E!{P?bdqLs zmu9Ms5%VMUZ8Dim>Em<3px-o!`y^pmJ)0f?tXB@!NW{_9_tZ^P>SuJiV$^J|P+DX> zPGj`?XiBqaPmO9ae`Y5DH+Ce;d5WFQJZ+)uZRVM7r8u;v0ilvh2^GmeEYwH@j8|E#~3I_N*>SkE&y$XqfDKQN+${HyzlSF82O z?=4UDw3&=20arsp46|02*)DnTK+I#kZF98@4~wPUU!8UmULq(LgPhuNMRr|ul#K%5 zBERT;Cqlj@d52oESjoJ8oueSS*XG;Lnh&K;87|NqPlM=zV+#vniG*r%)yX#;Tw9*| za?#c;J6$m?5E^IP=e5cKUNg^Kxr;3DA?#OEdB%|fUGY*oTjK>*SCK(r5w3?V9i#Nv zRmR>ktK-$Cv^Qepo7eKHRy!Mu9@h(mX6n~vh;WU5?(WRK&UERg@wc7S}1(It13_`i*a&k>o+fpqyFE5B?t1R8aiM1p=3fziz<}**R zk@EfL)k(@xyiLoCSN@OaI?Dp=(xhM_UHSY!bns{TY)5H$(sPPV?#GG zk)#7WkkA=8blRnzB!HL?$`CUch!KK`0V@RTXTW|%%n>j*#M}^bL(C1I>xOp1_mNY( Tad{0dBcdSjNK$-9TxR*-h>JX! literal 3924 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i1B%QlYbpRzjKx9jP7LeL$-D$|Sc;uI zLpXq-h9jkefr0Ozr;B4q#hkZy?epiROCET5{9B>#oQQ3cmMwJZ3VG4MF`sF|3N5uO z`iBmkIKUy~(;;}U;RnMCEg{!cH%_(9I>f-DxF9#F-KtMk{(q-rgn-bBZ-mO z^Sd@L|M~LId%j(|c=6)RlP6zR{CN0ySD%Pv1=WMDAh&(2<*T>SXej(w%MK5E*@ zFCRQOc{Bdsw}hD8zgBMCSy;Me?OMGmcLs(BlVe$7vIazz);Yco42O_?%w8&Hgn~6m$5Q1 z{E1(?c5UCwFBLlaRf0f+Cp$L)Lqw91iGd-7g@b`%f}jEegM%ALaOTNM_C9~VW7g&M z_R|mVuXz4BxwPn=C<8-7an-LcFK^tuIrH@9ygNG#r%l)Y7JF=`{a^b#Kac--_r0!Q z;o*OmzwiJ2w(#|}vpa3L85n-N=i}!;9-aTUO7DEt*Q?>je*OB@nihKyXsrpSUl=FRJ?{{FA#-Ol>$n>HEw`TOhJ?BBJ^s;|Fa-=@B( z$VzSURMvU*Mry`;@;~1xUj6vn+uP#v*cl!?erj`l1s@;ZvDfSO8%{5t-)Vk8-nPnQ z->*~G)2fc8$5&T%i*h6pTf8k&G_bbb7{{QRk|FZe^ZR^c)>TbDm$_<7O4&aB@S7_t{`=l@Sre7>s*7*#3f9T|K0xh z-?w%7K({pPoCQqI!1VgD;{VAV`?eZR>+9<~=jZ2l{4oFbWAE-YasZw6EG;*;GzXY` zUq?$P8%6#MZ_ugI1f|%6@rY!@A;7?(pzHuNvWEev2a=XX1xEv9G%)}R)zOSFnh{1b r!e~Yqts+O82cu1+(b^DOZTQ*$r1yo>`+0UfVgLe9S3j3^P6{Lb&3 zb6bK3gX1V~-n*sh==#YmE$AQ4T}SJC8;`uH6% zIvFxfBAePlsNjCg^QeDG$}WSKI~5j6BS#Z;cW)|yT+bCrvW#BgM)BQtMfouQ&(b~5 z&YkSQwDJ_92pnakb1wv~dhTJI*A9p>RuHAxv|8u(iH`@xqQk}h>*#jkSqr4=vk~a|Wwyxi%cdZM%`zzl@*#e^@uT_tg!D`EIf?zo-jEKQws+yrp#1`{>s>s`ZV<}bk6N2Z2WpZOPj|foLH!Bx z(s-Q}KDCph8w;-v0)pjd4%7@aQw1EfwwVKq(P-=t`*~@+av(ddVHlXbBFgbGu z)UsxORGDyf_Lg_pUEK>0=$EM_OIf?7iOb=PoIB}r`4YS`F+N@;E=Ja}2q=K6+rDZ# ze+^^8-)W^k-Sspw4#%X6(5O0{RSkqHo?9ZNi&Pujeu`B8Q-Bvcjq_PDK&32por3I6V8m z(^jGud2GK8on0EwK%)d&ecg^ykwlPcMPWF30OOc;h4fP_BZk{}_}^z4-()=jbQ&-| zeoCZ{Yp*ln2$9y`nKAuk#R956hyH(5Oqt4{z1toVqxA9E6N3E7{<T8J$n*{196#f^qD=7Q^7jqIO5avT zqXU>mt5$$NJCMH4vNnZ)%HJ&2ESq+?&}<82G)mg)7=@7!$dl%mXwxm8iNAg>fZ=C&BIPs86^teR!OxI_rYHB1@d|P>mjCxJfU7lMg`re4MVY|);OB)#F^!;R zcP!SkeqsIm`6}w0t<(>Kh${UQPmD0sagiM}t4mWqC9y-B0wCyJQn;@?+#Anr6Gn#S zn5@^qJkbqX@?-HJ354M@bNrco5BKr!?>D#|a(TV%yIFnbq1z#F27Ug|l+&H3&VBn| Dpub`5 delta 2031 zcmW+%dr;F?7XE>&tYS@TyW;Y6rj_g#knKu@66CcltQDaMNq_*+8VM#yup;68W0tW@ z2ea5ZNO=Ur$SXin2qBsfQl#=w1FIM!#1LsTD0%S;F@Yp|}|K$3+Ya}zu`OZhevsLY}*954kxOPQB(*X5+T?X5=`ojcM{=8{BS{Tui!CG!w zA}(>J*26l!@Z8sNWoLnD7wX_%%}QQMEb|f_9#gr~$58d@lN%5e8OfdNED&Oj>aaX~ zub}AMU?^j5d5Xp++Q!a16DdLh+8U+XGnKkvD^P#TbrmTKTB%)dww z^cxSwh^O!)Iu*8CgESMxY|{xY3u*eIslObOqf2;$eHc-0$4x`PyaIwMsI8O4)06~0 zOB~$UwpD$lJD8^KBkjA6Y{^YVx${a51(L@DfkP<}^s@M%H=S&Kn}uN}8p297ap&Sc zxIRftYw78kt!#7T&y-XmL4IT3jFQ9aXf%#6tE3^X=dOLcgBao|Mb_Xn)>s5(mhA#T zE2tkv6U&DB(|m?#-=j+}R_v`}re(mH$|BNRUci0`x|``8k;fj7NS*~?jn|xR;XG1UqO=&7)5gJvk6jyGlg^JeR#vD%(sOk}* z`sgf@-1ySQDDD@m)*6PB{|d0c(8qqH3PauL%Sa#VbgjSE6u!-9xZvsqL4^w68z+v# zw(W1!f)Z0Ms*y8PznsF6cS&^RI;~!I-)(BQ`6s-@bWMpD~#Z5RsunLOOk{Zz4EEwxP@*LnYd9RcWb3rTR;RXJj5Z%$rcS{rGX5mzP&Y;dXWqyFh5% z#fe~T9&YB>GZ5?af>AIT`IF;E_Um*N%43nI>S-J;I#Ehq*V(9Hv z@Tk&TQg;~KSrK^BV^A}>xRQ1Si}~a(=p?s>H)42grkJAB*OzZ;=hj^_c7Xo7n>O8i zoD)wn#EHH3?mJQ@s;emwhJKaS zoBG55Y<|P+`RUMmuiTGp!FRRqW)CA8bNGRqKZ74X*BNjeG!BEIFXn|P=I6RaqRQf} zk-3E|2L)JhS{W8-NN&Jm@pGqpqJI$N!Yx!i9JjDB4*;YP!4^*&^%LGF%S~h$@E(|LE)D^JKu3OAY213Ux6qanKu2)B- z2*fFxgkaVCV2kpFwbMd}?nxWF$wA}}u}_BML6KIg#{sdu3NO5<3#Z!D2xZYbZ0~VJ z&*J}jfo}a~_S#oNE<4vduVVUUCgtv z0)L2QS~j>##y5&KnIHtHhox|vC|bxI#veV(37@dkq7Pv(+@m^xiNf-RB+f@4fFL;U z_UK|_*#?);bkGNb)K&}yG2$I!lr{`^91t-42!8{;BpKui2^ydpHlU8#P22LM9k1Gu z-P7by#_XE)LBsLZY=Eh4V95nBh7B9wx56nW{3ysY#Ex+D9+WZC=+zL#7s_vG$A zpL_24+*6sHl;G#P#}@$n5N?RkAq`}$Ic|GrLTwTJ|fK@E!{P?bdqLs zmu9Ms5%VMUZ8Dim>Em<3px-o!`y^pmJ)0f?tXB@!NW{_9_tZ^P>SuJiV$^J|P+DX> zPGj`?XiBqaPmO9ae`Y5DH+Ce;d5WFQJZ+)uZRVM7r8u;v0ilvh2^GmeEYwH@j8|E#~3I_N*>SkE&y$XqfDKQN+${HyzlSF82O z?=4UDw3&=20arsp46|02*)DnTK+I#kZF98@4~wPUU!8UmULq(LgPhuNMRr|ul#K%5 zBERT;Cqlj@d52oESjoJ8oueSS*XG;Lnh&K;87|NqPlM=zV+#vniG*r%)yX#;Tw9*| za?#c;J6$m?5E^IP=e5cKUNg^Kxr;3DA?#OEdB%|fUGY*oTjK>*SCK(r5w3?V9i#Nv zRmR>ktK-$Cv^Qepo7eKHRy!Mu9@h(mX6n~vh;WU5?(WRK&UERg@wc7S}1(It13_`i*a&k>o+fpqyFE5B?t1R8aiM1p=3fziz<}**R zk@EfL)k(@xyiLoCSN@OaI?Dp=(xhM_UHSY!bns{TY)5H$(sPPV?#GG zk)#7WkkA=8blRnzB!HL?$`CUch!KK`0V@RTXTW|%%n>j*#M}^bL(C1I>xOp1_mNY( Tad{0dBcdSjNK$-9TxR*-h>JX! literal 3959 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i1B%QlYbpRzjKx9jP7LeL$-D$|Sc;uI zLpXq-h9jkefq`Gq)5S5QV$R#UzS(mtB^y50PntY$<`%`7R>oEwts7ef(=RqCG;zDK zy0RQRe<60^eCL9f%LT%lxLHMGIf4o;U-igpFS@*O<=kbNm*(ve5YKDR@-qt0dj37y z{`=?shi^W4WU0?xU0v|ftyU}$)L2UOR8 zrDT^4$h3-U-N3+*muF&OU^u}cz`&rO>;RNM^JE9}|2?}c?S4Es7}LJ@%cWmGo~&Mf z#*~|ZVa~j}ckdd`KKpFeT)*YXsi~>!ZYHyz;IIGk_~Y%_`{%#jpkMd!^W$S*vy0C% zF)%EDKDYdzP2SzA@^v@0!}a=mdsi2qm9uu>YHx3U{O#@jc`xL=y}ciQ_)zff=hLT8 zH}2nW|NYOwgAZr;Ogs1CPqX37Gygx$v)*o_7rV>kyA1))<^{Cxk9r*}T*@B6v) zj z-qOOtX6j}?5ry}6EB}W~1DbK}+_PtCWz)cx%uKdt+*$bd)(#+c-w2EqftxpWSecov zd-lAF31t7glMIm&5f&e9r~NzKp7F0;)z6}C+h=cY_20kMCkxlw0G(%d#rS9ZvtPe{ zReZlDf4+(rm=vejPWE>IC8NmIFbD)VXySjYM+I2lgyI+eRTRd0&@w>gc{!T#k!KSm{_V50_4Jdg0@uOd_B5vHi zZEa;?A@f~6r+@9+SV5o%=FECoQVG)C{d#q>QRFZGhB;q>nfk!-=KO;|X*^CtBw{4@ zjtYXJbu>V5r|8iXGMYm0mwBTpWHg10rVx~7;%J33TA{%E45Nu*I46eB`3pDQPA<7M R0c)I$ztaD0e0stw@Sv&v$ diff --git a/test/src/components/search_bar/golden/search_bar_sharp.png b/test/src/components/search_bar/golden/search_bar_sharp.png index 7bbccd7dc701a09f34ce391c04a50282c9a2aeea..27a2b4e39045270e765510ac680ea3bb2086d108 100644 GIT binary patch delta 704 zcmV;x0zdtS9m5@vL4R#YL_t(|obBDekKc0~!14E8uPFWD$0=$s_z{CmA`%0$MPfy# zN+eC1bg)R{HW5aHCXE3h5tILbL4t%q4C2RtDB3IfL#x#*|A1U_-_PCiN*|B;{rbG# z$=&k2?~j}N?DMbx9smFUfrT-~xbNOOHv1j`0Kos;7hiri<_9Q0ssJH7S=lO%dz9*x8HuBY1s`oTt9Bvee+DS006*rN00tAmY0`jnq6F69DDZM zHfjHw1LU1|-y0u&^65;=9(>^babW+xnPve1fa#7Ld2PJ$=36t(?tSWsv9xq((*89E zNa50@OXK?=ew_4-g)Ljg#g&zrf9JO@ER4S{UYvRU(#p!1-?C-X_Fr6C84FYPZ{0d> z-*fBC^8f(wZzcuEBM(0`c5J_H)ibBh{4w7D;KMaN_tfb#hSU=IOC(=dM-HoIG`M96o$x-F8k2kjEc;bS&O<F`ER*2^DS!G$gfG7Q%DDRKt5!X8{``e0D*yn14RY|{v*X%px2<~S z?Adecx^q&19R2C%^|}WD01*55<4^LP-hu@H0A$wu3dn)|`^JI&`!?b(006)STUt6a zmX;1}+J3Kq0001)%Q8Ub*5MiRbJLzOaeU0pPuxH8Co?zk0rT^h&zms}h>vR9_XLjeudH&4W z_ZNSBH~;_u0@E?ZIC=c}Ri6U@0C>V}Ub!~*tojfD0Kn5jfRWxFe*_2s0FV(N002N{ zI_tpwot?3H>B>@;pFDAVtWRe?0ssIoqX!QjjIFJ$r9A7vfdgaDp80;unEBiP?^}P2 z_ul_-u4Vu9!MpE_Lx&D712ibcgA#q_igs>T^~n|9GQC_0D%9OS*}stxpQZnJ$r5`dmoE8z;EBaJzhKY`a;im z;poxv%hjuM&%gT0%j2uBznOdf#EF;2rAwDrw*T15GEEI7W}Df!7{vA=&hUHEk&003D0 zCG^7n{rktyKmD-kV*mgE3wkVm0RaF2o(z+L1VkQ8#~5St%C%LW0{{Sc3as6`ckkh< s4*>uGJRMxY0<*yb>1YQ>N7Y&Sei1B%QlYbpRzjKx9jP7LeL$-D$|Sc;uI zLpXq-h9jkefr0O(r;B4q#hkZy4(81Xmuz_Wc;nj4$v$sd#1xgZRxyP$Narz##V~lp zFmiA6(1}nI-N?j}bF)A*C!zn!GLfKDN}RsOlGP<|3vYJtSka|a`DWkcUFSD%JzwK4 zrT^@!<>!1^dHMNs=dC;D-p_x$rqE>C>B)=?3)Xk%PflK* z{@t#|ATOpq_08FJ{Vz+(FITfLFs$o%`YGj%xclP|FK$*mKKDBL_5A-;1!l)?{&n-Gy=k%Tg8viZry7^vt28IV*w`a#ne?B_dFjCJyW;YW9gI(a|m+vC< z{EwwvmIk`O&Yp#XfnkCmFt8ll8i3&<$p}=Gf?M#W<#~Pk`hPWdem^)|%vTM6fBRg_`{V8Udvd?4&Gr8R z%{JR@DAs*=>-GA_uk(A$@73nN{knetx7Lpj9`3(B|NqwQ$E?eN^sCkDdzSmpU%U6; zv$HpU{9Rwa@mm$plh%BEe8{Vo7SWx`^-Th;0qkrrFxw+WAU%&R=EyhpZ^y`af{ssoL*@NHqKN79p$AoX#c1>c# zIVoUlP2Rk<^q>FB*J~;#0-g2d?OmDrKZoDzr5Z3WY>@o@`}gC$yX)_s$ccAiXJ9a5 z0w#+SHCjwhjPfS?>Q$(}T@-$6o0|BX`h(?3ubcD@X4Yu^6JcQ3@#tC|*R|A7cZC=j ze$;%(&u3szPoxJzvSHpOj z+$wtph6AEz@ivmxo3oRPs{bFeu&FY8XFtc96KIRx)uKuxAgBNE;^mLO{maQgUy)XvTgsR6=vT9I{(AZ2bvMYCQjm$@u*5-GW%#KjfN8b zl5aGHjHZy$6hcx9YBbA?W|`3}Gn!?H>~K-JxcJAstob~jPy6a;3_#%N>gTe~DWM4f D*@ybK literal 3920 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i1B%QlYbpRzjKx9jP7LeL$-D$|Sc;uI zLpXq-h9jkefr0Orr;B4q#hkZy&gRVtmp$+>x%!Qxr`k3phY-<;T(K8-H+XHxFkcaV zut`^ldz#182t}4`vpWuv4nG5~R%_&NHBJed;ud-=dAV%;5uFk2l85<6~fGa8Ic__pZZ(|wrf2Q% z@Amt4{QcMd_|{eZp1(J*8qR%}SJltJa3DBor;Y6AqaQ7-{-xZh`RASdTK@l^4@o`U zAD`;Wdn+9PyILNe{z0)`JFwL#cwMa80_}z z>FFIyD%^7EOio#DPHyhnJ=eVMNAQcgKW?}G_0fy_&!6S%`|-e?9y0xBZ_&AT#dQv5Mz&?Q?VfKA-pd)Q<o}bU&hs~<|dNur5$*%36qShqodSqH+QY+zAfvGjgNJyEByX_{rLL2pWR=>n1GSwQC(gAcD`xKR+s*~Xnws>HM{n0-$K=)9Z{Vmhb+i8XXb1<{h*Xn>3c2>t?T zG=+?&kkJ$ZY_^PMgwc#Jnh{1b0%n_iv}7QrWcbgvIN)M}w{!0^U^||{)78&qol`;+ E0Od(aq5uE@ diff --git a/test/src/components/search_bar/search_bar_test.dart b/test/src/components/search_bar/search_bar_test.dart index 97ab79b0..8c28fbe4 100644 --- a/test/src/components/search_bar/search_bar_test.dart +++ b/test/src/components/search_bar/search_bar_test.dart @@ -40,7 +40,7 @@ void main() { group('ZetaSearchBar', () { testWidgets('renders with default parameters', (WidgetTester tester) async { await tester.pumpWidget( - const TestApp( + TestApp( home: Scaffold( body: ZetaSearchBar(), ), @@ -53,7 +53,7 @@ void main() { testWidgets('golden: renders initializes correctly', (WidgetTester tester) async { await tester.pumpWidget( - const TestApp( + TestApp( home: ZetaSearchBar(), ), ); @@ -67,10 +67,8 @@ void main() { testWidgets('golden: renders size medium correctly', (WidgetTester tester) async { await tester.pumpWidget( - const TestApp( - home: ZetaSearchBar( - size: ZetaWidgetSize.medium, - ), + TestApp( + home: ZetaSearchBar(), ), ); expect(find.byType(ZetaSearchBar), findsOneWidget); @@ -83,7 +81,7 @@ void main() { testWidgets('golden: renders size small correctly', (WidgetTester tester) async { await tester.pumpWidget( - const TestApp( + TestApp( home: ZetaSearchBar( size: ZetaWidgetSize.small, ), @@ -99,7 +97,7 @@ void main() { testWidgets('golden: renders shape full correctly', (WidgetTester tester) async { await tester.pumpWidget( - const TestApp( + TestApp( home: ZetaSearchBar( shape: ZetaWidgetBorder.full, ), @@ -115,7 +113,7 @@ void main() { testWidgets('golden: renders shape sharp correctly', (WidgetTester tester) async { await tester.pumpWidget( - const TestApp( + TestApp( home: ZetaSearchBar( shape: ZetaWidgetBorder.sharp, ), @@ -133,7 +131,7 @@ void main() { const initialValue = 'Initial value'; await tester.pumpWidget( - const TestApp( + TestApp( home: Scaffold( body: ZetaSearchBar(initialValue: initialValue), ), @@ -149,7 +147,7 @@ void main() { const updatedValue = 'Updated value'; await tester.pumpWidget( - const TestApp( + TestApp( home: Scaffold( body: ZetaSearchBar(initialValue: initialValue), ), @@ -160,7 +158,7 @@ void main() { expect(find.text(initialValue), findsOneWidget); await tester.pumpWidget( - const TestApp( + TestApp( home: Scaffold( body: ZetaSearchBar(initialValue: updatedValue), ), @@ -175,7 +173,7 @@ void main() { await tester.pumpWidget( TestApp( home: Scaffold( - body: ZetaSearchBar(onChanged: callbacks.onChange), + body: ZetaSearchBar(onChange: callbacks.onChange), ), ), ); @@ -191,7 +189,7 @@ void main() { await tester.pumpWidget( TestApp( home: Scaffold( - body: ZetaSearchBar(onSubmit: callbacks.onSubmit), + body: ZetaSearchBar(onFieldSubmitted: callbacks.onSubmit), ), ), ); @@ -226,7 +224,7 @@ void main() { testWidgets('does not allow text input when disabled', (WidgetTester tester) async { await tester.pumpWidget( - const TestApp( + TestApp( home: Scaffold( body: ZetaSearchBar(disabled: true), ), @@ -240,12 +238,11 @@ void main() { expect(find.text('Disabled input'), findsNothing); }); - testWidgets('leading icon and speech-to-text button visibility', (WidgetTester tester) async { + testWidgets('speech-to-text button visibility', (WidgetTester tester) async { await tester.pumpWidget( - const TestApp( + TestApp( home: Scaffold( body: ZetaSearchBar( - showLeadingIcon: false, showSpeechToText: false, ), ), @@ -261,7 +258,7 @@ void main() { await tester.pumpWidget( TestApp( home: Scaffold( - body: ZetaSearchBar(onChanged: callbacks.onChange), + body: ZetaSearchBar(onChange: callbacks.onChange), ), ), ); @@ -278,45 +275,19 @@ void main() { verify(callbacks.onChange.call('')).called(1); }); - test('debugFillProperties sets the correct properties', () { - const size = ZetaWidgetSize.medium; - const shape = ZetaWidgetBorder.rounded; - const hint = 'Search here'; - const initialValue = 'Initial value'; - const disabled = true; - const showLeadingIcon = false; - const showSpeechToText = false; - const textInputAction = TextInputAction.search; - - final widget = ZetaSearchBar( - size: size, - shape: shape, - hint: hint, - initialValue: initialValue, - onChanged: callbacks.onChange, - onSubmit: callbacks.onSubmit, - onSpeechToText: callbacks.onSpeech, - disabled: disabled, - showLeadingIcon: showLeadingIcon, - showSpeechToText: showSpeechToText, - textInputAction: textInputAction, - ); - - final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); - widget.debugFillProperties(builder); - - expect(builder.findProperty('size'), size); - expect(builder.findProperty('shape'), shape); - expect(builder.findProperty('hint'), hint); - expect(builder.findProperty('enabled'), disabled); - expect(builder.findProperty('initialValue'), initialValue); - expect(builder.findProperty('showLeadingIcon'), showLeadingIcon); - expect(builder.findProperty('showSpeechToText'), showSpeechToText); - expect(builder.findProperty('textInputAction'), textInputAction); - expect(builder.findProperty('focusNode'), null); - expect(builder.findProperty('onChanged'), callbacks.onChange); - expect(builder.findProperty('onSpeechToText'), callbacks.onSpeech); - expect(builder.findProperty('onSubmit'), callbacks.onSubmit); + test('debugFillProperties', () { + final diagnostics = DiagnosticPropertiesBuilder(); + ZetaSearchBar().debugFillProperties(diagnostics); + + expect(diagnostics.finder('size'), 'medium'); + expect(diagnostics.finder('shape'), 'rounded'); + expect(diagnostics.finder('hintText'), 'null'); + expect(diagnostics.finder('textInputAction'), 'null'); + expect(diagnostics.finder('onSpeechToText'), 'null'); + expect(diagnostics.finder('showSpeechToText'), 'true'); + expect(diagnostics.finder('focusNode'), 'null'); + expect(diagnostics.finder('microphoneSemanticLabel'), 'null'); + expect(diagnostics.finder('clearSemanticLabel'), 'null'); }); }); }