diff --git a/.github/workflows/base.yaml b/.github/workflows/base.yaml index 4ea09a383d..c8a2319f19 100644 --- a/.github/workflows/base.yaml +++ b/.github/workflows/base.yaml @@ -42,11 +42,13 @@ jobs: - name: Install dependencies run: flutter pub get - name: Format code - run: dart format --set-exit-if-changed . + run: dart format --set-exit-if-changed lib/ test/ example/ - name: Analyze static code run: flutter analyze - name: Run tests run: flutter test --coverage + - name: Run fixes tests + run: dart fix --compare-to-golden test_fixes/ - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index b2e6a436a9..108f0b3eea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,61 @@ +# 10.0.0 + +## BREAKING CHANGES + +* Set minimal Flutter version to 3.29.0 +* Set minimal Dart version to 3.7.0 +* refactor: #1456 remove deprecated code by @deandreamatias in https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/pull/1465 + * Solve issue #1456 + * Remove `invalidateField` and `invalidateFirstField` methods from FormBuilderState. Use `fields[name]?.invalidate(errorText)` and `fields.first.invalidate(errorText)` instead. + * [FormBuilderTextField] Deprecate canRequestFocus property. Use `FocusNode.canRequestFocus.` instead. + * Assert on FormBuilderField.decoration.enabled property. Use FormBuilderField.enabled instead. + * Easy way! Only need execute `dart fix --apply` on your project to fix the following changes: + * Rename FormBuilderChoiceChip to FormBuilderChoiceChips. + * Rename FormBuilderFilterChip to FormBuilderFilterChips. + * [FormBuilderFilterChip] Remove maxChips property. + * [FormBuilderDateTimePicker] Remove resetIcon property. + * [FormBuilder] Remove onPopInvoked property. + +## Features + +* feat: #1455 improve input decoration enabled property by @deandreamatias in https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/pull/1464 + * Solve issue #1455 +* feat: #1458 improve autovalidate modes by @deandreamatias in https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/pull/1460 + * Solves issues #1364 and #1457 +* feat: #1297 Improve focus behavior by @deandreamatias in https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/pull/1453 + * Solves issues #1296, #1290, #1301, #1304 and #1292 + +# 10.0.0-dev.3 + +## BREAKING CHANGES + +* feat: #1456 remove deprecated code by @deandreamatias in https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/pull/1465 + * Solve issue #1456 + * Remove `invalidateField` and `invalidateFirstField` methods from FormBuilderState. Use `fields[name]?.invalidate(errorText)` and `fields.first.invalidate(errorText)` instead. + * [FormBuilderTextField] Deprecate canRequestFocus property. Use `FocusNode.canRequestFocus.` instead. + * Assert on FormBuilderField.decoration.enabled property. Use FormBuilderField.enabled instead. + * Easy way! Only need execute `dart fix --apply` on your project to fix the following changes: + * Rename FormBuilderChoiceChip to FormBuilderChoiceChips. + * Rename FormBuilderFilterChip to FormBuilderFilterChips. + * [FormBuilderFilterChip] Remove maxChips property. + * [FormBuilderDateTimePicker] Remove resetIcon property. + * [FormBuilder] Remove onPopInvoked property. + +## Features + +* feat: #1455 improve input decoration enabled property by @deandreamatias in https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/pull/1464 + * Solve issue #1455 + +# 10.0.0-dev.2 + +* feat: #1458 improve autovalidate modes by @deandreamatias in https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/pull/1460 + * Solves issues #1364 and #1457 + +# 10.0.0-dev.1 + +* feat: #1297 Improve focus behavior by @deandreamatias in https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/pull/1453 + * Solves issues #1296, #1290, #1301, #1304 and #1292 + # 9.7.0 ## Fixes @@ -789,11 +847,11 @@ * Export `flutter_typeahead` package so user gets access `TextFieldConfiguration` class * Deprecate `validator` attribute in FormBuilderDateTimePicker, only `validators` should be used * When TimePicker is cancelled, return original value instead of null -* Fix bug where initialTime for TimePicker defaults to 12:00, use currentTime. Closes [#234](https://github.com/danvick/flutter_form_builder/issues/234) +* Fix bug where initialTime for TimePicker defaults to 12:00, use currentTime. Closes [#234](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/234) ## [3.8.0+1] -* Fix bug where Changing readOnly of `FormBuilder` does not change readOnly of `FormBuilderDateTimePicker`. Closes [#179](https://github.com/danvick/flutter_form_builder/issues/179) +* Fix bug where Changing readOnly of `FormBuilder` does not change readOnly of `FormBuilderDateTimePicker`. Closes [#179](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/179) ## [3.8.0] @@ -802,7 +860,7 @@ * `FormBuilderFilterChip` - Creates a chip that acts like a checkbox. By [Cesar Flores](https://github.com/VOIDCRUSHER). Again! * `FormBuilderColorPicker` with help from [Benjamin](https://github.com/Reprevise) * `FormBuilderTouchSpin` replaced the confusingly named `FormBuilderStepper` which is now deprecated. -* Fix some inconsistencies in controller and focus node disposal. Courtesy of [Thomas Järvstrand](https://github.com/tjarvstrand). Should close [#230](https://github.com/danvick/flutter_form_builder/issues/230) +* Fix some inconsistencies in controller and focus node disposal. Courtesy of [Thomas Järvstrand](https://github.com/tjarvstrand). Should close [#230](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/230) * Bumped up `flutter_typeahead` from `1.7.0` to `1.8.0` ## [3.7.3] @@ -821,29 +879,29 @@ ## [3.7.0] - 5-Dec-2019 -* Included `onSaved` callback to all fields. Closes [#175](https://github.com/danvick/flutter_form_builder/issues/175) +* Included `onSaved` callback to all fields. Closes [#175](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/175) * Added `Key` option to all fields to make testing possible -* Fixed bug where custom controller not working in TypeAhead. Closes [#144](https://github.com/danvick/flutter_form_builder/issues/144) +* Fixed bug where custom controller not working in TypeAhead. Closes [#144](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/144) * Fix issue where `FormBuilderDateRangePicker` ignores `initialFirstDate` and `initialLastDate` -* Fixed bug where readOnly not working in FormBuilderDateTimePicker. Closes [#179](https://github.com/danvick/flutter_form_builder/issues/179) -* Allow double `values` for `FormBuilderStepper`. Closes [#182](https://github.com/danvick/flutter_form_builder/issues/182) +* Fixed bug where readOnly not working in FormBuilderDateTimePicker. Closes [#179](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/179) +* Allow double `values` for `FormBuilderStepper`. Closes [#182](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/182) * Only include clear icon next to DropdownButton if the value is not `null` -* Revert `intl`, upgrade `flutter_chips_input` & `datetime_picker_formfield` - due incompatibilities. Closes [#183](https://github.com/danvick/flutter_form_builder/issues/183), [#185](https://github.com/danvick/flutter_form_builder/issues/185) +* Revert `intl`, upgrade `flutter_chips_input` & `datetime_picker_formfield` - due incompatibilities. Closes [#183](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/183), [#185](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/185) ## [3.6.1] - 6-Nov-2019 -* Fixed bug caused by dropping unimplemented attribute `onChipTapped` of `flutter_chips_input`. Closes [#168](https://github.com/danvick/flutter_form_builder/issues/168) +* Fixed bug caused by dropping unimplemented attribute `onChipTapped` of `flutter_chips_input`. Closes [#168](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/168) ## [3.6.0] - 4-Nov-2019 -* Added clear option to FormBuilderDropdown - set `allowClear` to true. Closes [#148](https://github.com/danvick/flutter_form_builder/issues/148) -* Default `contentPadding` and `border` attributes removed from CheckboxList, Radio and SegmentedControl list. Closes [#160](https://github.com/danvick/flutter_form_builder/issues/160) -* Added `numberFormat` attribute to Slider. Closes [#156](https://github.com/danvick/flutter_form_builder/issues/156) +* Added clear option to FormBuilderDropdown - set `allowClear` to true. Closes [#148](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/148) +* Default `contentPadding` and `border` attributes removed from CheckboxList, Radio and SegmentedControl list. Closes [#160](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/160) +* Added `numberFormat` attribute to Slider. Closes [#156](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/156) * Add error text to date range picker. Thanks to [ffpetrovic](https://github.com/ffpetrovic) * Fixed bug where pushing cancel on timePicker causes crash. Thanks to [ayushin](https://github.com/ayushin) -* Fixed bug where Switch doesn't obey initialValue from FormBuilder. Closes [#159](https://github.com/danvick/flutter_form_builder/issues/159) -* Fixed bug where FormBuilderDropdown shows value instead of label when disabled/readOnly. Closes [#154](https://github.com/danvick/flutter_form_builder/issues/154) -* Fixed bug where FormBuilderDateTimePicker value is parsed from TextField string. Closes [#164](https://github.com/danvick/flutter_form_builder/issues/164) +* Fixed bug where Switch doesn't obey initialValue from FormBuilder. Closes [#159](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/159) +* Fixed bug where FormBuilderDropdown shows value instead of label when disabled/readOnly. Closes [#154](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/154) +* Fixed bug where FormBuilderDateTimePicker value is parsed from TextField string. Closes [#164](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/164) * Added default TextInputConfiguration options for ChipsInput * Fix example project - AndroidX compatibility. Thanks to [prasadsunny1](https://github.com/prasadsunny1) * Bumped up `flutter_typeahead` 1.6.1 -> 1.7.0 @@ -860,7 +918,7 @@ ## [3.5.3] -* Fixed DateTimePicker bug: '`DateTime is not a subtype of type TimeOfDay`' when Input type is Time only. Closes [#131](https://github.com/danvick/flutter_form_builder/issues/131) +* Fixed DateTimePicker bug: '`DateTime is not a subtype of type TimeOfDay`' when Input type is Time only. Closes [#131](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/131) ## [3.5.2] @@ -946,7 +1004,7 @@ ## [3.2.3] -* Allow `readonly` attribute for fields to be changed at runtime. Credit [Daniel Acorsi](https://github.com/dhaalves). Closes [#75](https://github.com/danvick/flutter_form_builder/issues/75) +* Allow `readonly` attribute for fields to be changed at runtime. Credit [Daniel Acorsi](https://github.com/dhaalves). Closes [#75](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/75) ## [3.2.2] @@ -954,7 +1012,7 @@ ## [3.2.1] -* Add missing attributes for `FormBuilderSlider` to customize `Slider` Widget including `activeColor`, `inactiveColor`, `onChangeStart`, `onChangeEnd`, `label` and `semanticFormatterCallback`. Closes [#80](https://github.com/danvick/flutter_form_builder/issues/80). +* Add missing attributes for `FormBuilderSlider` to customize `Slider` Widget including `activeColor`, `inactiveColor`, `onChangeStart`, `onChangeEnd`, `label` and `semanticFormatterCallback`. Closes [#80](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/80). * Add support for `underline` to `FormBuilderDropdown`. Credit Jordan Nelson (github/jrnelson333). * Minor fixes to README @@ -965,7 +1023,7 @@ ## [3.1.3] -* Made `flutter_typeahead`'s `onSuggestionSelected` available to `FormBuilderTypeAhead` - Closes [#73](https://github.com/danvick/flutter_form_builder/issues/73). Credit to daWeed (github/psrcek) +* Made `flutter_typeahead`'s `onSuggestionSelected` available to `FormBuilderTypeAhead` - Closes [#73](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/73). Credit to daWeed (github/psrcek) ## [3.1.2] @@ -983,7 +1041,7 @@ ## [3.0.1] -* Fixed bug in where `focuNode` for `FormBuilderTextField` is ignored. Closes [#53](https://github.com/danvick/flutter_form_builder/issues/53) +* Fixed bug in where `focuNode` for `FormBuilderTextField` is ignored. Closes [#53](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/53) * Fixed bug in where `textEditingConfiguration` for `FormBuilderTypeAhead` ignored ## [3.0.0] @@ -1010,7 +1068,7 @@ functions that do different validations * New Feature `FormBuilderValidators` comes with common validation functionality options such as: required, min, max, minLength, maxLength, email, url, credit card etc. * Added `valueTransformer` - transforms field value before saving to the final form value -* Added requested `onChanged` value notifier event on fields. Closes [#45](https://github.com/danvick/flutter_form_builder/issues/45) +* Added requested `onChanged` value notifier event on fields. Closes [#45](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/45) * Prevent duplicate `attribute` names in fields - assertion * **Breaking changes:** * `FormBuilderInputOption` becomes `FormBuilderFieldOption` @@ -1057,7 +1115,7 @@ Access form state using a `GlobalKey` ## [1.5.0] * Now using `datetime_picker_formfield` plugin from pub for DatePicker and TimePicker. -Should close [#33](https://github.com/danvick/flutter_form_builder/issues/33) +Should close [#33](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/33) * Added new `FormBuilderInput` - DateTimePicker * **Breaking change**: DatePicker, TimePicker & DateTimePicker now return an object of type `DateTime` instead of `String` @@ -1067,8 +1125,8 @@ type `DateTime` instead of `String` * The entire form or individual controls can now be made readonly by making `readonly` property to `true`. Default value is `false`. -Closes [#11](https://github.com/danvick/flutter_form_builder/issues/11) and -[#16](https://github.com/danvick/flutter_form_builder/issues/16) +Closes [#11](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/11) and +[#16](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/16) ## [1.3.5] @@ -1080,7 +1138,7 @@ Bug fix: Imported `dart:async` for use of `Future`s to be compatible with Dart < ## [1.3.3] -* Updated `flutter_typeahead` version. Closes [#15](https://github.com/danvick/flutter_form_builder/issues/15) +* Updated `flutter_typeahead` version. Closes [#15](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/15) ## [1.3.2] @@ -1092,7 +1150,7 @@ Bug fix: Imported `dart:async` for use of `Future`s to be compatible with Dart < * Moved ChipsInput into own library on pub.dartlang.org, check it out [here](https://pub.dartlang.org/packages/flutter_chips_input) * Updated example code to include proper use of Form's `onChanged` function after update. -Closes [#8](https://github.com/danvick/flutter_form_builder/issues/8) +Closes [#8](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/8) ## [1.3.0] @@ -1137,7 +1195,7 @@ Closes [#8](https://github.com/danvick/flutter_form_builder/issues/8) * Added resetButton ### Fixes -* Fixed bug where `TYPE_TEXT` validates as `TYPE_EMAIL` - Closes [#1](https://github.com/danvick/flutter_form_builder/issues/1) +* Fixed bug where `TYPE_TEXT` validates as `TYPE_EMAIL` - Closes [#1](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/1) * Fixed initial value setting `FormBuilderInput.checkboxList()` ### Breaking Changes diff --git a/README.md b/README.md index 340f84e65e..05b2c9afad 100644 --- a/README.md +++ b/README.md @@ -407,6 +407,20 @@ class _ClearFormBuilderTextFieldState } ``` +## Migrations + +### v9 to v10 + +- Remove `invalidateField` and `invalidateFirstField` methods from FormBuilderState. Use `fields[name]?.invalidate(errorText)` and `fields.first.invalidate(errorText)` instead +- [FormBuilderTextField] Deprecate canRequestFocus property. Use `FocusNode.canRequestFocus.` instead +- Assert on FormBuilderField.decoration.enabled property. Use FormBuilderField.enabled instead +- Easy way! Only need execute `dart fix --apply` on your project to fix the following changes: + - Rename FormBuilderChoiceChip to FormBuilderChoiceChips. + - Rename FormBuilderFilterChip to FormBuilderFilterChips. + - [FormBuilderFilterChip] Remove maxChips property. + - [FormBuilderDateTimePicker] Remove resetIcon property. + - [FormBuilder] Remove onPopInvoked property. + ## Support ### Contribute diff --git a/analysis_options.yaml b/analysis_options.yaml index f9b303465f..85faa3cefa 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1 +1,5 @@ include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + - "test_fixes/**" diff --git a/example/lib/minimal_code_example.dart b/example/lib/minimal_code_example.dart new file mode 100644 index 0000000000..99d06a5c2a --- /dev/null +++ b/example/lib/minimal_code_example.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; + +void main() => runApp(const MyApp()); + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter FormBuilder Example', + debugShowCheckedModeBanner: false, + localizationsDelegates: const [ + FormBuilderLocalizations.delegate, + ...GlobalMaterialLocalizations.delegates, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: FormBuilderLocalizations.supportedLocales, + home: const _ExamplePage(), + ); + } +} + +class _ExamplePage extends StatefulWidget { + const _ExamplePage(); + + @override + State<_ExamplePage> createState() => _ExamplePageState(); +} + +class _ExamplePageState extends State<_ExamplePage> { + final _formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Minimal code example')), + body: Padding( + padding: const EdgeInsets.all(16), + child: FormBuilder( + key: _formKey, + child: Column( + children: [ + FormBuilderFilterChips( + decoration: const InputDecoration( + labelText: 'The language of my people', + ), + name: 'languages_filter', + selectedColor: Colors.red, + options: const [ + FormBuilderChipOption( + value: 'Dart', + avatar: CircleAvatar(child: Text('D')), + ), + FormBuilderChipOption( + value: 'Kotlin', + avatar: CircleAvatar(child: Text('K')), + ), + FormBuilderChipOption( + value: 'Java', + avatar: CircleAvatar(child: Text('J')), + ), + FormBuilderChipOption( + value: 'Swift', + avatar: CircleAvatar(child: Text('S')), + ), + FormBuilderChipOption( + value: 'Objective-C', + avatar: CircleAvatar(child: Text('O')), + ), + ], + validator: FormBuilderValidators.compose([ + FormBuilderValidators.minLength(1), + FormBuilderValidators.maxLength(3), + ]), + ), + const SizedBox(height: 10), + ElevatedButton( + onPressed: () { + _formKey.currentState?.saveAndValidate(); + debugPrint(_formKey.currentState?.value.toString()); + }, + child: const Text('Print'), + ) + ], + ), + ), + ), + ); + } +} diff --git a/example/lib/sources/complete_form.dart b/example/lib/sources/complete_form.dart index 1bf1932374..844947af6d 100644 --- a/example/lib/sources/complete_form.dart +++ b/example/lib/sources/complete_form.dart @@ -252,7 +252,7 @@ class _CompleteFormState extends State { FormBuilderValidators.maxLength(3), ]), ), - FormBuilderFilterChip( + FormBuilderFilterChips( autovalidateMode: AutovalidateMode.onUserInteraction, decoration: const InputDecoration( labelText: 'The language of my people'), @@ -286,7 +286,7 @@ class _CompleteFormState extends State { FormBuilderValidators.maxLength(3), ]), ), - FormBuilderChoiceChip( + FormBuilderChoiceChips( autovalidateMode: AutovalidateMode.onUserInteraction, decoration: const InputDecoration( labelText: diff --git a/example/pubspec.lock b/example/pubspec.lock index 5abc1d7965..6df03e79b9 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,50 +5,50 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.12.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" flutter: dependency: "direct main" description: flutter @@ -60,7 +60,7 @@ packages: path: ".." relative: true source: path - version: "9.7.0" + version: "10.0.0-dev.3" flutter_lints: dependency: "direct dev" description: @@ -99,18 +99,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -123,18 +123,18 @@ packages: dependency: transitive description: name: lints - sha256: "4a16b3f03741e1252fda5de3ce712666d010ba2122f8e912c94f9f7b90e1a4c3" + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.1.1" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -147,18 +147,18 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" sky_engine: dependency: transitive description: flutter @@ -168,50 +168,50 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.4" vector_math: dependency: transitive description: @@ -224,10 +224,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "14.3.1" sdks: - dart: ">=3.6.0 <4.0.0" - flutter: ">=3.27.0" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.29.0" diff --git a/lib/fix_data.yaml b/lib/fix_data.yaml new file mode 100644 index 0000000000..27f55ffef4 --- /dev/null +++ b/lib/fix_data.yaml @@ -0,0 +1,45 @@ +version: 1 +transforms: + - title: 'Remove deprecated maxChips property on FormBuilderFilterChip' + date: 2025-01-15 + element: + uris: [ 'flutter_form_builder.dart' ] + constructor: '' + inClass: 'FormBuilderFilterChip' + changes: + - kind: 'removeParameter' + name: 'maxChips' + - title: 'Remove deprecated resetIcon property on FormBuilderDateTimePicker' + date: 2025-01-15 + element: + uris: [ 'flutter_form_builder.dart' ] + constructor: '' + inClass: 'FormBuilderDateTimePicker' + changes: + - kind: 'removeParameter' + name: 'resetIcon' + - title: 'Remove deprecated onPopInvoked property on FormBuilder' + date: 2025-01-15 + element: + uris: [ 'flutter_form_builder.dart' ] + constructor: '' + inClass: 'FormBuilder' + changes: + - kind: 'removeParameter' + name: 'onPopInvoked' + - title: 'Rename FormBuilderChoiceChip to be plural' + date: 2025-01-15 + element: + uris: [ 'flutter_form_builder.dart' ] + class: 'FormBuilderChoiceChip' + changes: + - kind: 'rename' + newName: 'FormBuilderChoiceChips' + - title: 'Rename FormBuilderFilterChip to be plural' + date: 2025-01-15 + element: + uris: [ 'flutter_form_builder.dart' ] + class: 'FormBuilderFilterChip' + changes: + - kind: 'rename' + newName: 'FormBuilderFilterChips' \ No newline at end of file diff --git a/lib/src/extensions/generic_validator.dart b/lib/src/extensions/generic_validator.dart index 8ad4b17067..885c4711ba 100644 --- a/lib/src/extensions/generic_validator.dart +++ b/lib/src/extensions/generic_validator.dart @@ -1,4 +1,5 @@ extension GenericValidator on T? { + /// Check if the value is empty in a generic way bool emptyValidator() { if (this == null) return true; if (this is Iterable) return (this as Iterable).isEmpty; diff --git a/lib/src/fields/form_builder_checkbox.dart b/lib/src/fields/form_builder_checkbox.dart index 6e1ceca5a2..8d7f5ba01e 100644 --- a/lib/src/fields/form_builder_checkbox.dart +++ b/lib/src/fields/form_builder_checkbox.dart @@ -120,37 +120,40 @@ class FormBuilderCheckbox extends FormBuilderFieldDecoration { this.shape, this.side, }) : super( - builder: (FormFieldState field) { - final state = field as _FormBuilderCheckboxState; - - return InputDecorator( - decoration: state.decoration, - child: CheckboxListTile( - dense: true, - isThreeLine: false, - title: title, - subtitle: subtitle, - value: tristate ? state.value : (state.value ?? false), - onChanged: state.enabled - ? (value) { - state.didChange(value); - } - : null, - checkColor: checkColor, - activeColor: activeColor, - secondary: secondary, - controlAffinity: controlAffinity, - autofocus: autofocus, - tristate: tristate, - contentPadding: contentPadding, - visualDensity: visualDensity, - selected: selected, - checkboxShape: shape, - side: side, - ), - ); - }, - ); + builder: (FormFieldState field) { + final state = field as _FormBuilderCheckboxState; + + return InputDecorator( + decoration: state.decoration, + isFocused: state.effectiveFocusNode.hasFocus, + child: CheckboxListTile( + dense: true, + isThreeLine: false, + focusNode: state.effectiveFocusNode, + title: title, + subtitle: subtitle, + value: tristate ? state.value : (state.value ?? false), + onChanged: + state.enabled + ? (value) { + state.didChange(value); + } + : null, + checkColor: checkColor, + activeColor: activeColor, + secondary: secondary, + controlAffinity: controlAffinity, + autofocus: autofocus, + tristate: tristate, + contentPadding: contentPadding, + visualDensity: visualDensity, + selected: selected, + checkboxShape: shape, + side: side, + ), + ); + }, + ); @override FormBuilderFieldDecorationState createState() => @@ -158,4 +161,26 @@ class FormBuilderCheckbox extends FormBuilderFieldDecoration { } class _FormBuilderCheckboxState - extends FormBuilderFieldDecorationState {} + extends FormBuilderFieldDecorationState { + void handleFocusChange() { + setState(() {}); + } + + @override + void initState() { + super.initState(); + effectiveFocusNode.addListener(handleFocusChange); + } + + @override + void dispose() { + effectiveFocusNode.removeListener(handleFocusChange); + super.dispose(); + } + + @override + void didChange(bool? value) { + focus(); + super.didChange(value); + } +} diff --git a/lib/src/fields/form_builder_checkbox_group.dart b/lib/src/fields/form_builder_checkbox_group.dart index f5ee8a687f..d40f5350f1 100644 --- a/lib/src/fields/form_builder_checkbox_group.dart +++ b/lib/src/fields/form_builder_checkbox_group.dart @@ -66,48 +66,57 @@ class FormBuilderCheckboxGroup extends FormBuilderFieldDecoration> { this.orientation = OptionsOrientation.wrap, this.itemDecoration, }) : super( - builder: (FormFieldState?> field) { - final state = field as _FormBuilderCheckboxGroupState; + builder: (FormFieldState?> field) { + final state = field as _FormBuilderCheckboxGroupState; - return InputDecorator( - decoration: state.decoration, - child: GroupedCheckbox( - orientation: orientation, - value: state.value, - options: options, - onChanged: (val) { - field.didChange(val); - }, - disabled: state.enabled - ? disabled - : options.map((e) => e.value).toList(), - activeColor: activeColor, - visualDensity: visualDensity, - focusColor: focusColor, - checkColor: checkColor, - materialTapTargetSize: materialTapTargetSize, - hoverColor: hoverColor, - tristate: tristate, - wrapAlignment: wrapAlignment, - wrapCrossAxisAlignment: wrapCrossAxisAlignment, - wrapDirection: wrapDirection, - wrapRunAlignment: wrapRunAlignment, - wrapRunSpacing: wrapRunSpacing, - wrapSpacing: wrapSpacing, - wrapTextDirection: wrapTextDirection, - wrapVerticalDirection: wrapVerticalDirection, - separator: separator, - controlAffinity: controlAffinity, - itemDecoration: itemDecoration, - ), - ); - }, - ); + return Focus( + focusNode: state.effectiveFocusNode, + skipTraversal: true, + canRequestFocus: state.enabled, + debugLabel: 'FormBuilderCheckboxGroup-$name', + child: InputDecorator( + decoration: state.decoration, + isFocused: state.effectiveFocusNode.hasFocus, + child: GroupedCheckbox( + orientation: orientation, + value: state.value, + options: options, + onChanged: (val) { + field.didChange(val); + }, + disabled: + state.enabled + ? disabled + : options.map((e) => e.value).toList(), + activeColor: activeColor, + visualDensity: visualDensity, + focusColor: focusColor, + checkColor: checkColor, + materialTapTargetSize: materialTapTargetSize, + hoverColor: hoverColor, + tristate: tristate, + wrapAlignment: wrapAlignment, + wrapCrossAxisAlignment: wrapCrossAxisAlignment, + wrapDirection: wrapDirection, + wrapRunAlignment: wrapRunAlignment, + wrapRunSpacing: wrapRunSpacing, + wrapSpacing: wrapSpacing, + wrapTextDirection: wrapTextDirection, + wrapVerticalDirection: wrapVerticalDirection, + separator: separator, + controlAffinity: controlAffinity, + itemDecoration: itemDecoration, + ), + ), + ); + }, + ); @override FormBuilderFieldDecorationState, List> - createState() => _FormBuilderCheckboxGroupState(); + createState() => _FormBuilderCheckboxGroupState(); } -class _FormBuilderCheckboxGroupState extends FormBuilderFieldDecorationState< - FormBuilderCheckboxGroup, List> {} +class _FormBuilderCheckboxGroupState + extends + FormBuilderFieldDecorationState, List> {} diff --git a/lib/src/fields/form_builder_choice_chips.dart b/lib/src/fields/form_builder_choice_chips.dart index 14a4851cbe..5322fcf838 100644 --- a/lib/src/fields/form_builder_choice_chips.dart +++ b/lib/src/fields/form_builder_choice_chips.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; /// A list of `Chip`s that acts like radio buttons -class FormBuilderChoiceChip extends FormBuilderFieldDecoration { +class FormBuilderChoiceChips extends FormBuilderFieldDecoration { /// The list of items the user can select. final List> options; @@ -344,7 +344,7 @@ class FormBuilderChoiceChip extends FormBuilderFieldDecoration { final String? tooltip; /// Creates a list of `Chip`s that acts like radio buttons - FormBuilderChoiceChip({ + FormBuilderChoiceChips({ super.autovalidateMode = AutovalidateMode.disabled, super.enabled, super.focusNode, @@ -392,67 +392,100 @@ class FormBuilderChoiceChip extends FormBuilderFieldDecoration { this.color, this.iconTheme, this.tooltip, - }) : super(builder: (FormFieldState field) { - final state = field as _FormBuilderChoiceChipState; - - return InputDecorator( - decoration: state.decoration, - child: Wrap( - direction: direction, - alignment: alignment, - crossAxisAlignment: crossAxisAlignment, - runAlignment: runAlignment, - runSpacing: runSpacing, - spacing: spacing, - textDirection: textDirection, - verticalDirection: verticalDirection, - children: [ - for (FormBuilderChipOption option in options) - ChoiceChip( - label: option, - side: side, - shape: shape, - selected: field.value == option.value, - onSelected: state.enabled - ? (selected) { - final choice = selected ? option.value : null; - state.didChange(choice); - } - : null, - avatar: option.avatar, - selectedColor: selectedColor, - disabledColor: disabledColor, - backgroundColor: backgroundColor, - shadowColor: shadowColor, - selectedShadowColor: selectedShadowColor, - elevation: elevation, - pressElevation: pressElevation, - materialTapTargetSize: materialTapTargetSize, - labelStyle: labelStyle, - labelPadding: labelPadding, - padding: padding, - visualDensity: visualDensity, - avatarBorder: avatarBorder, - showCheckmark: showCheckmark, - surfaceTintColor: surfaceTintColor, - clipBehavior: clipBehavior, - checkmarkColor: checkmarkColor, - autofocus: autofocus, - avatarBoxConstraints: avatarBoxConstraints, - chipAnimationStyle: chipAnimationStyle, - color: color, - iconTheme: iconTheme, - tooltip: tooltip, - ), - ], - ), - ); - }); + }) : super( + builder: (FormFieldState field) { + final state = field as _FormBuilderChoiceChipState; + + return Focus( + focusNode: state.effectiveFocusNode, + skipTraversal: true, + canRequestFocus: state.enabled, + debugLabel: 'FormBuilderChoiceChip-$name', + child: InputDecorator( + decoration: state.decoration, + isFocused: state.effectiveFocusNode.hasFocus, + child: Wrap( + direction: direction, + alignment: alignment, + crossAxisAlignment: crossAxisAlignment, + runAlignment: runAlignment, + runSpacing: runSpacing, + spacing: spacing, + textDirection: textDirection, + verticalDirection: verticalDirection, + children: [ + for (FormBuilderChipOption option in options) + ChoiceChip( + label: option, + side: side, + shape: shape, + selected: field.value == option.value, + onSelected: + state.enabled + ? (selected) { + final choice = selected ? option.value : null; + state.didChange(choice); + } + : null, + avatar: option.avatar, + selectedColor: selectedColor, + disabledColor: disabledColor, + backgroundColor: backgroundColor, + shadowColor: shadowColor, + selectedShadowColor: selectedShadowColor, + elevation: elevation, + pressElevation: pressElevation, + materialTapTargetSize: materialTapTargetSize, + labelStyle: labelStyle, + labelPadding: labelPadding, + padding: padding, + visualDensity: visualDensity, + avatarBorder: avatarBorder, + showCheckmark: showCheckmark, + surfaceTintColor: surfaceTintColor, + clipBehavior: clipBehavior, + checkmarkColor: checkmarkColor, + autofocus: autofocus, + avatarBoxConstraints: avatarBoxConstraints, + chipAnimationStyle: chipAnimationStyle, + color: color, + iconTheme: iconTheme, + tooltip: tooltip, + ), + ], + ), + ), + ); + }, + ); @override - FormBuilderFieldDecorationState, T> createState() => + FormBuilderFieldDecorationState, T> createState() => _FormBuilderChoiceChipState(); } class _FormBuilderChoiceChipState - extends FormBuilderFieldDecorationState, T> {} + extends FormBuilderFieldDecorationState, T> { + void handleFocusChange() { + setState(() {}); + } + + @override + void initState() { + super.initState(); + effectiveFocusNode.addListener(handleFocusChange); + } + + @override + void dispose() { + effectiveFocusNode.removeListener(handleFocusChange); + super.dispose(); + } + + @override + void didChange(T? value) { + focus(); + // effectiveFocusNode.requestFocus(); + super.didChange(value); + } +} diff --git a/lib/src/fields/form_builder_date_range_picker.dart b/lib/src/fields/form_builder_date_range_picker.dart index 3b71c75e65..ff4eacc77f 100644 --- a/lib/src/fields/form_builder_date_range_picker.dart +++ b/lib/src/fields/form_builder_date_range_picker.dart @@ -48,7 +48,7 @@ class FormBuilderDateRangePicker final DateTime? currentDate; // widget.currentDate, final String? errorFormatText; // widget.erroerrorFormatText, final Widget Function(BuildContext, Widget?)? - pickerBuilder; // widget.builder, + pickerBuilder; // widget.builder, final String? errorInvalidRangeText; // widget.errorInvalidRangeText, final String? errorInvalidText; // widget.errorInvalidText, final String? fieldEndHintText; // widget.fieldEndHintText, @@ -131,50 +131,54 @@ class FormBuilderDateRangePicker this.allowClear = false, this.clearIcon, }) : super( - builder: (FormFieldState field) { - final state = field as _FormBuilderDateRangePickerState; + builder: (FormFieldState field) { + final state = field as _FormBuilderDateRangePickerState; - return TextField( - enabled: state.enabled, - style: style, - focusNode: state.effectiveFocusNode, - decoration: state.decoration, - // initialValue: "${_initialValue ?? ''}", - maxLines: maxLines, - keyboardType: keyboardType, - obscureText: obscureText, - onEditingComplete: onEditingComplete, - controller: state._effectiveController, - autocorrect: autocorrect, - autofocus: autofocus, - buildCounter: buildCounter, - mouseCursor: mouseCursor, - cursorColor: cursorColor, - cursorRadius: cursorRadius, - cursorWidth: cursorWidth, - enableInteractiveSelection: enableInteractiveSelection, - maxLength: maxLength, - inputFormatters: inputFormatters, - keyboardAppearance: keyboardAppearance, - maxLengthEnforcement: maxLengthEnforcement, - scrollPadding: scrollPadding, - textAlign: textAlign, - textCapitalization: textCapitalization, - textDirection: textDirection, - textInputAction: textInputAction, - textAlignVertical: textAlignVertical, - strutStyle: strutStyle, - readOnly: true, - expands: expands, - minLines: minLines, - showCursor: showCursor, - ); - }, - ); + return FocusTraversalGroup( + policy: ReadingOrderTraversalPolicy(), + child: TextField( + onTap: () => state.showPicker(), + enabled: state.enabled, + style: style, + focusNode: state.effectiveFocusNode, + decoration: state.decoration, + // initialValue: "${_initialValue ?? ''}", + maxLines: maxLines, + keyboardType: keyboardType, + obscureText: obscureText, + onEditingComplete: onEditingComplete, + controller: state._effectiveController, + autocorrect: autocorrect, + autofocus: autofocus, + buildCounter: buildCounter, + mouseCursor: mouseCursor, + cursorColor: cursorColor, + cursorRadius: cursorRadius, + cursorWidth: cursorWidth, + enableInteractiveSelection: enableInteractiveSelection, + maxLength: maxLength, + inputFormatters: inputFormatters, + keyboardAppearance: keyboardAppearance, + maxLengthEnforcement: maxLengthEnforcement, + scrollPadding: scrollPadding, + textAlign: textAlign, + textCapitalization: textCapitalization, + textDirection: textDirection, + textInputAction: textInputAction, + textAlignVertical: textAlignVertical, + strutStyle: strutStyle, + readOnly: true, + expands: expands, + minLines: minLines, + showCursor: showCursor, + ), + ); + }, + ); @override FormBuilderFieldDecorationState - createState() => _FormBuilderDateRangePickerState(); + createState() => _FormBuilderDateRangePickerState(); static String tryFormat(DateTime date, intl.DateFormat format) { try { @@ -186,8 +190,12 @@ class FormBuilderDateRangePicker } } -class _FormBuilderDateRangePickerState extends FormBuilderFieldDecorationState< - FormBuilderDateRangePicker, DateTimeRange> { +class _FormBuilderDateRangePickerState + extends + FormBuilderFieldDecorationState< + FormBuilderDateRangePicker, + DateTimeRange + > { late TextEditingController _effectiveController; @override @@ -195,12 +203,21 @@ class _FormBuilderDateRangePickerState extends FormBuilderFieldDecorationState< super.initState(); _effectiveController = widget.controller ?? TextEditingController(text: _valueToText()); - effectiveFocusNode.addListener(_handleFocus); + + effectiveFocusNode.onKeyEvent = (node, event) { + if (enabled && + event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.space && + node.hasFocus) { + showPicker(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }; } @override void dispose() { - effectiveFocusNode.removeListener(_handleFocus); // Dispose the _effectiveController when initState created it if (null == widget.controller) { _effectiveController.dispose(); @@ -208,42 +225,35 @@ class _FormBuilderDateRangePickerState extends FormBuilderFieldDecorationState< super.dispose(); } - Future _handleFocus() async { - if (effectiveFocusNode.hasFocus && enabled) { - effectiveFocusNode.unfocus(); - /*final initialFirstDate = value?.isEmpty ?? true - ? (widget.initialFirstDate ?? DateTime.now()) - : value[0]; - final initialLastDate = value?.isEmpty ?? true - ? (widget.initialLastDate ?? initialFirstDate) - : (value.length < 2 ? initialFirstDate : value[1]);*/ - final picked = await showDateRangePicker( - context: context, - firstDate: widget.firstDate, - lastDate: widget.lastDate, - locale: widget.locale, - textDirection: widget.textDirection, - cancelText: widget.cancelText, - confirmText: widget.confirmText, - currentDate: widget.currentDate, - errorFormatText: widget.errorFormatText, - builder: widget.pickerBuilder, - errorInvalidRangeText: widget.errorInvalidRangeText, - errorInvalidText: widget.errorInvalidText, - fieldEndHintText: widget.fieldEndHintText, - fieldEndLabelText: widget.fieldEndLabelText, - fieldStartHintText: widget.fieldStartHintText, - fieldStartLabelText: widget.fieldStartLabelText, - helpText: widget.helpText, - initialDateRange: value, - initialEntryMode: widget.initialEntryMode, - routeSettings: widget.routeSettings, - saveText: widget.saveText, - useRootNavigator: widget.useRootNavigator, - ); - if (picked != null) { - didChange(picked); - } + Future showPicker() async { + effectiveFocusNode.requestFocus(); + + final picked = await showDateRangePicker( + context: context, + firstDate: widget.firstDate, + lastDate: widget.lastDate, + locale: widget.locale, + textDirection: widget.textDirection, + cancelText: widget.cancelText, + confirmText: widget.confirmText, + currentDate: widget.currentDate, + errorFormatText: widget.errorFormatText, + builder: widget.pickerBuilder, + errorInvalidRangeText: widget.errorInvalidRangeText, + errorInvalidText: widget.errorInvalidText, + fieldEndHintText: widget.fieldEndHintText, + fieldEndLabelText: widget.fieldEndLabelText, + fieldStartHintText: widget.fieldStartHintText, + fieldStartLabelText: widget.fieldStartLabelText, + helpText: widget.helpText, + initialDateRange: value, + initialEntryMode: widget.initialEntryMode, + routeSettings: widget.routeSettings, + saveText: widget.saveText, + useRootNavigator: widget.useRootNavigator, + ); + if (picked != null) { + didChange(picked); } } @@ -256,7 +266,9 @@ class _FormBuilderDateRangePickerState extends FormBuilderFieldDecorationState< } String format(DateTime date) => FormBuilderDateRangePicker.tryFormat( - date, widget.format ?? intl.DateFormat.yMd()); + date, + widget.format ?? intl.DateFormat.yMd(), + ); void _setTextFieldString() { setState(() => _effectiveController.text = _valueToText()); @@ -275,16 +287,18 @@ class _FormBuilderDateRangePickerState extends FormBuilderFieldDecorationState< } @override - InputDecoration get decoration => widget.allowClear - ? super.decoration.copyWith( - suffix: IconButton( + InputDecoration get decoration => + widget.allowClear + ? super.decoration.copyWith( + suffix: IconButton( padding: EdgeInsets.zero, constraints: const BoxConstraints(maxWidth: 24, maxHeight: 24), onPressed: () { focus(); didChange(null); - effectiveFocusNode.unfocus(); }, - icon: widget.clearIcon ?? const Icon(Icons.clear))) - : super.decoration; + icon: widget.clearIcon ?? const Icon(Icons.clear), + ), + ) + : super.decoration; } diff --git a/lib/src/fields/form_builder_date_time_picker.dart b/lib/src/fields/form_builder_date_time_picker.dart index 46c3ba422e..cc378b2a29 100644 --- a/lib/src/fields/form_builder_date_time_picker.dart +++ b/lib/src/fields/form_builder_date_time_picker.dart @@ -23,7 +23,7 @@ class FormBuilderDateTimePicker extends FormBuilderFieldDecoration { /// (Sunday, June 3, 2018 at 9:24pm) final DateFormat? format; - /// The date the calendar opens to when displayed. Defaults to the current date. + /// The date the calendar opens to when displayed. Defaults to null. /// /// To preset the widget's value, use [initialValue] instead. final DateTime? initialDate; @@ -40,10 +40,6 @@ class FormBuilderDateTimePicker extends FormBuilderFieldDecoration { /// to noon. Explicitly set this to `null` to use the current time. final TimeOfDay initialTime; - @Deprecated( - 'This property is no used anymore. Please use decoration.suffixIcon to set your desired icon') - final Widget? resetIcon; - /// Called when an enclosing form is saved. The value passed will be `null` /// if [format] fails to parse the text. // final FormFieldSetter onSaved; @@ -146,7 +142,6 @@ class FormBuilderDateTimePicker extends FormBuilderFieldDecoration { this.scrollPadding = const EdgeInsets.all(20.0), this.cursorWidth = 2.0, this.enableInteractiveSelection = true, - this.resetIcon = const Icon(Icons.close), this.initialTime = const TimeOfDay(hour: 12, minute: 0), this.keyboardType, this.textAlign = TextAlign.start, @@ -198,53 +193,58 @@ class FormBuilderDateTimePicker extends FormBuilderFieldDecoration { this.onEntryModeChanged, this.barrierDismissible = true, }) : super( - builder: (FormFieldState field) { - final state = field as _FormBuilderDateTimePickerState; - - return TextField( - textDirection: textDirection, - textAlign: textAlign, - textAlignVertical: textAlignVertical, - maxLength: maxLength, - autofocus: autofocus, - decoration: state.decoration, - readOnly: true, - enabled: state.enabled, - autocorrect: autocorrect, - controller: state._textFieldController, - focusNode: state.effectiveFocusNode, - inputFormatters: inputFormatters, - keyboardType: keyboardType, - maxLines: maxLines, - obscureText: obscureText, - showCursor: showCursor, - minLines: minLines, - expands: expands, - style: style, - onEditingComplete: onEditingComplete, - buildCounter: buildCounter, - mouseCursor: mouseCursor, - cursorColor: cursorColor, - cursorRadius: cursorRadius, - cursorWidth: cursorWidth, - enableInteractiveSelection: enableInteractiveSelection, - keyboardAppearance: keyboardAppearance, - scrollPadding: scrollPadding, - strutStyle: strutStyle, - textCapitalization: textCapitalization, - textInputAction: textInputAction, - maxLengthEnforcement: maxLengthEnforcement, - ); - }, - ); + builder: (FormFieldState field) { + final state = field as _FormBuilderDateTimePickerState; + + return FocusTraversalGroup( + policy: ReadingOrderTraversalPolicy(), + child: TextField( + onTap: () => state.showPicker(), + textDirection: textDirection, + textAlign: textAlign, + textAlignVertical: textAlignVertical, + maxLength: maxLength, + autofocus: autofocus, + decoration: state.decoration, + readOnly: true, + enabled: state.enabled, + autocorrect: autocorrect, + controller: state._textFieldController, + focusNode: state.effectiveFocusNode, + inputFormatters: inputFormatters, + keyboardType: keyboardType, + maxLines: maxLines, + obscureText: obscureText, + showCursor: showCursor, + minLines: minLines, + expands: expands, + style: style, + onEditingComplete: onEditingComplete, + buildCounter: buildCounter, + mouseCursor: mouseCursor, + cursorColor: cursorColor, + cursorRadius: cursorRadius, + cursorWidth: cursorWidth, + enableInteractiveSelection: enableInteractiveSelection, + keyboardAppearance: keyboardAppearance, + scrollPadding: scrollPadding, + strutStyle: strutStyle, + textCapitalization: textCapitalization, + textInputAction: textInputAction, + maxLengthEnforcement: maxLengthEnforcement, + ), + ); + }, + ); @override FormBuilderFieldDecorationState - createState() => _FormBuilderDateTimePickerState(); + createState() => _FormBuilderDateTimePickerState(); } -class _FormBuilderDateTimePickerState extends FormBuilderFieldDecorationState< - FormBuilderDateTimePicker, DateTime> { +class _FormBuilderDateTimePickerState + extends + FormBuilderFieldDecorationState { late TextEditingController _textFieldController; late DateFormat _dateFormat; @@ -258,12 +258,20 @@ class _FormBuilderDateTimePickerState extends FormBuilderFieldDecorationState< final initVal = value; _textFieldController.text = initVal == null ? '' : _dateFormat.format(initVal); - effectiveFocusNode.addListener(_handleFocus); + + effectiveFocusNode.onKeyEvent = (node, event) { + if (event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.space && + node.hasFocus) { + showPicker(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }; } @override void dispose() { - effectiveFocusNode.removeListener(_handleFocus); // Dispose the _textFieldController when initState created it if (null == widget.controller) { _textFieldController.dispose(); @@ -271,23 +279,17 @@ class _FormBuilderDateTimePickerState extends FormBuilderFieldDecorationState< super.dispose(); } - Future _handleFocus() async { - if (effectiveFocusNode.hasFocus && enabled) { - effectiveFocusNode.unfocus(); - await onShowPicker(value); - } - } - DateFormat _getDefaultDateTimeFormat() { final languageCode = widget.locale?.languageCode; - switch (widget.inputType) { - case InputType.time: - return DateFormat.Hm(languageCode); - case InputType.date: - return DateFormat.yMd(languageCode); - case InputType.both: - return DateFormat.yMd(languageCode).add_Hms(); - } + return switch (widget.inputType) { + InputType.time => DateFormat.Hm(languageCode), + InputType.date => DateFormat.yMd(languageCode), + InputType.both => DateFormat.yMd(languageCode).add_Hms(), + }; + } + + Future showPicker() async { + await onShowPicker(value); } Future onShowPicker(DateTime? currentValue) async { @@ -326,7 +328,7 @@ class _FormBuilderDateTimePickerState extends FormBuilderFieldDecorationState< context: context, selectableDayPredicate: widget.selectableDayPredicate, initialDatePickerMode: widget.initialDatePickerMode, - initialDate: currentValue ?? widget.initialDate ?? DateTime.now(), + initialDate: currentValue ?? widget.initialDate, firstDate: widget.firstDate ?? DateTime(1900), lastDate: widget.lastDate ?? DateTime(2100), locale: widget.locale, @@ -357,18 +359,20 @@ class _FormBuilderDateTimePickerState extends FormBuilderFieldDecorationState< return Localizations.override( context: context, locale: widget.locale, - child: transitionBuilder == null - ? child - : transitionBuilder(context, child), + child: + transitionBuilder == null + ? child + : transitionBuilder(context, child), ); }; } return await showTimePicker( context: context, - initialTime: currentValue != null - ? TimeOfDay.fromDateTime(currentValue) - : widget.initialTime, + initialTime: + currentValue != null + ? TimeOfDay.fromDateTime(currentValue) + : widget.initialTime, builder: builder, useRootNavigator: widget.useRootNavigator, routeSettings: widget.routeSettings, @@ -385,7 +389,12 @@ class _FormBuilderDateTimePickerState extends FormBuilderFieldDecorationState< /// Sets the hour and minute of a [DateTime] from a [TimeOfDay]. DateTime combine(DateTime date, TimeOfDay? time) => DateTime( - date.year, date.month, date.day, time?.hour ?? 0, time?.minute ?? 0); + date.year, + date.month, + date.day, + time?.hour ?? 0, + time?.minute ?? 0, + ); DateTime? convert(TimeOfDay? time) => time == null ? null : DateTime(1, 1, 1, time.hour, time.minute); diff --git a/lib/src/fields/form_builder_dropdown.dart b/lib/src/fields/form_builder_dropdown.dart index a70151dacb..87f9dfacb6 100644 --- a/lib/src/fields/form_builder_dropdown.dart +++ b/lib/src/fields/form_builder_dropdown.dart @@ -289,53 +289,57 @@ class FormBuilderDropdown extends FormBuilderFieldDecoration { this.padding, this.menuWidth, }) : super( - builder: (FormFieldState field) { - final state = field as _FormBuilderDropdownState; + builder: (FormFieldState field) { + final state = field as _FormBuilderDropdownState; - final hasValue = items.map((e) => e.value).contains(field.value); - return InputDecorator( - decoration: state.decoration, - child: DropdownButton( - menuWidth: menuWidth, - padding: padding, - underline: underline, - isExpanded: isExpanded, - items: items, - value: hasValue ? field.value : null, - style: style, - isDense: isDense, - disabledHint: hasValue - ? items - .firstWhere( - (dropDownItem) => dropDownItem.value == field.value) - .child - : disabledHint, - elevation: elevation, - iconSize: iconSize, - icon: icon, - iconDisabledColor: iconDisabledColor, - iconEnabledColor: iconEnabledColor, - onChanged: state.enabled - ? (T? value) { - field.didChange(value); - } - : null, - onTap: onTap, - focusNode: state.effectiveFocusNode, - autofocus: autofocus, - dropdownColor: dropdownColor, - focusColor: focusColor, - itemHeight: itemHeight, - selectedItemBuilder: selectedItemBuilder, - menuMaxHeight: menuMaxHeight, - borderRadius: borderRadius, - enableFeedback: enableFeedback, - alignment: alignment, - hint: hint, - ), - ); - }, - ); + final hasValue = items.map((e) => e.value).contains(field.value); + return InputDecorator( + decoration: state.decoration, + child: DropdownButton( + menuWidth: menuWidth, + padding: padding, + underline: underline, + isExpanded: isExpanded, + items: items, + value: hasValue ? field.value : null, + style: style, + isDense: isDense, + disabledHint: + hasValue + ? items + .firstWhere( + (dropDownItem) => + dropDownItem.value == field.value, + ) + .child + : disabledHint, + elevation: elevation, + iconSize: iconSize, + icon: icon, + iconDisabledColor: iconDisabledColor, + iconEnabledColor: iconEnabledColor, + onChanged: + state.enabled + ? (T? value) { + field.didChange(value); + } + : null, + onTap: onTap, + focusNode: state.effectiveFocusNode, + autofocus: autofocus, + dropdownColor: dropdownColor, + focusColor: focusColor, + itemHeight: itemHeight, + selectedItemBuilder: selectedItemBuilder, + menuMaxHeight: menuMaxHeight, + borderRadius: borderRadius, + enableFeedback: enableFeedback, + alignment: alignment, + hint: hint, + ), + ); + }, + ); @override FormBuilderFieldDecorationState, T> createState() => diff --git a/lib/src/fields/form_builder_filter_chips.dart b/lib/src/fields/form_builder_filter_chips.dart index e30d8aa4b7..b8c549a45e 100644 --- a/lib/src/fields/form_builder_filter_chips.dart +++ b/lib/src/fields/form_builder_filter_chips.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; /// Field with chips that acts like a list checkboxes. -class FormBuilderFilterChip extends FormBuilderFieldDecoration> { +class FormBuilderFilterChips extends FormBuilderFieldDecoration> { //TODO: Add documentation final Color? backgroundColor; final Color? disabledColor; @@ -35,7 +35,7 @@ class FormBuilderFilterChip extends FormBuilderFieldDecoration> { final ShapeBorder avatarBorder; /// Creates field with chips that acts like a list checkboxes. - FormBuilderFilterChip({ + FormBuilderFilterChips({ super.autovalidateMode = AutovalidateMode.disabled, super.enabled, super.focusNode, @@ -59,7 +59,7 @@ class FormBuilderFilterChip extends FormBuilderFieldDecoration> { this.labelPadding, this.labelStyle, this.materialTapTargetSize, - this.maxChips, + @Deprecated('Useless property. Please remove it.') this.maxChips, this.padding, this.pressElevation, this.runAlignment = WrapAlignment.start, @@ -76,70 +76,78 @@ class FormBuilderFilterChip extends FormBuilderFieldDecoration> { super.onChanged, super.valueTransformer, super.onReset, - }) : assert((maxChips == null) || ((initialValue ?? []).length <= maxChips)), - super( - builder: (FormFieldState?> field) { - final state = field as _FormBuilderFilterChipState; - final fieldValue = field.value ?? []; + }) : assert((maxChips == null) || ((initialValue ?? []).length <= maxChips)), + super( + builder: (FormFieldState?> field) { + final state = field as _FormBuilderFilterChipState; + final fieldValue = field.value ?? []; + return Focus( + focusNode: state.effectiveFocusNode, + skipTraversal: true, + canRequestFocus: state.enabled, + debugLabel: 'FormBuilderFilterChip-$name', + child: InputDecorator( + decoration: state.decoration, + isFocused: state.effectiveFocusNode.hasFocus, + child: Wrap( + direction: direction, + alignment: alignment, + crossAxisAlignment: crossAxisAlignment, + runAlignment: runAlignment, + runSpacing: runSpacing, + spacing: spacing, + textDirection: textDirection, + verticalDirection: verticalDirection, + children: [ + for (FormBuilderChipOption option in options) + FilterChip( + label: option, + selected: fieldValue.contains(option.value), + avatar: option.avatar, + onSelected: + state.enabled && + (null == maxChips || + fieldValue.length < maxChips || + fieldValue.contains(option.value)) + ? (selected) { + final currentValue = [...fieldValue]; + selected + ? currentValue.add(option.value) + : currentValue.remove(option.value); - return InputDecorator( - decoration: state.decoration, - child: Wrap( - direction: direction, - alignment: alignment, - crossAxisAlignment: crossAxisAlignment, - runAlignment: runAlignment, - runSpacing: runSpacing, - spacing: spacing, - textDirection: textDirection, - verticalDirection: verticalDirection, - children: [ - for (FormBuilderChipOption option in options) - FilterChip( - label: option, - selected: fieldValue.contains(option.value), - avatar: option.avatar, - onSelected: state.enabled && - (null == maxChips || - fieldValue.length < maxChips || - fieldValue.contains(option.value)) - ? (selected) { - final currentValue = [...fieldValue]; - selected - ? currentValue.add(option.value) - : currentValue.remove(option.value); - - field.didChange(currentValue); - } - : null, - selectedColor: selectedColor, - disabledColor: disabledColor, - backgroundColor: backgroundColor, - shadowColor: shadowColor, - selectedShadowColor: selectedShadowColor, - elevation: elevation, - pressElevation: pressElevation, - materialTapTargetSize: materialTapTargetSize, - padding: padding, - side: side, - shape: shape, - checkmarkColor: checkmarkColor, - clipBehavior: clipBehavior, - labelStyle: labelStyle, - showCheckmark: showCheckmark, - labelPadding: labelPadding, - avatarBorder: avatarBorder, - ), - ], - ), - ); - }, - ); + field.didChange(currentValue); + } + : null, + selectedColor: selectedColor, + disabledColor: disabledColor, + backgroundColor: backgroundColor, + shadowColor: shadowColor, + selectedShadowColor: selectedShadowColor, + elevation: elevation, + pressElevation: pressElevation, + materialTapTargetSize: materialTapTargetSize, + padding: padding, + side: side, + shape: shape, + checkmarkColor: checkmarkColor, + clipBehavior: clipBehavior, + labelStyle: labelStyle, + showCheckmark: showCheckmark, + labelPadding: labelPadding, + avatarBorder: avatarBorder, + ), + ], + ), + ), + ); + }, + ); @override - FormBuilderFieldDecorationState, List> - createState() => _FormBuilderFilterChipState(); + FormBuilderFieldDecorationState, List> + createState() => _FormBuilderFilterChipState(); } -class _FormBuilderFilterChipState extends FormBuilderFieldDecorationState< - FormBuilderFilterChip, List> {} +class _FormBuilderFilterChipState + extends + FormBuilderFieldDecorationState, List> {} diff --git a/lib/src/fields/form_builder_radio_group.dart b/lib/src/fields/form_builder_radio_group.dart index 8e08de3e18..c470e097e3 100644 --- a/lib/src/fields/form_builder_radio_group.dart +++ b/lib/src/fields/form_builder_radio_group.dart @@ -60,40 +60,48 @@ class FormBuilderRadioGroup extends FormBuilderFieldDecoration { super.restorationId, this.itemDecoration, }) : super( - builder: (FormFieldState field) { - final state = field as _FormBuilderRadioGroupState; + builder: (FormFieldState field) { + final state = field as _FormBuilderRadioGroupState; - return InputDecorator( - decoration: state.decoration, - child: GroupedRadio( - activeColor: activeColor, - controlAffinity: controlAffinity, - disabled: state.enabled - ? disabled - : options.map((option) => option.value).toList(), - focusColor: focusColor, - hoverColor: hoverColor, - materialTapTargetSize: materialTapTargetSize, - onChanged: (value) { - state.didChange(value); - }, - options: options, - orientation: orientation, - separator: separator, - value: state.value, - wrapAlignment: wrapAlignment, - wrapCrossAxisAlignment: wrapCrossAxisAlignment, - wrapDirection: wrapDirection, - wrapRunAlignment: wrapRunAlignment, - wrapRunSpacing: wrapRunSpacing, - wrapSpacing: wrapSpacing, - wrapTextDirection: wrapTextDirection, - wrapVerticalDirection: wrapVerticalDirection, - itemDecoration: itemDecoration, - ), - ); - }, - ); + return Focus( + focusNode: state.effectiveFocusNode, + skipTraversal: true, + canRequestFocus: state.enabled, + debugLabel: 'FormBuilderRadioGroup-$name', + child: InputDecorator( + decoration: state.decoration, + isFocused: state.effectiveFocusNode.hasFocus, + child: GroupedRadio( + activeColor: activeColor, + controlAffinity: controlAffinity, + disabled: + state.enabled + ? disabled + : options.map((option) => option.value).toList(), + focusColor: focusColor, + hoverColor: hoverColor, + materialTapTargetSize: materialTapTargetSize, + onChanged: (value) { + state.didChange(value); + }, + options: options, + orientation: orientation, + separator: separator, + value: state.value, + wrapAlignment: wrapAlignment, + wrapCrossAxisAlignment: wrapCrossAxisAlignment, + wrapDirection: wrapDirection, + wrapRunAlignment: wrapRunAlignment, + wrapRunSpacing: wrapRunSpacing, + wrapSpacing: wrapSpacing, + wrapTextDirection: wrapTextDirection, + wrapVerticalDirection: wrapVerticalDirection, + itemDecoration: itemDecoration, + ), + ), + ); + }, + ); @override FormBuilderFieldDecorationState, T> createState() => diff --git a/lib/src/fields/form_builder_range_slider.dart b/lib/src/fields/form_builder_range_slider.dart index 8a3f57f855..0bf374dfc0 100644 --- a/lib/src/fields/form_builder_range_slider.dart +++ b/lib/src/fields/form_builder_range_slider.dart @@ -188,78 +188,95 @@ class FormBuilderRangeSlider extends FormBuilderFieldDecoration { this.valueWidget, this.maxValueWidget, this.numberFormat, - }) : super(builder: (FormFieldState field) { - final state = field as _FormBuilderRangeSliderState; - final effectiveNumberFormat = numberFormat ?? NumberFormat.compact(); - if (field.value == null || - field.value!.start < min || - field.value!.start > max || - field.value!.end < min || - field.value!.end > max) { - if (initialValue == null) { - field.setValue(RangeValues(min, min)); - } else { - field.setValue( - RangeValues(initialValue.start, initialValue.end), - ); - } - } - return InputDecorator( - decoration: state.decoration, - child: Container( - padding: const EdgeInsets.only(top: 10.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RangeSlider( - values: field.value!, - min: min, - max: max, - divisions: divisions, - activeColor: activeColor, - inactiveColor: inactiveColor, - onChangeEnd: onChangeEnd, - onChangeStart: onChangeStart, - labels: labels, - semanticFormatterCallback: semanticFormatterCallback, - onChanged: state.enabled - ? (values) { - field.didChange(values); - } - : null, - ), - Row( - children: [ - if (displayValues != DisplayValues.none && - displayValues != DisplayValues.current) - minValueWidget - ?.call(effectiveNumberFormat.format(min)) ?? - Text(effectiveNumberFormat.format(min)), - const Spacer(), - if (displayValues != DisplayValues.none && - displayValues != DisplayValues.minMax) - valueWidget?.call( - '${effectiveNumberFormat.format(field.value!.start)} - ${effectiveNumberFormat.format(field.value!.end)}') ?? - Text( - '${effectiveNumberFormat.format(field.value!.start)} - ${effectiveNumberFormat.format(field.value!.end)}'), - const Spacer(), - if (displayValues != DisplayValues.none && - displayValues != DisplayValues.current) - maxValueWidget - ?.call(effectiveNumberFormat.format(max)) ?? - Text(effectiveNumberFormat.format(max)), - ], - ), - ], - ), - ), - ); - }); + }) : super( + builder: (FormFieldState field) { + final state = field as _FormBuilderRangeSliderState; + final effectiveNumberFormat = numberFormat ?? NumberFormat.compact(); + if (field.value == null || + field.value!.start < min || + field.value!.start > max || + field.value!.end < min || + field.value!.end > max) { + if (initialValue == null) { + field.setValue(RangeValues(min, min)); + } else { + field.setValue( + RangeValues(initialValue.start, initialValue.end), + ); + } + } + // TODO: Solve focus issue when Flutter team solve this issue + // https://github.com/flutter/flutter/issues/53958 + return Focus( + focusNode: state.effectiveFocusNode, + skipTraversal: true, + canRequestFocus: state.enabled, + debugLabel: 'FormBuilderRangeSlider-$name', + child: InputDecorator( + decoration: state.decoration, + isFocused: state.effectiveFocusNode.hasFocus, + child: Container( + padding: const EdgeInsets.only(top: 10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RangeSlider( + values: field.value!, + min: min, + max: max, + divisions: divisions, + activeColor: activeColor, + inactiveColor: inactiveColor, + onChangeEnd: onChangeEnd, + onChangeStart: onChangeStart, + labels: labels, + semanticFormatterCallback: semanticFormatterCallback, + onChanged: + state.enabled + ? (values) { + field.didChange(values); + } + : null, + ), + Row( + children: [ + if (displayValues != DisplayValues.none && + displayValues != DisplayValues.current) + minValueWidget?.call( + effectiveNumberFormat.format(min), + ) ?? + Text(effectiveNumberFormat.format(min)), + const Spacer(), + if (displayValues != DisplayValues.none && + displayValues != DisplayValues.minMax) + valueWidget?.call( + '${effectiveNumberFormat.format(field.value!.start)} - ${effectiveNumberFormat.format(field.value!.end)}', + ) ?? + Text( + '${effectiveNumberFormat.format(field.value!.start)} - ${effectiveNumberFormat.format(field.value!.end)}', + ), + const Spacer(), + if (displayValues != DisplayValues.none && + displayValues != DisplayValues.current) + maxValueWidget?.call( + effectiveNumberFormat.format(max), + ) ?? + Text(effectiveNumberFormat.format(max)), + ], + ), + ], + ), + ), + ), + ); + }, + ); @override FormBuilderFieldDecorationState - createState() => _FormBuilderRangeSliderState(); + createState() => _FormBuilderRangeSliderState(); } -class _FormBuilderRangeSliderState extends FormBuilderFieldDecorationState< - FormBuilderRangeSlider, RangeValues> {} +class _FormBuilderRangeSliderState + extends + FormBuilderFieldDecorationState {} diff --git a/lib/src/fields/form_builder_slider.dart b/lib/src/fields/form_builder_slider.dart index ab0799612c..e394d11051 100644 --- a/lib/src/fields/form_builder_slider.dart +++ b/lib/src/fields/form_builder_slider.dart @@ -212,65 +212,68 @@ class FormBuilderSlider extends FormBuilderFieldDecoration { this.minValueWidget, this.valueWidget, }) : super( - builder: (FormFieldState field) { - final state = field as _FormBuilderSliderState; - final effectiveNumberFormat = - numberFormat ?? NumberFormat.compact(); + builder: (FormFieldState field) { + final state = field as _FormBuilderSliderState; + final effectiveNumberFormat = numberFormat ?? NumberFormat.compact(); - return InputDecorator( - decoration: state.decoration, - child: Container( - padding: const EdgeInsets.only(top: 10.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Slider( - value: field.value!, - min: min, - max: max, - divisions: divisions, - activeColor: activeColor, - inactiveColor: inactiveColor, - onChangeEnd: onChangeEnd, - onChangeStart: onChangeStart, - label: label, - semanticFormatterCallback: semanticFormatterCallback, - onChanged: state.enabled - ? (value) { - field.didChange(value); - } - : null, - autofocus: autofocus, - mouseCursor: mouseCursor, - focusNode: state.effectiveFocusNode, - ), - Row( - children: [ - if (displayValues != DisplayValues.none && - displayValues != DisplayValues.current) - minValueWidget - ?.call(effectiveNumberFormat.format(min)) ?? - Text(effectiveNumberFormat.format(min)), - const Spacer(), - if (displayValues != DisplayValues.none && - displayValues != DisplayValues.minMax) - valueWidget?.call( - effectiveNumberFormat.format(field.value)) ?? - Text(effectiveNumberFormat.format(field.value)), - const Spacer(), - if (displayValues != DisplayValues.none && - displayValues != DisplayValues.current) - maxValueWidget - ?.call(effectiveNumberFormat.format(max)) ?? - Text(effectiveNumberFormat.format(max)), - ], - ), - ], - ), - ), - ); - }, - ); + return InputDecorator( + decoration: state.decoration, + child: Container( + padding: const EdgeInsets.only(top: 10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Slider( + value: field.value!, + min: min, + max: max, + divisions: divisions, + activeColor: activeColor, + inactiveColor: inactiveColor, + onChangeEnd: onChangeEnd, + onChangeStart: onChangeStart, + label: label, + semanticFormatterCallback: semanticFormatterCallback, + onChanged: + state.enabled + ? (value) { + field.didChange(value); + } + : null, + autofocus: autofocus, + mouseCursor: mouseCursor, + focusNode: state.effectiveFocusNode, + ), + Row( + children: [ + if (displayValues != DisplayValues.none && + displayValues != DisplayValues.current) + minValueWidget?.call( + effectiveNumberFormat.format(min), + ) ?? + Text(effectiveNumberFormat.format(min)), + const Spacer(), + if (displayValues != DisplayValues.none && + displayValues != DisplayValues.minMax) + valueWidget?.call( + effectiveNumberFormat.format(field.value), + ) ?? + Text(effectiveNumberFormat.format(field.value)), + const Spacer(), + if (displayValues != DisplayValues.none && + displayValues != DisplayValues.current) + maxValueWidget?.call( + effectiveNumberFormat.format(max), + ) ?? + Text(effectiveNumberFormat.format(max)), + ], + ), + ], + ), + ), + ); + }, + ); @override FormBuilderFieldDecorationState createState() => diff --git a/lib/src/fields/form_builder_switch.dart b/lib/src/fields/form_builder_switch.dart index c6f7b01d07..fa7b0520f8 100644 --- a/lib/src/fields/form_builder_switch.dart +++ b/lib/src/fields/form_builder_switch.dart @@ -117,37 +117,40 @@ class FormBuilderSwitch extends FormBuilderFieldDecoration { this.autofocus = false, this.selected = false, }) : super( - builder: (FormFieldState field) { - final state = field as _FormBuilderSwitchState; - - return InputDecorator( - decoration: state.decoration, - child: SwitchListTile( - dense: true, - isThreeLine: false, - contentPadding: contentPadding, - title: title, - value: state.value ?? false, - onChanged: state.enabled - ? (value) { - field.didChange(value); - } - : null, - activeColor: activeColor, - activeThumbImage: activeThumbImage, - activeTrackColor: activeTrackColor, - inactiveThumbColor: inactiveThumbColor, - inactiveThumbImage: activeThumbImage, - inactiveTrackColor: inactiveTrackColor, - secondary: secondary, - subtitle: subtitle, - autofocus: autofocus, - selected: selected, - controlAffinity: controlAffinity, - ), - ); - }, - ); + builder: (FormFieldState field) { + final state = field as _FormBuilderSwitchState; + + return InputDecorator( + decoration: state.decoration, + isFocused: state.effectiveFocusNode.hasFocus, + child: SwitchListTile( + focusNode: state.effectiveFocusNode, + dense: true, + isThreeLine: false, + contentPadding: contentPadding, + title: title, + value: state.value ?? false, + onChanged: + state.enabled + ? (value) { + field.didChange(value); + } + : null, + activeColor: activeColor, + activeThumbImage: activeThumbImage, + activeTrackColor: activeTrackColor, + inactiveThumbColor: inactiveThumbColor, + inactiveThumbImage: activeThumbImage, + inactiveTrackColor: inactiveTrackColor, + secondary: secondary, + subtitle: subtitle, + autofocus: autofocus, + selected: selected, + controlAffinity: controlAffinity, + ), + ); + }, + ); @override FormBuilderFieldDecorationState createState() => diff --git a/lib/src/fields/form_builder_text_field.dart b/lib/src/fields/form_builder_text_field.dart index ea8b577d0a..4568ce2007 100644 --- a/lib/src/fields/form_builder_text_field.dart +++ b/lib/src/fields/form_builder_text_field.dart @@ -331,6 +331,9 @@ class FormBuilderTextField extends FormBuilderFieldDecoration { /// enabled, [onTap] is called for every tap including consecutive taps. final bool onTapAlwaysCalled; + /// {@macro flutter.widgets.editableText.stylusHandwritingEnabled} + final bool stylusHandwritingEnabled; + /// {@macro flutter.widgets.editableText.scribbleEnabled} final bool scribbleEnabled; @@ -424,6 +427,11 @@ class FormBuilderTextField extends FormBuilderFieldDecoration { this.contentInsertionConfiguration, this.spellCheckConfiguration, this.clipBehavior = Clip.hardEdge, + @Deprecated( + 'This property will be removed in the next Flutter stable versions. ' + 'Use FocusNode.canRequestFocus instead. ' + 'Ref: https://docs.flutter.dev/release/breaking-changes/can-request-focus', + ) this.canRequestFocus = true, this.cursorErrorColor, this.cursorOpacityAnimates, @@ -431,96 +439,104 @@ class FormBuilderTextField extends FormBuilderFieldDecoration { this.groupId = EditableText, this.onAppPrivateCommand, this.onTapAlwaysCalled = false, + @Deprecated( + 'Use `stylusHandwritingEnabled` instead. ' + 'This feature was deprecated after v3.27.0-0.2.pre.', + ) this.scribbleEnabled = true, + this.stylusHandwritingEnabled = + EditableText.defaultStylusHandwritingEnabled, this.selectionControls, this.statesController, this.undoController, - }) : assert(initialValue == null || controller == null), - assert(minLines == null || minLines > 0), - assert(maxLines == null || maxLines > 0), - assert( - (minLines == null) || (maxLines == null) || (maxLines >= minLines), - 'minLines can\'t be greater than maxLines', - ), - assert( - !expands || (minLines == null && maxLines == null), - 'minLines and maxLines must be null when expands is true.', - ), - assert(!obscureText || maxLines == 1, - 'Obscured fields cannot be multiline.'), - assert(maxLength == null || maxLength > 0), - super( - initialValue: controller != null ? controller.text : initialValue, - builder: (FormFieldState field) { - final state = field as _FormBuilderTextFieldState; - - return TextField( - restorationId: restorationId, - controller: state._effectiveController, - focusNode: state.effectiveFocusNode, - decoration: state.decoration, - keyboardType: keyboardType, - textInputAction: textInputAction, - style: style, - strutStyle: strutStyle, - textAlign: textAlign, - textAlignVertical: textAlignVertical, - textDirection: textDirection, - textCapitalization: textCapitalization, - autofocus: autofocus, - readOnly: readOnly, - showCursor: showCursor, - obscureText: obscureText, - autocorrect: autocorrect, - enableSuggestions: enableSuggestions, - maxLengthEnforcement: maxLengthEnforcement, - maxLines: maxLines, - minLines: minLines, - expands: expands, - maxLength: maxLength, - onTap: onTap, - onTapOutside: onTapOutside, - onEditingComplete: onEditingComplete, - onSubmitted: onSubmitted, - inputFormatters: inputFormatters, - enabled: state.enabled, - cursorWidth: cursorWidth, - cursorHeight: cursorHeight, - cursorRadius: cursorRadius, - cursorColor: cursorColor, - scrollPadding: scrollPadding, - keyboardAppearance: keyboardAppearance, - enableInteractiveSelection: enableInteractiveSelection, - buildCounter: buildCounter, - dragStartBehavior: dragStartBehavior, - scrollController: scrollController, - scrollPhysics: scrollPhysics, - selectionHeightStyle: selectionHeightStyle, - selectionWidthStyle: selectionWidthStyle, - smartDashesType: smartDashesType, - smartQuotesType: smartQuotesType, - mouseCursor: mouseCursor, - contextMenuBuilder: contextMenuBuilder, - obscuringCharacter: obscuringCharacter, - autofillHints: autofillHints, - magnifierConfiguration: magnifierConfiguration, - contentInsertionConfiguration: contentInsertionConfiguration, - spellCheckConfiguration: spellCheckConfiguration, - clipBehavior: clipBehavior, - canRequestFocus: canRequestFocus, - cursorErrorColor: cursorErrorColor, - cursorOpacityAnimates: cursorOpacityAnimates, - enableIMEPersonalizedLearning: enableIMEPersonalizedLearning, - groupId: groupId, - onAppPrivateCommand: onAppPrivateCommand, - onTapAlwaysCalled: onTapAlwaysCalled, - scribbleEnabled: scribbleEnabled, - selectionControls: selectionControls, - statesController: statesController, - undoController: undoController, - ); - }, - ); + }) : assert(initialValue == null || controller == null), + assert(minLines == null || minLines > 0), + assert(maxLines == null || maxLines > 0), + assert( + (minLines == null) || (maxLines == null) || (maxLines >= minLines), + 'minLines can\'t be greater than maxLines', + ), + assert( + !expands || (minLines == null && maxLines == null), + 'minLines and maxLines must be null when expands is true.', + ), + assert( + !obscureText || maxLines == 1, + 'Obscured fields cannot be multiline.', + ), + assert(maxLength == null || maxLength > 0), + super( + initialValue: controller != null ? controller.text : initialValue, + builder: (FormFieldState field) { + final state = field as _FormBuilderTextFieldState; + + return TextField( + restorationId: restorationId, + controller: state._effectiveController, + focusNode: state.effectiveFocusNode, + decoration: state.decoration, + keyboardType: keyboardType, + textInputAction: textInputAction, + style: style, + strutStyle: strutStyle, + textAlign: textAlign, + textAlignVertical: textAlignVertical, + textDirection: textDirection, + textCapitalization: textCapitalization, + autofocus: autofocus, + readOnly: readOnly, + showCursor: showCursor, + obscureText: obscureText, + autocorrect: autocorrect, + enableSuggestions: enableSuggestions, + maxLengthEnforcement: maxLengthEnforcement, + maxLines: maxLines, + minLines: minLines, + expands: expands, + maxLength: maxLength, + onTap: onTap, + onTapOutside: onTapOutside, + onEditingComplete: onEditingComplete, + onSubmitted: onSubmitted, + inputFormatters: inputFormatters, + enabled: state.enabled, + cursorWidth: cursorWidth, + cursorHeight: cursorHeight, + cursorRadius: cursorRadius, + cursorColor: cursorColor, + scrollPadding: scrollPadding, + keyboardAppearance: keyboardAppearance, + enableInteractiveSelection: enableInteractiveSelection, + buildCounter: buildCounter, + dragStartBehavior: dragStartBehavior, + scrollController: scrollController, + scrollPhysics: scrollPhysics, + selectionHeightStyle: selectionHeightStyle, + selectionWidthStyle: selectionWidthStyle, + smartDashesType: smartDashesType, + smartQuotesType: smartQuotesType, + mouseCursor: mouseCursor, + contextMenuBuilder: contextMenuBuilder, + obscuringCharacter: obscuringCharacter, + autofillHints: autofillHints, + magnifierConfiguration: magnifierConfiguration, + contentInsertionConfiguration: contentInsertionConfiguration, + spellCheckConfiguration: spellCheckConfiguration, + clipBehavior: clipBehavior, + canRequestFocus: canRequestFocus, + cursorErrorColor: cursorErrorColor, + cursorOpacityAnimates: cursorOpacityAnimates, + enableIMEPersonalizedLearning: enableIMEPersonalizedLearning, + groupId: groupId, + onAppPrivateCommand: onAppPrivateCommand, + onTapAlwaysCalled: onTapAlwaysCalled, + stylusHandwritingEnabled: stylusHandwritingEnabled, + selectionControls: selectionControls, + statesController: statesController, + undoController: undoController, + ); + }, + ); static Widget _defaultContextMenuBuilder( BuildContext context, diff --git a/lib/src/form_builder.dart b/lib/src/form_builder.dart index 0734e4514a..f5ce4239a0 100644 --- a/lib/src/form_builder.dart +++ b/lib/src/form_builder.dart @@ -1,5 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_form_builder/src/extensions/autovalidatemode_extension.dart'; /// A container for form fields. class FormBuilder extends StatefulWidget { @@ -9,9 +10,6 @@ class FormBuilder extends StatefulWidget { /// will rebuild. final VoidCallback? onChanged; - /// DEPRECATED: Use [onPopInvokedWithResult] instead. - final void Function(bool)? onPopInvoked; - /// {@macro flutter.widgets.navigator.onPopInvokedWithResult} /// /// {@tool dartpad} @@ -104,11 +102,6 @@ class FormBuilder extends StatefulWidget { required this.child, this.onChanged, this.autovalidateMode, - @Deprecated( - 'Use onPopInvokedWithResult instead. ' - 'This feature was deprecated after v3.22.0-12.0.pre.', - ) - this.onPopInvoked, this.onPopInvokedWithResult, this.initialValue = const {}, this.skipDisabled = false, @@ -125,8 +118,8 @@ class FormBuilder extends StatefulWidget { } /// A type alias for a map of form fields. -typedef FormBuilderFields - = Map, dynamic>>; +typedef FormBuilderFields = + Map, dynamic>>; class FormBuilderState extends State { final GlobalKey _formKey = GlobalKey(); @@ -142,6 +135,7 @@ class FormBuilderState extends State { /// Only used to internal logic bool get focusOnInvalid => _focusOnInvalid; + /// Get if form is enabled bool get enabled => widget.enabled; /// Verify if all fields on form are valid. @@ -159,10 +153,11 @@ class FormBuilderState extends State { /// Get a map of errors Map get errors => { - for (var element - in fields.entries.where((element) => element.value.hasError)) - element.key.toString(): element.value.errorText ?? '' - }; + for (var element in fields.entries.where( + (element) => element.value.hasError, + )) + element.key.toString(): element.value.errorText ?? '', + }; /// Get initialValue. Map get initialValue => widget.initialValue; @@ -170,24 +165,25 @@ class FormBuilderState extends State { /// Get all fields of form. FormBuilderFields get fields => _fields; + /// Get all fields values of form. Map get instantValue => Map.unmodifiable( - _instantValue.map( - (key, value) => MapEntry( - key, - _transformers[key] == null ? value : _transformers[key]!(value), - ), - ), - ); + _instantValue.map( + (key, value) => MapEntry( + key, + _transformers[key] == null ? value : _transformers[key]!(value), + ), + ), + ); /// Returns the saved value only Map get value => Map.unmodifiable( - _savedValue.map( - (key, value) => MapEntry( - key, - _transformers[key] == null ? value : _transformers[key]!(value), - ), - ), - ); + _savedValue.map( + (key, value) => MapEntry( + key, + _transformers[key] == null ? value : _transformers[key]!(value), + ), + ), + ); dynamic transformValue(String name, T? v) { final t = _transformers[name]; @@ -203,15 +199,18 @@ class FormBuilderState extends State { initialValue[name]; } + /// Get a field value by name void setInternalFieldValue(String name, T? value) { _instantValue[name] = value; widget.onChanged?.call(); } + /// Remove a field value by name void removeInternalFieldValue(String name) { _instantValue.remove(name); } + /// Register a field on form void registerField(String name, FormBuilderFieldState field) { // Each field must have a unique name. Ideally we could simply: // assert(!_fields.containsKey(name)); @@ -222,8 +221,10 @@ class FormBuilderState extends State { final oldField = _fields[name]; assert(() { if (oldField != null) { - debugPrint('Warning! Replacing duplicate Field for $name' - ' -- this is OK to ignore as long as the field was intentionally replaced'); + debugPrint( + 'Warning! Replacing duplicate Field for $name' + ' -- this is OK to ignore as long as the field was intentionally replaced', + ); } return true; }()); @@ -235,12 +236,10 @@ class FormBuilderState extends State { _instantValue[name] = field.initialValue ?? initialValue[name]; } - field.setValue( - _instantValue[name], - populateForm: false, - ); + field.setValue(_instantValue[name], populateForm: false); } + /// Unregister a field on form void unregisterField(String name, FormBuilderFieldState field) { assert( _fields.containsKey(name), @@ -262,13 +261,16 @@ class FormBuilderState extends State { assert(() { // This is OK to ignore when you are intentionally replacing a field // with another field using the same name. - debugPrint('Warning! Ignoring Field unregistration for $name' - ' -- this is OK to ignore as long as the field was intentionally replaced'); + debugPrint( + 'Warning! Ignoring Field unregistration for $name' + ' -- this is OK to ignore as long as the field was intentionally replaced', + ); return true; }()); } } + /// Save form values void save() { _formKey.currentState!.save(); // Copy values from instant to saved @@ -276,16 +278,6 @@ class FormBuilderState extends State { _savedValue.addAll(_instantValue); } - @Deprecated( - 'Will be remove to avoid redundancy. Use fields[name]?.invalidate(errorText) instead') - void invalidateField({required String name, String? errorText}) => - fields[name]?.invalidate(errorText ?? ''); - - @Deprecated( - 'Will be remove to avoid redundancy. Use fields.first.invalidate(errorText) instead') - void invalidateFirstField({required String errorText}) => - fields.values.first.invalidate(errorText); - /// Validate all fields of form /// /// Focus to first invalid field when has field invalid, if [focusOnInvalid] is `true`. @@ -342,7 +334,7 @@ class FormBuilderState extends State { ); } - /// Reset form to `initialValue` + /// Reset form to `initialValue` set on FormBuilder or on each field. void reset() { _formKey.currentState?.reset(); } @@ -357,22 +349,29 @@ class FormBuilderState extends State { }); } + @override + void initState() { + // Verify if need auto validate form + if (enabled && (widget.autovalidateMode?.isAlways ?? false)) { + WidgetsBinding.instance.addPostFrameCallback((_) { + // No focus on invalid, like default behavior on Flutter base Form + validate(focusOnInvalid: false); + }); + } + super.initState(); + } + @override Widget build(BuildContext context) { return Form( key: _formKey, autovalidateMode: widget.autovalidateMode, onPopInvokedWithResult: widget.onPopInvokedWithResult, - // ignore: deprecated_member_use - onPopInvoked: widget.onPopInvoked, canPop: widget.canPop, // `onChanged` is called during setInternalFieldValue else will be called early child: _FormBuilderScope( formState: this, - child: FocusTraversalGroup( - policy: WidgetOrderTraversalPolicy(), - child: widget.child, - ), + child: FocusTraversalGroup(child: widget.child), ), ); } diff --git a/lib/src/form_builder_field.dart b/lib/src/form_builder_field.dart index 043afffa7a..588f65a7fe 100644 --- a/lib/src/form_builder_field.dart +++ b/lib/src/form_builder_field.dart @@ -1,6 +1,5 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; -import 'package:flutter_form_builder/src/extensions/autovalidatemode_extension.dart'; enum OptionsOrientation { horizontal, vertical, wrap, auto } @@ -72,12 +71,17 @@ class FormBuilderFieldState, T> FormBuilderState? _formBuilderState; bool _touched = false; bool _dirty = false; + + /// The focus node that is used to focus this field. late FocusNode effectiveFocusNode; + + /// The focus attachment for the [effectiveFocusNode]. FocusAttachment? focusAttachment; @override F get widget => super.widget as F; + /// Returns the parent [FormBuilderState] if it exists. FormBuilderState? get formState => _formBuilderState; /// Returns the initial value, which may be declared at the field, or by the @@ -85,29 +89,39 @@ class FormBuilderFieldState, T> /// initialValue prevails. T? get initialValue => widget.initialValue ?? - (_formBuilderState?.initialValue ?? - const {})[widget.name] as T?; + (_formBuilderState?.initialValue ?? const {})[widget + .name] + as T?; dynamic get transformedValue => widget.valueTransformer == null ? value : widget.valueTransformer!(value); @override + /// Returns the current error text, + /// which may be a validation error or a custom error text. String? get errorText => super.errorText ?? _customErrorText; @override + /// Returns `true` if the field has an error or has a custom error text. bool get hasError => super.hasError || errorText != null; @override - bool get isValid => super.isValid && errorText == null; + /// Returns `true` if the field is valid and has no custom error text. + bool get isValid => super.isValid && _customErrorText == null; + /// Returns `true` if the field is valid. bool get valueIsValid => super.isValid; + + /// Returns `true` if the field has an error. bool get valueHasError => super.hasError; + /// Returns `true` if the field is enabled and the parent FormBuilder is enabled. bool get enabled => widget.enabled && (_formBuilderState?.enabled ?? true); + + /// Returns `true` if the field is read only. + /// + /// See [FormBuilder.skipDisabled] for more information. bool get readOnly => !(_formBuilderState?.widget.skipDisabled ?? false); - bool get _isAlwaysValidate => - widget.autovalidateMode.isAlways || - (_formBuilderState?.widget.autovalidateMode?.isAlways ?? false); /// Will be true if the field is dirty /// @@ -140,11 +154,6 @@ class FormBuilderFieldState, T> focusAttachment = effectiveFocusNode.attach(context); // Verify if need auto validate form - if ((enabled || readOnly) && _isAlwaysValidate) { - WidgetsBinding.instance.addPostFrameCallback((_) { - validate(); - }); - } } @override @@ -157,7 +166,8 @@ class FormBuilderFieldState, T> if (widget.focusNode != oldWidget.focusNode) { focusAttachment?.detach(); effectiveFocusNode.removeListener(_touchedHandler); - effectiveFocusNode = widget.focusNode ?? FocusNode(); + effectiveFocusNode = + widget.focusNode ?? FocusNode(canRequestFocus: enabled); effectiveFocusNode.addListener(_touchedHandler); focusAttachment = effectiveFocusNode.attach(context); } @@ -207,6 +217,9 @@ class FormBuilderFieldState, T> } @override + /// Reset field value to initial value + /// + /// Also reset custom error text if exists, and set [isDirty] to `false`. void reset() { super.reset(); didChange(initialValue); @@ -243,7 +256,8 @@ class FormBuilderFieldState, T> } final isValid = super.validate() && !hasError; - final fields = _formBuilderState?.fields ?? + final fields = + _formBuilderState?.fields ?? , dynamic>>{}; if (!isValid && @@ -284,10 +298,12 @@ class FormBuilderFieldState, T> ); } + /// Focus field void focus() { FocusScope.of(context).requestFocus(effectiveFocusNode); } + /// Scroll to show field void ensureScrollableVisibility() { Scrollable.ensureVisible(context); } diff --git a/lib/src/form_builder_field_decoration.dart b/lib/src/form_builder_field_decoration.dart index d52fb81018..bff8e0fd9d 100644 --- a/lib/src/form_builder_field_decoration.dart +++ b/lib/src/form_builder_field_decoration.dart @@ -6,7 +6,7 @@ import 'package:flutter_form_builder/flutter_form_builder.dart'; /// /// This class override `decoration.enable` with [enable] value class FormBuilderFieldDecoration extends FormBuilderField { - const FormBuilderFieldDecoration({ + FormBuilderFieldDecoration({ super.key, super.onSaved, super.initialValue, @@ -21,26 +21,39 @@ class FormBuilderFieldDecoration extends FormBuilderField { super.focusNode, required super.builder, this.decoration = const InputDecoration(), - }); + }) : assert( + decoration.enabled == enabled || + (enabled == false && decoration.enabled), + '''decoration.enabled will be used instead of enabled FormBuilderField property. + This will create conflicts and unexpected behaviors on focus, errorText, and other properties. + Please, to enable or disable the field, use the enabled property of FormBuilderField.''', + ); final InputDecoration decoration; @override FormBuilderFieldDecorationState, T> - createState() => - FormBuilderFieldDecorationState, T>(); + createState() => + FormBuilderFieldDecorationState, T>(); } -class FormBuilderFieldDecorationState, - T> extends FormBuilderFieldState, T> { +class FormBuilderFieldDecorationState< + F extends FormBuilderFieldDecoration, + T +> + extends FormBuilderFieldState, T> { @override F get widget => super.widget as F; + /// Get the decoration with the current state InputDecoration get decoration => widget.decoration.copyWith( - errorText: widget.enabled || readOnly + // Read only allow show error to support property skipDisabled + errorText: + widget.enabled || readOnly ? widget.decoration.errorText ?? errorText : null, - enabled: widget.enabled, - ); + enabled: + widget.decoration.enabled ? widget.enabled : widget.decoration.enabled, + ); @override bool get hasError => super.hasError || widget.decoration.errorText != null; diff --git a/lib/src/form_builder_field_option.dart b/lib/src/form_builder_field_option.dart index fe3e8ba0f3..95e41994d9 100644 --- a/lib/src/form_builder_field_option.dart +++ b/lib/src/form_builder_field_option.dart @@ -5,15 +5,14 @@ import 'package:flutter/widgets.dart'; /// The type `T` is the type of the value the entry represents. All the entries /// in a given menu must represent values with consistent types. class FormBuilderFieldOption extends StatelessWidget { + /// The widget to display in list of options. final Widget? child; + + /// The value of the option. final T value; /// Creates an option for fields with selection options - const FormBuilderFieldOption({ - super.key, - required this.value, - this.child, - }); + const FormBuilderFieldOption({super.key, required this.value, this.child}); @override Widget build(BuildContext context) { diff --git a/lib/src/options/form_builder_chip_option.dart b/lib/src/options/form_builder_chip_option.dart index d8f390bd69..481e6f7f45 100644 --- a/lib/src/options/form_builder_chip_option.dart +++ b/lib/src/options/form_builder_chip_option.dart @@ -6,6 +6,7 @@ import 'package:flutter_form_builder/flutter_form_builder.dart'; /// The type `T` is the type of the value the entry represents. All the entries /// in a given menu must represent values with consistent types. class FormBuilderChipOption extends FormBuilderFieldOption { + /// The avatar to display in list of options. final Widget? avatar; /// Creates an option for fields with selection options diff --git a/lib/src/widgets/grouped_checkbox.dart b/lib/src/widgets/grouped_checkbox.dart index 992950d5ac..28a216fa96 100644 --- a/lib/src/widgets/grouped_checkbox.dart +++ b/lib/src/widgets/grouped_checkbox.dart @@ -23,6 +23,9 @@ class GroupedCheckbox extends StatelessWidget { /// The color to use when this checkbox is checked. /// /// Defaults to [ColorScheme.secondary]. + /// + /// If [fillColor] returns a non-null color in the [WidgetState.selected] + /// state, it will be used instead of this color. final Color? activeColor; final VisualDensity? visualDensity; @@ -223,31 +226,29 @@ class GroupedCheckbox extends StatelessWidget { for (var i = 0; i < options.length; i++) { widgetList.add(buildItem(i)); } - Widget finalWidget; - if (orientation == OptionsOrientation.auto) { - finalWidget = OverflowBar( + + return switch (orientation) { + OptionsOrientation.auto => OverflowBar( alignment: MainAxisAlignment.spaceEvenly, children: widgetList, - ); - } else if (orientation == OptionsOrientation.vertical) { - finalWidget = SingleChildScrollView( + ), + OptionsOrientation.vertical => SingleChildScrollView( scrollDirection: Axis.vertical, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: widgetList, ), - ); - } else if (orientation == OptionsOrientation.horizontal) { - finalWidget = SingleChildScrollView( + ), + OptionsOrientation.horizontal => SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( - children: widgetList.map((item) { - return Column(children: [item]); - }).toList(), + children: + widgetList.map((item) { + return Column(children: [item]); + }).toList(), ), - ); - } else { - finalWidget = SingleChildScrollView( + ), + OptionsOrientation.wrap => SingleChildScrollView( child: Wrap( spacing: wrapSpacing, runSpacing: wrapRunSpacing, @@ -259,9 +260,8 @@ class GroupedCheckbox extends StatelessWidget { runAlignment: wrapRunAlignment, children: widgetList, ), - ); - } - return finalWidget; + ), + }; } /// the composite of all the components for the option at index @@ -276,30 +276,35 @@ class GroupedCheckbox extends StatelessWidget { focusColor: focusColor, hoverColor: hoverColor, materialTapTargetSize: materialTapTargetSize, - value: tristate - ? value?.contains(optionValue) - : true == value?.contains(optionValue), + value: + tristate + ? value?.contains(optionValue) + : true == value?.contains(optionValue), tristate: tristate, - onChanged: isOptionDisabled - ? null - : (selected) { - List selectedListItems = value == null ? [] : List.of(value!); - selected! - ? selectedListItems.add(optionValue) - : selectedListItems.remove(optionValue); - onChanged(selectedListItems); - }, + onChanged: + isOptionDisabled + ? null + : (selected) { + List selectedListItems = + value == null ? [] : List.of(value!); + selected! + ? selectedListItems.add(optionValue) + : selectedListItems.remove(optionValue); + onChanged(selectedListItems); + }, ); final label = GestureDetector( - onTap: isOptionDisabled - ? null - : () { - List selectedListItems = value == null ? [] : List.of(value!); - selectedListItems.contains(optionValue) - ? selectedListItems.remove(optionValue) - : selectedListItems.add(optionValue); - onChanged(selectedListItems); - }, + onTap: + isOptionDisabled + ? null + : () { + List selectedListItems = + value == null ? [] : List.of(value!); + selectedListItems.contains(optionValue) + ? selectedListItems.remove(optionValue) + : selectedListItems.add(optionValue); + onChanged(selectedListItems); + }, child: option, ); diff --git a/lib/src/widgets/grouped_radio.dart b/lib/src/widgets/grouped_radio.dart index 855ff9cda9..f990cd6625 100644 --- a/lib/src/widgets/grouped_radio.dart +++ b/lib/src/widgets/grouped_radio.dart @@ -217,40 +217,36 @@ class _GroupedRadioState extends State> { widgetList.add(buildItem(i)); } - switch (widget.orientation) { - case OptionsOrientation.auto: - return OverflowBar( - alignment: MainAxisAlignment.spaceEvenly, + return switch (widget.orientation) { + OptionsOrientation.auto => OverflowBar( + alignment: MainAxisAlignment.spaceEvenly, + children: widgetList, + ), + OptionsOrientation.vertical => SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: widgetList, - ); - case OptionsOrientation.vertical: - return SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: widgetList, - ), - ); - case OptionsOrientation.horizontal: - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row(children: widgetList), - ); - case OptionsOrientation.wrap: - return SingleChildScrollView( - child: Wrap( - spacing: widget.wrapSpacing, - runSpacing: widget.wrapRunSpacing, - textDirection: widget.wrapTextDirection, - crossAxisAlignment: widget.wrapCrossAxisAlignment, - verticalDirection: widget.wrapVerticalDirection, - alignment: widget.wrapAlignment, - direction: Axis.horizontal, - runAlignment: widget.wrapRunAlignment, - children: widgetList, - ), - ); - } + ), + ), + OptionsOrientation.horizontal => SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row(children: widgetList), + ), + OptionsOrientation.wrap => SingleChildScrollView( + child: Wrap( + spacing: widget.wrapSpacing, + runSpacing: widget.wrapRunSpacing, + textDirection: widget.wrapTextDirection, + crossAxisAlignment: widget.wrapCrossAxisAlignment, + verticalDirection: widget.wrapVerticalDirection, + alignment: widget.wrapAlignment, + direction: Axis.horizontal, + runAlignment: widget.wrapRunAlignment, + children: widgetList, + ), + ), + }; } /// the composite of all the components for the option at index @@ -265,19 +261,21 @@ class _GroupedRadioState extends State> { hoverColor: widget.hoverColor, materialTapTargetSize: widget.materialTapTargetSize, value: optionValue, - onChanged: isOptionDisabled - ? null - : (T? selected) { - widget.onChanged(selected); - }, + onChanged: + isOptionDisabled + ? null + : (T? selected) { + widget.onChanged(selected); + }, ); final label = GestureDetector( - onTap: isOptionDisabled - ? null - : () { - widget.onChanged(optionValue); - }, + onTap: + isOptionDisabled + ? null + : () { + widget.onChanged(optionValue); + }, child: option, ); @@ -308,12 +306,14 @@ class _GroupedRadioState extends State> { compositeItem = Container( decoration: widget.itemDecoration, margin: EdgeInsets.only( - bottom: widget.orientation == OptionsOrientation.vertical - ? widget.wrapSpacing - : 0.0, - right: widget.orientation == OptionsOrientation.horizontal - ? widget.wrapSpacing - : 0.0, + bottom: + widget.orientation == OptionsOrientation.vertical + ? widget.wrapSpacing + : 0.0, + right: + widget.orientation == OptionsOrientation.horizontal + ? widget.wrapSpacing + : 0.0, ), child: compositeItem, ); diff --git a/pubspec.lock b/pubspec.lock index 3b55fffaa8..0fe23277cd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,50 +5,50 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.12.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" flutter: dependency: "direct main" description: flutter @@ -79,18 +79,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -103,18 +103,18 @@ packages: dependency: transitive description: name: lints - sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413" + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.1.1" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -127,18 +127,18 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" sky_engine: dependency: transitive description: flutter @@ -148,50 +148,50 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.4" vector_math: dependency: transitive description: @@ -204,10 +204,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "14.3.1" sdks: - dart: ">=3.6.0 <4.0.0" - flutter: ">=3.27.0" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index 0a8dded737..6d792c103e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,18 +1,17 @@ name: flutter_form_builder description: This package helps in creation of forms in Flutter by removing the boilerplate code, reusing validation, react to changes, and collect final user input. -version: 9.7.0 +version: 10.0.0 repository: https://github.com/flutter-form-builder-ecosystem/flutter_form_builder issue_tracker: https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues homepage: https://github.com/flutter-form-builder-ecosystem topics: - form - - forms funding: - https://opencollective.com/flutter-form-builder-ecosystem environment: - sdk: ">=3.6.0 <4.0.0" - flutter: ">=3.27.0" + sdk: ">=3.7.0 <4.0.0" + flutter: ">=3.29.0" dependencies: flutter: diff --git a/test/form_builder_checkbox_group_test.dart b/test/form_builder_checkbox_group_test.dart deleted file mode 100644 index 0e74d588cb..0000000000 --- a/test/form_builder_checkbox_group_test.dart +++ /dev/null @@ -1,164 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_form_builder/flutter_form_builder.dart'; - -import 'form_builder_tester.dart'; - -void main() { - testWidgets('FormBuilderCheckboxGroup -- 1,3', (WidgetTester tester) async { - const widgetName = 'cbg1'; - final testWidget = FormBuilderCheckboxGroup( - name: widgetName, - options: const [ - FormBuilderFieldOption(key: ValueKey('1'), value: 1), - FormBuilderFieldOption(key: ValueKey('2'), value: 2), - FormBuilderFieldOption(key: ValueKey('3'), value: 3), - ], - ); - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - - expect(formSave(), isTrue); - expect(formValue(widgetName), isNull); - await tester.tap(find.byKey(const ValueKey('1'))); - await tester.pumpAndSettle(); - expect(formSave(), isTrue); - expect(formValue(widgetName), equals(const [1])); - await tester.tap(find.byKey(const ValueKey('3'))); - await tester.pumpAndSettle(); - expect(formSave(), isTrue); - expect(formValue(widgetName), equals(const [1, 3])); - }); - - testWidgets('FormBuilderCheckboxGroup -- decoration horizontal', - (WidgetTester tester) async { - const widgetName = 'cbg1'; - final testWidget = FormBuilderCheckboxGroup( - name: widgetName, - orientation: OptionsOrientation.horizontal, - wrapSpacing: 10.0, - options: const [ - FormBuilderFieldOption(key: ValueKey('1'), value: 1), - FormBuilderFieldOption(key: ValueKey('2'), value: 2), - ], - itemDecoration: - BoxDecoration(border: Border.all(color: Colors.blueAccent)), - ); - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - - // this is a brittle test knowing how we use container for a border - // there is one container for each option - expect(find.byType(Container), findsExactly(2)); - // same as wrapSpacing - Container foo = tester.firstWidget(find.byType(Container)); - expect(foo.margin, const EdgeInsets.only(right: 10.0)); - // verify separator counts - expect(find.byType(VerticalDivider), findsNothing); - }); - - testWidgets('FormBuilderCheckboxGroup -- decoration vertical', - (WidgetTester tester) async { - const widgetName = 'cbg1'; - final testWidget = FormBuilderCheckboxGroup( - name: widgetName, - orientation: OptionsOrientation.vertical, - wrapSpacing: 10.0, - options: const [ - FormBuilderFieldOption(key: ValueKey('1'), value: 1), - FormBuilderFieldOption(key: ValueKey('2'), value: 2), - ], - itemDecoration: - BoxDecoration(border: Border.all(color: Colors.blueAccent)), - ); - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - - // this is a brittle test knowing how we use container for a border - // there is one container for each option - expect(find.byType(Container), findsExactly(2)); - // same as wrapSpacing - Container foo = tester.firstWidget(find.byType(Container)); - expect(foo.margin, const EdgeInsets.only(bottom: 10.0)); - // verify separator counts - expect(find.byType(VerticalDivider), findsNothing); - }); - - testWidgets('FormBuilderCheckboxGroup -- separator horizontal', - (WidgetTester tester) async { - const widgetName = 'cbg1'; - final testWidget = FormBuilderCheckboxGroup( - name: widgetName, - orientation: OptionsOrientation.horizontal, - wrapSpacing: 10.0, - options: const [ - FormBuilderFieldOption(key: ValueKey('1'), value: 1), - FormBuilderFieldOption(key: ValueKey('2'), value: 2), - FormBuilderFieldOption(key: ValueKey('3'), value: 2), - ], - separator: const VerticalDivider(width: 8.0, color: Colors.red), - ); - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - - // verify separator counts - expect(find.byType(VerticalDivider), findsNWidgets(2)); - }); - - testWidgets('FormBuilderCheckboxGroup -- separator vertical', - (WidgetTester tester) async { - const widgetName = 'cbg1'; - final testWidget = FormBuilderCheckboxGroup( - name: widgetName, - orientation: OptionsOrientation.vertical, - wrapSpacing: 10.0, - options: const [ - FormBuilderFieldOption(key: ValueKey('1'), value: 1), - FormBuilderFieldOption(key: ValueKey('2'), value: 2), - FormBuilderFieldOption(key: ValueKey('3'), value: 2), - ], - separator: const VerticalDivider(width: 8.0, color: Colors.red), - ); - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - - // verify separator counts - expect(find.byType(VerticalDivider), findsNWidgets(2)); - }); - testWidgets('FormBuilderCheckboxGroup -- didChange', - (WidgetTester tester) async { - const fieldName = 'cbg1'; - final testWidget = FormBuilderCheckboxGroup( - name: fieldName, - options: const [ - FormBuilderFieldOption(key: ValueKey('1'), value: 1), - FormBuilderFieldOption(key: ValueKey('2'), value: 2), - FormBuilderFieldOption(key: ValueKey('3'), value: 3), - ], - ); - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - - expect(formSave(), isTrue); - expect(formValue(fieldName), isNull); - formFieldDidChange(fieldName, [1, 3]); - await tester.pumpAndSettle(); - expect(formSave(), isTrue); - expect(formValue(fieldName), [1, 3]); - - Checkbox checkbox1 = tester - .element(find.byKey(const ValueKey('1'))) - .findAncestorWidgetOfExactType()! - .children - .first as Checkbox; - Checkbox checkbox2 = tester - .element(find.byKey(const ValueKey('2'))) - .findAncestorWidgetOfExactType()! - .children - .first as Checkbox; - Checkbox checkbox3 = tester - .element(find.byKey(const ValueKey('3'))) - .findAncestorWidgetOfExactType()! - .children - .first as Checkbox; - - // checkboxes should represent the state of the didChange value - expect(checkbox1.value, true); - expect(checkbox2.value, false); - expect(checkbox3.value, true); - }); -} diff --git a/test/form_builder_checkbox_test.dart b/test/form_builder_checkbox_test.dart deleted file mode 100644 index 38979d53b7..0000000000 --- a/test/form_builder_checkbox_test.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_form_builder/src/fields/form_builder_checkbox.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'form_builder_tester.dart'; - -void main() { - testWidgets('FormBuilderCheckbox -- Off/On/Off', (WidgetTester tester) async { - const checkboxName = 'cb1'; - final testWidget = FormBuilderCheckbox( - name: checkboxName, - title: const Text('Checkbox 1'), - initialValue: false, - ); - final widgetFinder = find.byWidget(testWidget); - - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - - expect(formSave(), isTrue); - expect(formValue(checkboxName), isFalse); - await tester.tap(widgetFinder); - await tester.pumpAndSettle(); - expect(formSave(), isTrue); - expect(formValue(checkboxName), isTrue); - await tester.tap(widgetFinder); - await tester.pumpAndSettle(); - expect(formSave(), isTrue); - expect(formValue(checkboxName), isFalse); - }); -} diff --git a/test/form_builder_radio_group_test.dart b/test/form_builder_radio_group_test.dart deleted file mode 100644 index 68dc191095..0000000000 --- a/test/form_builder_radio_group_test.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_form_builder/flutter_form_builder.dart'; -import 'form_builder_tester.dart'; - -void main() { - testWidgets('FormBuilderRadioGroup -- 1,3', (WidgetTester tester) async { - const widgetName = 'rg1'; - final testWidget = FormBuilderRadioGroup( - name: widgetName, - options: const [ - FormBuilderFieldOption(key: ValueKey('1'), value: 1), - FormBuilderFieldOption(key: ValueKey('2'), value: 2), - FormBuilderFieldOption(key: ValueKey('3'), value: 3), - ], - ); - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - - expect(formSave(), isTrue); - expect(formValue(widgetName), isNull); - await tester.tap(find.byKey(const ValueKey('1'))); - await tester.pumpAndSettle(); - expect(formSave(), isTrue); - expect(formValue(widgetName), equals(1)); - await tester.tap(find.byKey(const ValueKey('3'))); - await tester.pumpAndSettle(); - expect(formSave(), isTrue); - expect(formValue(widgetName), equals(3)); - }); - - testWidgets('FormBuilderRadioGroup -- decoration horizontal', - (WidgetTester tester) async { - const widgetName = 'rg1'; - final testWidget = FormBuilderRadioGroup( - name: widgetName, - orientation: OptionsOrientation.horizontal, - wrapSpacing: 10.0, - options: const [ - FormBuilderFieldOption(key: ValueKey('1'), value: 1), - FormBuilderFieldOption(key: ValueKey('2'), value: 2), - ], - itemDecoration: - BoxDecoration(border: Border.all(color: Colors.blueAccent)), - ); - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - - // this is a brittle test knowing how we use container for a border - // there is one container for each option - expect(find.byType(Container), findsExactly(2)); - // same as wrapSpacing - Container foo = tester.firstWidget(find.byType(Container)); - expect(foo.margin, const EdgeInsets.only(right: 10.0)); - // verify separator counts - expect(find.byType(VerticalDivider), findsNothing); - }); - - testWidgets('FormBuilderRadioGroup -- decoration vertical', - (WidgetTester tester) async { - const widgetName = 'rg1'; - final testWidget = FormBuilderRadioGroup( - name: widgetName, - orientation: OptionsOrientation.vertical, - wrapSpacing: 10.0, - options: const [ - FormBuilderFieldOption(key: ValueKey('1'), value: 1), - FormBuilderFieldOption(key: ValueKey('2'), value: 2), - ], - itemDecoration: - BoxDecoration(border: Border.all(color: Colors.blueAccent)), - ); - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - - // this is a brittle test knowing how we use container for a border - // there is one container for each option - expect(find.byType(Container), findsExactly(2)); - // same as wrapSpacing - Container foo = tester.firstWidget(find.byType(Container)); - expect(foo.margin, const EdgeInsets.only(bottom: 10.0)); - // verify separator counts - expect(find.byType(VerticalDivider), findsNothing); - }); - - testWidgets('FormBuilderRadioGroup -- separators horizontal', - (WidgetTester tester) async { - const widgetName = 'rg1'; - final testWidget = FormBuilderRadioGroup( - name: widgetName, - orientation: OptionsOrientation.horizontal, - wrapSpacing: 10.0, - options: const [ - FormBuilderFieldOption(key: ValueKey('1'), value: 1), - FormBuilderFieldOption(key: ValueKey('2'), value: 2), - FormBuilderFieldOption(key: ValueKey('3'), value: 2), - ], - separator: const VerticalDivider(width: 8.0, color: Colors.red), - ); - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - - // verify separator counts - expect(find.byType(VerticalDivider), findsNWidgets(2)); - }); - - testWidgets('FormBuilderRadioGroup -- separators vertical', - (WidgetTester tester) async { - const widgetName = 'rg1'; - final testWidget = FormBuilderRadioGroup( - name: widgetName, - orientation: OptionsOrientation.vertical, - wrapSpacing: 10.0, - options: const [ - FormBuilderFieldOption(key: ValueKey('1'), value: 1), - FormBuilderFieldOption(key: ValueKey('2'), value: 2), - FormBuilderFieldOption(key: ValueKey('2'), value: 2), - ], - itemDecoration: BoxDecoration( - border: Border.all(color: Colors.blueAccent), - ), - separator: const VerticalDivider(width: 8.0, color: Colors.red), - ); - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - - // verify separator counts - expect(find.byType(VerticalDivider), findsNWidgets(2)); - }); -} diff --git a/test/form_builder_slider_test.dart b/test/form_builder_slider_test.dart deleted file mode 100644 index df51112bd0..0000000000 --- a/test/form_builder_slider_test.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_form_builder/src/fields/form_builder_slider.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'form_builder_tester.dart'; - -void main() { - testWidgets('FormBuilderSlider -- 5.0', (WidgetTester tester) async { - const widgetName = 'slider1'; - final testWidget = FormBuilderSlider( - name: widgetName, - initialValue: 2, - min: 0, - max: 10, - divisions: 20, - decoration: const InputDecoration( - labelText: 'Number of things', - ), - ); - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - - expect(formSave(), isTrue); - expect(formValue(widgetName), equals(2.0)); - await tester.tap(find.byType(Slider)); - await tester.pumpAndSettle(); - expect(formSave(), isTrue); - expect(formValue(widgetName), equals(5.0)); - }); -} diff --git a/test/form_builder_switch_test.dart b/test/form_builder_switch_test.dart deleted file mode 100644 index 85083343af..0000000000 --- a/test/form_builder_switch_test.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_form_builder/src/fields/form_builder_switch.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'form_builder_tester.dart'; - -void main() { - testWidgets('FormBuilderSwitch -- Off/On/Off', (WidgetTester tester) async { - const switchName = 'switch1'; - final testWidget = FormBuilderSwitch( - name: switchName, - title: const Text('Switch 1'), - initialValue: false, - ); - final widgetFinder = find.byWidget(testWidget); - - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - - expect(formSave(), isTrue); - expect(formValue(switchName), isFalse); - await tester.tap(widgetFinder); - await tester.pumpAndSettle(); - expect(formSave(), isTrue); - expect(formValue(switchName), isTrue); - await tester.tap(widgetFinder); - await tester.pumpAndSettle(); - expect(formSave(), isTrue); - expect(formValue(switchName), isFalse); - }); -} diff --git a/test/form_builder_text_field_test.dart b/test/form_builder_text_field_test.dart deleted file mode 100644 index 1542016b3a..0000000000 --- a/test/form_builder_text_field_test.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_form_builder/flutter_form_builder.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'form_builder_tester.dart'; - -void main() { - testWidgets('FormBuilderTextField -- Hello Planet', - (WidgetTester tester) async { - const newTextValue = 'Hello 🪐'; - const textFieldName = 'text1'; - final testWidget = FormBuilderTextField( - name: textFieldName, - ); - final widgetFinder = find.byWidget(testWidget); - - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - expect(formSave(), isTrue); - expect(formValue(textFieldName), isNull); - await tester.enterText(widgetFinder, newTextValue); - expect(formSave(), isTrue); - expect(formValue(textFieldName), equals(newTextValue)); - await tester.enterText(widgetFinder, ''); - expect(formSave(), isTrue); - expect(formValue(textFieldName), isEmpty); - }); - testWidgets( - 'FormBuilderTextField without initialValue', - (tester) => _testFormBuilderTextFieldWithInitialValue( - tester, - ), - ); - testWidgets( - 'FormBuilderTextField has initialValue on Field', - (tester) => _testFormBuilderTextFieldWithInitialValue( - tester, - initialValueOnField: 'ok', - ), - ); - testWidgets( - 'FormBuilderTextField has initialValue on Form', - (tester) => _testFormBuilderTextFieldWithInitialValue( - tester, - initialValueOnForm: 'ok', - ), - ); - - testWidgets( - 'FormBuilderTextField triggers onTapOutside', - (tester) => _testFormBuilderTextFieldOnTapOutsideCallback(tester), - ); -} - -Future _testFormBuilderTextFieldWithInitialValue( - WidgetTester tester, { - String? initialValueOnField, - String? initialValueOnForm, -}) async { - int changedCount = 0; - const newTextValue = 'Hello 🪐'; - const textFieldName = 'text1'; - - var testWidget = FormBuilderTextField( - name: textFieldName, - initialValue: initialValueOnField, - onChanged: (String? value) => changedCount++, - ); - await tester.pumpWidget(buildTestableFieldWidget( - testWidget, - initialValue: { - textFieldName: initialValueOnForm, - }, - )); - expect(formSave(), isTrue); - expect(formValue(textFieldName), initialValueOnField ?? initialValueOnForm); - expect(changedCount, 0); - - await tester.enterText(find.byWidget(testWidget), newTextValue); - expect(formSave(), isTrue); - expect(formValue(textFieldName), newTextValue); - - expect(changedCount, 1); -} - -Future _testFormBuilderTextFieldOnTapOutsideCallback( - WidgetTester tester) async { - const textFieldName = 'Hello 🪐'; - bool triggered = false; - - var testWidget = FormBuilderTextField( - name: textFieldName, - onTapOutside: (event) => triggered = true, - ); - await tester.pumpWidget(buildTestableFieldWidget( - testWidget, - )); - final textField = tester.firstWidget(find.byType(TextField)) as TextField; - textField.onTapOutside?.call(const PointerDownEvent()); - expect(triggered, true); -} diff --git a/test/src/fields/form_builder_checkbox_group_test.dart b/test/src/fields/form_builder_checkbox_group_test.dart new file mode 100644 index 0000000000..2a2d455db0 --- /dev/null +++ b/test/src/fields/form_builder_checkbox_group_test.dart @@ -0,0 +1,197 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; + +import '../../form_builder_tester.dart'; + +void main() { + group('FormBuilderCheckboxGroup -', () { + testWidgets('1,3', (WidgetTester tester) async { + const widgetName = 'cbg1'; + final testWidget = FormBuilderCheckboxGroup( + name: widgetName, + options: const [ + FormBuilderFieldOption(key: ValueKey('1'), value: 1), + FormBuilderFieldOption(key: ValueKey('2'), value: 2), + FormBuilderFieldOption(key: ValueKey('3'), value: 3), + ], + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + expect(formSave(), isTrue); + expect(formValue(widgetName), isNull); + await tester.tap(find.byKey(const ValueKey('1'))); + await tester.pumpAndSettle(); + expect(formSave(), isTrue); + expect(formValue(widgetName), equals(const [1])); + await tester.tap(find.byKey(const ValueKey('3'))); + await tester.pumpAndSettle(); + expect(formSave(), isTrue); + expect(formValue(widgetName), equals(const [1, 3])); + }); + + testWidgets('decoration horizontal', (WidgetTester tester) async { + const widgetName = 'cbg1'; + final testWidget = FormBuilderCheckboxGroup( + name: widgetName, + orientation: OptionsOrientation.horizontal, + wrapSpacing: 10.0, + options: const [ + FormBuilderFieldOption(key: ValueKey('1'), value: 1), + FormBuilderFieldOption(key: ValueKey('2'), value: 2), + ], + itemDecoration: BoxDecoration( + border: Border.all(color: Colors.blueAccent), + ), + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + // this is a brittle test knowing how we use container for a border + // there is one container for each option + expect(find.byType(Container), findsExactly(2)); + // same as wrapSpacing + Container foo = tester.firstWidget(find.byType(Container)); + expect(foo.margin, const EdgeInsets.only(right: 10.0)); + // verify separator counts + expect(find.byType(VerticalDivider), findsNothing); + }); + + testWidgets('decoration vertical', (WidgetTester tester) async { + const widgetName = 'cbg1'; + final testWidget = FormBuilderCheckboxGroup( + name: widgetName, + orientation: OptionsOrientation.vertical, + wrapSpacing: 10.0, + options: const [ + FormBuilderFieldOption(key: ValueKey('1'), value: 1), + FormBuilderFieldOption(key: ValueKey('2'), value: 2), + ], + itemDecoration: BoxDecoration( + border: Border.all(color: Colors.blueAccent), + ), + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + // this is a brittle test knowing how we use container for a border + // there is one container for each option + expect(find.byType(Container), findsExactly(2)); + // same as wrapSpacing + Container foo = tester.firstWidget(find.byType(Container)); + expect(foo.margin, const EdgeInsets.only(bottom: 10.0)); + // verify separator counts + expect(find.byType(VerticalDivider), findsNothing); + }); + + testWidgets('separator horizontal', (WidgetTester tester) async { + const widgetName = 'cbg1'; + final testWidget = FormBuilderCheckboxGroup( + name: widgetName, + orientation: OptionsOrientation.horizontal, + wrapSpacing: 10.0, + options: const [ + FormBuilderFieldOption(key: ValueKey('1'), value: 1), + FormBuilderFieldOption(key: ValueKey('2'), value: 2), + FormBuilderFieldOption(key: ValueKey('3'), value: 2), + ], + separator: const VerticalDivider(width: 8.0, color: Colors.red), + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + // verify separator counts + expect(find.byType(VerticalDivider), findsNWidgets(2)); + }); + + testWidgets('separator vertical', (WidgetTester tester) async { + const widgetName = 'cbg1'; + final testWidget = FormBuilderCheckboxGroup( + name: widgetName, + orientation: OptionsOrientation.vertical, + wrapSpacing: 10.0, + options: const [ + FormBuilderFieldOption(key: ValueKey('1'), value: 1), + FormBuilderFieldOption(key: ValueKey('2'), value: 2), + FormBuilderFieldOption(key: ValueKey('3'), value: 2), + ], + separator: const VerticalDivider(width: 8.0, color: Colors.red), + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + // verify separator counts + expect(find.byType(VerticalDivider), findsNWidgets(2)); + }); + testWidgets('didChange', (WidgetTester tester) async { + const fieldName = 'cbg1'; + final testWidget = FormBuilderCheckboxGroup( + name: fieldName, + options: const [ + FormBuilderFieldOption(key: ValueKey('1'), value: 1), + FormBuilderFieldOption(key: ValueKey('2'), value: 2), + FormBuilderFieldOption(key: ValueKey('3'), value: 3), + ], + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + expect(formSave(), isTrue); + expect(formValue(fieldName), isNull); + formFieldDidChange(fieldName, [1, 3]); + await tester.pumpAndSettle(); + expect(formSave(), isTrue); + expect(formValue(fieldName), [1, 3]); + + Checkbox checkbox1 = + tester + .element(find.byKey(const ValueKey('1'))) + .findAncestorWidgetOfExactType()! + .children + .first + as Checkbox; + Checkbox checkbox2 = + tester + .element(find.byKey(const ValueKey('2'))) + .findAncestorWidgetOfExactType()! + .children + .first + as Checkbox; + Checkbox checkbox3 = + tester + .element(find.byKey(const ValueKey('3'))) + .findAncestorWidgetOfExactType()! + .children + .first + as Checkbox; + + // checkboxes should represent the state of the didChange value + expect(checkbox1.value, true); + expect(checkbox2.value, false); + expect(checkbox3.value, true); + }); + testWidgets('When press tab, field will be focused', ( + WidgetTester tester, + ) async { + const widgetName = 'cb1'; + final testWidget = FormBuilderCheckboxGroup( + name: widgetName, + options: const [ + FormBuilderFieldOption(key: ValueKey('1'), value: 1), + FormBuilderFieldOption(key: ValueKey('2'), value: 2), + FormBuilderFieldOption(key: ValueKey('3'), value: 3), + ], + ); + final widgetFinder = find.byWidget(testWidget); + + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + final focusNode = + formKey.currentState?.fields[widgetName]?.effectiveFocusNode; + + expect(formSave(), isTrue); + expect(formValue(widgetName), isNull); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, false); + expect(focusNode?.hasFocus, false); + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pumpAndSettle(); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, true); + expect(focusNode?.hasFocus, true); + }); + }); +} diff --git a/test/src/fields/form_builder_checkbox_test.dart b/test/src/fields/form_builder_checkbox_test.dart new file mode 100644 index 0000000000..018ffb9538 --- /dev/null +++ b/test/src/fields/form_builder_checkbox_test.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_form_builder/src/fields/form_builder_checkbox.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../form_builder_tester.dart'; + +void main() { + group('FormBuilderCheckbox -', () { + testWidgets('Off/On/Off', (WidgetTester tester) async { + const widgetName = 'cb1'; + final testWidget = FormBuilderCheckbox( + name: widgetName, + title: const Text('Checkbox 1'), + initialValue: false, + ); + final widgetFinder = find.byWidget(testWidget); + + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + expect(formSave(), isTrue); + expect(formValue(widgetName), isFalse); + await tester.tap(widgetFinder); + await tester.pumpAndSettle(); + expect(formSave(), isTrue); + expect(formValue(widgetName), isTrue); + await tester.tap(widgetFinder); + await tester.pumpAndSettle(); + expect(formSave(), isTrue); + expect(formValue(widgetName), isFalse); + }); + testWidgets('When press tab, field will be focused', ( + WidgetTester tester, + ) async { + const widgetName = 'cb1'; + final testWidget = FormBuilderCheckbox( + name: widgetName, + title: const Text('Checkbox 1'), + initialValue: false, + ); + final widgetFinder = find.byWidget(testWidget); + + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + final focusNode = + formKey.currentState?.fields[widgetName]?.effectiveFocusNode; + + expect(formSave(), isTrue); + expect(formValue(widgetName), isFalse); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, false); + expect(focusNode?.hasFocus, false); + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pumpAndSettle(); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, true); + expect(focusNode?.hasFocus, true); + }); + }); +} diff --git a/test/form_builder_choice_chips_test.dart b/test/src/fields/form_builder_choice_chips_test.dart similarity index 65% rename from test/form_builder_choice_chips_test.dart rename to test/src/fields/form_builder_choice_chips_test.dart index 9e1837de34..a5ebff0458 100644 --- a/test/form_builder_choice_chips_test.dart +++ b/test/src/fields/form_builder_choice_chips_test.dart @@ -1,16 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; -import 'form_builder_tester.dart'; +import '../../form_builder_tester.dart'; void main() { group('FormBuilderChoiceChip --', () { testWidgets('basic', (WidgetTester tester) async { const widgetName = 'cc1'; - final testWidget = FormBuilderChoiceChip( + final testWidget = FormBuilderChoiceChips( name: widgetName, options: const [ FormBuilderChipOption(key: ValueKey('1'), value: 1), @@ -36,7 +37,7 @@ void main() { testWidgets('to FormBuilder', (WidgetTester tester) async { const widgetName = 'cc2'; - final testWidget = FormBuilderChoiceChip( + final testWidget = FormBuilderChoiceChips( name: widgetName, options: const [ FormBuilderChipOption(key: ValueKey('1'), value: 1), @@ -44,10 +45,9 @@ void main() { FormBuilderChipOption(key: ValueKey('3'), value: 3), ], ); - await tester.pumpWidget(buildTestableFieldWidget( - testWidget, - initialValue: {widgetName: 1}, - )); + await tester.pumpWidget( + buildTestableFieldWidget(testWidget, initialValue: {widgetName: 1}), + ); await tester.ensureVisible(find.byKey(const ValueKey('1'))); expect(formInstantValue(widgetName), equals(1)); @@ -61,7 +61,7 @@ void main() { testWidgets('to Widget', (WidgetTester tester) async { const widgetName = 'cc3'; - final testWidget = FormBuilderChoiceChip( + final testWidget = FormBuilderChoiceChips( name: widgetName, initialValue: 1, options: const [ @@ -82,5 +82,33 @@ void main() { expect(formSave(), isTrue); expect(formValue(widgetName), equals(3)); }); + + testWidgets('When press tab, field will be focused', ( + WidgetTester tester, + ) async { + const widgetName = 'cb1'; + final testWidget = FormBuilderChoiceChips( + name: widgetName, + options: const [ + FormBuilderChipOption(key: ValueKey('1'), value: 1), + FormBuilderChipOption(key: ValueKey('2'), value: 2), + FormBuilderChipOption(key: ValueKey('3'), value: 3), + ], + ); + final widgetFinder = find.byWidget(testWidget); + + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + final focusNode = + formKey.currentState?.fields[widgetName]?.effectiveFocusNode; + + expect(formSave(), isTrue); + expect(formValue(widgetName), isNull); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, false); + expect(focusNode?.hasFocus, false); + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pumpAndSettle(); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, true); + expect(focusNode?.hasFocus, true); + }); }); } diff --git a/test/form_builder_date_range_picker_test.dart b/test/src/fields/form_builder_date_range_picker_test.dart similarity index 51% rename from test/form_builder_date_range_picker_test.dart rename to test/src/fields/form_builder_date_range_picker_test.dart index e5175188d3..58c62ce10f 100644 --- a/test/form_builder_date_range_picker_test.dart +++ b/test/src/fields/form_builder_date_range_picker_test.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:intl/intl.dart'; -import 'form_builder_tester.dart'; +import '../../form_builder_tester.dart'; void main() { - group('FormBuilderDateRangePicker --', () { + group('FormBuilderDateRangePicker -', () { testWidgets('basic', (WidgetTester tester) async { const widgetName = 'formBuilderDateRangePicker'; final testWidget = FormBuilderDateRangePicker( @@ -39,10 +40,9 @@ void main() { expect(formSave(), isTrue); expect( formValue(widgetName), - equals(DateTimeRange( - start: DateTime(2010, 1, 2), - end: DateTime(2010, 1, 4), - )), + equals( + DateTimeRange(start: DateTime(2010, 1, 2), end: DateTime(2010, 1, 4)), + ), ); }); @@ -62,15 +62,15 @@ void main() { expect(formSave(), isTrue); expect( formValue(widgetName), - equals(DateTimeRange( - start: DateTime(2010, 1, 2), - end: DateTime(2010, 1, 4), - )), + equals( + DateTimeRange(start: DateTime(2010, 1, 2), end: DateTime(2010, 1, 4)), + ), ); }); - testWidgets('text field empty when value is null', - (WidgetTester tester) async { + testWidgets('text field empty when value is null', ( + WidgetTester tester, + ) async { const widgetName = 'formBuilderDateRangePicker'; final testWidget = FormBuilderDateRangePicker( name: widgetName, @@ -102,4 +102,65 @@ void main() { ); }); }); + testWidgets('When press tab, field will be focused', ( + WidgetTester tester, + ) async { + const widgetName = 'cb1'; + const saveText = 'SAVE THE DATE'; + final testWidget = FormBuilderDateRangePicker( + name: widgetName, + firstDate: DateTime(2010), + // Using last date < today to make date picker always open on 01/01/2010 + // If last date >= today, it opens on DateTime.now month, which complicates testing. + lastDate: DateTime(2020), + saveText: saveText, + ); + final widgetFinder = find.byWidget(testWidget); + + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + final focusNode = + formKey.currentState?.fields[widgetName]?.effectiveFocusNode; + + expect(formSave(), isTrue); + expect(formValue(widgetName), isNull); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, false); + expect(focusNode?.hasFocus, false); + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pumpAndSettle(); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, true); + expect(focusNode?.hasFocus, true); + expect(find.text(saveText), findsNothing); + + // Open picker + await tester.sendKeyEvent(LogicalKeyboardKey.space); + await tester.pumpAndSettle(); + expect(find.text(saveText), findsOneWidget); + }); + testWidgets('When press clear button then clear value text field', ( + WidgetTester tester, + ) async { + const widgetName = 'cb1'; + final initialValue = DateTimeRange( + start: DateTime(2010, 1, 2), + end: DateTime(2010, 1, 4), + ); + final testWidget = FormBuilderDateRangePicker( + name: widgetName, + initialValue: initialValue, + allowClear: true, + firstDate: DateTime(2010), + // Using last date < today to make date picker always open on 01/01/2010 + // If last date >= today, it opens on DateTime.now month, which complicates testing. + lastDate: DateTime(2020), + ); + + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + expect(formSave(), isTrue); + expect(formValue(widgetName), equals(initialValue)); + await tester.tap(find.byType(IconButton)); + await tester.pumpAndSettle(); + + expect(find.widgetWithText(TextField, ''), findsOneWidget); + }); } diff --git a/test/src/fields/form_builder_date_time_picker_test.dart b/test/src/fields/form_builder_date_time_picker_test.dart index 077f001f67..5555a952dc 100644 --- a/test/src/fields/form_builder_date_time_picker_test.dart +++ b/test/src/fields/form_builder_date_time_picker_test.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -37,30 +38,62 @@ void main() { await tester.pumpAndSettle(); expect(formSave(), isTrue); - expect(formValue(widgetName), - DateTime(dateNow.year, dateNow.month, testDay, 12)); + expect( + formValue(widgetName), + DateTime(dateNow.year, dateNow.month, testDay, 12), + ); }); - testWidgets('input keyboard type', (WidgetTester tester) async { + testWidgets( + 'should change to text field and show keyboard when edit icon is pressed', + (WidgetTester tester) async { + const widgetName = 'fdtp3'; + final widgetKey = UniqueKey(); + const keyboardType = TextInputType.datetime; + + final testWidget = FormBuilderDateTimePicker( + key: widgetKey, + name: widgetName, + keyboardType: keyboardType, + inputType: InputType.date, + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + await tester.tap(find.byKey(widgetKey)); + await tester.pumpAndSettle(); + + // change to input edition + await tester.tap(find.byIcon(Icons.edit_outlined)); + await tester.pumpAndSettle(); + + final textField = tester.widget( + find.byType(TextField).first, + ); + expect(textField.keyboardType, equals(keyboardType)); + }, + ); + testWidgets('should show a past year when set on lastDate', ( + WidgetTester tester, + ) async { const widgetName = 'fdtp3'; final widgetKey = UniqueKey(); - const keyboardType = TextInputType.datetime; + const confirmText = 'OK'; + const cancelText = 'CANCEL'; + final year = 2006; final testWidget = FormBuilderDateTimePicker( key: widgetKey, name: widgetName, - keyboardType: keyboardType, - inputType: InputType.date, + confirmText: confirmText, + cancelText: cancelText, + initialDate: null, + lastDate: DateTime(year, 12, 31), ); await tester.pumpWidget(buildTestableFieldWidget(testWidget)); await tester.tap(find.byKey(widgetKey)); await tester.pumpAndSettle(); - // change to input edition - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); - - final textField = tester.widget(find.byType(TextField).first); - expect(textField.keyboardType, equals(keyboardType)); + expect(find.text(confirmText), findsOneWidget); + expect(find.text(cancelText), findsOneWidget); + expect(find.text('December ${year.toString()}'), findsOneWidget); }); group('initial value -', () { testWidgets('to FormBuilder', (WidgetTester tester) async { @@ -77,10 +110,12 @@ void main() { cancelText: cancelText, ); - await tester.pumpWidget(buildTestableFieldWidget( - testWidget, - initialValue: {widgetName: dateFuture}, - )); + await tester.pumpWidget( + buildTestableFieldWidget( + testWidget, + initialValue: {widgetName: dateFuture}, + ), + ); expect(formSave(), isTrue); expect(formValue(widgetName), dateFuture); @@ -101,8 +136,15 @@ void main() { expect(formSave(), isTrue); expect( formValue(widgetName), - DateTime(dateFuture.year, dateFuture.month, testDay, dateFuture.hour, - dateFuture.minute, 0, 0), + DateTime( + dateFuture.year, + dateFuture.month, + testDay, + dateFuture.hour, + dateFuture.minute, + 0, + 0, + ), ); }); testWidgets('to Widget', (WidgetTester tester) async { @@ -141,10 +183,53 @@ void main() { expect(formSave(), isTrue); expect( formValue(widgetName), - DateTime(datePast.year, datePast.month, testDay, datePast.hour, - datePast.minute, 0, 0), + DateTime( + datePast.year, + datePast.month, + testDay, + datePast.hour, + datePast.minute, + 0, + 0, + ), ); }); }); }); + + testWidgets('When press tab, field will be focused', ( + WidgetTester tester, + ) async { + const widgetName = 'cb1'; + final widgetKey = UniqueKey(); + const confirmText = 'OK'; + const cancelText = 'CANCEL'; + + final testWidget = FormBuilderDateTimePicker( + key: widgetKey, + name: widgetName, + confirmText: confirmText, + cancelText: cancelText, + ); + final widgetFinder = find.byWidget(testWidget); + + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + final focusNode = + formKey.currentState?.fields[widgetName]?.effectiveFocusNode; + + expect(formSave(), isTrue); + expect(formValue(widgetName), equals(null)); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, false); + expect(focusNode?.hasFocus, false); + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pumpAndSettle(); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, true); + expect(focusNode?.hasFocus, true); + + // Open picker + await tester.sendKeyEvent(LogicalKeyboardKey.space); + await tester.pumpAndSettle(); + expect(find.text(confirmText), findsOneWidget); + expect(find.text(cancelText), findsOneWidget); + }); } diff --git a/test/src/fields/form_builder_dropdown_test.dart b/test/src/fields/form_builder_dropdown_test.dart index a582bb01ea..4f29a38324 100644 --- a/test/src/fields/form_builder_dropdown_test.dart +++ b/test/src/fields/form_builder_dropdown_test.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_form_builder/src/fields/form_builder_dropdown.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -11,18 +12,9 @@ void main() { final testWidget = FormBuilderDropdown( name: widgetName, items: const [ - DropdownMenuItem( - value: 1, - child: Text('One'), - ), - DropdownMenuItem( - value: 2, - child: Text('Two'), - ), - DropdownMenuItem( - value: 3, - child: Text('Three'), - ), + DropdownMenuItem(value: 1, child: Text('One')), + DropdownMenuItem(value: 2, child: Text('Two')), + DropdownMenuItem(value: 3, child: Text('Three')), ], ); final widgetFinder = find.byWidget(testWidget); @@ -44,8 +36,9 @@ void main() { expect(formSave(), isTrue); expect(formValue(widgetName), equals(1)); }); - testWidgets('reset to initial value when update items', - (WidgetTester tester) async { + testWidgets('reset to initial value when update items', ( + WidgetTester tester, + ) async { const widgetName = 'dropdown_field'; const buttonKey = Key('update_button'); @@ -84,8 +77,9 @@ void main() { expect(formSave(), isTrue); expect(formValue(widgetName), equals(3)); }); - testWidgets('reset to initial value when update items with same values', - (WidgetTester tester) async { + testWidgets('reset to initial value when update items with same values', ( + WidgetTester tester, + ) async { const widgetName = 'dropdown_field'; const buttonKey = Key('update_button'); @@ -132,8 +126,9 @@ void main() { expect(formValue(widgetName), equals(1)); }); - testWidgets('reset to initial value when update items with same children', - (WidgetTester tester) async { + testWidgets('reset to initial value when update items with same children', ( + WidgetTester tester, + ) async { const widgetName = 'dropdown_field'; const buttonKey = Key('update_button'); const option1 = Text('Option 1'); @@ -182,8 +177,9 @@ void main() { expect(formSave(), isTrue); expect(formValue(widgetName), equals(3)); }); - testWidgets('maintain initial value when update to equals items', - (WidgetTester tester) async { + testWidgets('maintain initial value when update to equals items', ( + WidgetTester tester, + ) async { const widgetName = 'dropdown_field'; const buttonKey = Key('update_button'); @@ -234,8 +230,10 @@ void main() { DropdownMenuItem(value: 2, child: Text('Option 2')), ]; - final testWidget = - FormBuilderDropdown(name: widgetName, items: initialItems); + final testWidget = FormBuilderDropdown( + name: widgetName, + items: initialItems, + ); await tester.pumpWidget(buildTestableFieldWidget(testWidget)); formKey.currentState?.patchValue({widgetName: 1}); @@ -253,12 +251,13 @@ void main() { DropdownMenuItem(value: 1, child: Text('Option 1')), DropdownMenuItem(value: 2, child: Text('Option 2')), ]; - final testWidget = - FormBuilderDropdown(name: widgetName, items: initialItems); - await tester.pumpWidget(buildTestableFieldWidget( - testWidget, - initialValue: initialValue, - )); + final testWidget = FormBuilderDropdown( + name: widgetName, + items: initialItems, + ); + await tester.pumpWidget( + buildTestableFieldWidget(testWidget, initialValue: initialValue), + ); formKey.currentState?.patchValue({widgetName: 2}); await tester.pumpAndSettle(); @@ -266,6 +265,34 @@ void main() { expect(formKey.currentState?.instantValue, equals(initialValue)); }); + + testWidgets('When press tab, field will be focused', ( + WidgetTester tester, + ) async { + const widgetName = 'cb1'; + final testWidget = FormBuilderDropdown( + name: widgetName, + items: const [ + DropdownMenuItem(value: 1, child: Text('One')), + DropdownMenuItem(value: 2, child: Text('Two')), + DropdownMenuItem(value: 3, child: Text('Three')), + ], + ); + final widgetFinder = find.byWidget(testWidget); + + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + final focusNode = + formKey.currentState?.fields[widgetName]?.effectiveFocusNode; + + expect(formSave(), isTrue); + expect(formValue(widgetName), isNull); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, false); + expect(focusNode?.hasFocus, false); + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pumpAndSettle(); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, true); + expect(focusNode?.hasFocus, true); + }); } class MyTestWidget extends StatefulWidget { @@ -322,7 +349,7 @@ class _MyTestWidgetState extends State { }); }, child: const Text('update'), - ) + ), ], ); } diff --git a/test/form_builder_filter_chips_test.dart b/test/src/fields/form_builder_filter_chips_test.dart similarity index 68% rename from test/form_builder_filter_chips_test.dart rename to test/src/fields/form_builder_filter_chips_test.dart index 94e9c788cb..ab88def2f8 100644 --- a/test/form_builder_filter_chips_test.dart +++ b/test/src/fields/form_builder_filter_chips_test.dart @@ -1,15 +1,16 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'form_builder_tester.dart'; +import '../../form_builder_tester.dart'; void main() { group('FormBuilderFilterChip --', () { testWidgets('basic', (WidgetTester tester) async { const widgetName = 'formBuilderFilterChip'; - final testWidget = FormBuilderFilterChip( + final testWidget = FormBuilderFilterChips( name: widgetName, options: const [ FormBuilderChipOption(key: ValueKey('1'), value: 1), @@ -34,7 +35,7 @@ void main() { testWidgets('to FormBuilder', (WidgetTester tester) async { const widgetName = 'fc2'; - final testWidget = FormBuilderFilterChip( + final testWidget = FormBuilderFilterChips( name: widgetName, options: const [ FormBuilderChipOption(key: ValueKey('1'), value: 1), @@ -42,12 +43,14 @@ void main() { FormBuilderChipOption(key: ValueKey('3'), value: 3), ], ); - await tester.pumpWidget(buildTestableFieldWidget( - testWidget, - initialValue: { - widgetName: [1] - }, - )); + await tester.pumpWidget( + buildTestableFieldWidget( + testWidget, + initialValue: { + widgetName: [1], + }, + ), + ); await tester.ensureVisible(find.byKey(const ValueKey('1'))); expect(formInstantValue(widgetName), equals([1])); @@ -65,7 +68,7 @@ void main() { testWidgets('to Widget', (WidgetTester tester) async { const widgetName = 'fc3'; - final testWidget = FormBuilderFilterChip( + final testWidget = FormBuilderFilterChips( name: widgetName, initialValue: const [1], options: const [ @@ -91,5 +94,32 @@ void main() { expect(formValue?>(widgetName), equals([3])); }); }); + testWidgets('When press tab, field will be focused', ( + WidgetTester tester, + ) async { + const widgetName = 'key'; + final testWidget = FormBuilderFilterChips( + name: widgetName, + options: const [ + FormBuilderChipOption(key: ValueKey('1'), value: 1), + FormBuilderChipOption(key: ValueKey('2'), value: 2), + FormBuilderChipOption(key: ValueKey('3'), value: 3), + ], + ); + final widgetFinder = find.byWidget(testWidget); + + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + final focusNode = + formKey.currentState?.fields[widgetName]?.effectiveFocusNode; + + expect(formSave(), isTrue); + expect(formValue?>(widgetName), equals(null)); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, false); + expect(focusNode?.hasFocus, false); + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pumpAndSettle(); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, true); + expect(focusNode?.hasFocus, true); + }); }); } diff --git a/test/src/fields/form_builder_radio_group_test.dart b/test/src/fields/form_builder_radio_group_test.dart new file mode 100644 index 0000000000..27d2cdb344 --- /dev/null +++ b/test/src/fields/form_builder_radio_group_test.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import '../../form_builder_tester.dart'; + +void main() { + group('FormBuilderRadioGroup -', () { + testWidgets('1,3', (WidgetTester tester) async { + const widgetName = 'rg1'; + final testWidget = FormBuilderRadioGroup( + name: widgetName, + options: const [ + FormBuilderFieldOption(key: ValueKey('1'), value: 1), + FormBuilderFieldOption(key: ValueKey('2'), value: 2), + FormBuilderFieldOption(key: ValueKey('3'), value: 3), + ], + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + expect(formSave(), isTrue); + expect(formValue(widgetName), isNull); + await tester.tap(find.byKey(const ValueKey('1'))); + await tester.pumpAndSettle(); + expect(formSave(), isTrue); + expect(formValue(widgetName), equals(1)); + await tester.tap(find.byKey(const ValueKey('3'))); + await tester.pumpAndSettle(); + expect(formSave(), isTrue); + expect(formValue(widgetName), equals(3)); + }); + testWidgets( + 'When had a disabled option then can not set this value option', + (WidgetTester tester) async { + const widgetName = 'rg1'; + final testWidget = FormBuilderRadioGroup( + name: widgetName, + disabled: [2], + options: const [ + FormBuilderFieldOption(key: ValueKey('1'), value: 1), + FormBuilderFieldOption(key: ValueKey('2'), value: 2), + FormBuilderFieldOption(key: ValueKey('3'), value: 3), + ], + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + expect(formSave(), isTrue); + expect(formValue(widgetName), isNull); + await tester.tap(find.byKey(const ValueKey('2'))); + await tester.pumpAndSettle(); + expect(formSave(), isTrue); + expect(formValue(widgetName), isNull); + }, + ); + + testWidgets('decoration horizontal', (WidgetTester tester) async { + const widgetName = 'rg1'; + final testWidget = FormBuilderRadioGroup( + name: widgetName, + orientation: OptionsOrientation.horizontal, + wrapSpacing: 10.0, + options: const [ + FormBuilderFieldOption(key: ValueKey('1'), value: 1), + FormBuilderFieldOption(key: ValueKey('2'), value: 2), + ], + itemDecoration: BoxDecoration( + border: Border.all(color: Colors.blueAccent), + ), + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + // this is a brittle test knowing how we use container for a border + // there is one container for each option + expect(find.byType(Container), findsExactly(2)); + // same as wrapSpacing + Container foo = tester.firstWidget(find.byType(Container)); + expect(foo.margin, const EdgeInsets.only(right: 10.0)); + // verify separator counts + expect(find.byType(VerticalDivider), findsNothing); + }); + + testWidgets('decoration vertical', (WidgetTester tester) async { + const widgetName = 'rg1'; + final testWidget = FormBuilderRadioGroup( + name: widgetName, + orientation: OptionsOrientation.vertical, + wrapSpacing: 10.0, + options: const [ + FormBuilderFieldOption(key: ValueKey('1'), value: 1), + FormBuilderFieldOption(key: ValueKey('2'), value: 2), + ], + itemDecoration: BoxDecoration( + border: Border.all(color: Colors.blueAccent), + ), + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + // this is a brittle test knowing how we use container for a border + // there is one container for each option + expect(find.byType(Container), findsExactly(2)); + // same as wrapSpacing + Container foo = tester.firstWidget(find.byType(Container)); + expect(foo.margin, const EdgeInsets.only(bottom: 10.0)); + // verify separator counts + expect(find.byType(VerticalDivider), findsNothing); + }); + + testWidgets('separators horizontal', (WidgetTester tester) async { + const widgetName = 'rg1'; + final testWidget = FormBuilderRadioGroup( + name: widgetName, + orientation: OptionsOrientation.horizontal, + wrapSpacing: 10.0, + options: const [ + FormBuilderFieldOption(key: ValueKey('1'), value: 1), + FormBuilderFieldOption(key: ValueKey('2'), value: 2), + FormBuilderFieldOption(key: ValueKey('3'), value: 2), + ], + separator: const VerticalDivider(width: 8.0, color: Colors.red), + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + // verify separator counts + expect(find.byType(VerticalDivider), findsNWidgets(2)); + }); + + testWidgets('separators vertical', (WidgetTester tester) async { + const widgetName = 'rg1'; + final testWidget = FormBuilderRadioGroup( + name: widgetName, + orientation: OptionsOrientation.vertical, + wrapSpacing: 10.0, + options: const [ + FormBuilderFieldOption(key: ValueKey('1'), value: 1), + FormBuilderFieldOption(key: ValueKey('2'), value: 2), + FormBuilderFieldOption(key: ValueKey('2'), value: 2), + ], + itemDecoration: BoxDecoration( + border: Border.all(color: Colors.blueAccent), + ), + separator: const VerticalDivider(width: 8.0, color: Colors.red), + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + // verify separator counts + expect(find.byType(VerticalDivider), findsNWidgets(2)); + }); + testWidgets('When press tab, field will be focused', ( + WidgetTester tester, + ) async { + const widgetName = 'key'; + final testWidget = FormBuilderRadioGroup( + name: widgetName, + options: const [ + FormBuilderFieldOption(key: ValueKey('1'), value: 1), + FormBuilderFieldOption(key: ValueKey('2'), value: 2), + FormBuilderFieldOption(key: ValueKey('3'), value: 3), + ], + ); + final widgetFinder = find.byWidget(testWidget); + + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + final focusNode = + formKey.currentState?.fields[widgetName]?.effectiveFocusNode; + + expect(formSave(), isTrue); + expect(formValue(widgetName), isNull); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, false); + expect(focusNode?.hasFocus, false); + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pumpAndSettle(); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, true); + expect(focusNode?.hasFocus, true); + }); + }); +} diff --git a/test/form_builder_range_slider_test.dart b/test/src/fields/form_builder_range_slider_test.dart similarity index 56% rename from test/form_builder_range_slider_test.dart rename to test/src/fields/form_builder_range_slider_test.dart index a83a64577c..2f47888ffa 100644 --- a/test/form_builder_range_slider_test.dart +++ b/test/src/fields/form_builder_range_slider_test.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'form_builder_tester.dart'; +import '../../form_builder_tester.dart'; void main() { - group('FormBuilderRangeSlider --', () { + group('FormBuilderRangeSlider -', () { testWidgets('basic', (WidgetTester tester) async { const widgetName = 'formBuilderRangeSlider'; final testWidget = FormBuilderRangeSlider( @@ -16,15 +17,19 @@ void main() { await tester.pumpWidget(buildTestableFieldWidget(testWidget)); expect(formSave(), isTrue); - expect(formValue(widgetName), - equals(const RangeValues(10.0, 10.0))); + expect( + formValue(widgetName), + equals(const RangeValues(10.0, 10.0)), + ); // Inspired by https://github.com/flutter/flutter/blob/master/packages/flutter/test/material/range_slider_test.dart // Tap at the center of the slider. - final Offset topLeft = - tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); - final Offset bottomRight = - tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); + final Offset topLeft = tester + .getTopLeft(find.byType(RangeSlider)) + .translate(24, 0); + final Offset bottomRight = tester + .getBottomRight(find.byType(RangeSlider)) + .translate(-24, 0); final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.5; await tester.tapAt(rightTarget); await tester.pumpAndSettle(); @@ -44,15 +49,19 @@ void main() { await tester.pumpWidget(buildTestableFieldWidget(testWidget)); expect(formSave(), isTrue); - expect(formValue(widgetName), - equals(const RangeValues(14.0, 18.0))); + expect( + formValue(widgetName), + equals(const RangeValues(14.0, 18.0)), + ); // Inspired by https://github.com/flutter/flutter/blob/master/packages/flutter/test/material/range_slider_test.dart // Tap a small offset after the start of the slider. - final Offset topLeft = - tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); - final Offset bottomRight = - tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); + final Offset topLeft = tester + .getTopLeft(find.byType(RangeSlider)) + .translate(24, 0); + final Offset bottomRight = tester + .getBottomRight(find.byType(RangeSlider)) + .translate(-24, 0); final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.1; await tester.tapAt(leftTarget); await tester.pumpAndSettle(); @@ -61,6 +70,28 @@ void main() { expect(formValue(widgetName), const RangeValues(11.0, 18.0)); }); + testWidgets('when set valueWidget then show on FormBuilderRangeSlider', ( + WidgetTester tester, + ) async { + const widgetName = 'formBuilderRangeSlider'; + final keyValueWidget = Key('valueWidget'); + final testWidget = FormBuilderRangeSlider( + name: widgetName, + min: 10.0, + max: 20.0, + valueWidget: (value) => Text(value, key: keyValueWidget), + initialValue: const RangeValues(14.0, 18.0), + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + expect(formSave(), isTrue); + expect( + formValue(widgetName), + equals(const RangeValues(14.0, 18.0)), + ); + expect(find.byKey(keyValueWidget), findsOneWidget); + }); + testWidgets('Stateful Update', (WidgetTester tester) async { const widgetName = 'formBuilderRangeSlider'; const testWidget = _FormBuilderRangeSliderStateTest(widgetName); @@ -81,6 +112,34 @@ void main() { equals(const RangeValues(-9.0, 9.0)), ); }); + + testWidgets('When press tab, field will be focused', ( + WidgetTester tester, + ) async { + const widgetName = 'key'; + final testWidget = FormBuilderRangeSlider( + name: widgetName, + min: 10.0, + max: 20.0, + ); + final widgetFinder = find.byWidget(testWidget); + + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + final focusNode = + formKey.currentState?.fields[widgetName]?.effectiveFocusNode; + + expect(formSave(), isTrue); + expect( + formValue(widgetName), + equals(const RangeValues(10.0, 10.0)), + ); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, false); + expect(focusNode?.hasFocus, false); + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pumpAndSettle(); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, true); + expect(focusNode?.hasFocus, true); + }); }); } @@ -113,7 +172,7 @@ class _FormBuilderRangeSliderStateTestState TextButton( onPressed: () => setState(() => range -= 1), child: const Text('Reduce Range'), - ) + ), ], ); } diff --git a/test/src/fields/form_builder_slider_test.dart b/test/src/fields/form_builder_slider_test.dart new file mode 100644 index 0000000000..6e79f35f5c --- /dev/null +++ b/test/src/fields/form_builder_slider_test.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_form_builder/src/fields/form_builder_slider.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../form_builder_tester.dart'; + +void main() { + group('FormBuilderSlider -', () { + testWidgets('Basic', (WidgetTester tester) async { + const widgetName = 'slider1'; + final testWidget = FormBuilderSlider( + name: widgetName, + initialValue: 2, + min: 0, + max: 10, + divisions: 20, + decoration: const InputDecoration(labelText: 'Number of things'), + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + expect(formSave(), isTrue); + expect(formValue(widgetName), equals(2.0)); + await tester.tap(find.byType(Slider)); + await tester.pumpAndSettle(); + expect(formSave(), isTrue); + expect(formValue(widgetName), equals(5.0)); + }); + + testWidgets('When press tab, field will be focused', ( + WidgetTester tester, + ) async { + const widgetName = 'key'; + final testWidget = FormBuilderSlider( + name: widgetName, + initialValue: 2, + min: 0, + max: 10, + ); + final widgetFinder = find.byWidget(testWidget); + + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + final focusNode = + formKey.currentState?.fields[widgetName]?.effectiveFocusNode; + + expect(formSave(), isTrue); + expect(formValue(widgetName), equals(2.0)); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, false); + expect(focusNode?.hasFocus, false); + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pumpAndSettle(); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, true); + expect(focusNode?.hasFocus, true); + }); + }); +} diff --git a/test/src/fields/form_builder_switch_test.dart b/test/src/fields/form_builder_switch_test.dart new file mode 100644 index 0000000000..d71acb0ebc --- /dev/null +++ b/test/src/fields/form_builder_switch_test.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_form_builder/src/fields/form_builder_switch.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../form_builder_tester.dart'; + +void main() { + group('FormBuilderSwitch -', () { + testWidgets('Off/On/Off', (WidgetTester tester) async { + const widgetName = 'switch1'; + final testWidget = FormBuilderSwitch( + name: widgetName, + title: const Text('Switch 1'), + initialValue: false, + ); + final widgetFinder = find.byWidget(testWidget); + + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + expect(formSave(), isTrue); + expect(formValue(widgetName), isFalse); + await tester.tap(widgetFinder); + await tester.pumpAndSettle(); + expect(formSave(), isTrue); + expect(formValue(widgetName), isTrue); + await tester.tap(widgetFinder); + await tester.pumpAndSettle(); + expect(formSave(), isTrue); + expect(formValue(widgetName), isFalse); + }); + + testWidgets('When press tab, field will be focused', ( + WidgetTester tester, + ) async { + const widgetName = 'cb1'; + final testWidget = FormBuilderSwitch( + name: widgetName, + title: const Text('Switch 1'), + initialValue: false, + ); + final widgetFinder = find.byWidget(testWidget); + + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + final focusNode = + formKey.currentState?.fields[widgetName]?.effectiveFocusNode; + + expect(formSave(), isTrue); + expect(formValue(widgetName), isFalse); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, false); + expect(focusNode?.hasFocus, false); + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pumpAndSettle(); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, true); + expect(focusNode?.hasFocus, true); + }); + }); +} diff --git a/test/src/fields/form_builder_text_field_test.dart b/test/src/fields/form_builder_text_field_test.dart new file mode 100644 index 0000000000..d60ab7191d --- /dev/null +++ b/test/src/fields/form_builder_text_field_test.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../form_builder_tester.dart'; + +void main() { + group('FormBuilderTextField -', () { + testWidgets('Hello Planet', (WidgetTester tester) async { + const newTextValue = 'Hello 🪐'; + const textFieldName = 'text1'; + final testWidget = FormBuilderTextField(name: textFieldName); + final widgetFinder = find.byWidget(testWidget); + + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + expect(formSave(), isTrue); + expect(formValue(textFieldName), isNull); + await tester.enterText(widgetFinder, newTextValue); + expect(formSave(), isTrue); + expect(formValue(textFieldName), equals(newTextValue)); + await tester.enterText(widgetFinder, ''); + expect(formSave(), isTrue); + expect(formValue(textFieldName), isEmpty); + }); + testWidgets( + 'without initialValue', + (tester) => _testFormBuilderTextFieldWithInitialValue(tester), + ); + testWidgets( + 'has initialValue on Field', + (tester) => _testFormBuilderTextFieldWithInitialValue( + tester, + initialValueOnField: 'ok', + ), + ); + testWidgets( + 'has initialValue on Form', + (tester) => _testFormBuilderTextFieldWithInitialValue( + tester, + initialValueOnForm: 'ok', + ), + ); + + testWidgets( + 'triggers onTapOutside', + (tester) => _testFormBuilderTextFieldOnTapOutsideCallback(tester), + ); + testWidgets('When press tab, field will be focused', ( + WidgetTester tester, + ) async { + const widgetName = 'cb1'; + var testWidget = FormBuilderTextField(name: widgetName); + final widgetFinder = find.byWidget(testWidget); + + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + final focusNode = + formKey.currentState?.fields[widgetName]?.effectiveFocusNode; + + expect(formSave(), isTrue); + expect(formValue(widgetName), isNull); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, false); + expect(focusNode?.hasFocus, false); + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pumpAndSettle(); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, true); + expect(focusNode?.hasFocus, true); + }); + }); +} + +Future _testFormBuilderTextFieldWithInitialValue( + WidgetTester tester, { + String? initialValueOnField, + String? initialValueOnForm, +}) async { + int changedCount = 0; + const newTextValue = 'Hello 🪐'; + const textFieldName = 'text1'; + + var testWidget = FormBuilderTextField( + name: textFieldName, + initialValue: initialValueOnField, + onChanged: (String? value) => changedCount++, + ); + await tester.pumpWidget( + buildTestableFieldWidget( + testWidget, + initialValue: {textFieldName: initialValueOnForm}, + ), + ); + expect(formSave(), isTrue); + expect(formValue(textFieldName), initialValueOnField ?? initialValueOnForm); + expect(changedCount, 0); + + await tester.enterText(find.byWidget(testWidget), newTextValue); + expect(formSave(), isTrue); + expect(formValue(textFieldName), newTextValue); + + expect(changedCount, 1); +} + +Future _testFormBuilderTextFieldOnTapOutsideCallback( + WidgetTester tester, +) async { + const textFieldName = 'Hello 🪐'; + bool triggered = false; + + var testWidget = FormBuilderTextField( + name: textFieldName, + onTapOutside: (event) => triggered = true, + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + final textField = tester.firstWidget(find.byType(TextField)) as TextField; + textField.onTapOutside?.call(const PointerDownEvent()); + expect(triggered, true); +} diff --git a/test/src/form_builder_field_decoration_test.dart b/test/src/form_builder_field_decoration_test.dart new file mode 100644 index 0000000000..96a8484594 --- /dev/null +++ b/test/src/form_builder_field_decoration_test.dart @@ -0,0 +1,201 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; + +import '../form_builder_tester.dart'; + +void main() { + group('FormBuilderFieldDecoration -', () { + testWidgets('when change the error text then the field should be invalid', ( + WidgetTester tester, + ) async { + final decorationFieldKey = GlobalKey(); + const name = 'testField'; + const errorTextField = 'error text field'; + final widget = FormBuilderFieldDecoration( + key: decorationFieldKey, + name: name, + builder: (FormFieldState field) { + return InputDecorator( + decoration: (field as FormBuilderFieldDecorationState).decoration, + child: TextField( + onChanged: (value) { + field.didChange(value); + }, + ), + ); + }, + ); + + await tester.pumpWidget(buildTestableFieldWidget(widget)); + + // Initially, the field should be valid + expect(decorationFieldKey.currentState?.isValid, isTrue); + + decorationFieldKey.currentState?.invalidate(errorTextField); + + // The field should be invalid + expect(decorationFieldKey.currentState?.isValid, isFalse); + + // Clear the error + decorationFieldKey.currentState?.reset(); + + // The field should be valid again + expect(decorationFieldKey.currentState?.isValid, isTrue); + }); + group('decoration enabled -', () { + testWidgets( + 'when change the error text then the field should be invalid', + (WidgetTester tester) async { + final decorationFieldKey = + GlobalKey(); + const name = 'testField'; + const errorTextField = 'error text field'; + final widget = FormBuilderFieldDecoration( + key: decorationFieldKey, + name: name, + builder: (FormFieldState field) { + return InputDecorator( + decoration: + (field as FormBuilderFieldDecorationState).decoration, + child: TextField( + onChanged: (value) { + field.didChange(value); + }, + ), + ); + }, + ); + + await tester.pumpWidget(buildTestableFieldWidget(widget)); + + // Initially, the field should be valid + expect(decorationFieldKey.currentState?.isValid, isTrue); + + decorationFieldKey.currentState?.invalidate(errorTextField); + + // The field should be invalid + expect(decorationFieldKey.currentState?.isValid, isFalse); + + // Clear the error + decorationFieldKey.currentState?.reset(); + + // The field should be valid again + expect(decorationFieldKey.currentState?.isValid, isTrue); + }, + ); + test( + 'when enable property on decoration is false and enabled true, then throw an assert', + () async { + final decorationFieldKey = + GlobalKey(); + const name = 'testField'; + + expect( + () => FormBuilderFieldDecoration( + key: decorationFieldKey, + name: name, + decoration: const InputDecoration(enabled: false), + builder: (FormFieldState field) { + return InputDecorator( + decoration: + (field as FormBuilderFieldDecorationState).decoration, + child: TextField( + onChanged: (value) { + field.didChange(value); + }, + ), + ); + }, + ), + throwsAssertionError, + ); + }, + ); + test( + 'when enable property on decoration is false and enable is false, then build normally', + () async { + final decorationFieldKey = + GlobalKey(); + const name = 'testField'; + + expect( + () => FormBuilderFieldDecoration( + key: decorationFieldKey, + name: name, + decoration: const InputDecoration(enabled: false), + enabled: false, + builder: (FormFieldState field) { + return InputDecorator( + decoration: + (field as FormBuilderFieldDecorationState).decoration, + child: TextField( + onChanged: (value) { + field.didChange(value); + }, + ), + ); + }, + ), + returnsNormally, + ); + }, + ); + test( + 'when decoration is default (enabled: true), then build normally', + () async { + final decorationFieldKey = + GlobalKey(); + const name = 'testField'; + + expect( + () => FormBuilderFieldDecoration( + key: decorationFieldKey, + name: name, + builder: (FormFieldState field) { + return InputDecorator( + decoration: + (field as FormBuilderFieldDecorationState).decoration, + child: TextField( + onChanged: (value) { + field.didChange(value); + }, + ), + ); + }, + ), + returnsNormally, + ); + }, + ); + test( + 'when decoration is default (enabled: true) and enable false, then build normally', + () async { + final decorationFieldKey = + GlobalKey(); + const name = 'testField'; + + expect( + () => FormBuilderFieldDecoration( + key: decorationFieldKey, + name: name, + enabled: false, + builder: (FormFieldState field) { + return InputDecorator( + decoration: + (field as FormBuilderFieldDecorationState).decoration, + child: TextField( + onChanged: (value) { + field.didChange(value); + }, + ), + ); + }, + ), + returnsNormally, + ); + }, + ); + }); + }); +} diff --git a/test/src/form_builder_field_test.dart b/test/src/form_builder_field_test.dart index 24ce41ffc4..122fcc6304 100644 --- a/test/src/form_builder_field_test.dart +++ b/test/src/form_builder_field_test.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -7,8 +8,9 @@ import '../form_builder_tester.dart'; void main() { group('FormBuilderField -', () { group('custom error -', () { - testWidgets('Should show custom error when invalidate field', - (tester) async { + testWidgets('Should show custom error when invalidate field', ( + tester, + ) async { final textFieldKey = GlobalKey(); const textFieldName = 'text2'; const errorTextField = 'error text field'; @@ -43,51 +45,55 @@ void main() { expect(textFieldKey.currentState?.isValid, isFalse); }); testWidgets( - 'Should valid when no has error and autovalidateMode is always', - (tester) async { - final textFieldKey = GlobalKey(); - const textFieldName = 'text'; - const errorTextField = 'error text field'; - final testWidget = FormBuilderTextField( - name: textFieldName, - key: textFieldKey, - autovalidateMode: AutovalidateMode.always, - validator: (value) => - value == null || value.isEmpty ? errorTextField : null, - ); - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - - expect(textFieldKey.currentState?.isValid, isFalse); - - final widgetFinder = find.byWidget(testWidget); - await tester.enterText(widgetFinder, 'test'); - await tester.pumpAndSettle(); - - expect(textFieldKey.currentState?.isValid, isTrue); - }); + 'Should valid when no has error and autovalidateMode is always', + (tester) async { + final textFieldKey = GlobalKey(); + const textFieldName = 'text'; + const errorTextField = 'error text field'; + final testWidget = FormBuilderTextField( + name: textFieldName, + key: textFieldKey, + autovalidateMode: AutovalidateMode.always, + validator: + (value) => + value == null || value.isEmpty ? errorTextField : null, + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + expect(textFieldKey.currentState?.isValid, isFalse); + + final widgetFinder = find.byWidget(testWidget); + await tester.enterText(widgetFinder, 'test'); + await tester.pumpAndSettle(); + + expect(textFieldKey.currentState?.isValid, isTrue); + }, + ); testWidgets( - 'Should invalid when has error and autovalidateMode is always', - (tester) async { - final textFieldKey = GlobalKey(); - const textFieldName = 'text'; - const errorTextField = 'error text field'; - final testWidget = FormBuilderTextField( - name: textFieldName, - key: textFieldKey, - autovalidateMode: AutovalidateMode.always, - validator: (value) => - value == null || value.length < 10 ? errorTextField : null, - ); - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - - expect(textFieldKey.currentState?.isValid, isFalse); - - final widgetFinder = find.byWidget(testWidget); - await tester.enterText(widgetFinder, 'test'); - await tester.pumpAndSettle(); - - expect(textFieldKey.currentState?.isValid, isFalse); - }); + 'Should invalid when has error and autovalidateMode is always', + (tester) async { + final textFieldKey = GlobalKey(); + const textFieldName = 'text'; + const errorTextField = 'error text field'; + final testWidget = FormBuilderTextField( + name: textFieldName, + key: textFieldKey, + autovalidateMode: AutovalidateMode.always, + validator: + (value) => + value == null || value.length < 10 ? errorTextField : null, + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + expect(textFieldKey.currentState?.isValid, isFalse); + + final widgetFinder = find.byWidget(testWidget); + await tester.enterText(widgetFinder, 'test'); + await tester.pumpAndSettle(); + + expect(textFieldKey.currentState?.isValid, isFalse); + }, + ); }); group('hasErrors -', () { @@ -107,8 +113,9 @@ void main() { expect(textFieldKey.currentState?.hasError, isTrue); }); - testWidgets('Should no has errors when is empty and no has validators', - (tester) async { + testWidgets('Should no has errors when is empty and no has validators', ( + tester, + ) async { final textFieldKey = GlobalKey(); const textFieldName = 'text'; final testWidget = FormBuilderTextField( @@ -127,28 +134,30 @@ void main() { group('valueIsValid -', () { testWidgets( - 'Should value is valid when validator passes, despite set custom error', - (tester) async { - final textFieldKey = GlobalKey(); - const textFieldName = 'text'; - const errorTextField = 'error text field'; - final testWidget = FormBuilderTextField( - name: textFieldName, - key: textFieldKey, - ); - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - - // Set custom error - textFieldKey.currentState?.invalidate(errorTextField); - await tester.pumpAndSettle(); - - expect(textFieldKey.currentState?.valueIsValid, isTrue); - }); + 'Should value is valid when validator passes, despite set custom error', + (tester) async { + final textFieldKey = GlobalKey(); + const textFieldName = 'text'; + const errorTextField = 'error text field'; + final testWidget = FormBuilderTextField( + name: textFieldName, + key: textFieldKey, + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + // Set custom error + textFieldKey.currentState?.invalidate(errorTextField); + await tester.pumpAndSettle(); + + expect(textFieldKey.currentState?.valueIsValid, isTrue); + }, + ); }); group('valueHasError -', () { - testWidgets('Should value is invalid when validator passes', - (tester) async { + testWidgets('Should value is invalid when validator passes', ( + tester, + ) async { final textFieldKey = GlobalKey(); const textFieldName = 'text'; const invalidValue = 'invalid'; @@ -164,60 +173,126 @@ void main() { expect(textFieldKey.currentState?.valueHasError, isTrue); }); }); - group('autovalidateMode -', () { testWidgets( - 'Should show error when init form and AutovalidateMode is always', - (tester) async { - const textFieldName = 'text4'; - const errorTextField = 'error text field'; - final testWidget = FormBuilderTextField( - name: textFieldName, - validator: (value) => - value == null || value.isEmpty ? errorTextField : null, - autovalidateMode: AutovalidateMode.always, - ); - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - await tester.pumpAndSettle(); - - expect(find.text(errorTextField), findsOneWidget); - }); + 'Should show error when init form and AutovalidateMode is always', + (tester) async { + const textFieldName = 'text4'; + const errorTextField = 'error text field'; + final testWidget = FormBuilderTextField( + name: textFieldName, + validator: + (value) => + value == null || value.isEmpty ? errorTextField : null, + autovalidateMode: AutovalidateMode.always, + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + await tester.pumpAndSettle(); + + expect(find.text(errorTextField), findsOneWidget); + }, + ); testWidgets( - 'Should show error when AutovalidateMode is onUserInteraction and change field', - (tester) async { - const textFieldName = 'text4'; - const errorTextField = 'error text field'; - final testWidget = FormBuilderTextField( - name: textFieldName, - autovalidateMode: AutovalidateMode.onUserInteraction, - validator: (value) => errorTextField, - ); - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - expect(find.text(errorTextField), findsNothing); - - await tester.enterText(find.byWidget(testWidget), 'hola'); - await tester.pumpAndSettle(); - - expect(find.text(errorTextField), findsOneWidget); - }); + 'Should not show error when init form and AutovalidateMode is disabled', + (tester) async { + const textFieldName = 'text4'; + const errorTextField = 'error text field'; + final testWidget = FormBuilderTextField( + name: textFieldName, + autovalidateMode: AutovalidateMode.disabled, + validator: (value) => errorTextField, + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + await tester.pumpAndSettle(); + + expect(find.text(errorTextField), findsNothing); + }, + ); + testWidgets( + 'Should show error when AutovalidateMode is onUserInteraction and change field', + (tester) async { + const textFieldName = 'text4'; + const errorTextField = 'error text field'; + final testWidget = FormBuilderTextField( + name: textFieldName, + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (value) => errorTextField, + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + expect(find.text(errorTextField), findsNothing); + + await tester.enterText(find.byWidget(testWidget), 'hola'); + await tester.pumpAndSettle(); + + expect(find.text(errorTextField), findsOneWidget); + }, + ); + testWidgets( + 'Should show error when init form and AutovalidateMode is onUnfocus', + (tester) async { + const textFieldName = 'text4'; + const errorTextField = 'error text field'; + final testWidget = FormBuilderTextField( + name: textFieldName, + autovalidateMode: AutovalidateMode.onUnfocus, + validator: (value) => errorTextField, + ); + final widgetFinder = find.byWidget(testWidget); + + // Init form + await tester.pumpWidget( + buildTestableFieldWidget( + Column( + children: [ + testWidget, + ElevatedButton(onPressed: () {}, child: const Text('Submit')), + ], + ), + autovalidateMode: AutovalidateMode.onUnfocus, + ), + ); + await tester.pumpAndSettle(); + final focusNode = + formKey.currentState?.fields[textFieldName]?.effectiveFocusNode; + expect(find.text(errorTextField), findsNothing); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, false); + expect(focusNode?.hasFocus, false); + + // Focus input and write text + await tester.enterText(widgetFinder, 'test'); + await tester.pumpAndSettle(); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, true); + expect(focusNode?.hasFocus, true); + expect(find.text(errorTextField), findsNothing); + + // Unfocus input and show error + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pumpAndSettle(); + expect(find.text(errorTextField), findsOneWidget); + }, + ); }); - group('isDirty - ', () { testWidgets('Should not dirty by default', (tester) async { const textFieldName = 'text'; final textFieldKey = GlobalKey(); - final testWidget = - FormBuilderTextField(name: textFieldName, key: textFieldKey); + final testWidget = FormBuilderTextField( + name: textFieldName, + key: textFieldKey, + ); await tester.pumpWidget(buildTestableFieldWidget(testWidget)); expect(textFieldKey.currentState?.isDirty, false); }); - testWidgets('Should dirty when update field value by user', - (tester) async { + testWidgets('Should dirty when update field value by user', ( + tester, + ) async { const textFieldName = 'text'; final textFieldKey = GlobalKey(); - final testWidget = - FormBuilderTextField(name: textFieldName, key: textFieldKey); + final testWidget = FormBuilderTextField( + name: textFieldName, + key: textFieldKey, + ); await tester.pumpWidget(buildTestableFieldWidget(testWidget)); final widgetFinder = find.byWidget(testWidget); @@ -225,12 +300,15 @@ void main() { expect(textFieldKey.currentState?.isDirty, true); }); - testWidgets('Should dirty when update field value by method', - (tester) async { + testWidgets('Should dirty when update field value by method', ( + tester, + ) async { const textFieldName = 'text'; final textFieldKey = GlobalKey(); - final testWidget = - FormBuilderTextField(name: textFieldName, key: textFieldKey); + final testWidget = FormBuilderTextField( + name: textFieldName, + key: textFieldKey, + ); await tester.pumpWidget(buildTestableFieldWidget(testWidget)); textFieldKey.currentState?.setValue('test'); @@ -238,8 +316,9 @@ void main() { expect(textFieldKey.currentState?.isDirty, true); }); - testWidgets('Should dirty when update field with initial value by user', - (tester) async { + testWidgets('Should dirty when update field with initial value by user', ( + tester, + ) async { const textFieldName = 'text'; final textFieldKey = GlobalKey(); final testWidget = FormBuilderTextField( @@ -254,37 +333,42 @@ void main() { expect(textFieldKey.currentState?.isDirty, true); }); - testWidgets('Should dirty when update field with initial value by method', - (tester) async { + testWidgets( + 'Should dirty when update field with initial value by method', + (tester) async { + const textFieldName = 'text'; + final textFieldKey = GlobalKey(); + final testWidget = FormBuilderTextField( + name: textFieldName, + key: textFieldKey, + initialValue: 'hi', + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + textFieldKey.currentState?.setValue('test'); + await tester.pumpAndSettle(); + + expect(textFieldKey.currentState?.isDirty, true); + }, + ); + testWidgets('Should not dirty when reset field value', (tester) async { const textFieldName = 'text'; final textFieldKey = GlobalKey(); final testWidget = FormBuilderTextField( name: textFieldName, key: textFieldKey, - initialValue: 'hi', ); await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - textFieldKey.currentState?.setValue('test'); - await tester.pumpAndSettle(); - - expect(textFieldKey.currentState?.isDirty, true); - }); - testWidgets('Should not dirty when reset field value', (tester) async { - const textFieldName = 'text'; - final textFieldKey = GlobalKey(); - final testWidget = - FormBuilderTextField(name: textFieldName, key: textFieldKey); - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - textFieldKey.currentState?.setValue('test'); await tester.pumpAndSettle(); textFieldKey.currentState?.reset(); expect(textFieldKey.currentState?.isDirty, false); }); - testWidgets('Should not dirty when reset field with initial value', - (tester) async { + testWidgets('Should not dirty when reset field with initial value', ( + tester, + ) async { const textFieldName = 'text'; final textFieldKey = GlobalKey(); final testWidget = FormBuilderTextField( @@ -301,13 +385,14 @@ void main() { expect(textFieldKey.currentState?.isDirty, false); }); }); - group('isTouched - ', () { testWidgets('Should not touched by default', (tester) async { const textFieldName = 'text'; final textFieldKey = GlobalKey(); - final testWidget = - FormBuilderTextField(name: textFieldName, key: textFieldKey); + final testWidget = FormBuilderTextField( + name: textFieldName, + key: textFieldKey, + ); await tester.pumpWidget(buildTestableFieldWidget(testWidget)); expect(textFieldKey.currentState?.isTouched, false); @@ -315,8 +400,10 @@ void main() { testWidgets('Should touched when focus input', (tester) async { const textFieldName = 'text'; final textFieldKey = GlobalKey(); - final testWidget = - FormBuilderTextField(name: textFieldName, key: textFieldKey); + final testWidget = FormBuilderTextField( + name: textFieldName, + key: textFieldKey, + ); await tester.pumpWidget(buildTestableFieldWidget(testWidget)); final widgetFinder = find.byWidget(testWidget); @@ -325,13 +412,14 @@ void main() { expect(textFieldKey.currentState?.isTouched, true); }); }); - group('reset -', () { testWidgets('Should reset to null when call reset', (tester) async { const textFieldName = 'text'; final textFieldKey = GlobalKey(); - final testWidget = - FormBuilderTextField(name: textFieldName, key: textFieldKey); + final testWidget = FormBuilderTextField( + name: textFieldName, + key: textFieldKey, + ); await tester.pumpWidget(buildTestableFieldWidget(testWidget)); textFieldKey.currentState?.setValue('test'); @@ -358,25 +446,78 @@ void main() { expect(textFieldKey.currentState?.value, equals(initialValue)); }); testWidgets( - 'Should reset custom error when invalidate field and then reset', - (tester) async { + 'Should reset custom error when invalidate field and then reset', + (tester) async { + final textFieldKey = GlobalKey(); + const textFieldName = 'text'; + const errorTextField = 'error text field'; + final testWidget = FormBuilderTextField( + name: textFieldName, + key: textFieldKey, + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + textFieldKey.currentState?.invalidate(errorTextField); + await tester.pumpAndSettle(); + + // Reset custom error + textFieldKey.currentState?.reset(); + await tester.pumpAndSettle(); + expect(find.text(errorTextField), findsNothing); + }, + ); + }); + group('focus -', () { + testWidgets('Should focus on field when invalidate it', (tester) async { final textFieldKey = GlobalKey(); - const textFieldName = 'text'; + const widgetName = 'text'; const errorTextField = 'error text field'; final testWidget = FormBuilderTextField( - name: textFieldName, + name: widgetName, key: textFieldKey, ); + final widgetFinder = find.byWidget(testWidget); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + final focusNode = + formKey.currentState?.fields[widgetName]?.effectiveFocusNode; + + expect(Focus.of(tester.element(widgetFinder)).hasFocus, false); + expect(focusNode?.hasFocus, false); textFieldKey.currentState?.invalidate(errorTextField); await tester.pumpAndSettle(); - // Reset custom error - textFieldKey.currentState?.reset(); - await tester.pumpAndSettle(); - expect(find.text(errorTextField), findsNothing); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, true); + expect(focusNode?.hasFocus, true); }); + testWidgets( + 'Should not focus on field when invalidate field and is disabled', + (tester) async { + final textFieldKey = GlobalKey(); + const widgetName = 'text'; + const errorTextField = 'error text field'; + final testWidget = FormBuilderTextField( + name: widgetName, + key: textFieldKey, + enabled: false, + ); + final widgetFinder = find.byWidget(testWidget); + + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + final focusNode = + formKey.currentState?.fields[widgetName]?.effectiveFocusNode; + + expect(Focus.of(tester.element(widgetFinder)).hasFocus, false); + expect(focusNode?.hasFocus, false); + + textFieldKey.currentState?.invalidate(errorTextField); + await tester.pumpAndSettle(); + + expect(Focus.of(tester.element(widgetFinder)).hasFocus, false); + expect(focusNode?.hasFocus, false); + }, + ); }); }); } diff --git a/test/src/form_builder_test.dart b/test/src/form_builder_test.dart index db9e59e404..ff4c212cdc 100644 --- a/test/src/form_builder_test.dart +++ b/test/src/form_builder_test.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -6,8 +7,9 @@ import '../form_builder_tester.dart'; void main() { group('custom error -', () { - testWidgets('Should show custom error when invalidate field', - (tester) async { + testWidgets('Should show custom error when invalidate field', ( + tester, + ) async { const textFieldName = 'text1'; const errorTextField = 'error text field'; final testWidget = FormBuilderTextField(name: textFieldName); @@ -21,105 +23,105 @@ void main() { }); group('dynamic fields', () { - testWidgets( - 'FormBuilder Dynamic Field -- keeping value', - (WidgetTester tester) async { - const String testWidgetName = 'test_widget_name'; - - await tester.pumpWidget( - buildTestableFieldWidget( - _DynamicFormFields( - name: testWidgetName, - valueTransformer: (value) { - return value is String ? int.tryParse(value) : null; - }, - ), - // the value is kept - clearValueOnUnregister: false, + testWidgets('FormBuilder Dynamic Field -- keeping value', ( + WidgetTester tester, + ) async { + const String testWidgetName = 'test_widget_name'; + + await tester.pumpWidget( + buildTestableFieldWidget( + _DynamicFormFields( + name: testWidgetName, + valueTransformer: (value) { + return value is String ? int.tryParse(value) : null; + }, ), - ); - - // Write an input resulting in a null value after transformation - formFieldDidChange(testWidgetName, 'a'); - expect(formInstantValue(testWidgetName), isNull); + // the value is kept + clearValueOnUnregister: false, + ), + ); - // Write an input and test the transformer - formFieldDidChange(testWidgetName, '1'); - expect(formInstantValue(testWidgetName).runtimeType, int); - expect(formInstantValue(testWidgetName), 1); + // Write an input resulting in a null value after transformation + formFieldDidChange(testWidgetName, 'a'); + expect(formInstantValue(testWidgetName), isNull); - // Remove the dynamic field from the widget tree - final _DynamicFormFieldsState dynamicFieldState = - tester.state(find.byType(_DynamicFormFields)); - dynamicFieldState.show = false; + // Write an input and test the transformer + formFieldDidChange(testWidgetName, '1'); + expect(formInstantValue(testWidgetName).runtimeType, int); + expect(formInstantValue(testWidgetName), 1); - // Pump the next frame, disposing the field's state - await tester.pump(); + // Remove the dynamic field from the widget tree + final _DynamicFormFieldsState dynamicFieldState = tester.state( + find.byType(_DynamicFormFields), + ); + dynamicFieldState.show = false; - // With the field unregistered, the form does not have its transformer - // but it still has its value, now recovered as type String - expect(formInstantValue(testWidgetName).runtimeType, String); - expect(formInstantValue(testWidgetName), '1'); + // Pump the next frame, disposing the field's state + await tester.pump(); - // Show and recreate the field's state - dynamicFieldState.show = true; - await tester.pump(); + // With the field unregistered, the form does not have its transformer + // but it still has its value, now recovered as type String + expect(formInstantValue(testWidgetName).runtimeType, String); + expect(formInstantValue(testWidgetName), '1'); - // The transformer is registered again and with the internal value that - // was kept, it's expected an int of value 1 - expect(formInstantValue(testWidgetName).runtimeType, int); - expect(formInstantValue(testWidgetName), 1); - }, - ); + // Show and recreate the field's state + dynamicFieldState.show = true; + await tester.pump(); - testWidgets( - 'FormBuilder Dynamic Field -- clearing value', - (WidgetTester tester) async { - const String testWidgetName = 'test_widget_name'; + // The transformer is registered again and with the internal value that + // was kept, it's expected an int of value 1 + expect(formInstantValue(testWidgetName).runtimeType, int); + expect(formInstantValue(testWidgetName), 1); + }); - await tester.pumpWidget( - buildTestableFieldWidget( - _DynamicFormFields( - name: testWidgetName, - valueTransformer: (value) { - return value is String ? int.tryParse(value) : null; - }, - ), - // the value is cleared - clearValueOnUnregister: true, + testWidgets('FormBuilder Dynamic Field -- clearing value', ( + WidgetTester tester, + ) async { + const String testWidgetName = 'test_widget_name'; + + await tester.pumpWidget( + buildTestableFieldWidget( + _DynamicFormFields( + name: testWidgetName, + valueTransformer: (value) { + return value is String ? int.tryParse(value) : null; + }, ), - ); + // the value is cleared + clearValueOnUnregister: true, + ), + ); - // Write an input and test the transformer - formFieldDidChange(testWidgetName, '1'); - await tester.pump(); - expect(int, formInstantValue(testWidgetName).runtimeType); - expect(1, formInstantValue(testWidgetName)); - - // Remove the dynamic field from the widget tree - final _DynamicFormFieldsState dynamicFieldState = - tester.state(find.byType(_DynamicFormFields)); - dynamicFieldState.show = false; - - // Pump the next frame, disposing the field's state - await tester.pump(); - - // With the field unregistered, the form does not have its transformer, - // and since the value was cleared, neither its value - expect(formInstantValue(testWidgetName).runtimeType, Null); - expect(formInstantValue(testWidgetName), isNull); - - // Show and recreate the field's state - dynamicFieldState.show = true; - await tester.pump(); - - // A new input is needed to get another value - formFieldDidChange(testWidgetName, '2'); - await tester.pump(); - expect(formInstantValue(testWidgetName).runtimeType, int); - expect(formInstantValue(testWidgetName), 2); - }, - ); + // Write an input and test the transformer + formFieldDidChange(testWidgetName, '1'); + await tester.pump(); + expect(int, formInstantValue(testWidgetName).runtimeType); + expect(1, formInstantValue(testWidgetName)); + + // Remove the dynamic field from the widget tree + final _DynamicFormFieldsState dynamicFieldState = tester.state( + find.byType(_DynamicFormFields), + ); + dynamicFieldState.show = false; + + // Pump the next frame, disposing the field's state + await tester.pump(); + + // With the field unregistered, the form does not have its transformer, + // and since the value was cleared, neither its value + expect(formInstantValue(testWidgetName).runtimeType, Null); + expect(formInstantValue(testWidgetName), isNull); + + // Show and recreate the field's state + dynamicFieldState.show = true; + await tester.pump(); + + // A new input is needed to get another value + formFieldDidChange(testWidgetName, '2'); + await tester.pump(); + expect(formInstantValue(testWidgetName).runtimeType, int); + expect(formInstantValue(testWidgetName), 2); + }); }); group('isValid -', () { @@ -135,119 +137,264 @@ void main() { expect(formKey.currentState?.isValid, isFalse); }); - testWidgets('Should valid when no has error and autovalidateMode is always', - (tester) async { - const textFieldName = 'text'; - const errorTextField = 'error text field'; - final testWidget = FormBuilderTextField( - name: textFieldName, - validator: (value) => - value == null || value.isEmpty ? errorTextField : null, - ); - await tester.pumpWidget(buildTestableFieldWidget( - testWidget, - autovalidateMode: AutovalidateMode.always, - )); + testWidgets( + 'Should valid when no has error and autovalidateMode is always', + (tester) async { + const textFieldName = 'text'; + const errorTextField = 'error text field'; + final testWidget = FormBuilderTextField( + name: textFieldName, + validator: + (value) => value == null || value.isEmpty ? errorTextField : null, + ); + await tester.pumpWidget( + buildTestableFieldWidget( + testWidget, + autovalidateMode: AutovalidateMode.always, + ), + ); - expect(formKey.currentState?.isValid, isFalse); + expect(formKey.currentState?.isValid, isFalse); - final widgetFinder = find.byWidget(testWidget); - await tester.enterText(widgetFinder, 'test'); - await tester.pumpAndSettle(); + final widgetFinder = find.byWidget(testWidget); + await tester.enterText(widgetFinder, 'test'); + await tester.pumpAndSettle(); - expect(formKey.currentState?.isValid, isTrue); - }); - testWidgets('Should invalid when has error and autovalidateMode is always', - (tester) async { - const textFieldName = 'text'; - const errorTextField = 'error text field'; - final testWidget = FormBuilderTextField( - name: textFieldName, - validator: (value) => - value == null || value.length < 10 ? errorTextField : null, - ); - await tester.pumpWidget(buildTestableFieldWidget( - testWidget, - autovalidateMode: AutovalidateMode.always, - )); + expect(formKey.currentState?.isValid, isTrue); + }, + ); + testWidgets( + 'Should invalid when has error and autovalidateMode is always', + (tester) async { + const textFieldName = 'text'; + const errorTextField = 'error text field'; + final testWidget = FormBuilderTextField( + name: textFieldName, + validator: + (value) => + value == null || value.length < 10 ? errorTextField : null, + ); + await tester.pumpWidget( + buildTestableFieldWidget( + testWidget, + autovalidateMode: AutovalidateMode.always, + ), + ); - expect(formKey.currentState?.isValid, isFalse); + expect(formKey.currentState?.isValid, isFalse); - final widgetFinder = find.byWidget(testWidget); - await tester.enterText(widgetFinder, 'test'); - await tester.pumpAndSettle(); + final widgetFinder = find.byWidget(testWidget); + await tester.enterText(widgetFinder, 'test'); + await tester.pumpAndSettle(); - expect(formKey.currentState?.isValid, isFalse); - }); + expect(formKey.currentState?.isValid, isFalse); + }, + ); }); group('skipDisabled -', () { testWidgets( - 'Should not show error when field is not enabled and skipDisabled is true', - (tester) async { - const textFieldName = 'text3'; - const errorTextField = 'error text field'; - final testWidget = FormBuilderTextField( - name: textFieldName, - enabled: false, - validator: (value) => errorTextField, - ); - await tester.pumpWidget(buildTestableFieldWidget( - testWidget, - skipDisabled: true, - )); + 'Should not show error when field is disabled and skipDisabled is true', + (tester) async { + const textFieldName = 'text3'; + const errorTextField = 'error text field'; + final testWidget = FormBuilderTextField( + name: textFieldName, + enabled: false, + validator: (value) => errorTextField, + ); + await tester.pumpWidget( + buildTestableFieldWidget(testWidget, skipDisabled: true), + ); - formKey.currentState?.validate(); - await tester.pumpAndSettle(); - expect(find.text(errorTextField), findsNothing); + formKey.currentState?.validate(); + await tester.pumpAndSettle(); + expect(find.text(errorTextField), findsNothing); - formKey.currentState?.fields[textFieldName]?.validate(); - await tester.pumpAndSettle(); - expect(find.text(errorTextField), findsNothing); - }); + formKey.currentState?.fields[textFieldName]?.validate(); + await tester.pumpAndSettle(); + expect(find.text(errorTextField), findsNothing); + }, + ); testWidgets( - 'Should show error when field is not enabled and skipDisabled is false', - (tester) async { - const textFieldName = 'text4'; - const errorTextField = 'error text field'; - final testWidget = FormBuilderTextField( - name: textFieldName, - enabled: false, - validator: (value) => errorTextField, - ); - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + 'Should show error when field is disabled and skipDisabled is false', + (tester) async { + const textFieldName = 'text4'; + const errorTextField = 'error text field'; + final testWidget = FormBuilderTextField( + name: textFieldName, + enabled: false, + validator: (value) => errorTextField, + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - formKey.currentState?.validate(); - await tester.pumpAndSettle(); - expect(find.text(errorTextField), findsOneWidget); + formKey.currentState?.validate(); + await tester.pumpAndSettle(); + expect(find.text(errorTextField), findsOneWidget); - formKey.currentState?.reset(); - await tester.pumpAndSettle(); - expect(find.text(errorTextField), findsNothing); + formKey.currentState?.reset(); + await tester.pumpAndSettle(); + expect(find.text(errorTextField), findsNothing); - formKey.currentState?.fields[textFieldName]?.validate(); - await tester.pumpAndSettle(); - expect(find.text(errorTextField), findsOneWidget); - }); + formKey.currentState?.fields[textFieldName]?.validate(); + await tester.pumpAndSettle(); + expect(find.text(errorTextField), findsOneWidget); + }, + ); }); group('autovalidateMode -', () { testWidgets( - 'Should show error when init form and AutovalidateMode is always', + 'Should show error when init form and AutovalidateMode is always', + (tester) async { + const textFieldName = 'text4'; + const errorTextField = 'error text field'; + final testWidget = FormBuilderTextField( + name: textFieldName, + validator: (value) => errorTextField, + ); + await tester.pumpWidget( + buildTestableFieldWidget( + testWidget, + autovalidateMode: AutovalidateMode.always, + ), + ); + await tester.pumpAndSettle(); + + expect(find.text(errorTextField), findsOneWidget); + }, + ); + testWidgets( + 'Should not show error when init form and AutovalidateMode is disabled', + (tester) async { + const textFieldName = 'text4'; + const errorTextField = 'error text field'; + final testWidget = FormBuilderTextField( + name: textFieldName, + validator: (value) => errorTextField, + ); + await tester.pumpWidget( + buildTestableFieldWidget( + testWidget, + autovalidateMode: AutovalidateMode.disabled, + ), + ); + await tester.pumpAndSettle(); + + expect(find.text(errorTextField), findsNothing); + }, + ); + testWidgets( + 'Should show error when init form and AutovalidateMode is onUserInteraction', + (tester) async { + const textFieldName = 'text4'; + const errorTextField = 'error text field'; + final testWidget = FormBuilderTextField( + name: textFieldName, + validator: (value) => errorTextField, + ); + await tester.pumpWidget( + buildTestableFieldWidget( + testWidget, + autovalidateMode: AutovalidateMode.onUserInteraction, + ), + ); + await tester.pumpAndSettle(); + + expect(find.text(errorTextField), findsNothing); + + final widgetFinder = find.byWidget(testWidget); + await tester.enterText(widgetFinder, 'test'); + await tester.pumpAndSettle(); + + expect(find.text(errorTextField), findsOneWidget); + }, + ); + testWidgets( + 'Should show error when init form and AutovalidateMode is onUnfocus', + (tester) async { + const textFieldName = 'text4'; + const errorTextField = 'error text field'; + final testWidget = FormBuilderTextField( + name: textFieldName, + validator: (value) => errorTextField, + ); + final widgetFinder = find.byWidget(testWidget); + + // Init form + await tester.pumpWidget( + buildTestableFieldWidget( + Column( + children: [ + testWidget, + ElevatedButton(onPressed: () {}, child: const Text('Submit')), + ], + ), + autovalidateMode: AutovalidateMode.onUnfocus, + ), + ); + await tester.pumpAndSettle(); + final focusNode = + formKey.currentState?.fields[textFieldName]?.effectiveFocusNode; + expect(find.text(errorTextField), findsNothing); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, false); + expect(focusNode?.hasFocus, false); + + // Focus input and write text + await tester.enterText(widgetFinder, 'test'); + await tester.pumpAndSettle(); + expect(Focus.of(tester.element(widgetFinder)).hasFocus, true); + expect(focusNode?.hasFocus, true); + expect(find.text(errorTextField), findsNothing); + + // Unfocus input and show error + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pumpAndSettle(); + expect(find.text(errorTextField), findsOneWidget); + }, + ); + group('Interact with FormBuilderField -', () { + testWidgets( + 'Should show error when FormBuilder is disabled and FormBuilderField is always', (tester) async { - const textFieldName = 'text4'; - const errorTextField = 'error text field'; - final testWidget = FormBuilderTextField( - name: textFieldName, - validator: (value) => errorTextField, - ); - await tester.pumpWidget(buildTestableFieldWidget( - testWidget, - autovalidateMode: AutovalidateMode.always, - )); - await tester.pumpAndSettle(); + const textFieldName = 'text4'; + const errorTextField = 'error text field'; + final testWidget = FormBuilderTextField( + name: textFieldName, + validator: (value) => errorTextField, + autovalidateMode: AutovalidateMode.always, + ); + await tester.pumpWidget( + buildTestableFieldWidget( + testWidget, + autovalidateMode: AutovalidateMode.disabled, + ), + ); + await tester.pumpAndSettle(); - expect(find.text(errorTextField), findsOneWidget); + expect(find.text(errorTextField), findsOneWidget); + }, + ); + // TODO: Enable when solve issue + // https://github.com/flutter/flutter/issues/125766 + // testWidgets( + // 'Should not show error when FormBuilder is always and FormBuilderField is disabled', + // (tester) async { + // const textFieldName = 'text4'; + // const errorTextField = 'error text field'; + // final testWidget = FormBuilderTextField( + // name: textFieldName, + // validator: (value) => errorTextField, + // autovalidateMode: AutovalidateMode.disabled, + // ); + // await tester.pumpWidget(buildTestableFieldWidget( + // testWidget, + // autovalidateMode: AutovalidateMode.always, + // )); + // await tester.pumpAndSettle(); + + // expect(find.text(errorTextField), findsNothing); + // }); }); }); @@ -269,8 +416,9 @@ void main() { expect(formKey.currentState?.isDirty, true); }); - testWidgets('Should dirty when update field value by method', - (tester) async { + testWidgets('Should dirty when update field value by method', ( + tester, + ) async { const textFieldName = 'text'; final testWidget = FormBuilderTextField(name: textFieldName); await tester.pumpWidget(buildTestableFieldWidget(testWidget)); @@ -280,28 +428,34 @@ void main() { expect(formKey.currentState?.isDirty, true); }); - testWidgets('Should dirty when update field with initial value by user', - (tester) async { + testWidgets('Should dirty when update field with initial value by user', ( + tester, + ) async { const textFieldName = 'text'; final testWidget = FormBuilderTextField(name: textFieldName); - await tester.pumpWidget(buildTestableFieldWidget( - testWidget, - initialValue: {textFieldName: 'hi'}, - )); + await tester.pumpWidget( + buildTestableFieldWidget( + testWidget, + initialValue: {textFieldName: 'hi'}, + ), + ); final widgetFinder = find.byWidget(testWidget); await tester.enterText(widgetFinder, 'test'); expect(formKey.currentState?.isDirty, true); }); - testWidgets('Should dirty when update field with initial value by method', - (tester) async { + testWidgets('Should dirty when update field with initial value by method', ( + tester, + ) async { const textFieldName = 'text'; final testWidget = FormBuilderTextField(name: textFieldName); - await tester.pumpWidget(buildTestableFieldWidget( - testWidget, - initialValue: {textFieldName: 'hi'}, - )); + await tester.pumpWidget( + buildTestableFieldWidget( + testWidget, + initialValue: {textFieldName: 'hi'}, + ), + ); formKey.currentState?.patchValue({textFieldName: 'test'}); await tester.pumpAndSettle(); @@ -319,14 +473,17 @@ void main() { expect(formKey.currentState?.isDirty, false); }); - testWidgets('Should not dirty when reset field with initial value', - (tester) async { + testWidgets('Should not dirty when reset field with initial value', ( + tester, + ) async { const textFieldName = 'text'; final testWidget = FormBuilderTextField(name: textFieldName); - await tester.pumpWidget(buildTestableFieldWidget( - testWidget, - initialValue: {textFieldName: 'hi'}, - )); + await tester.pumpWidget( + buildTestableFieldWidget( + testWidget, + initialValue: {textFieldName: 'hi'}, + ), + ); formKey.currentState?.patchValue({textFieldName: 'test'}); await tester.pumpAndSettle(); @@ -371,13 +528,10 @@ void main() { testWidgets('Should reset to initial when call reset', (tester) async { const textFieldName = 'text'; const initialValue = {textFieldName: 'test'}; - final testWidget = FormBuilderTextField( - name: textFieldName, + final testWidget = FormBuilderTextField(name: textFieldName); + await tester.pumpWidget( + buildTestableFieldWidget(testWidget, initialValue: initialValue), ); - await tester.pumpWidget(buildTestableFieldWidget( - testWidget, - initialValue: initialValue, - )); formKey.currentState?.patchValue({textFieldName: 'hello'}); await tester.pumpAndSettle(); @@ -386,26 +540,28 @@ void main() { expect(formKey.currentState?.instantValue, equals(initialValue)); }); testWidgets( - 'Should reset custom error when invalidate field and then reset', - (tester) async { - const textFieldName = 'text'; - const errorTextField = 'error text field'; - final testWidget = FormBuilderTextField(name: textFieldName); - await tester.pumpWidget(buildTestableFieldWidget(testWidget)); - - formKey.currentState?.fields[textFieldName]?.invalidate(errorTextField); - await tester.pumpAndSettle(); - - // Reset custom error - formKey.currentState?.reset(); - await tester.pumpAndSettle(); - expect(find.text(errorTextField), findsNothing); - }); + 'Should reset custom error when invalidate field and then reset', + (tester) async { + const textFieldName = 'text'; + const errorTextField = 'error text field'; + final testWidget = FormBuilderTextField(name: textFieldName); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + formKey.currentState?.fields[textFieldName]?.invalidate(errorTextField); + await tester.pumpAndSettle(); + + // Reset custom error + formKey.currentState?.reset(); + await tester.pumpAndSettle(); + expect(find.text(errorTextField), findsNothing); + }, + ); }); group('errors -', () { - testWidgets('Should get errors when more than one fields are invalid', - (tester) async { + testWidgets('Should get errors when more than one fields are invalid', ( + tester, + ) async { const textFieldName = 'text'; const checkboxName = 'checkbox'; const textFieldError = 'error text'; @@ -419,17 +575,16 @@ void main() { name: checkboxName, validator: (value) => checkboxError, ); - await tester.pumpWidget(buildTestableFieldWidget( - Column(children: [testTextField, testCheckbox]), - autovalidateMode: AutovalidateMode.always, - )); + await tester.pumpWidget( + buildTestableFieldWidget( + Column(children: [testTextField, testCheckbox]), + autovalidateMode: AutovalidateMode.always, + ), + ); expect( formKey.currentState?.errors, - equals({ - textFieldName: textFieldError, - checkboxName: checkboxError, - }), + equals({textFieldName: textFieldError, checkboxName: checkboxError}), ); }); testWidgets('Should get errors when one field are invalid', (tester) async { @@ -439,26 +594,29 @@ void main() { name: textFieldName, validator: (value) => textFieldError, ); - await tester.pumpWidget(buildTestableFieldWidget( - testTextField, - autovalidateMode: AutovalidateMode.always, - )); + await tester.pumpWidget( + buildTestableFieldWidget( + testTextField, + autovalidateMode: AutovalidateMode.always, + ), + ); expect( formKey.currentState?.errors, equals({textFieldName: textFieldError}), ); }); - testWidgets('Should not get errors when all fields are valid', - (tester) async { + testWidgets('Should not get errors when all fields are valid', ( + tester, + ) async { const textFieldName = 'text'; - final testTextField = FormBuilderTextField( - name: textFieldName, + final testTextField = FormBuilderTextField(name: textFieldName); + await tester.pumpWidget( + buildTestableFieldWidget( + testTextField, + autovalidateMode: AutovalidateMode.always, + ), ); - await tester.pumpWidget(buildTestableFieldWidget( - testTextField, - autovalidateMode: AutovalidateMode.always, - )); expect(formKey.currentState?.errors, equals({})); }); @@ -468,10 +626,7 @@ void main() { // simple stateful widget that can hide and show its child with the intent of // disposing it from the tree class _DynamicFormFields extends StatefulWidget { - const _DynamicFormFields({ - required this.name, - this.valueTransformer, - }); + const _DynamicFormFields({required this.name, this.valueTransformer}); final String name; final ValueTransformer? valueTransformer; @@ -496,9 +651,7 @@ class _DynamicFormFieldsState extends State<_DynamicFormFields> { name: widget.name, valueTransformer: widget.valueTransformer, builder: (FormFieldState field) { - return TextField( - onChanged: (value) => field.didChange(value), - ); + return TextField(onChanged: (value) => field.didChange(value)); }, ), ); diff --git a/test_fixes/analysis_options.yaml b/test_fixes/analysis_options.yaml new file mode 100644 index 0000000000..f9b303465f --- /dev/null +++ b/test_fixes/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/test_fixes/fixes_10.0.0.dart b/test_fixes/fixes_10.0.0.dart new file mode 100644 index 0000000000..f23e6f09d6 --- /dev/null +++ b/test_fixes/fixes_10.0.0.dart @@ -0,0 +1,98 @@ +// test_fixes/fix_test.dart +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; + +void main() => runApp(const MyApp()); + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter FormBuilder Example', + debugShowCheckedModeBanner: false, + home: const _ExamplePage(), + ); + } +} + +class _ExamplePage extends StatefulWidget { + const _ExamplePage(); + + @override + State<_ExamplePage> createState() => _ExamplePageState(); +} + +class _ExamplePageState extends State<_ExamplePage> { + final _formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Minimal code example')), + body: Padding( + padding: const EdgeInsets.all(16), + child: FormBuilder( + key: _formKey, + onPopInvoked: (p0) {}, + child: Column( + children: [ + FormBuilderDateTimePicker( + name: 'date', + resetIcon: const Icon(Icons.clear), + ), + FormBuilderChoiceChip( + name: 'choice_chip', + options: const [ + 'Option 1', + 'Option 2', + 'Option 3', + ].map((e) => FormBuilderChipOption(value: e)).toList(), + ), + FormBuilderFilterChip( + maxChips: 2, + decoration: const InputDecoration( + labelText: 'The language of my people', + enabled: false, + ), + name: 'languages_filter', + selectedColor: Colors.red, + options: const [ + FormBuilderChipOption( + value: 'Dart', + avatar: CircleAvatar(child: Text('D')), + ), + FormBuilderChipOption( + value: 'Kotlin', + avatar: CircleAvatar(child: Text('K')), + ), + FormBuilderChipOption( + value: 'Java', + avatar: CircleAvatar(child: Text('J')), + ), + FormBuilderChipOption( + value: 'Swift', + avatar: CircleAvatar(child: Text('S')), + ), + FormBuilderChipOption( + value: 'Objective-C', + avatar: CircleAvatar(child: Text('O')), + ), + ], + ), + const SizedBox(height: 10), + ElevatedButton( + onPressed: () { + _formKey.currentState?.saveAndValidate(); + debugPrint(_formKey.currentState?.value.toString()); + }, + child: const Text('Print'), + ) + ], + ), + ), + ), + ); + } +} diff --git a/test_fixes/fixes_10.0.0.dart.expect b/test_fixes/fixes_10.0.0.dart.expect new file mode 100644 index 0000000000..5bc87a0969 --- /dev/null +++ b/test_fixes/fixes_10.0.0.dart.expect @@ -0,0 +1,95 @@ +// test_fixes/fix_test.dart +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; + +void main() => runApp(const MyApp()); + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter FormBuilder Example', + debugShowCheckedModeBanner: false, + home: const _ExamplePage(), + ); + } +} + +class _ExamplePage extends StatefulWidget { + const _ExamplePage(); + + @override + State<_ExamplePage> createState() => _ExamplePageState(); +} + +class _ExamplePageState extends State<_ExamplePage> { + final _formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Minimal code example')), + body: Padding( + padding: const EdgeInsets.all(16), + child: FormBuilder( + key: _formKey, + child: Column( + children: [ + FormBuilderDateTimePicker( + name: 'date', + ), + FormBuilderChoiceChips( + name: 'choice_chip', + options: const [ + 'Option 1', + 'Option 2', + 'Option 3', + ].map((e) => FormBuilderChipOption(value: e)).toList(), + ), + FormBuilderFilterChips( + decoration: const InputDecoration( + labelText: 'The language of my people', + enabled: false, + ), + name: 'languages_filter', + selectedColor: Colors.red, + options: const [ + FormBuilderChipOption( + value: 'Dart', + avatar: CircleAvatar(child: Text('D')), + ), + FormBuilderChipOption( + value: 'Kotlin', + avatar: CircleAvatar(child: Text('K')), + ), + FormBuilderChipOption( + value: 'Java', + avatar: CircleAvatar(child: Text('J')), + ), + FormBuilderChipOption( + value: 'Swift', + avatar: CircleAvatar(child: Text('S')), + ), + FormBuilderChipOption( + value: 'Objective-C', + avatar: CircleAvatar(child: Text('O')), + ), + ], + ), + const SizedBox(height: 10), + ElevatedButton( + onPressed: () { + _formKey.currentState?.saveAndValidate(); + debugPrint(_formKey.currentState?.value.toString()); + }, + child: const Text('Print'), + ) + ], + ), + ), + ), + ); + } +}