From bbe10d8bc7153dbdd054c9caf112fca7797baee1 Mon Sep 17 00:00:00 2001 From: cedvdb Date: Fri, 2 Feb 2024 01:26:43 +0100 Subject: [PATCH 01/25] fix merge issue --- .../widgets/country_selector/search_box.dart | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/lib/src/widgets/country_selector/search_box.dart b/lib/src/widgets/country_selector/search_box.dart index d0b1bc08..194748c0 100644 --- a/lib/src/widgets/country_selector/search_box.dart +++ b/lib/src/widgets/country_selector/search_box.dart @@ -22,33 +22,6 @@ class SearchBox extends StatefulWidget { State createState() => _SearchBoxState(); } -class _SearchBoxState extends State { - String _previousValue = ''; - - @override - void initState() { - super.initState(); - } - - void handleChange(e) { - widget.onChanged(e); - - // detect length difference - final diff = e.length - _previousValue.length; - if (diff > 3) { - // more than 3 characters added, probably a paste / autofill of country name - widget.onSubmitted(); - } - - setState(() { - _previousValue = e; - }); - } - - @override - State createState() => _SearchBoxState(); -} - class _SearchBoxState extends State { String _previousValue = ''; From 66a45def0eb1f541fefd69be56cfba66266fbb54 Mon Sep 17 00:00:00 2001 From: cedvdb Date: Fri, 2 Feb 2024 01:30:47 +0100 Subject: [PATCH 02/25] remove generated files from git --- .gitignore | 6 ++++- .../flutter/generated_plugin_registrant.cc | 11 --------- .../flutter/generated_plugin_registrant.h | 15 ------------ .../windows/flutter/generated_plugins.cmake | 23 ------------------- 4 files changed, 5 insertions(+), 50 deletions(-) delete mode 100644 example/windows/flutter/generated_plugin_registrant.cc delete mode 100644 example/windows/flutter/generated_plugin_registrant.h delete mode 100644 example/windows/flutter/generated_plugins.cmake diff --git a/.gitignore b/.gitignore index 5bce2af0..2aad77e9 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,8 @@ .dart_tool/ .packages build/ -example/build \ No newline at end of file +example/build +**/generated_plugin_registrant.dart +**/generated_plugin_registrant.cc +**/generated_plugin_registrant.h +**/generated_plugin_registrant.cmake diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 8b6d4680..00000000 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void RegisterPlugins(flutter::PluginRegistry* registry) { -} diff --git a/example/windows/flutter/generated_plugin_registrant.h b/example/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d85..00000000 --- a/example/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake deleted file mode 100644 index b93c4c30..00000000 --- a/example/windows/flutter/generated_plugins.cmake +++ /dev/null @@ -1,23 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) From b9c593a8f1ac0178dd14998d6dd53f67d4895697 Mon Sep 17 00:00:00 2001 From: cedvdb Date: Fri, 2 Feb 2024 10:19:49 +0100 Subject: [PATCH 03/25] saving --- lib/phone_form_field.dart | 14 +- .../country_chip.dart} | 15 +- lib/src/country/localize_country.dart | 249 ++++++++++++++++++ lib/src/country/localized_country.dart | 45 ++++ .../country_finder.dart | 60 ++--- .../country_list_view.dart} | 18 +- .../country_selector.dart | 72 ++--- .../country_selector_navigator.dart | 34 +-- .../country_selector_page.dart | 29 +- .../localized_country_registry.dart | 9 +- .../search_box.dart | 0 .../{controllers => }/phone_controller.dart | 0 lib/src/{widgets => }/phone_field.dart | 4 +- .../phone_field_controller.dart | 0 lib/src/{widgets => }/phone_field_state.dart | 4 +- lib/src/{widgets => }/phone_form_field.dart | 15 +- .../{widgets => }/phone_form_field_state.dart | 3 +- .../allowed_characters.dart} | 2 +- lib/src/widgets/country_selector/country.dart | 35 --- test/_country_selector_test.dart | 2 +- test/phone_form_field_test.dart | 16 +- 21 files changed, 444 insertions(+), 182 deletions(-) rename lib/src/{widgets/country_code_chip.dart => country/country_chip.dart} (84%) create mode 100644 lib/src/country/localize_country.dart create mode 100644 lib/src/country/localized_country.dart rename lib/src/{widgets/country_selector => country_selection}/country_finder.dart (54%) rename lib/src/{widgets/country_selector/country_list.dart => country_selection/country_list_view.dart} (87%) rename lib/src/{widgets/country_selector => country_selection}/country_selector.dart (71%) rename lib/src/{widgets/country_selector => country_selection}/country_selector_navigator.dart (92%) rename lib/src/{widgets/country_selector => country_selection}/country_selector_page.dart (83%) rename lib/src/{widgets/country_selector => country_selection}/localized_country_registry.dart (97%) rename lib/src/{widgets/country_selector => country_selection}/search_box.dart (100%) rename lib/src/{controllers => }/phone_controller.dart (100%) rename lib/src/{widgets => }/phone_field.dart (96%) rename lib/src/{controllers => }/phone_field_controller.dart (100%) rename lib/src/{widgets => }/phone_field_state.dart (97%) rename lib/src/{widgets => }/phone_form_field.dart (95%) rename lib/src/{widgets => }/phone_form_field_state.dart (97%) rename lib/src/{constants/patterns.dart => validation/allowed_characters.dart} (90%) delete mode 100644 lib/src/widgets/country_selector/country.dart diff --git a/lib/phone_form_field.dart b/lib/phone_form_field.dart index d685b512..64653355 100644 --- a/lib/phone_form_field.dart +++ b/lib/phone_form_field.dart @@ -1,17 +1,17 @@ library phone_number_input; -export 'src/widgets/phone_form_field.dart'; -export 'src/widgets/country_selector/country_selector_navigator.dart'; -export 'src/widgets/country_selector/country_selector.dart'; -export 'src/widgets/country_code_chip.dart'; +export 'src/phone_form_field.dart'; +export 'src/country_selection/country_selector_navigator.dart'; +export 'src/country_selection/country_selector.dart'; +export 'src/country/country_chip.dart'; export 'src/validation/phone_validator.dart'; export 'l10n/generated/phone_field_localization.dart'; -export 'src/controllers/phone_controller.dart'; -export 'src/widgets/country_selector/country.dart'; -export 'src/widgets/country_selector/localized_country_registry.dart'; +export 'src/phone_controller.dart'; +export 'src/country/localized_country.dart'; +export 'src/country_selection/localized_country_registry.dart'; export 'package:phone_numbers_parser/phone_numbers_parser.dart' show PhoneNumber, PhoneNumberType, IsoCode; diff --git a/lib/src/widgets/country_code_chip.dart b/lib/src/country/country_chip.dart similarity index 84% rename from lib/src/widgets/country_code_chip.dart rename to lib/src/country/country_chip.dart index af7f1638..fdcec7d5 100644 --- a/lib/src/widgets/country_code_chip.dart +++ b/lib/src/country/country_chip.dart @@ -2,10 +2,10 @@ import 'package:circle_flags/circle_flags.dart'; import 'package:flutter/material.dart'; import 'package:phone_numbers_parser/phone_numbers_parser.dart'; -import 'country_selector/country.dart'; +import 'localized_country.dart'; -class CountryCodeChip extends StatelessWidget { - final Country country; +class CountryChip extends StatelessWidget { + final IsoCode isoCode; final bool showFlag; final bool showDialCode; final TextStyle textStyle; @@ -15,9 +15,9 @@ class CountryCodeChip extends StatelessWidget { final bool showIsoCode; final bool enabled; - CountryCodeChip({ + const CountryChip({ super.key, - required IsoCode isoCode, + required this.isoCode, this.textStyle = const TextStyle(), this.showFlag = true, this.showDialCode = true, @@ -26,10 +26,11 @@ class CountryCodeChip extends StatelessWidget { this.textDirection, this.showIsoCode = false, this.enabled = true, - }) : country = Country(isoCode, ''); + }); @override Widget build(BuildContext context) { + final country = LocalizedCountry.fromContext(context, isoCode); return Row( mainAxisSize: MainAxisSize.min, children: [ @@ -51,7 +52,7 @@ class CountryCodeChip extends StatelessWidget { ], if (showDialCode) Text( - country.displayCountryCode, + country.formattedCountryDialingCode, style: textStyle.copyWith( color: enabled ? null : Theme.of(context).disabledColor, ), diff --git a/lib/src/country/localize_country.dart b/lib/src/country/localize_country.dart new file mode 100644 index 00000000..a8061be0 --- /dev/null +++ b/lib/src/country/localize_country.dart @@ -0,0 +1,249 @@ +import 'package:phone_form_field/l10n/generated/phone_field_localization.dart'; +import 'package:phone_numbers_parser/phone_numbers_parser.dart'; + +extension DynamicLocalization on PhoneFieldLocalization { + countryName(IsoCode isoCode) { + return switch (isoCode) { + IsoCode.AC => ac_, + IsoCode.AD => ad_, + IsoCode.AE => ae_, + IsoCode.AF => af_, + IsoCode.AG => ag_, + IsoCode.AI => ai_, + IsoCode.AL => al_, + IsoCode.AM => am_, + IsoCode.AO => ao_, + IsoCode.AR => ar_, + IsoCode.AS => as_, + IsoCode.AT => at_, + IsoCode.AU => au_, + IsoCode.AW => aw_, + IsoCode.AX => ax_, + IsoCode.AZ => az_, + IsoCode.BA => ba_, + IsoCode.BB => bb_, + IsoCode.BD => bd_, + IsoCode.BE => be_, + IsoCode.BF => bf_, + IsoCode.BG => bg_, + IsoCode.BH => bh_, + IsoCode.BI => bi_, + IsoCode.BJ => bj_, + IsoCode.BL => bl_, + IsoCode.BM => bm_, + IsoCode.BN => bn_, + IsoCode.BO => bo_, + IsoCode.BQ => bq_, + IsoCode.BR => br_, + IsoCode.BS => bs_, + IsoCode.BT => bt_, + IsoCode.BW => bw_, + IsoCode.BY => by_, + IsoCode.BZ => bz_, + IsoCode.CA => ca_, + IsoCode.CC => cc_, + IsoCode.CD => cd_, + IsoCode.CF => cf_, + IsoCode.CG => cg_, + IsoCode.CH => ch_, + IsoCode.CI => ci_, + IsoCode.CK => ck_, + IsoCode.CL => cl_, + IsoCode.CM => cm_, + IsoCode.CN => cn_, + IsoCode.CO => co_, + IsoCode.CR => cr_, + IsoCode.CU => cu_, + IsoCode.CV => cv_, + IsoCode.CX => cx_, + IsoCode.CY => cy_, + IsoCode.CZ => cz_, + IsoCode.DE => de_, + IsoCode.DJ => dj_, + IsoCode.DK => dk_, + IsoCode.DM => dm_, + IsoCode.DO => do_, + IsoCode.DZ => dz_, + IsoCode.EC => ec_, + IsoCode.EE => ee_, + IsoCode.EG => eg_, + IsoCode.ER => er_, + IsoCode.ES => es_, + IsoCode.ET => et_, + IsoCode.FI => fi_, + IsoCode.FJ => fj_, + IsoCode.FK => fk_, + IsoCode.FM => fm_, + IsoCode.FO => fo_, + IsoCode.FR => fr_, + IsoCode.GA => ga_, + IsoCode.GB => gb_, + IsoCode.GD => gd_, + IsoCode.GE => ge_, + IsoCode.GF => gf_, + IsoCode.GG => gg_, + IsoCode.GH => gh_, + IsoCode.GI => gi_, + IsoCode.GL => gl_, + IsoCode.GM => gm_, + IsoCode.GN => gn_, + IsoCode.GP => gp_, + IsoCode.GQ => gq_, + IsoCode.GR => gr_, + IsoCode.GT => gt_, + IsoCode.GU => gu_, + IsoCode.GW => gw_, + IsoCode.GY => gy_, + IsoCode.HK => hk_, + IsoCode.HN => hn_, + IsoCode.HR => hr_, + IsoCode.HT => ht_, + IsoCode.HU => hu_, + IsoCode.ID => id_, + IsoCode.IE => ie_, + IsoCode.IL => il_, + IsoCode.IM => im_, + IsoCode.IN => in_, + IsoCode.IO => io_, + IsoCode.IQ => iq_, + IsoCode.IR => ir_, + IsoCode.IS => is_, + IsoCode.IT => it_, + IsoCode.JE => je_, + IsoCode.JM => jm_, + IsoCode.JO => jo_, + IsoCode.JP => jp_, + IsoCode.KE => ke_, + IsoCode.KG => kg_, + IsoCode.KH => kh_, + IsoCode.KI => ki_, + IsoCode.KM => km_, + IsoCode.KN => kn_, + IsoCode.KP => kp_, + IsoCode.KR => kr_, + IsoCode.KW => kw_, + IsoCode.KY => ky_, + IsoCode.KZ => kz_, + IsoCode.LA => la_, + IsoCode.LB => lb_, + IsoCode.LC => lc_, + IsoCode.LI => li_, + IsoCode.LK => lk_, + IsoCode.LR => lr_, + IsoCode.LS => ls_, + IsoCode.LT => lt_, + IsoCode.LU => lu_, + IsoCode.LV => lv_, + IsoCode.LY => ly_, + IsoCode.MA => ma_, + IsoCode.MC => mc_, + IsoCode.MD => md_, + IsoCode.ME => me_, + IsoCode.MF => mf_, + IsoCode.MG => mg_, + IsoCode.MH => mh_, + IsoCode.MK => mk_, + IsoCode.ML => ml_, + IsoCode.MM => mm_, + IsoCode.MN => mn_, + IsoCode.MO => mo_, + IsoCode.MP => mp_, + IsoCode.MQ => mq_, + IsoCode.MR => mr_, + IsoCode.MS => ms_, + IsoCode.MT => mt_, + IsoCode.MU => mu_, + IsoCode.MV => mv_, + IsoCode.MW => mw_, + IsoCode.MX => mx_, + IsoCode.MY => my_, + IsoCode.MZ => mz_, + IsoCode.NA => na_, + IsoCode.NC => nc_, + IsoCode.NE => ne_, + IsoCode.NF => nf_, + IsoCode.NG => ng_, + IsoCode.NI => ni_, + IsoCode.NL => nl_, + IsoCode.NO => no_, + IsoCode.NP => np_, + IsoCode.NR => nr_, + IsoCode.NU => nu_, + IsoCode.NZ => nz_, + IsoCode.OM => om_, + IsoCode.PA => pa_, + IsoCode.PE => pe_, + IsoCode.PF => pf_, + IsoCode.PG => pg_, + IsoCode.PH => ph_, + IsoCode.PK => pk_, + IsoCode.PL => pl_, + IsoCode.PM => pm_, + IsoCode.PR => pr_, + IsoCode.PS => ps_, + IsoCode.PT => pt_, + IsoCode.PW => pw_, + IsoCode.PY => py_, + IsoCode.QA => qa_, + IsoCode.RE => re_, + IsoCode.RO => ro_, + IsoCode.RS => rs_, + IsoCode.RU => ru_, + IsoCode.RW => rw_, + IsoCode.SA => sa_, + IsoCode.SB => sb_, + IsoCode.SC => sc_, + IsoCode.SD => sd_, + IsoCode.SE => se_, + IsoCode.SG => sg_, + IsoCode.SI => si_, + IsoCode.SK => sk_, + IsoCode.SL => sl_, + IsoCode.SM => sm_, + IsoCode.SN => sn_, + IsoCode.SO => so_, + IsoCode.SR => sr_, + IsoCode.SS => ss_, + IsoCode.ST => st_, + IsoCode.SV => sv_, + IsoCode.SY => sy_, + IsoCode.SZ => sz_, + IsoCode.TA => ta_, + IsoCode.TC => tc_, + IsoCode.TD => td_, + IsoCode.TG => tg_, + IsoCode.TH => th_, + IsoCode.TJ => tj_, + IsoCode.TK => tk_, + IsoCode.TL => tl_, + IsoCode.TM => tm_, + IsoCode.TN => tn_, + IsoCode.TO => to_, + IsoCode.TR => tr_, + IsoCode.TT => tt_, + IsoCode.TV => tv_, + IsoCode.TW => tw_, + IsoCode.TZ => tz_, + IsoCode.UA => ua_, + IsoCode.UG => ug_, + IsoCode.US => us_, + IsoCode.UY => uy_, + IsoCode.UZ => uz_, + IsoCode.VA => va_, + IsoCode.VC => vc_, + IsoCode.VE => ve_, + IsoCode.VG => vg_, + IsoCode.VI => vi_, + IsoCode.VN => vn_, + IsoCode.VU => vu_, + IsoCode.WF => wf_, + IsoCode.WS => ws_, + IsoCode.YE => ye_, + IsoCode.YT => yt_, + IsoCode.ZA => za_, + IsoCode.ZM => zm_, + IsoCode.ZW => zw_, + _ => '?' + }; + } +} diff --git a/lib/src/country/localized_country.dart b/lib/src/country/localized_country.dart new file mode 100644 index 00000000..33f47d6c --- /dev/null +++ b/lib/src/country/localized_country.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:phone_form_field/l10n/generated/phone_field_localization_en.dart'; +import 'package:phone_form_field/phone_form_field.dart'; +import 'package:phone_form_field/src/country/localize_country.dart'; +import 'package:phone_numbers_parser/metadata.dart'; +import 'package:phone_numbers_parser/phone_numbers_parser.dart'; + +/// Country regroup informations for displaying a list of countries +class LocalizedCountry { + /// Country alpha-2 iso code + final IsoCode isoCode; + + /// localized name of the country + final String name; + + /// country dialing code to call them internationally + final String countryDialingCode; + + /// returns "+ [countryDialingCode]" + String get formattedCountryDialingCode => '+ $countryDialingCode'; + + factory LocalizedCountry.fromContext(BuildContext context, IsoCode isoCode) { + final localization = + PhoneFieldLocalization.of(context) ?? PhoneFieldLocalizationEn(); + return LocalizedCountry(isoCode, localization.countryName(isoCode)); + } + + LocalizedCountry(this.isoCode, this.name) + : countryDialingCode = metadataByIsoCode[isoCode]?.countryCode ?? ''; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is LocalizedCountry && + runtimeType == other.runtimeType && + isoCode == other.isoCode; + + @override + int get hashCode => isoCode.hashCode; + + @override + String toString() { + return 'Country{isoCode: $isoCode}'; + } +} diff --git a/lib/src/widgets/country_selector/country_finder.dart b/lib/src/country_selection/country_finder.dart similarity index 54% rename from lib/src/widgets/country_selector/country_finder.dart rename to lib/src/country_selection/country_finder.dart index 00788f8b..8cc35250 100644 --- a/lib/src/widgets/country_selector/country_finder.dart +++ b/lib/src/country_selection/country_finder.dart @@ -4,56 +4,46 @@ import 'package:diacritic/diacritic.dart'; import 'package:phone_form_field/phone_form_field.dart'; class CountryFinder { - late final List _allCountries; - late List _filteredCountries; - List get filteredCountries => _filteredCountries; - - bool get isNotEmpty => _filteredCountries.isNotEmpty; - String _searchedText = ''; - String get searchedText => _searchedText; - - CountryFinder(List allCountries, {bool sort = true}) { - _allCountries = [...allCountries]; - if (sort) { - _allCountries.sort((a, b) => a.name.compareTo(b.name)); - } - _filteredCountries = [..._allCountries]; - } - - // filter a - void filter(String txt) { - if (txt == _searchedText) { - return; - } - _searchedText = txt; + List whereText({ + required String text, + required List countries, + }) { // reset search - if (txt.isEmpty) { - _filteredCountries = [..._allCountries]; + if (text.isEmpty) { + return countries; } // if the txt is a number we check the country code instead - final asInt = int.tryParse(txt); + final asInt = int.tryParse(text); final isInt = asInt != null; if (isInt) { // toString to remove any + in front if its an int - _filterByCountryCallingCode(txt); + return _filterByCountryCallingCode( + countryCallingCode: text, countries: countries); } else { - _filterByName(txt); + return _filterByName(searchTxt: text, countries: countries); } } - void _filterByCountryCallingCode(String countryCallingCode) { - int getSortPoint(Country country) => - country.countryCode == countryCallingCode ? 1 : 0; + List _filterByCountryCallingCode({ + required String countryCallingCode, + required List countries, + }) { + int getSortPoint(LocalizedCountry country) => + country.countryDialingCode == countryCallingCode ? 1 : 0; - _filteredCountries = _allCountries - .where((country) => country.countryCode.contains(countryCallingCode)) + return countries + .where((country) => + country.countryDialingCode.contains(countryCallingCode)) .toList() // puts the closest match at the top ..sort((a, b) => getSortPoint(b) - getSortPoint(a)); } - void _filterByName(String searchTxt) { + List _filterByName({ + required String searchTxt, + required List countries, + }) { searchTxt = removeDiacritics(searchTxt.toLowerCase()); // since we keep countries that contain the searched text, // we need to put the countries that start with that text in front. @@ -63,14 +53,14 @@ class CountryFinder { return isStartOfString ? 1 : 0; } - int compareCountries(Country a, Country b) { + int compareCountries(LocalizedCountry a, LocalizedCountry b) { final sortPoint = getSortPoint(b.name, b.isoCode) - getSortPoint(a.name, a.isoCode); // sort alphabetically when comparison with search term get same result return sortPoint == 0 ? a.name.compareTo(b.name) : sortPoint; } - _filteredCountries = _allCountries.where((country) { + return countries.where((country) { final countryName = removeDiacritics(country.name.toLowerCase()); return countryName.contains(searchTxt) || country.isoCode.name.toLowerCase().contains(searchTxt); diff --git a/lib/src/widgets/country_selector/country_list.dart b/lib/src/country_selection/country_list_view.dart similarity index 87% rename from lib/src/widgets/country_selector/country_list.dart rename to lib/src/country_selection/country_list_view.dart index 613ccff9..a3b8e8ba 100644 --- a/lib/src/widgets/country_selector/country_list.dart +++ b/lib/src/country_selection/country_list_view.dart @@ -1,19 +1,19 @@ import 'package:circle_flags/circle_flags.dart'; import 'package:flutter/material.dart'; -import '../../../l10n/generated/phone_field_localization.dart'; -import 'country.dart'; +import '../../l10n/generated/phone_field_localization.dart'; +import '../country/localized_country.dart'; -class CountryList extends StatelessWidget { +class CountryListView extends StatelessWidget { /// Callback function triggered when user select a country - final Function(Country) onTap; + final Function(LocalizedCountry) onTap; /// List of countries to display - final List countries; + final List countries; final double flagSize; /// list of favorite countries to display at the top - final List favorites; + final List favorites; /// proxy to the ListView.builder controller (ie: [ScrollView.controller]) final ScrollController? scrollController; @@ -26,13 +26,13 @@ class CountryList extends StatelessWidget { final String? noResultMessage; - late final List _allListElement; + late final List _allListElement; final TextStyle? subtitleStyle; final TextStyle? titleStyle; final FlagCache? flagCache; - CountryList({ + CountryListView({ super.key, required this.countries, required this.favorites, @@ -95,7 +95,7 @@ class CountryList extends StatelessWidget { ? Align( alignment: AlignmentDirectional.centerStart, child: Text( - country.displayCountryCode, + country.formattedCountryDialingCode, textDirection: TextDirection.ltr, textAlign: TextAlign.start, style: subtitleStyle, diff --git a/lib/src/widgets/country_selector/country_selector.dart b/lib/src/country_selection/country_selector.dart similarity index 71% rename from lib/src/widgets/country_selector/country_selector.dart rename to lib/src/country_selection/country_selector.dart index 4771b89b..1510cd73 100644 --- a/lib/src/widgets/country_selector/country_selector.dart +++ b/lib/src/country_selection/country_selector.dart @@ -3,22 +3,27 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:phone_form_field/l10n/generated/phone_field_localization.dart'; import 'package:phone_form_field/l10n/generated/phone_field_localization_en.dart'; -import 'package:phone_form_field/src/widgets/country_selector/localized_country_registry.dart'; +import 'package:phone_form_field/src/country/localize_country.dart'; import 'package:phone_numbers_parser/phone_numbers_parser.dart'; +import '../country/localized_country.dart'; import 'country_finder.dart'; -import 'country.dart'; -import 'country_list.dart'; +import 'country_list_view.dart'; import 'search_box.dart'; class CountrySelector extends StatefulWidget { /// List of countries to display in the selector /// Value optional in constructor. /// when omitted, the full country list is displayed - final List? countries; + final List countries; + + /// Determine the countries to be displayed on top of the list + /// Check [addFavoritesSeparator] property to enable/disable adding a + /// list divider between favorites and others defaults countries + final List favoriteCountries; /// Callback triggered when user select a country - final ValueChanged onCountrySelected; + final ValueChanged onCountrySelected; /// ListView.builder scroll controller (ie: [ScrollView.controller]) final ScrollController? scrollController; @@ -26,11 +31,6 @@ class CountrySelector extends StatefulWidget { /// The [ScrollPhysics] of the Country List final ScrollPhysics? scrollPhysics; - /// Determine the countries to be displayed on top of the list - /// Check [addFavoritesSeparator] property to enable/disable adding a - /// list divider between favorites and others defaults countries - final List favoriteCountries; - /// Whether to add a list divider between favorites & defaults /// countries. final bool addFavoritesSeparator; @@ -72,7 +72,7 @@ class CountrySelector extends StatefulWidget { this.showCountryCode = false, this.noResultMessage, this.favoriteCountries = const [], - this.countries, + this.countries = IsoCode.values, this.searchAutofocus = kIsWeb, this.subtitleStyle, this.titleStyle, @@ -87,35 +87,47 @@ class CountrySelector extends StatefulWidget { } class CountrySelectorState extends State { - late CountryFinder _countryFinder; - late CountryFinder _favoriteCountryFinder; + final countryFinder = CountryFinder(); + List _localizedCountries = []; + List _filteredLocalizedCountries = []; + List _favoriteLocalizedCountries = []; + List _filteredFavoriteLocalizedCountries = []; @override didChangeDependencies() { super.didChangeDependencies(); + _localizedCountries = _buildLocalizedCountryList(context, widget.countries); + _favoriteLocalizedCountries = + _buildLocalizedCountryList(context, widget.favoriteCountries); + _filteredLocalizedCountries = _localizedCountries; + } + + _buildLocalizedCountryList(BuildContext context, List isoCodes) { final localization = PhoneFieldLocalization.of(context) ?? PhoneFieldLocalizationEn(); - final isoCodes = widget.countries ?? IsoCode.values; - final countryRegistry = LocalizedCountryRegistry.cached(localization); - final notFavoriteCountries = - countryRegistry.whereIsoIn(isoCodes, omit: widget.favoriteCountries); - final favoriteCountries = - countryRegistry.whereIsoIn(widget.favoriteCountries); - _countryFinder = CountryFinder(notFavoriteCountries); - _favoriteCountryFinder = CountryFinder(favoriteCountries, sort: false); + return isoCodes + .map((isoCode) => + LocalizedCountry(isoCode, localization.countryName(isoCode))) + .toList(); } _onSearch(String searchedText) { - _countryFinder.filter(searchedText); - _favoriteCountryFinder.filter(searchedText); + _filteredLocalizedCountries = countryFinder.whereText( + text: searchedText, + countries: _localizedCountries, + ); + _filteredFavoriteLocalizedCountries = countryFinder.whereText( + text: searchedText, + countries: _favoriteLocalizedCountries, + ); setState(() {}); } onSubmitted() { - if (_favoriteCountryFinder.filteredCountries.isNotEmpty) { - widget.onCountrySelected(_favoriteCountryFinder.filteredCountries.first); - } else if (_countryFinder.filteredCountries.isNotEmpty) { - widget.onCountrySelected(_countryFinder.filteredCountries.first); + if (_filteredFavoriteLocalizedCountries.isNotEmpty) { + widget.onCountrySelected(_filteredFavoriteLocalizedCountries.first); + } else if (_filteredLocalizedCountries.isNotEmpty) { + widget.onCountrySelected(_filteredLocalizedCountries.first); } } @@ -147,9 +159,9 @@ class CountrySelectorState extends State { const SizedBox(height: 16), const Divider(height: 0, thickness: 1.2), Flexible( - child: CountryList( - favorites: _favoriteCountryFinder.filteredCountries, - countries: _countryFinder.filteredCountries, + child: CountryListView( + favorites: _filteredFavoriteLocalizedCountries, + countries: _filteredLocalizedCountries, showDialCode: widget.showCountryCode, onTap: widget.onCountrySelected, flagSize: widget.flagSize, diff --git a/lib/src/widgets/country_selector/country_selector_navigator.dart b/lib/src/country_selection/country_selector_navigator.dart similarity index 92% rename from lib/src/widgets/country_selector/country_selector_navigator.dart rename to lib/src/country_selection/country_selector_navigator.dart index c7d313f8..02fdecd8 100644 --- a/lib/src/widgets/country_selector/country_selector_navigator.dart +++ b/lib/src/country_selection/country_selector_navigator.dart @@ -2,7 +2,7 @@ import 'package:circle_flags/circle_flags.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:phone_form_field/phone_form_field.dart'; -import 'package:phone_form_field/src/widgets/country_selector/country_selector_page.dart'; +import 'package:phone_form_field/src/country_selection/country_selector_page.dart'; abstract class CountrySelectorNavigator { final List? countries; @@ -39,17 +39,17 @@ abstract class CountrySelectorNavigator { this.useRootNavigator = true, }); - Future navigate(BuildContext context, FlagCache flagCache); + Future navigate(BuildContext context, FlagCache flagCache); CountrySelector _getCountrySelector({ - required ValueChanged onCountrySelected, + required ValueChanged onCountrySelected, required FlagCache flagCache, ScrollController? scrollController, }) { return CountrySelector( - countries: countries, - onCountrySelected: onCountrySelected, + countries: countries ?? IsoCode.values, favoriteCountries: favorites ?? [], + onCountrySelected: onCountrySelected, addFavoritesSeparator: addSeparator, showCountryCode: showCountryCode, noResultMessage: noResultMessage, @@ -179,7 +179,8 @@ class DialogNavigator extends CountrySelectorNavigator { }); @override - Future navigate(BuildContext context, FlagCache flagCache) { + Future navigate( + BuildContext context, FlagCache flagCache) { return showDialog( context: context, builder: (_) => Dialog( @@ -218,7 +219,7 @@ class SearchDelegateNavigator extends CountrySelectorNavigator { final ThemeData? appBarTheme; CountrySelectorSearchDelegate _getCountrySelectorSearchDelegate({ - required ValueChanged onCountrySelected, + required ValueChanged onCountrySelected, required FlagCache flagCache, ScrollController? scrollController, }) { @@ -226,7 +227,7 @@ class SearchDelegateNavigator extends CountrySelectorNavigator { onCountrySelected: onCountrySelected, scrollController: scrollController, addFavoritesSeparator: addSeparator, - countries: countries, + countries: countries ?? IsoCode.values, favoriteCountries: favorites ?? [], noResultMessage: noResultMessage, searchAutofocus: searchAutofocus, @@ -239,7 +240,8 @@ class SearchDelegateNavigator extends CountrySelectorNavigator { } @override - Future navigate(BuildContext context, FlagCache flagCache) { + Future navigate( + BuildContext context, FlagCache flagCache) { return showSearch( context: context, delegate: _getCountrySelectorSearchDelegate( @@ -268,8 +270,9 @@ class BottomSheetNavigator extends CountrySelectorNavigator { }); @override - Future navigate(BuildContext context, FlagCache flagCache) { - Country? selected; + Future navigate( + BuildContext context, FlagCache flagCache) { + LocalizedCountry? selected; final ctrl = showBottomSheet( context: context, builder: (_) => MediaQuery( @@ -310,11 +313,11 @@ class ModalBottomSheetNavigator extends CountrySelectorNavigator { }); @override - Future navigate( + Future navigate( BuildContext context, FlagCache flagCache, ) { - return showModalBottomSheet( + return showModalBottomSheet( context: context, builder: (_) => SizedBox( height: height ?? MediaQuery.of(context).size.height - 90, @@ -357,14 +360,15 @@ class DraggableModalBottomSheetNavigator extends CountrySelectorNavigator { }); @override - Future navigate(BuildContext context, FlagCache flagCache) { + Future navigate( + BuildContext context, FlagCache flagCache) { final effectiveBorderRadius = borderRadius ?? const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), ); - return showModalBottomSheet( + return showModalBottomSheet( context: context, shape: RoundedRectangleBorder( borderRadius: effectiveBorderRadius, diff --git a/lib/src/widgets/country_selector/country_selector_page.dart b/lib/src/country_selection/country_selector_page.dart similarity index 83% rename from lib/src/widgets/country_selector/country_selector_page.dart rename to lib/src/country_selection/country_selector_page.dart index a51769c5..790e1bc5 100644 --- a/lib/src/widgets/country_selector/country_selector_page.dart +++ b/lib/src/country_selection/country_selector_page.dart @@ -3,14 +3,14 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:phone_form_field/l10n/generated/phone_field_localization.dart'; import 'package:phone_form_field/l10n/generated/phone_field_localization_en.dart'; -import 'package:phone_form_field/src/widgets/country_selector/localized_country_registry.dart'; +import 'package:phone_form_field/src/country_selection/localized_country_registry.dart'; import 'package:phone_numbers_parser/phone_numbers_parser.dart'; import 'country_finder.dart'; -import 'country.dart'; -import 'country_list.dart'; +import '../country/localized_country.dart'; +import 'country_list_view.dart'; -class CountrySelectorSearchDelegate extends SearchDelegate { +class CountrySelectorSearchDelegate extends SearchDelegate { late CountryFinder _countryFinder; late CountryFinder _favoriteCountryFinder; @@ -20,7 +20,7 @@ class CountrySelectorSearchDelegate extends SearchDelegate { final List countriesIso; /// Callback triggered when user select a country - final ValueChanged onCountrySelected; + final ValueChanged onCountrySelected; /// ListView.builder scroll controller (ie: [ScrollView.controller]) final ScrollController? scrollController; @@ -48,8 +48,6 @@ class CountrySelectorSearchDelegate extends SearchDelegate { final bool searchAutofocus; final double flagSize; - LocalizedCountryRegistry? _localizedCountryRegistry; - /// Override default title TextStyle final TextStyle? titleStyle; @@ -71,13 +69,13 @@ class CountrySelectorSearchDelegate extends SearchDelegate { this.showCountryCode = false, this.noResultMessage, List favoriteCountries = const [], - List? countries, + List countries = IsoCode.values, this.searchAutofocus = kIsWeb, this.flagSize = 40, this.titleStyle, this.subtitleStyle, this.customAppBarTheme, - }) : countriesIso = countries ?? IsoCode.values, + }) : countriesIso = countries, favoriteCountriesIso = favoriteCountries; @override @@ -93,12 +91,7 @@ class CountrySelectorSearchDelegate extends SearchDelegate { void _initIfRequired(BuildContext context) { final localization = PhoneFieldLocalization.of(context) ?? PhoneFieldLocalizationEn(); - final countryRegistry = LocalizedCountryRegistry.cached(localization); - // if localization has not changed no need to do anything - if (countryRegistry == _localizedCountryRegistry) { - return; - } - _localizedCountryRegistry = countryRegistry; + final notFavoriteCountries = countryRegistry.whereIsoIn( countriesIso, omit: favoriteCountriesIso, @@ -109,8 +102,8 @@ class CountrySelectorSearchDelegate extends SearchDelegate { } void _updateList() { - _countryFinder.filter(query); - _favoriteCountryFinder.filter(query); + _countryFinder.whereText(text: query, countries: ); + _favoriteCountryFinder.whereText(query); } @override @@ -125,7 +118,7 @@ class CountrySelectorSearchDelegate extends SearchDelegate { _initIfRequired(context); _updateList(); - return CountryList( + return CountryListView( favorites: _favoriteCountryFinder.filteredCountries, countries: _countryFinder.filteredCountries, showDialCode: showCountryCode, diff --git a/lib/src/widgets/country_selector/localized_country_registry.dart b/lib/src/country_selection/localized_country_registry.dart similarity index 97% rename from lib/src/widgets/country_selector/localized_country_registry.dart rename to lib/src/country_selection/localized_country_registry.dart index 3418e157..78f638e5 100644 --- a/lib/src/widgets/country_selector/localized_country_registry.dart +++ b/lib/src/country_selection/localized_country_registry.dart @@ -12,10 +12,11 @@ class LocalizedCountryRegistry { static LocalizedCountryRegistry? _instance; - late final Map _localizedCountries = Map.fromIterable( + late final Map _localizedCountries = + Map.fromIterable( // remove iso codes that do not have a traduction yet.. IsoCode.values.where((iso) => _names.containsKey(iso)), - value: (isoCode) => Country(isoCode, _names[isoCode]!), + value: (isoCode) => LocalizedCountry(isoCode, _names[isoCode]!), ); LocalizedCountryRegistry._(this._localization); @@ -28,10 +29,10 @@ class LocalizedCountryRegistry { return LocalizedCountryRegistry._(localization); } - Country? find(IsoCode isoCode) => _localizedCountries[isoCode]; + LocalizedCountry? find(IsoCode isoCode) => _localizedCountries[isoCode]; /// gets localized countries from isocodes - List whereIsoIn( + List whereIsoIn( List isoCodes, { List omit = const [], }) { diff --git a/lib/src/widgets/country_selector/search_box.dart b/lib/src/country_selection/search_box.dart similarity index 100% rename from lib/src/widgets/country_selector/search_box.dart rename to lib/src/country_selection/search_box.dart diff --git a/lib/src/controllers/phone_controller.dart b/lib/src/phone_controller.dart similarity index 100% rename from lib/src/controllers/phone_controller.dart rename to lib/src/phone_controller.dart diff --git a/lib/src/widgets/phone_field.dart b/lib/src/phone_field.dart similarity index 96% rename from lib/src/widgets/phone_field.dart rename to lib/src/phone_field.dart index 93388c9b..8c000122 100644 --- a/lib/src/widgets/phone_field.dart +++ b/lib/src/phone_field.dart @@ -3,10 +3,10 @@ import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; import 'package:circle_flags/circle_flags.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:phone_form_field/src/constants/patterns.dart'; -import 'package:phone_form_field/src/controllers/phone_field_controller.dart'; +import 'package:phone_form_field/src/validation/allowed_characters.dart'; import '../../phone_form_field.dart'; +import 'phone_field_controller.dart'; part 'phone_field_state.dart'; diff --git a/lib/src/controllers/phone_field_controller.dart b/lib/src/phone_field_controller.dart similarity index 100% rename from lib/src/controllers/phone_field_controller.dart rename to lib/src/phone_field_controller.dart diff --git a/lib/src/widgets/phone_field_state.dart b/lib/src/phone_field_state.dart similarity index 97% rename from lib/src/widgets/phone_field_state.dart rename to lib/src/phone_field_state.dart index d2a6d6ae..9ce024cc 100644 --- a/lib/src/widgets/phone_field_state.dart +++ b/lib/src/phone_field_state.dart @@ -76,7 +76,7 @@ class PhoneFieldState extends State { inputFormatters: widget.inputFormatters ?? [ FilteringTextInputFormatter.allow(RegExp( - '[${Patterns.plus}${Patterns.digits}${Patterns.punctuation}]')), + '[${AllowedCharacters.plus}${AllowedCharacters.digits}${AllowedCharacters.punctuation}]')), ], autofillHints: widget.autofillHints, keyboardType: widget.keyboardType, @@ -134,7 +134,7 @@ class PhoneFieldState extends State { padding: !widget.showDialCode && !widget.showFlagInInput ? EdgeInsets.zero : const EdgeInsetsDirectional.fromSTEB(8, 0, 8, 0), - child: CountryCodeChip( + child: CountryChip( key: const ValueKey('country-code-chip'), isoCode: controller.isoCode, showFlag: widget.showFlagInInput, diff --git a/lib/src/widgets/phone_form_field.dart b/lib/src/phone_form_field.dart similarity index 95% rename from lib/src/widgets/phone_form_field.dart rename to lib/src/phone_form_field.dart index 254ca9ea..8f4d2c2f 100644 --- a/lib/src/widgets/phone_form_field.dart +++ b/lib/src/phone_form_field.dart @@ -5,12 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:phone_numbers_parser/phone_numbers_parser.dart'; -import '../constants/patterns.dart'; -import '../controllers/phone_controller.dart'; -import '../controllers/phone_field_controller.dart'; -import '../validation/phone_validator.dart'; -import '../validation/validator_translator.dart'; -import 'country_selector/country_selector_navigator.dart'; +import 'validation/allowed_characters.dart'; +import 'phone_controller.dart'; +import 'phone_field_controller.dart'; +import 'validation/phone_validator.dart'; +import 'validation/validator_translator.dart'; +import 'country_selection/country_selector_navigator.dart'; import 'phone_field.dart'; part 'phone_form_field_state.dart'; @@ -114,7 +114,8 @@ class PhoneFormField extends FormField { this.defaultCountry = IsoCode.US, InputDecoration decoration = const InputDecoration(border: UnderlineInputBorder()), - AutovalidateMode super.autovalidateMode = AutovalidateMode.onUserInteraction, + AutovalidateMode super.autovalidateMode = + AutovalidateMode.onUserInteraction, PhoneNumber? initialValue, double flagSize = 16, PhoneNumberInputValidator? validator, diff --git a/lib/src/widgets/phone_form_field_state.dart b/lib/src/phone_form_field_state.dart similarity index 97% rename from lib/src/widgets/phone_form_field_state.dart rename to lib/src/phone_form_field_state.dart index 7fada3e1..84b36948 100644 --- a/lib/src/widgets/phone_form_field_state.dart +++ b/lib/src/phone_form_field_state.dart @@ -79,7 +79,8 @@ class PhoneFormFieldState extends FormFieldState { // we parse it accordingly. // we assume it's a whole phone number if it starts with + final childNsn = _childController.national; - if (childNsn != null && childNsn.startsWith(RegExp('[${Patterns.plus}]'))) { + if (childNsn != null && + childNsn.startsWith(RegExp('[${AllowedCharacters.plus}]'))) { // if starts with + then we parse the whole number // to figure out the country code final international = childNsn; diff --git a/lib/src/constants/patterns.dart b/lib/src/validation/allowed_characters.dart similarity index 90% rename from lib/src/constants/patterns.dart rename to lib/src/validation/allowed_characters.dart index fa0833a0..78871b29 100644 --- a/lib/src/constants/patterns.dart +++ b/lib/src/validation/allowed_characters.dart @@ -1,4 +1,4 @@ -class Patterns { +class AllowedCharacters { /// accepted punctuation within a phone number static const String punctuation = r' ()\[\]\-\.\/\\'; static const String plus = r'\++'; diff --git a/lib/src/widgets/country_selector/country.dart b/lib/src/widgets/country_selector/country.dart deleted file mode 100644 index cc978241..00000000 --- a/lib/src/widgets/country_selector/country.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:phone_numbers_parser/metadata.dart'; -import 'package:phone_numbers_parser/phone_numbers_parser.dart'; - -/// Country regroup informations for displaying a list of countries -class Country { - /// Country alpha-2 iso code - final IsoCode isoCode; - - /// localized name of the country - final String name; - - /// country dialing code to call them internationally - final String countryCode; - - /// returns "+ [countryCode]" - String get displayCountryCode => '+ $countryCode'; - - Country(this.isoCode, this.name) - : countryCode = metadataByIsoCode[isoCode]?.countryCode ?? ''; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Country && - runtimeType == other.runtimeType && - isoCode == other.isoCode; - - @override - int get hashCode => isoCode.hashCode; - - @override - String toString() { - return 'Country{isoCode: $isoCode}'; - } -} diff --git a/test/_country_selector_test.dart b/test/_country_selector_test.dart index 5d97bd7a..4f7e4d9d 100644 --- a/test/_country_selector_test.dart +++ b/test/_country_selector_test.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:phone_form_field/phone_form_field.dart'; -import 'package:phone_form_field/src/widgets/country_selector/search_box.dart'; +import 'package:phone_form_field/src/country_selection/search_box.dart'; void main() { group('CountrySelector', () { diff --git a/test/phone_form_field_test.dart b/test/phone_form_field_test.dart index 22420631..91e0d7d7 100644 --- a/test/phone_form_field_test.dart +++ b/test/phone_form_field_test.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:phone_form_field/phone_form_field.dart'; -import 'package:phone_form_field/src/widgets/country_selector/country_list.dart'; +import 'package:phone_form_field/src/country_selection/country_list_view.dart'; void main() { group('PhoneFormField', () { @@ -57,7 +57,7 @@ void main() { testWidgets('Should display country code', (tester) async { await tester.pumpWidget(getWidget()); - expect(find.byType(CountryCodeChip), findsWidgets); + expect(find.byType(CountryChip), findsWidgets); }); testWidgets('Should display flag', (tester) async { @@ -70,13 +70,13 @@ void main() { (tester) async { await tester.pumpWidget(getWidget(enabled: false)); final countryChip = - tester.widget(find.byType(CountryCodeChip)); + tester.widget(find.byType(CountryChip)); expect(countryChip.enabled, false); - await tester.tap(find.byType(CountryCodeChip)); + await tester.tap(find.byType(CountryChip)); await tester.pumpAndSettle(); - expect(find.byType(CountryList), findsNothing); + expect(find.byType(CountryListView), findsNothing); }); }); @@ -84,12 +84,12 @@ void main() { testWidgets('Should open dialog when country code is clicked', (tester) async { await tester.pumpWidget(getWidget()); - expect(find.byType(CountryList), findsNothing); + expect(find.byType(CountryListView), findsNothing); await tester.tap(find.byType(PhoneFormField)); await tester.pump(const Duration(seconds: 1)); - await tester.tap(find.byType(CountryCodeChip)); + await tester.tap(find.byType(CountryChip)); await tester.pumpAndSettle(); - expect(find.byType(CountryList), findsOneWidget); + expect(find.byType(CountryListView), findsOneWidget); }); testWidgets('Should have a default country', (tester) async { await tester.pumpWidget(getWidget(defaultCountry: IsoCode.FR)); From 07358b2797e429ed43a29e40571b7f52fa182434 Mon Sep 17 00:00:00 2001 From: cedvdb Date: Fri, 2 Feb 2024 12:39:46 +0100 Subject: [PATCH 04/25] refactor --- example/lib/main.dart | 17 +- .../windows/flutter/generated_plugins.cmake | 23 + lib/l10n/en.arb | 1 + .../generated/phone_field_localization.dart | 14 +- .../phone_field_localization_ar.dart | 3 + .../phone_field_localization_ckb.dart | 756 ++++++++++++++++++ .../phone_field_localization_de.dart | 3 + .../phone_field_localization_el.dart | 6 +- .../phone_field_localization_en.dart | 3 + .../phone_field_localization_es.dart | 3 + .../phone_field_localization_fa.dart | 3 + .../phone_field_localization_fr.dart | 3 + .../phone_field_localization_hi.dart | 3 + .../phone_field_localization_it.dart | 3 + .../phone_field_localization_ku.dart | 756 ++++++++++++++++++ .../phone_field_localization_nb.dart | 3 + .../phone_field_localization_nl.dart | 3 + .../phone_field_localization_pt.dart | 3 + .../phone_field_localization_ru.dart | 3 + .../phone_field_localization_sv.dart | 3 + .../phone_field_localization_tr.dart | 3 + .../phone_field_localization_uk.dart | 3 + .../phone_field_localization_uz.dart | 3 + .../phone_field_localization_zh.dart | 3 + lib/phone_form_field.dart | 1 - lib/src/country/localized_country.dart | 1 - lib/src/country_selection/country_finder.dart | 5 + .../country_selection/country_selector.dart | 96 +-- .../country_selector_controller.dart | 61 ++ .../country_selector_navigator.dart | 28 +- .../country_selector_page.dart | 168 ++-- .../localized_country_registry.dart | 288 ------- lib/src/country_selection/search_box.dart | 46 +- lib/src/phone_form_field.dart | 2 +- 34 files changed, 1846 insertions(+), 474 deletions(-) create mode 100644 example/windows/flutter/generated_plugins.cmake create mode 100644 lib/l10n/generated/phone_field_localization_ckb.dart create mode 100644 lib/l10n/generated/phone_field_localization_ku.dart create mode 100644 lib/src/country_selection/country_selector_controller.dart delete mode 100644 lib/src/country_selection/localized_country_registry.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 2f0b915b..67e74abb 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -106,7 +106,7 @@ class MyApp extends StatelessWidget { ], title: 'Phone field demo', theme: ThemeData( - brightness: Brightness.light, + brightness: Brightness.dark, primarySwatch: Colors.blue, ), home: const PhoneFormFieldScreen(), @@ -130,7 +130,7 @@ class PhoneFormFieldScreenState extends State { bool withLabel = true; bool useRtl = false; CountrySelectorNavigator selectorNavigator = - const CountrySelectorNavigator.searchDelegate(); + const CountrySelectorNavigator.page(); final formKey = GlobalKey(); final phoneKey = GlobalKey>(); @@ -150,7 +150,6 @@ class PhoneFormFieldScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - // drawer: AppDrawer(), appBar: AppBar( title: const Text('Phone_form_field'), ), @@ -221,19 +220,17 @@ class PhoneFormFieldScreenState extends State { ), DropdownMenuItem( value: - CountrySelectorNavigator.modalBottomSheet( - favorites: [IsoCode.US, IsoCode.BE], - ), + CountrySelectorNavigator.modalBottomSheet(), child: Text('Modal sheet'), ), DropdownMenuItem( - value: - CountrySelectorNavigator.dialog(width: 720), + value: CountrySelectorNavigator.dialog( + width: 720, + ), child: Text('Dialog'), ), DropdownMenuItem( - value: - CountrySelectorNavigator.searchDelegate(), + value: CountrySelectorNavigator.page(), child: Text('Page'), ), ], diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..b93c4c30 --- /dev/null +++ b/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/lib/l10n/en.arb b/lib/l10n/en.arb index 1de71e5d..0d59cb97 100644 --- a/lib/l10n/en.arb +++ b/lib/l10n/en.arb @@ -6,6 +6,7 @@ "invalidFixedLinePhoneNumber": "Invalid fixed line phone number", "requiredPhoneNumber": "Required phone number", "noResultMessage": "No result", + "search": "Search", "ac_": "Ascension Island", "ad_": "Andorra", "ae_": "United Arab Emirates", diff --git a/lib/l10n/generated/phone_field_localization.dart b/lib/l10n/generated/phone_field_localization.dart index d352c0e2..63900696 100644 --- a/lib/l10n/generated/phone_field_localization.dart +++ b/lib/l10n/generated/phone_field_localization.dart @@ -6,6 +6,7 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:intl/intl.dart' as intl; import 'phone_field_localization_ar.dart'; +import 'phone_field_localization_ckb.dart'; import 'phone_field_localization_de.dart'; import 'phone_field_localization_el.dart'; import 'phone_field_localization_en.dart'; @@ -14,6 +15,7 @@ import 'phone_field_localization_fa.dart'; import 'phone_field_localization_fr.dart'; import 'phone_field_localization_hi.dart'; import 'phone_field_localization_it.dart'; +import 'phone_field_localization_ku.dart'; import 'phone_field_localization_nb.dart'; import 'phone_field_localization_nl.dart'; import 'phone_field_localization_pt.dart'; @@ -106,6 +108,7 @@ abstract class PhoneFieldLocalization { /// A list of this localizations delegate's supported locales. static const List supportedLocales = [ Locale('ar'), + Locale('ckb'), Locale('de'), Locale('el'), Locale('en'), @@ -114,6 +117,7 @@ abstract class PhoneFieldLocalization { Locale('fr'), Locale('hi'), Locale('it'), + Locale('ku'), Locale('nb'), Locale('nl'), Locale('pt'), @@ -161,6 +165,12 @@ abstract class PhoneFieldLocalization { /// **'No result'** String get noResultMessage; + /// No description provided for @search. + /// + /// In en, this message translates to: + /// **'Search'** + String get search; + /// No description provided for @ac_. /// /// In en, this message translates to: @@ -1629,7 +1639,7 @@ class _PhoneFieldLocalizationDelegate extends LocalizationsDelegate ['ar', 'de', 'el', 'en', 'es', 'fa', 'fr', 'hi', 'it', 'nb', 'nl', 'pt', 'ru', 'sv', 'tr', 'uk', 'uz', 'zh'].contains(locale.languageCode); + bool isSupported(Locale locale) => ['ar', 'ckb', 'de', 'el', 'en', 'es', 'fa', 'fr', 'hi', 'it', 'ku', 'nb', 'nl', 'pt', 'ru', 'sv', 'tr', 'uk', 'uz', 'zh'].contains(locale.languageCode); @override bool shouldReload(_PhoneFieldLocalizationDelegate old) => false; @@ -1641,6 +1651,7 @@ PhoneFieldLocalization lookupPhoneFieldLocalization(Locale locale) { // Lookup logic when only language code is specified. switch (locale.languageCode) { case 'ar': return PhoneFieldLocalizationAr(); + case 'ckb': return PhoneFieldLocalizationCkb(); case 'de': return PhoneFieldLocalizationDe(); case 'el': return PhoneFieldLocalizationEl(); case 'en': return PhoneFieldLocalizationEn(); @@ -1649,6 +1660,7 @@ PhoneFieldLocalization lookupPhoneFieldLocalization(Locale locale) { case 'fr': return PhoneFieldLocalizationFr(); case 'hi': return PhoneFieldLocalizationHi(); case 'it': return PhoneFieldLocalizationIt(); + case 'ku': return PhoneFieldLocalizationKu(); case 'nb': return PhoneFieldLocalizationNb(); case 'nl': return PhoneFieldLocalizationNl(); case 'pt': return PhoneFieldLocalizationPt(); diff --git a/lib/l10n/generated/phone_field_localization_ar.dart b/lib/l10n/generated/phone_field_localization_ar.dart index c20d907c..9635806c 100644 --- a/lib/l10n/generated/phone_field_localization_ar.dart +++ b/lib/l10n/generated/phone_field_localization_ar.dart @@ -22,6 +22,9 @@ class PhoneFieldLocalizationAr extends PhoneFieldLocalization { @override String get noResultMessage => 'لا نتيجة'; + @override + String get search => 'Search'; + @override String get ac_ => 'جزيرة أسنسيون'; diff --git a/lib/l10n/generated/phone_field_localization_ckb.dart b/lib/l10n/generated/phone_field_localization_ckb.dart new file mode 100644 index 00000000..78d1e0ac --- /dev/null +++ b/lib/l10n/generated/phone_field_localization_ckb.dart @@ -0,0 +1,756 @@ +import 'phone_field_localization.dart'; + +/// The translations for Central Kurdish (`ckb`). +class PhoneFieldLocalizationCkb extends PhoneFieldLocalization { + PhoneFieldLocalizationCkb([String locale = 'ckb']) : super(locale); + + @override + String get invalidPhoneNumber => 'ژمارەی تەلەفۆنی نادروست'; + + @override + String get invalidCountry => 'وڵاتێکی نادروست'; + + @override + String get invalidMobilePhoneNumber => 'ژمارەی مۆبایل نادروستە'; + + @override + String get invalidFixedLinePhoneNumber => 'ژمارەی تەلەفۆنی هێڵی جێگیر نادروستە'; + + @override + String get requiredPhoneNumber => 'ژمارەی تەلەفۆنی پێویست'; + + @override + String get noResultMessage => 'بێ ئه‌نجام'; + + @override + String get search => 'Search'; + + @override + String get ac_ => 'دوورگەی ئاسنشن'; + + @override + String get ad_ => 'ئەندۆرا'; + + @override + String get ae_ => 'شانشینی عەرەبی'; + + @override + String get af_ => 'ئەفغانستان'; + + @override + String get ag_ => 'ئەنتیگوا و باربودا'; + + @override + String get ai_ => 'ئەنگویلا'; + + @override + String get al_ => 'ئەلبانیا'; + + @override + String get am_ => 'ئەرمینیا'; + + @override + String get an_ => 'دورگه‌کانی ئه‌نتیلسی هۆڵه‌ندا'; + + @override + String get ao_ => 'ئەنگۆلا'; + + @override + String get aq_ => 'ئەنتارکتیکا'; + + @override + String get ar_ => 'ئەرجەنتین'; + + @override + String get as_ => 'سامۆای ئەمریکی'; + + @override + String get at_ => 'نەمسا'; + + @override + String get au_ => 'ئوسترالیا'; + + @override + String get aw_ => 'ئاروبا'; + + @override + String get ax_ => 'دوورگەکانی ئالاند'; + + @override + String get az_ => 'ئازەربایجان'; + + @override + String get ba_ => 'بۆسنە و هێرزۆگۆبینیا'; + + @override + String get bb_ => 'باربادۆس'; + + @override + String get bd_ => 'بەنگلادیش'; + + @override + String get be_ => 'بەلجیکا'; + + @override + String get bf_ => 'بورکينا فاسۆ'; + + @override + String get bg_ => 'بولگاریا'; + + @override + String get bh_ => 'بەحرەین'; + + @override + String get bi_ => 'بوروندی'; + + @override + String get bj_ => 'بێنین'; + + @override + String get bl_ => 'سانت بارتێلمی'; + + @override + String get bm_ => 'بەرمودا'; + + @override + String get bn_ => 'برونێی داروسالام'; + + @override + String get bo_ => 'بۆلیڤیا، ویلایەتی فرەنەتەوەیی'; + + @override + String get bq_ => 'بۆنایر'; + + @override + String get br_ => 'بەڕازیل'; + + @override + String get bs_ => 'باهاماس'; + + @override + String get bt_ => 'بۆتان'; + + @override + String get bw_ => 'بۆتسوانا'; + + @override + String get by_ => 'بێلاڕوس'; + + @override + String get bz_ => 'بەلیز'; + + @override + String get ca_ => 'کەنەدا'; + + @override + String get cc_ => 'دوورگەکانی کۆکۆس (کیلینگ).'; + + @override + String get cd_ => 'کۆنگۆ، کۆماری دیموکراتیی کۆنگۆ'; + + @override + String get cf_ => 'کۆماری ئەفریقیای ناوەراست'; + + @override + String get cg_ => 'کۆنگۆ'; + + @override + String get ch_ => 'سویسرا'; + + @override + String get ci_ => 'کۆت دیڤوار'; + + @override + String get ck_ => 'دوورگەکانی کوک'; + + @override + String get cl_ => 'شیلی'; + + @override + String get cm_ => 'کامیرۆن'; + + @override + String get cn_ => 'چین'; + + @override + String get co_ => 'کۆڵۆمبیا'; + + @override + String get cr_ => 'کۆستەریکا'; + + @override + String get cu_ => 'کوبا'; + + @override + String get cv_ => 'کیپ ڤێردی'; + + @override + String get cx_ => 'دوورگەی کریسمس'; + + @override + String get cy_ => 'قوبرس'; + + @override + String get cz_ => 'کۆماری چیک'; + + @override + String get de_ => 'ئەڵمانیا'; + + @override + String get dj_ => 'جیبۆتی'; + + @override + String get dk_ => 'دانیمارک'; + + @override + String get dm_ => 'دۆمینیکا'; + + @override + String get do_ => 'کۆماری دۆمینیکەن'; + + @override + String get dz_ => 'جەزائیر'; + + @override + String get ec_ => 'ئیکوادۆر'; + + @override + String get ee_ => 'ئیستۆنیا'; + + @override + String get eg_ => 'میسر'; + + @override + String get er_ => 'ئێریتریا'; + + @override + String get es_ => 'ئیسپانیا'; + + @override + String get et_ => 'ئەسیوپیا'; + + @override + String get fi_ => 'فینلاند'; + + @override + String get fj_ => 'فیجی'; + + @override + String get fk_ => 'دوورگەکانی فۆڵکلاند (ماڵڤیناس)'; + + @override + String get fm_ => 'مایکرۆنیزیا، ویلایەتە فیدراڵیەکانی مایکرۆنیزیا'; + + @override + String get fo_ => 'دورگەکانی فارۆ'; + + @override + String get fr_ => 'فەرەنسا'; + + @override + String get ga_ => 'گابۆن'; + + @override + String get gb_ => 'شانشینە یەگرتۆکان'; + + @override + String get gd_ => 'گرێنادا'; + + @override + String get ge_ => 'جۆرجیا'; + + @override + String get gf_ => 'دورگەیەکی فەڕەنسایە کە دەکەوێتە باکوری خۆرهەڵاتی ئەمەریکای باشور'; + + @override + String get gg_ => 'گێرنسی'; + + @override + String get gh_ => 'غانا'; + + @override + String get gi_ => 'جبل طارق'; + + @override + String get gl_ => 'گرینلاند'; + + @override + String get gm_ => 'گامبیا'; + + @override + String get gn_ => 'گینیا'; + + @override + String get gp_ => 'گوادلۆپ'; + + @override + String get gq_ => 'گینیا ئیکواتۆریال'; + + @override + String get gr_ => 'یۆنان'; + + @override + String get gs_ => 'باشووری جۆرجیا و دوورگەکانی ساندویچی باشوور'; + + @override + String get gt_ => 'گواتیمالا'; + + @override + String get gu_ => 'گوام'; + + @override + String get gw_ => 'گینیا-بیساو'; + + @override + String get gy_ => 'گویانا'; + + @override + String get hk_ => 'هۆنگ کۆنگ'; + + @override + String get hn_ => 'هندۆراس'; + + @override + String get hr_ => 'کرواتیا'; + + @override + String get ht_ => 'هایتی'; + + @override + String get hu_ => 'هەنگاریا'; + + @override + String get id_ => 'ئەندەنوسیا'; + + @override + String get ie_ => 'ئێرلەندا'; + + @override + String get il_ => 'ئیسرائیل'; + + @override + String get im_ => 'دوورگەی مان'; + + @override + String get in_ => 'هیندستان'; + + @override + String get io_ => 'خاکی زەریای هیندی بەریتانیا'; + + @override + String get iq_ => 'عێراق'; + + @override + String get ir_ => 'ئێران، کۆماری ئیسلامیی...'; + + @override + String get is_ => 'ئایسلەندا'; + + @override + String get it_ => 'ئیتاڵیا'; + + @override + String get je_ => 'جێرسی'; + + @override + String get jm_ => 'جامایکا'; + + @override + String get jo_ => 'ئوردن'; + + @override + String get jp_ => 'ژاپۆن'; + + @override + String get ke_ => 'کینیا'; + + @override + String get kg_ => 'قیرغیزستان'; + + @override + String get kh_ => 'کەمبۆدیا'; + + @override + String get ki_ => 'کیریباتی'; + + @override + String get km_ => 'کۆمۆرۆس'; + + @override + String get kn_ => 'سەینت کیتس و نیڤیس'; + + @override + String get kp_ => 'کۆریا، کۆماری گەلی دیموکراتی کۆریا'; + + @override + String get kr_ => 'کۆریا، کۆماری کۆریای باشوور'; + + @override + String get kw_ => 'کوێت'; + + @override + String get ky_ => 'دوورگەکانی کایمان'; + + @override + String get kz_ => 'کازاخستان'; + + @override + String get la_ => 'لائۆس'; + + @override + String get lb_ => 'لوبنان'; + + @override + String get lc_ => 'سانت لوسیا'; + + @override + String get li_ => 'لیختنشتاین'; + + @override + String get lk_ => 'سری لانکا'; + + @override + String get lr_ => 'لیبێریا'; + + @override + String get ls_ => 'لێسۆتۆ'; + + @override + String get lt_ => 'لیتوانیا'; + + @override + String get lu_ => 'لۆکسمبۆرگ'; + + @override + String get lv_ => 'لاتڤیا'; + + @override + String get ly_ => 'لیبیا'; + + @override + String get ma_ => 'مەغریب'; + + @override + String get mc_ => 'مۆناکۆ'; + + @override + String get md_ => 'مۆڵدۆڤا'; + + @override + String get me_ => 'مۆنتینیگرۆ'; + + @override + String get mf_ => 'سانت مارتن'; + + @override + String get mg_ => 'ماداگاسکار'; + + @override + String get mh_ => 'دوورگەکانی مارشال'; + + @override + String get mk_ => 'مەقدۆنیا'; + + @override + String get ml_ => 'مالی'; + + @override + String get mm_ => 'میانمار'; + + @override + String get mn_ => 'مەنگۆلیا'; + + @override + String get mo_ => 'ماکاو'; + + @override + String get mp_ => 'دوورگەکانی باکووری ماریانا'; + + @override + String get mq_ => 'مارتینیک'; + + @override + String get mr_ => 'مۆریتانیا'; + + @override + String get ms_ => 'مۆنتسێرات'; + + @override + String get mt_ => 'ماڵتا'; + + @override + String get mu_ => 'مۆریس'; + + @override + String get mv_ => 'ماڵدیڤ'; + + @override + String get mw_ => 'مالاوی'; + + @override + String get mx_ => 'مەکسیک'; + + @override + String get my_ => 'مالیزیا'; + + @override + String get mz_ => 'مۆزەمبیق'; + + @override + String get na_ => 'نامیبیا'; + + @override + String get nc_ => 'کاڵێدۆنیای نوێ'; + + @override + String get ne_ => 'نیجەر'; + + @override + String get nf_ => 'دوورگەی نۆرفۆلک'; + + @override + String get ng_ => 'نەیجیریا'; + + @override + String get ni_ => 'نیکاراگوا'; + + @override + String get nl_ => 'هۆڵەندا'; + + @override + String get no_ => 'نەرویج'; + + @override + String get np_ => 'نیپاڵ'; + + @override + String get nr_ => 'ناورو'; + + @override + String get nu_ => 'نیوێ'; + + @override + String get nz_ => 'وڵاتی نیوزله‌ندا'; + + @override + String get om_ => 'عومان'; + + @override + String get pa_ => 'پەنەما'; + + @override + String get pe_ => 'پیرۆ'; + + @override + String get pf_ => 'پۆلینیزیای فەرەنسی'; + + @override + String get pg_ => 'پاپوای نیوگینیا'; + + @override + String get ph_ => 'فلیپین'; + + @override + String get pk_ => 'پاکستان'; + + @override + String get pl_ => 'پۆڵەندا'; + + @override + String get pm_ => 'سانت پیێر و میکێلۆن'; + + @override + String get pn_ => 'پیتکایرن'; + + @override + String get pr_ => 'پورتوگال'; + + @override + String get ps_ => 'خاکی فەلەستین، داگیرکراوە'; + + @override + String get pt_ => 'پورتوگال'; + + @override + String get pw_ => 'پالاو'; + + @override + String get py_ => 'پاراگوای'; + + @override + String get qa_ => 'قەتەر'; + + @override + String get re_ => 'یەکگرتنەوە'; + + @override + String get ro_ => 'ڕۆمانیا'; + + @override + String get rs_ => 'سربیا'; + + @override + String get ru_ => 'ڕووسیا'; + + @override + String get rw_ => 'ڕواندا'; + + @override + String get sa_ => 'عەرەبستانی سوعوودی'; + + @override + String get sb_ => 'دوورگەکانی سلێمان'; + + @override + String get sc_ => 'سیشێل'; + + @override + String get sd_ => 'سودان'; + + @override + String get se_ => 'سویدی'; + + @override + String get sg_ => 'سەنگافورە'; + + @override + String get si_ => 'سلۆڤینیا'; + + @override + String get sk_ => 'سلۆڤاکیا'; + + @override + String get sl_ => 'سیرالیۆن'; + + @override + String get sm_ => 'سان مارینۆ'; + + @override + String get sn_ => 'سەنیگال'; + + @override + String get so_ => 'سۆماڵ'; + + @override + String get sr_ => 'سورینام'; + + @override + String get ss_ => 'باشووری سودان'; + + @override + String get st_ => 'ساو تۆمێ و پرینسیپی'; + + @override + String get sv_ => 'سلڤادۆر'; + + @override + String get sy_ => 'کۆماری عەرەبی سوریا'; + + @override + String get sz_ => 'سوازیلاند'; + + @override + String get ta_ => 'تریستان دا کونها'; + + @override + String get tc_ => 'دوورگەکانی تورک و کایکۆس'; + + @override + String get td_ => 'چاد'; + + @override + String get tg_ => 'تۆگۆ'; + + @override + String get th_ => 'تایلەند'; + + @override + String get tj_ => 'تاجیکستان'; + + @override + String get tk_ => 'تۆکێلاو'; + + @override + String get tl_ => 'تیمۆر-لێستێ'; + + @override + String get tm_ => 'تورکمانستان'; + + @override + String get tn_ => 'تونس'; + + @override + String get to_ => 'تۆنگا'; + + @override + String get tr_ => 'تورکیە'; + + @override + String get tt_ => 'ترینیداد و تۆباگۆ'; + + @override + String get tv_ => 'توڤالو'; + + @override + String get tw_ => 'تایوان'; + + @override + String get tz_ => 'تانزانیا، کۆماری یەکگرتووی تانزانیا'; + + @override + String get ua_ => 'ئۆکرانیا'; + + @override + String get ug_ => 'ئۆگاندا'; + + @override + String get us_ => 'ویلایەتە یەکگرتووەکان'; + + @override + String get uy_ => 'ئۆرۆگوای'; + + @override + String get uz_ => 'ئۆزبەکستان'; + + @override + String get va_ => 'کورسی پیرۆز (وڵاتی ڤاتیکان سیتی)'; + + @override + String get vc_ => 'سانت ڤینسێنت و گرێنادین'; + + @override + String get ve_ => 'ڤەنزوێلا'; + + @override + String get vg_ => 'دوورگەکانی ڤێرجینیا، بەریتانیا'; + + @override + String get vi_ => 'دوورگەکانی ڤێرجینیا، ئەمریکا.'; + + @override + String get vn_ => 'ڤێتنام'; + + @override + String get vu_ => 'ڤانواتو'; + + @override + String get wf_ => 'والیس و فوتونا'; + + @override + String get ws_ => 'ساموا'; + + @override + String get ye_ => 'یەمەن'; + + @override + String get yt_ => 'مایۆت'; + + @override + String get za_ => 'باشوری ئەفریقا'; + + @override + String get zm_ => 'زامبیا'; + + @override + String get zw_ => 'زیمبابۆی'; +} diff --git a/lib/l10n/generated/phone_field_localization_de.dart b/lib/l10n/generated/phone_field_localization_de.dart index 92c5f78d..289eb198 100644 --- a/lib/l10n/generated/phone_field_localization_de.dart +++ b/lib/l10n/generated/phone_field_localization_de.dart @@ -22,6 +22,9 @@ class PhoneFieldLocalizationDe extends PhoneFieldLocalization { @override String get noResultMessage => 'Kein Ergebnis'; + @override + String get search => 'Search'; + @override String get ac_ => 'Himmelfahrtsinsel'; diff --git a/lib/l10n/generated/phone_field_localization_el.dart b/lib/l10n/generated/phone_field_localization_el.dart index b1d36d5a..21563b9a 100644 --- a/lib/l10n/generated/phone_field_localization_el.dart +++ b/lib/l10n/generated/phone_field_localization_el.dart @@ -14,7 +14,8 @@ class PhoneFieldLocalizationEl extends PhoneFieldLocalization { String get invalidMobilePhoneNumber => 'Μη έγκυρος αριθμός κινητού τηλεφώνου'; @override - String get invalidFixedLinePhoneNumber => 'Μη έγκυρος αριθμός σταθερού τηλεφώνου'; + String get invalidFixedLinePhoneNumber => + 'Μη έγκυρος αριθμός σταθερού τηλεφώνου'; @override String get requiredPhoneNumber => 'Απαιτούμενος αριθμός τηλεφώνου'; @@ -22,6 +23,9 @@ class PhoneFieldLocalizationEl extends PhoneFieldLocalization { @override String get noResultMessage => 'Κανένα αποτέλεσμα'; + @override + String get search => 'Search'; + @override String get ac_ => 'Νησί της Ανάληψης'; diff --git a/lib/l10n/generated/phone_field_localization_en.dart b/lib/l10n/generated/phone_field_localization_en.dart index 4667b3c5..62a23426 100644 --- a/lib/l10n/generated/phone_field_localization_en.dart +++ b/lib/l10n/generated/phone_field_localization_en.dart @@ -22,6 +22,9 @@ class PhoneFieldLocalizationEn extends PhoneFieldLocalization { @override String get noResultMessage => 'No result'; + @override + String get search => 'Search'; + @override String get ac_ => 'Ascension Island'; diff --git a/lib/l10n/generated/phone_field_localization_es.dart b/lib/l10n/generated/phone_field_localization_es.dart index 7d866ca5..4aa34f26 100644 --- a/lib/l10n/generated/phone_field_localization_es.dart +++ b/lib/l10n/generated/phone_field_localization_es.dart @@ -22,6 +22,9 @@ class PhoneFieldLocalizationEs extends PhoneFieldLocalization { @override String get noResultMessage => 'Sin resultados'; + @override + String get search => 'Search'; + @override String get ac_ => 'Isla Ascencion'; diff --git a/lib/l10n/generated/phone_field_localization_fa.dart b/lib/l10n/generated/phone_field_localization_fa.dart index 73576037..71dadad9 100644 --- a/lib/l10n/generated/phone_field_localization_fa.dart +++ b/lib/l10n/generated/phone_field_localization_fa.dart @@ -22,6 +22,9 @@ class PhoneFieldLocalizationFa extends PhoneFieldLocalization { @override String get noResultMessage => 'بدون نتیجه'; + @override + String get search => 'Search'; + @override String get ac_ => 'جزیره اسنشن'; diff --git a/lib/l10n/generated/phone_field_localization_fr.dart b/lib/l10n/generated/phone_field_localization_fr.dart index 700a6e4e..d5548a85 100644 --- a/lib/l10n/generated/phone_field_localization_fr.dart +++ b/lib/l10n/generated/phone_field_localization_fr.dart @@ -22,6 +22,9 @@ class PhoneFieldLocalizationFr extends PhoneFieldLocalization { @override String get noResultMessage => 'Aucun résultat'; + @override + String get search => 'Search'; + @override String get ac_ => 'Île de l\'Ascension'; diff --git a/lib/l10n/generated/phone_field_localization_hi.dart b/lib/l10n/generated/phone_field_localization_hi.dart index 4d389339..a30c442b 100644 --- a/lib/l10n/generated/phone_field_localization_hi.dart +++ b/lib/l10n/generated/phone_field_localization_hi.dart @@ -22,6 +22,9 @@ class PhoneFieldLocalizationHi extends PhoneFieldLocalization { @override String get noResultMessage => 'कोई परिणाम नही'; + @override + String get search => 'Search'; + @override String get ac_ => 'असेंशन द्वीप'; diff --git a/lib/l10n/generated/phone_field_localization_it.dart b/lib/l10n/generated/phone_field_localization_it.dart index c8ff8386..f6bf00ff 100644 --- a/lib/l10n/generated/phone_field_localization_it.dart +++ b/lib/l10n/generated/phone_field_localization_it.dart @@ -22,6 +22,9 @@ class PhoneFieldLocalizationIt extends PhoneFieldLocalization { @override String get noResultMessage => 'Nessun risultato'; + @override + String get search => 'Search'; + @override String get ac_ => 'Isola dell\'Ascensione'; diff --git a/lib/l10n/generated/phone_field_localization_ku.dart b/lib/l10n/generated/phone_field_localization_ku.dart new file mode 100644 index 00000000..1cf39550 --- /dev/null +++ b/lib/l10n/generated/phone_field_localization_ku.dart @@ -0,0 +1,756 @@ +import 'phone_field_localization.dart'; + +/// The translations for Kurdish (`ku`). +class PhoneFieldLocalizationKu extends PhoneFieldLocalization { + PhoneFieldLocalizationKu([String locale = 'ku']) : super(locale); + + @override + String get invalidPhoneNumber => 'ژمارەی تەلەفۆنی نادروست'; + + @override + String get invalidCountry => 'وڵاتێکی نادروست'; + + @override + String get invalidMobilePhoneNumber => 'ژمارەی مۆبایل نادروستە'; + + @override + String get invalidFixedLinePhoneNumber => 'ژمارەی تەلەفۆنی هێڵی جێگیر نادروستە'; + + @override + String get requiredPhoneNumber => 'ژمارەی تەلەفۆنی پێویست'; + + @override + String get noResultMessage => 'بێ ئه‌نجام'; + + @override + String get search => 'Search'; + + @override + String get ac_ => 'دوورگەی ئاسنشن'; + + @override + String get ad_ => 'ئەندۆرا'; + + @override + String get ae_ => 'شانشینی عەرەبی'; + + @override + String get af_ => 'ئەفغانستان'; + + @override + String get ag_ => 'ئەنتیگوا و باربودا'; + + @override + String get ai_ => 'ئەنگویلا'; + + @override + String get al_ => 'ئەلبانیا'; + + @override + String get am_ => 'ئەرمینیا'; + + @override + String get an_ => 'دورگه‌کانی ئه‌نتیلسی هۆڵه‌ندا'; + + @override + String get ao_ => 'ئەنگۆلا'; + + @override + String get aq_ => 'ئەنتارکتیکا'; + + @override + String get ar_ => 'ئەرجەنتین'; + + @override + String get as_ => 'سامۆای ئەمریکی'; + + @override + String get at_ => 'نەمسا'; + + @override + String get au_ => 'ئوسترالیا'; + + @override + String get aw_ => 'ئاروبا'; + + @override + String get ax_ => 'دوورگەکانی ئالاند'; + + @override + String get az_ => 'ئازەربایجان'; + + @override + String get ba_ => 'بۆسنە و هێرزۆگۆبینیا'; + + @override + String get bb_ => 'باربادۆس'; + + @override + String get bd_ => 'بەنگلادیش'; + + @override + String get be_ => 'بەلجیکا'; + + @override + String get bf_ => 'بورکينا فاسۆ'; + + @override + String get bg_ => 'بولگاریا'; + + @override + String get bh_ => 'بەحرەین'; + + @override + String get bi_ => 'بوروندی'; + + @override + String get bj_ => 'بێنین'; + + @override + String get bl_ => 'سانت بارتێلمی'; + + @override + String get bm_ => 'بەرمودا'; + + @override + String get bn_ => 'برونێی داروسالام'; + + @override + String get bo_ => 'بۆلیڤیا، ویلایەتی فرەنەتەوەیی'; + + @override + String get bq_ => 'بۆنایر'; + + @override + String get br_ => 'بەڕازیل'; + + @override + String get bs_ => 'باهاماس'; + + @override + String get bt_ => 'بۆتان'; + + @override + String get bw_ => 'بۆتسوانا'; + + @override + String get by_ => 'بێلاڕوس'; + + @override + String get bz_ => 'بەلیز'; + + @override + String get ca_ => 'کەنەدا'; + + @override + String get cc_ => 'دوورگەکانی کۆکۆس (کیلینگ).'; + + @override + String get cd_ => 'کۆنگۆ، کۆماری دیموکراتیی کۆنگۆ'; + + @override + String get cf_ => 'کۆماری ئەفریقیای ناوەراست'; + + @override + String get cg_ => 'کۆنگۆ'; + + @override + String get ch_ => 'سویسرا'; + + @override + String get ci_ => 'کۆت دیڤوار'; + + @override + String get ck_ => 'دوورگەکانی کوک'; + + @override + String get cl_ => 'شیلی'; + + @override + String get cm_ => 'کامیرۆن'; + + @override + String get cn_ => 'چین'; + + @override + String get co_ => 'کۆڵۆمبیا'; + + @override + String get cr_ => 'کۆستەریکا'; + + @override + String get cu_ => 'کوبا'; + + @override + String get cv_ => 'کیپ ڤێردی'; + + @override + String get cx_ => 'دوورگەی کریسمس'; + + @override + String get cy_ => 'قوبرس'; + + @override + String get cz_ => 'کۆماری چیک'; + + @override + String get de_ => 'ئەڵمانیا'; + + @override + String get dj_ => 'جیبۆتی'; + + @override + String get dk_ => 'دانیمارک'; + + @override + String get dm_ => 'دۆمینیکا'; + + @override + String get do_ => 'کۆماری دۆمینیکەن'; + + @override + String get dz_ => 'جەزائیر'; + + @override + String get ec_ => 'ئیکوادۆر'; + + @override + String get ee_ => 'ئیستۆنیا'; + + @override + String get eg_ => 'میسر'; + + @override + String get er_ => 'ئێریتریا'; + + @override + String get es_ => 'ئیسپانیا'; + + @override + String get et_ => 'ئەسیوپیا'; + + @override + String get fi_ => 'فینلاند'; + + @override + String get fj_ => 'فیجی'; + + @override + String get fk_ => 'دوورگەکانی فۆڵکلاند (ماڵڤیناس)'; + + @override + String get fm_ => 'مایکرۆنیزیا، ویلایەتە فیدراڵیەکانی مایکرۆنیزیا'; + + @override + String get fo_ => 'دورگەکانی فارۆ'; + + @override + String get fr_ => 'فەرەنسا'; + + @override + String get ga_ => 'گابۆن'; + + @override + String get gb_ => 'شانشینە یەگرتۆکان'; + + @override + String get gd_ => 'گرێنادا'; + + @override + String get ge_ => 'جۆرجیا'; + + @override + String get gf_ => 'دورگەیەکی فەڕەنسایە کە دەکەوێتە باکوری خۆرهەڵاتی ئەمەریکای باشور'; + + @override + String get gg_ => 'گێرنسی'; + + @override + String get gh_ => 'غانا'; + + @override + String get gi_ => 'جبل طارق'; + + @override + String get gl_ => 'گرینلاند'; + + @override + String get gm_ => 'گامبیا'; + + @override + String get gn_ => 'گینیا'; + + @override + String get gp_ => 'گوادلۆپ'; + + @override + String get gq_ => 'گینیا ئیکواتۆریال'; + + @override + String get gr_ => 'یۆنان'; + + @override + String get gs_ => 'باشووری جۆرجیا و دوورگەکانی ساندویچی باشوور'; + + @override + String get gt_ => 'گواتیمالا'; + + @override + String get gu_ => 'گوام'; + + @override + String get gw_ => 'گینیا-بیساو'; + + @override + String get gy_ => 'گویانا'; + + @override + String get hk_ => 'هۆنگ کۆنگ'; + + @override + String get hn_ => 'هندۆراس'; + + @override + String get hr_ => 'کرواتیا'; + + @override + String get ht_ => 'هایتی'; + + @override + String get hu_ => 'هەنگاریا'; + + @override + String get id_ => 'ئەندەنوسیا'; + + @override + String get ie_ => 'ئێرلەندا'; + + @override + String get il_ => 'ئیسرائیل'; + + @override + String get im_ => 'دوورگەی مان'; + + @override + String get in_ => 'هیندستان'; + + @override + String get io_ => 'خاکی زەریای هیندی بەریتانیا'; + + @override + String get iq_ => 'عێراق'; + + @override + String get ir_ => 'ئێران، کۆماری ئیسلامیی...'; + + @override + String get is_ => 'ئایسلەندا'; + + @override + String get it_ => 'ئیتاڵیا'; + + @override + String get je_ => 'جێرسی'; + + @override + String get jm_ => 'جامایکا'; + + @override + String get jo_ => 'ئوردن'; + + @override + String get jp_ => 'ژاپۆن'; + + @override + String get ke_ => 'کینیا'; + + @override + String get kg_ => 'قیرغیزستان'; + + @override + String get kh_ => 'کەمبۆدیا'; + + @override + String get ki_ => 'کیریباتی'; + + @override + String get km_ => 'کۆمۆرۆس'; + + @override + String get kn_ => 'سەینت کیتس و نیڤیس'; + + @override + String get kp_ => 'کۆریا، کۆماری گەلی دیموکراتی کۆریا'; + + @override + String get kr_ => 'کۆریا، کۆماری کۆریای باشوور'; + + @override + String get kw_ => 'کوێت'; + + @override + String get ky_ => 'دوورگەکانی کایمان'; + + @override + String get kz_ => 'کازاخستان'; + + @override + String get la_ => 'لائۆس'; + + @override + String get lb_ => 'لوبنان'; + + @override + String get lc_ => 'سانت لوسیا'; + + @override + String get li_ => 'لیختنشتاین'; + + @override + String get lk_ => 'سری لانکا'; + + @override + String get lr_ => 'لیبێریا'; + + @override + String get ls_ => 'لێسۆتۆ'; + + @override + String get lt_ => 'لیتوانیا'; + + @override + String get lu_ => 'لۆکسمبۆرگ'; + + @override + String get lv_ => 'لاتڤیا'; + + @override + String get ly_ => 'لیبیا'; + + @override + String get ma_ => 'مەغریب'; + + @override + String get mc_ => 'مۆناکۆ'; + + @override + String get md_ => 'مۆڵدۆڤا'; + + @override + String get me_ => 'مۆنتینیگرۆ'; + + @override + String get mf_ => 'سانت مارتن'; + + @override + String get mg_ => 'ماداگاسکار'; + + @override + String get mh_ => 'دوورگەکانی مارشال'; + + @override + String get mk_ => 'مەقدۆنیا'; + + @override + String get ml_ => 'مالی'; + + @override + String get mm_ => 'میانمار'; + + @override + String get mn_ => 'مەنگۆلیا'; + + @override + String get mo_ => 'ماکاو'; + + @override + String get mp_ => 'دوورگەکانی باکووری ماریانا'; + + @override + String get mq_ => 'مارتینیک'; + + @override + String get mr_ => 'مۆریتانیا'; + + @override + String get ms_ => 'مۆنتسێرات'; + + @override + String get mt_ => 'ماڵتا'; + + @override + String get mu_ => 'مۆریس'; + + @override + String get mv_ => 'ماڵدیڤ'; + + @override + String get mw_ => 'مالاوی'; + + @override + String get mx_ => 'مەکسیک'; + + @override + String get my_ => 'مالیزیا'; + + @override + String get mz_ => 'مۆزەمبیق'; + + @override + String get na_ => 'نامیبیا'; + + @override + String get nc_ => 'کاڵێدۆنیای نوێ'; + + @override + String get ne_ => 'نیجەر'; + + @override + String get nf_ => 'دوورگەی نۆرفۆلک'; + + @override + String get ng_ => 'نەیجیریا'; + + @override + String get ni_ => 'نیکاراگوا'; + + @override + String get nl_ => 'هۆڵەندا'; + + @override + String get no_ => 'نەرویج'; + + @override + String get np_ => 'نیپاڵ'; + + @override + String get nr_ => 'ناورو'; + + @override + String get nu_ => 'نیوێ'; + + @override + String get nz_ => 'وڵاتی نیوزله‌ندا'; + + @override + String get om_ => 'عومان'; + + @override + String get pa_ => 'پەنەما'; + + @override + String get pe_ => 'پیرۆ'; + + @override + String get pf_ => 'پۆلینیزیای فەرەنسی'; + + @override + String get pg_ => 'پاپوای نیوگینیا'; + + @override + String get ph_ => 'فلیپین'; + + @override + String get pk_ => 'پاکستان'; + + @override + String get pl_ => 'پۆڵەندا'; + + @override + String get pm_ => 'سانت پیێر و میکێلۆن'; + + @override + String get pn_ => 'پیتکایرن'; + + @override + String get pr_ => 'پورتوگال'; + + @override + String get ps_ => 'خاکی فەلەستین، داگیرکراوە'; + + @override + String get pt_ => 'پورتوگال'; + + @override + String get pw_ => 'پالاو'; + + @override + String get py_ => 'پاراگوای'; + + @override + String get qa_ => 'قەتەر'; + + @override + String get re_ => 'یەکگرتنەوە'; + + @override + String get ro_ => 'ڕۆمانیا'; + + @override + String get rs_ => 'سربیا'; + + @override + String get ru_ => 'ڕووسیا'; + + @override + String get rw_ => 'ڕواندا'; + + @override + String get sa_ => 'عەرەبستانی سوعوودی'; + + @override + String get sb_ => 'دوورگەکانی سلێمان'; + + @override + String get sc_ => 'سیشێل'; + + @override + String get sd_ => 'سودان'; + + @override + String get se_ => 'سویدی'; + + @override + String get sg_ => 'سەنگافورە'; + + @override + String get si_ => 'سلۆڤینیا'; + + @override + String get sk_ => 'سلۆڤاکیا'; + + @override + String get sl_ => 'سیرالیۆن'; + + @override + String get sm_ => 'سان مارینۆ'; + + @override + String get sn_ => 'سەنیگال'; + + @override + String get so_ => 'سۆماڵ'; + + @override + String get sr_ => 'سورینام'; + + @override + String get ss_ => 'باشووری سودان'; + + @override + String get st_ => 'ساو تۆمێ و پرینسیپی'; + + @override + String get sv_ => 'سلڤادۆر'; + + @override + String get sy_ => 'کۆماری عەرەبی سوریا'; + + @override + String get sz_ => 'سوازیلاند'; + + @override + String get ta_ => 'تریستان دا کونها'; + + @override + String get tc_ => 'دوورگەکانی تورک و کایکۆس'; + + @override + String get td_ => 'چاد'; + + @override + String get tg_ => 'تۆگۆ'; + + @override + String get th_ => 'تایلەند'; + + @override + String get tj_ => 'تاجیکستان'; + + @override + String get tk_ => 'تۆکێلاو'; + + @override + String get tl_ => 'تیمۆر-لێستێ'; + + @override + String get tm_ => 'تورکمانستان'; + + @override + String get tn_ => 'تونس'; + + @override + String get to_ => 'تۆنگا'; + + @override + String get tr_ => 'تورکیە'; + + @override + String get tt_ => 'ترینیداد و تۆباگۆ'; + + @override + String get tv_ => 'توڤالو'; + + @override + String get tw_ => 'تایوان'; + + @override + String get tz_ => 'تانزانیا، کۆماری یەکگرتووی تانزانیا'; + + @override + String get ua_ => 'ئۆکرانیا'; + + @override + String get ug_ => 'ئۆگاندا'; + + @override + String get us_ => 'ویلایەتە یەکگرتووەکان'; + + @override + String get uy_ => 'ئۆرۆگوای'; + + @override + String get uz_ => 'ئۆزبەکستان'; + + @override + String get va_ => 'کورسی پیرۆز (وڵاتی ڤاتیکان سیتی)'; + + @override + String get vc_ => 'سانت ڤینسێنت و گرێنادین'; + + @override + String get ve_ => 'ڤەنزوێلا'; + + @override + String get vg_ => 'دوورگەکانی ڤێرجینیا، بەریتانیا'; + + @override + String get vi_ => 'دوورگەکانی ڤێرجینیا، ئەمریکا.'; + + @override + String get vn_ => 'ڤێتنام'; + + @override + String get vu_ => 'ڤانواتو'; + + @override + String get wf_ => 'والیس و فوتونا'; + + @override + String get ws_ => 'ساموا'; + + @override + String get ye_ => 'یەمەن'; + + @override + String get yt_ => 'مایۆت'; + + @override + String get za_ => 'باشوری ئەفریقا'; + + @override + String get zm_ => 'زامبیا'; + + @override + String get zw_ => 'زیمبابۆی'; +} diff --git a/lib/l10n/generated/phone_field_localization_nb.dart b/lib/l10n/generated/phone_field_localization_nb.dart index 6789e8dc..2fbdc576 100644 --- a/lib/l10n/generated/phone_field_localization_nb.dart +++ b/lib/l10n/generated/phone_field_localization_nb.dart @@ -22,6 +22,9 @@ class PhoneFieldLocalizationNb extends PhoneFieldLocalization { @override String get noResultMessage => 'Ingen resultater'; + @override + String get search => 'Search'; + @override String get ac_ => 'Ascension Island'; diff --git a/lib/l10n/generated/phone_field_localization_nl.dart b/lib/l10n/generated/phone_field_localization_nl.dart index d8e4f4b5..efa85fb7 100644 --- a/lib/l10n/generated/phone_field_localization_nl.dart +++ b/lib/l10n/generated/phone_field_localization_nl.dart @@ -22,6 +22,9 @@ class PhoneFieldLocalizationNl extends PhoneFieldLocalization { @override String get noResultMessage => 'Geen resultaat'; + @override + String get search => 'Search'; + @override String get ac_ => 'Hemelvaart Eiland'; diff --git a/lib/l10n/generated/phone_field_localization_pt.dart b/lib/l10n/generated/phone_field_localization_pt.dart index 76ae0d1a..c6ab94cf 100644 --- a/lib/l10n/generated/phone_field_localization_pt.dart +++ b/lib/l10n/generated/phone_field_localization_pt.dart @@ -22,6 +22,9 @@ class PhoneFieldLocalizationPt extends PhoneFieldLocalization { @override String get noResultMessage => 'Sem resultado'; + @override + String get search => 'Search'; + @override String get ac_ => 'Ilha da Ascensão'; diff --git a/lib/l10n/generated/phone_field_localization_ru.dart b/lib/l10n/generated/phone_field_localization_ru.dart index 0d1e0123..badd55c0 100644 --- a/lib/l10n/generated/phone_field_localization_ru.dart +++ b/lib/l10n/generated/phone_field_localization_ru.dart @@ -22,6 +22,9 @@ class PhoneFieldLocalizationRu extends PhoneFieldLocalization { @override String get noResultMessage => 'Безрезультатно'; + @override + String get search => 'Search'; + @override String get ac_ => 'Остров Вознесения'; diff --git a/lib/l10n/generated/phone_field_localization_sv.dart b/lib/l10n/generated/phone_field_localization_sv.dart index d4decae0..3195ba51 100644 --- a/lib/l10n/generated/phone_field_localization_sv.dart +++ b/lib/l10n/generated/phone_field_localization_sv.dart @@ -22,6 +22,9 @@ class PhoneFieldLocalizationSv extends PhoneFieldLocalization { @override String get noResultMessage => 'Inget resultat'; + @override + String get search => 'Search'; + @override String get ac_ => 'Ascension Island'; diff --git a/lib/l10n/generated/phone_field_localization_tr.dart b/lib/l10n/generated/phone_field_localization_tr.dart index a04d2cef..65507e4a 100644 --- a/lib/l10n/generated/phone_field_localization_tr.dart +++ b/lib/l10n/generated/phone_field_localization_tr.dart @@ -22,6 +22,9 @@ class PhoneFieldLocalizationTr extends PhoneFieldLocalization { @override String get noResultMessage => 'Sonuç yok'; + @override + String get search => 'Search'; + @override String get ac_ => 'Yükselme adası'; diff --git a/lib/l10n/generated/phone_field_localization_uk.dart b/lib/l10n/generated/phone_field_localization_uk.dart index 1647cfaf..d40b7261 100644 --- a/lib/l10n/generated/phone_field_localization_uk.dart +++ b/lib/l10n/generated/phone_field_localization_uk.dart @@ -22,6 +22,9 @@ class PhoneFieldLocalizationUk extends PhoneFieldLocalization { @override String get noResultMessage => 'Немає результату'; + @override + String get search => 'Search'; + @override String get ac_ => 'Острів Вознесіння'; diff --git a/lib/l10n/generated/phone_field_localization_uz.dart b/lib/l10n/generated/phone_field_localization_uz.dart index 9451ad63..b59e7450 100644 --- a/lib/l10n/generated/phone_field_localization_uz.dart +++ b/lib/l10n/generated/phone_field_localization_uz.dart @@ -22,6 +22,9 @@ class PhoneFieldLocalizationUz extends PhoneFieldLocalization { @override String get noResultMessage => 'Ma\'lumot topilmadi'; + @override + String get search => 'Search'; + @override String get ac_ => 'Ascension Island'; diff --git a/lib/l10n/generated/phone_field_localization_zh.dart b/lib/l10n/generated/phone_field_localization_zh.dart index a5d67983..eeaa18aa 100644 --- a/lib/l10n/generated/phone_field_localization_zh.dart +++ b/lib/l10n/generated/phone_field_localization_zh.dart @@ -22,6 +22,9 @@ class PhoneFieldLocalizationZh extends PhoneFieldLocalization { @override String get noResultMessage => '没有结果'; + @override + String get search => 'Search'; + @override String get ac_ => '阿森松岛'; diff --git a/lib/phone_form_field.dart b/lib/phone_form_field.dart index 64653355..6dd2ba7a 100644 --- a/lib/phone_form_field.dart +++ b/lib/phone_form_field.dart @@ -11,7 +11,6 @@ export 'l10n/generated/phone_field_localization.dart'; export 'src/phone_controller.dart'; export 'src/country/localized_country.dart'; -export 'src/country_selection/localized_country_registry.dart'; export 'package:phone_numbers_parser/phone_numbers_parser.dart' show PhoneNumber, PhoneNumberType, IsoCode; diff --git a/lib/src/country/localized_country.dart b/lib/src/country/localized_country.dart index 33f47d6c..b525814b 100644 --- a/lib/src/country/localized_country.dart +++ b/lib/src/country/localized_country.dart @@ -3,7 +3,6 @@ import 'package:phone_form_field/l10n/generated/phone_field_localization_en.dart import 'package:phone_form_field/phone_form_field.dart'; import 'package:phone_form_field/src/country/localize_country.dart'; import 'package:phone_numbers_parser/metadata.dart'; -import 'package:phone_numbers_parser/phone_numbers_parser.dart'; /// Country regroup informations for displaying a list of countries class LocalizedCountry { diff --git a/lib/src/country_selection/country_finder.dart b/lib/src/country_selection/country_finder.dart index 8cc35250..fa950df4 100644 --- a/lib/src/country_selection/country_finder.dart +++ b/lib/src/country_selection/country_finder.dart @@ -2,12 +2,17 @@ import 'package:diacritic/diacritic.dart'; import 'package:phone_form_field/phone_form_field.dart'; +import 'package:phone_form_field/src/validation/allowed_characters.dart'; class CountryFinder { List whereText({ required String text, required List countries, }) { + // remove + if search text starts with + + if (text.startsWith(AllowedCharacters.plus)) { + text = text.substring(1); + } // reset search if (text.isEmpty) { return countries; diff --git a/lib/src/country_selection/country_selector.dart b/lib/src/country_selection/country_selector.dart index 1510cd73..8acbdd4f 100644 --- a/lib/src/country_selection/country_selector.dart +++ b/lib/src/country_selection/country_selector.dart @@ -1,13 +1,10 @@ import 'package:circle_flags/circle_flags.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:phone_form_field/l10n/generated/phone_field_localization.dart'; -import 'package:phone_form_field/l10n/generated/phone_field_localization_en.dart'; -import 'package:phone_form_field/src/country/localize_country.dart'; +import 'package:phone_form_field/src/country_selection/country_selector_controller.dart'; import 'package:phone_numbers_parser/phone_numbers_parser.dart'; import '../country/localized_country.dart'; -import 'country_finder.dart'; import 'country_list_view.dart'; import 'search_box.dart'; @@ -87,47 +84,30 @@ class CountrySelector extends StatefulWidget { } class CountrySelectorState extends State { - final countryFinder = CountryFinder(); - List _localizedCountries = []; - List _filteredLocalizedCountries = []; - List _favoriteLocalizedCountries = []; - List _filteredFavoriteLocalizedCountries = []; + late final CountrySelectorController _controller; + String _searchedText = ''; @override didChangeDependencies() { super.didChangeDependencies(); - _localizedCountries = _buildLocalizedCountryList(context, widget.countries); - _favoriteLocalizedCountries = - _buildLocalizedCountryList(context, widget.favoriteCountries); - _filteredLocalizedCountries = _localizedCountries; - } - - _buildLocalizedCountryList(BuildContext context, List isoCodes) { - final localization = - PhoneFieldLocalization.of(context) ?? PhoneFieldLocalizationEn(); - return isoCodes - .map((isoCode) => - LocalizedCountry(isoCode, localization.countryName(isoCode))) - .toList(); + _controller = CountrySelectorController( + context, + widget.countries, + widget.favoriteCountries, + ); + // language might have changed + _controller.search(_searchedText); } _onSearch(String searchedText) { - _filteredLocalizedCountries = countryFinder.whereText( - text: searchedText, - countries: _localizedCountries, - ); - _filteredFavoriteLocalizedCountries = countryFinder.whereText( - text: searchedText, - countries: _favoriteLocalizedCountries, - ); - setState(() {}); + _searchedText = searchedText; + _controller.search(searchedText); } onSubmitted() { - if (_filteredFavoriteLocalizedCountries.isNotEmpty) { - widget.onCountrySelected(_filteredFavoriteLocalizedCountries.first); - } else if (_filteredLocalizedCountries.isNotEmpty) { - widget.onCountrySelected(_filteredLocalizedCountries.first); + final first = _controller.findFirst(); + if (first != null) { + widget.onCountrySelected(first); } } @@ -147,30 +127,38 @@ class CountrySelectorState extends State { SizedBox( height: 70, width: double.infinity, - child: SearchBox( - autofocus: widget.searchAutofocus, - onChanged: _onSearch, - onSubmitted: onSubmitted, - decoration: widget.searchBoxDecoration, - style: widget.searchBoxTextStyle, - searchIconColor: widget.searchBoxIconColor, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: SearchBox( + autofocus: widget.searchAutofocus, + onChanged: _onSearch, + onSubmitted: onSubmitted, + decoration: widget.searchBoxDecoration, + style: widget.searchBoxTextStyle, + searchIconColor: widget.searchBoxIconColor, + ), ), ), const SizedBox(height: 16), const Divider(height: 0, thickness: 1.2), Flexible( - child: CountryListView( - favorites: _filteredFavoriteLocalizedCountries, - countries: _filteredLocalizedCountries, - showDialCode: widget.showCountryCode, - onTap: widget.onCountrySelected, - flagSize: widget.flagSize, - scrollController: widget.scrollController, - scrollPhysics: widget.scrollPhysics, - noResultMessage: widget.noResultMessage, - titleStyle: widget.titleStyle, - subtitleStyle: widget.subtitleStyle, - flagCache: widget.flagCache, + child: AnimatedBuilder( + animation: _controller, + builder: (context, _) { + return CountryListView( + countries: _controller.filteredCountries, + favorites: _controller.filteredFavorites, + showDialCode: widget.showCountryCode, + onTap: widget.onCountrySelected, + flagSize: widget.flagSize, + scrollController: widget.scrollController, + scrollPhysics: widget.scrollPhysics, + noResultMessage: widget.noResultMessage, + titleStyle: widget.titleStyle, + subtitleStyle: widget.subtitleStyle, + flagCache: widget.flagCache, + ); + }, ), ), ], diff --git a/lib/src/country_selection/country_selector_controller.dart b/lib/src/country_selection/country_selector_controller.dart new file mode 100644 index 00000000..dc640f03 --- /dev/null +++ b/lib/src/country_selection/country_selector_controller.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:phone_form_field/l10n/generated/phone_field_localization_en.dart'; +import 'package:phone_form_field/phone_form_field.dart'; +import 'package:phone_form_field/src/country/localize_country.dart'; +import 'package:phone_form_field/src/country_selection/country_finder.dart'; + +class CountrySelectorController with ChangeNotifier { + final _finder = CountryFinder(); + List _countries = []; + List _filteredCountries = []; + List _favoriteCountries = []; + List _filteredFavoriteCountries = []; + + List get filteredCountries => _filteredCountries; + List get filteredFavorites => _filteredFavoriteCountries; + + CountrySelectorController( + BuildContext context, + List countriesIsoCode, + List favoriteCountriesIsoCode, + ) { + _countries = _buildLocalizedCountryList(context, countriesIsoCode); + _favoriteCountries = + _buildLocalizedCountryList(context, favoriteCountriesIsoCode); + _filteredCountries = _countries; + } + + void search(String searchedText) { + _filteredCountries = _finder.whereText( + text: searchedText, + countries: _countries, + ); + _filteredFavoriteCountries = _finder.whereText( + text: searchedText, + countries: _favoriteCountries, + ); + notifyListeners(); + } + + LocalizedCountry? findFirst() { + if (_filteredFavoriteCountries.isNotEmpty) { + return _filteredFavoriteCountries.first; + } else if (_filteredCountries.isNotEmpty) { + return _filteredCountries.first; + } + return null; + } + + List _buildLocalizedCountryList( + BuildContext context, + List isoCodes, + ) { + // we need the localized names in order to search + final localization = + PhoneFieldLocalization.of(context) ?? PhoneFieldLocalizationEn(); + return isoCodes + .map((isoCode) => + LocalizedCountry(isoCode, localization.countryName(isoCode))) + .toList(); + } +} diff --git a/lib/src/country_selection/country_selector_navigator.dart b/lib/src/country_selection/country_selector_navigator.dart index 02fdecd8..a03d6ccd 100644 --- a/lib/src/country_selection/country_selector_navigator.dart +++ b/lib/src/country_selection/country_selector_navigator.dart @@ -84,7 +84,7 @@ abstract class CountrySelectorNavigator { ScrollPhysics? scrollPhysics, }) = DialogNavigator._; - const factory CountrySelectorNavigator.searchDelegate({ + const factory CountrySelectorNavigator.page({ List? countries, List? favorites, bool addSeparator, @@ -99,7 +99,7 @@ abstract class CountrySelectorNavigator { Color? searchBoxIconColor, ScrollPhysics? scrollPhysics, ThemeData? appBarTheme, - }) = SearchDelegateNavigator._; + }) = PageNavigator._; const factory CountrySelectorNavigator.bottomSheet({ List? countries, @@ -198,8 +198,8 @@ class DialogNavigator extends CountrySelectorNavigator { } } -class SearchDelegateNavigator extends CountrySelectorNavigator { - const SearchDelegateNavigator._({ +class PageNavigator extends CountrySelectorNavigator { + const PageNavigator._({ super.countries, super.favorites, super.addSeparator, @@ -218,12 +218,12 @@ class SearchDelegateNavigator extends CountrySelectorNavigator { final ThemeData? appBarTheme; - CountrySelectorSearchDelegate _getCountrySelectorSearchDelegate({ + CountrySelectorPage _getCountrySelectorPage({ required ValueChanged onCountrySelected, required FlagCache flagCache, ScrollController? scrollController, }) { - return CountrySelectorSearchDelegate( + return CountrySelectorPage( onCountrySelected: onCountrySelected, scrollController: scrollController, addFavoritesSeparator: addSeparator, @@ -235,18 +235,20 @@ class SearchDelegateNavigator extends CountrySelectorNavigator { titleStyle: titleStyle, subtitleStyle: subtitleStyle, flagCache: flagCache, - customAppBarTheme: appBarTheme, ); } @override Future navigate( - BuildContext context, FlagCache flagCache) { - return showSearch( - context: context, - delegate: _getCountrySelectorSearchDelegate( - onCountrySelected: (country) => Navigator.pop(context, country), - flagCache: flagCache, + BuildContext context, + FlagCache flagCache, + ) { + return Navigator.of(context).push( + MaterialPageRoute( + builder: (ctx) => _getCountrySelectorPage( + onCountrySelected: (country) => Navigator.pop(context, country), + flagCache: flagCache, + ), ), ); } diff --git a/lib/src/country_selection/country_selector_page.dart b/lib/src/country_selection/country_selector_page.dart index 790e1bc5..34607e94 100644 --- a/lib/src/country_selection/country_selector_page.dart +++ b/lib/src/country_selection/country_selector_page.dart @@ -3,21 +3,23 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:phone_form_field/l10n/generated/phone_field_localization.dart'; import 'package:phone_form_field/l10n/generated/phone_field_localization_en.dart'; -import 'package:phone_form_field/src/country_selection/localized_country_registry.dart'; +import 'package:phone_form_field/src/country_selection/country_selector_controller.dart'; import 'package:phone_numbers_parser/phone_numbers_parser.dart'; -import 'country_finder.dart'; import '../country/localized_country.dart'; import 'country_list_view.dart'; +import 'search_box.dart'; -class CountrySelectorSearchDelegate extends SearchDelegate { - late CountryFinder _countryFinder; - late CountryFinder _favoriteCountryFinder; - +class CountrySelectorPage extends StatefulWidget { /// List of countries to display in the selector /// Value optional in constructor. /// when omitted, the full country list is displayed - final List countriesIso; + final List countries; + + /// Determine the countries to be displayed on top of the list + /// Check [addFavoritesSeparator] property to enable/disable adding a + /// list divider between favorites and others defaults countries + final List favoriteCountries; /// Callback triggered when user select a country final ValueChanged onCountrySelected; @@ -28,11 +30,6 @@ class CountrySelectorSearchDelegate extends SearchDelegate { /// The [ScrollPhysics] of the Country List final ScrollPhysics? scrollPhysics; - /// Determine the countries to be displayed on top of the list - /// Check [addFavoritesSeparator] property to enable/disable adding a - /// list divider between favorites and others defaults countries - final List favoriteCountriesIso; - /// Whether to add a list divider between favorites & defaults /// countries. final bool addFavoritesSeparator; @@ -46,21 +43,26 @@ class CountrySelectorSearchDelegate extends SearchDelegate { /// whether the search input is auto focussed final bool searchAutofocus; - final double flagSize; - /// Override default title TextStyle + /// The [TextStyle] of the country subtitle + final TextStyle? subtitleStyle; + + /// The [TextStyle] of the country title final TextStyle? titleStyle; - /// Override default subtitle TextStyle - final TextStyle? subtitleStyle; + /// The [InputDecoration] of the Search Box + final InputDecoration? searchBoxDecoration; - final FlagCache? flagCache; + /// The [TextStyle] of the Search Box + final TextStyle? searchBoxTextStyle; - /// Override default app bar theme - final ThemeData? customAppBarTheme; + /// The [Color] of the Search Icon in the Search Box + final Color? searchBoxIconColor; + final double flagSize; + final FlagCache flagCache; - CountrySelectorSearchDelegate({ - Key? key, + const CountrySelectorPage({ + super.key, required this.onCountrySelected, required this.flagCache, this.scrollController, @@ -68,78 +70,86 @@ class CountrySelectorSearchDelegate extends SearchDelegate { this.addFavoritesSeparator = true, this.showCountryCode = false, this.noResultMessage, - List favoriteCountries = const [], - List countries = IsoCode.values, + this.favoriteCountries = const [], + this.countries = IsoCode.values, this.searchAutofocus = kIsWeb, - this.flagSize = 40, - this.titleStyle, this.subtitleStyle, - this.customAppBarTheme, - }) : countriesIso = countries, - favoriteCountriesIso = favoriteCountries; + this.titleStyle, + this.searchBoxDecoration, + this.searchBoxTextStyle, + this.searchBoxIconColor, + this.flagSize = 40, + }); @override - List? buildActions(BuildContext context) { - return [ - IconButton( - onPressed: () => query = '', - icon: const Icon(Icons.clear), - ), - ]; - } - - void _initIfRequired(BuildContext context) { - final localization = - PhoneFieldLocalization.of(context) ?? PhoneFieldLocalizationEn(); - - final notFavoriteCountries = countryRegistry.whereIsoIn( - countriesIso, - omit: favoriteCountriesIso, - ); - final favoriteCountries = countryRegistry.whereIsoIn(favoriteCountriesIso); - _countryFinder = CountryFinder(notFavoriteCountries); - _favoriteCountryFinder = CountryFinder(favoriteCountries, sort: false); - } + CountrySelectorPageState createState() => CountrySelectorPageState(); +} - void _updateList() { - _countryFinder.whereText(text: query, countries: ); - _favoriteCountryFinder.whereText(query); - } +class CountrySelectorPageState extends State { + late final CountrySelectorController _controller; + String searchText = ''; @override - Widget? buildLeading(BuildContext context) { - return BackButton( - onPressed: () => Navigator.of(context).pop(), + didChangeDependencies() { + super.didChangeDependencies(); + _controller = CountrySelectorController( + context, + widget.countries, + widget.favoriteCountries, ); + // language might have changed + _controller.search(searchText); } - @override - Widget buildSuggestions(BuildContext context) { - _initIfRequired(context); - _updateList(); - - return CountryListView( - favorites: _favoriteCountryFinder.filteredCountries, - countries: _countryFinder.filteredCountries, - showDialCode: showCountryCode, - onTap: onCountrySelected, - flagSize: flagSize, - scrollController: scrollController, - scrollPhysics: scrollPhysics, - noResultMessage: noResultMessage, - titleStyle: titleStyle, - subtitleStyle: subtitleStyle, - flagCache: flagCache, - ); + _onSearch(String searchedText) { + _controller.search(searchedText); } - @override - Widget buildResults(BuildContext context) { - return buildSuggestions(context); + onSubmitted() { + final first = _controller.findFirst(); + if (first != null) { + widget.onCountrySelected(first); + } } @override - ThemeData appBarTheme(BuildContext context) { - return customAppBarTheme ?? super.appBarTheme(context); + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 2, + shadowColor: Theme.of(context).colorScheme.shadow, + title: SearchBox( + autofocus: widget.searchAutofocus, + onChanged: _onSearch, + onSubmitted: onSubmitted, + decoration: widget.searchBoxDecoration ?? + InputDecoration( + border: InputBorder.none, + hintText: PhoneFieldLocalization.of(context)?.search ?? + PhoneFieldLocalizationEn().search, + ), + style: widget.searchBoxTextStyle, + searchIconColor: widget.searchBoxIconColor, + ), + ), + body: AnimatedBuilder( + animation: _controller, + builder: (context, _) { + return CountryListView( + countries: _controller.filteredCountries, + favorites: _controller.filteredFavorites, + showDialCode: widget.showCountryCode, + onTap: widget.onCountrySelected, + flagSize: widget.flagSize, + scrollController: widget.scrollController, + scrollPhysics: widget.scrollPhysics, + noResultMessage: widget.noResultMessage, + titleStyle: widget.titleStyle, + subtitleStyle: widget.subtitleStyle, + flagCache: widget.flagCache, + ); + }, + ), + ); } } diff --git a/lib/src/country_selection/localized_country_registry.dart b/lib/src/country_selection/localized_country_registry.dart deleted file mode 100644 index 78f638e5..00000000 --- a/lib/src/country_selection/localized_country_registry.dart +++ /dev/null @@ -1,288 +0,0 @@ -import 'package:phone_form_field/phone_form_field.dart'; - -// NOTE: this is ugly, something else needs to be used. -// If someone has the hearth for some refactor... - -/// this saves the localized countries for each country -/// for a given language in a cache, so it does not -/// have to be recreated - -class LocalizedCountryRegistry { - final PhoneFieldLocalization _localization; - - static LocalizedCountryRegistry? _instance; - - late final Map _localizedCountries = - Map.fromIterable( - // remove iso codes that do not have a traduction yet.. - IsoCode.values.where((iso) => _names.containsKey(iso)), - value: (isoCode) => LocalizedCountry(isoCode, _names[isoCode]!), - ); - - LocalizedCountryRegistry._(this._localization); - - factory LocalizedCountryRegistry.cached(PhoneFieldLocalization localization) { - final instance = _instance; - if (instance != null && instance._localization == localization) { - return instance; - } - return LocalizedCountryRegistry._(localization); - } - - LocalizedCountry? find(IsoCode isoCode) => _localizedCountries[isoCode]; - - /// gets localized countries from isocodes - List whereIsoIn( - List isoCodes, { - List omit = const [], - }) { - final omitSet = Set.from(omit); - return isoCodes - .where((isoCode) => !omitSet.contains(isoCode)) - .where((isoCode) => _localizedCountries.containsKey(isoCode)) - .map((iso) => _localizedCountries[iso]!) - .toList(); - } - - late final Map _names = { - IsoCode.AC: _localization.ac_, - IsoCode.AD: _localization.ad_, - IsoCode.AE: _localization.ae_, - IsoCode.AF: _localization.af_, - IsoCode.AG: _localization.ag_, - IsoCode.AI: _localization.ai_, - IsoCode.AL: _localization.al_, - IsoCode.AM: _localization.am_, - IsoCode.AO: _localization.ao_, - IsoCode.AR: _localization.ar_, - IsoCode.AS: _localization.as_, - IsoCode.AT: _localization.at_, - IsoCode.AU: _localization.au_, - IsoCode.AW: _localization.aw_, - IsoCode.AX: _localization.ax_, - IsoCode.AZ: _localization.az_, - IsoCode.BA: _localization.ba_, - IsoCode.BB: _localization.bb_, - IsoCode.BD: _localization.bd_, - IsoCode.BE: _localization.be_, - IsoCode.BF: _localization.bf_, - IsoCode.BG: _localization.bg_, - IsoCode.BH: _localization.bh_, - IsoCode.BI: _localization.bi_, - IsoCode.BJ: _localization.bj_, - IsoCode.BL: _localization.bl_, - IsoCode.BM: _localization.bm_, - IsoCode.BN: _localization.bn_, - IsoCode.BO: _localization.bo_, - IsoCode.BQ: _localization.bq_, - IsoCode.BR: _localization.br_, - IsoCode.BS: _localization.bs_, - IsoCode.BT: _localization.bt_, - IsoCode.BW: _localization.bw_, - IsoCode.BY: _localization.by_, - IsoCode.BZ: _localization.bz_, - IsoCode.CA: _localization.ca_, - IsoCode.CC: _localization.cc_, - IsoCode.CD: _localization.cd_, - IsoCode.CF: _localization.cf_, - IsoCode.CG: _localization.cg_, - IsoCode.CH: _localization.ch_, - IsoCode.CI: _localization.ci_, - IsoCode.CK: _localization.ck_, - IsoCode.CL: _localization.cl_, - IsoCode.CM: _localization.cm_, - IsoCode.CN: _localization.cn_, - IsoCode.CO: _localization.co_, - IsoCode.CR: _localization.cr_, - IsoCode.CU: _localization.cu_, - IsoCode.CV: _localization.cv_, - IsoCode.CX: _localization.cx_, - IsoCode.CY: _localization.cy_, - IsoCode.CZ: _localization.cz_, - IsoCode.DE: _localization.de_, - IsoCode.DJ: _localization.dj_, - IsoCode.DK: _localization.dk_, - IsoCode.DM: _localization.dm_, - IsoCode.DO: _localization.do_, - IsoCode.DZ: _localization.dz_, - IsoCode.EC: _localization.ec_, - IsoCode.EE: _localization.ee_, - IsoCode.EG: _localization.eg_, - IsoCode.ER: _localization.er_, - IsoCode.ES: _localization.es_, - IsoCode.ET: _localization.et_, - IsoCode.FI: _localization.fi_, - IsoCode.FJ: _localization.fj_, - IsoCode.FK: _localization.fk_, - IsoCode.FM: _localization.fm_, - IsoCode.FO: _localization.fo_, - IsoCode.FR: _localization.fr_, - IsoCode.GA: _localization.ga_, - IsoCode.GB: _localization.gb_, - IsoCode.GD: _localization.gd_, - IsoCode.GE: _localization.ge_, - IsoCode.GF: _localization.gf_, - IsoCode.GG: _localization.gg_, - IsoCode.GH: _localization.gh_, - IsoCode.GI: _localization.gi_, - IsoCode.GL: _localization.gl_, - IsoCode.GM: _localization.gm_, - IsoCode.GN: _localization.gn_, - IsoCode.GP: _localization.gp_, - IsoCode.GQ: _localization.gq_, - IsoCode.GR: _localization.gr_, - IsoCode.GT: _localization.gt_, - IsoCode.GU: _localization.gu_, - IsoCode.GW: _localization.gw_, - IsoCode.GY: _localization.gy_, - IsoCode.HK: _localization.hk_, - IsoCode.HN: _localization.hn_, - IsoCode.HR: _localization.hr_, - IsoCode.HT: _localization.ht_, - IsoCode.HU: _localization.hu_, - IsoCode.ID: _localization.id_, - IsoCode.IE: _localization.ie_, - IsoCode.IL: _localization.il_, - IsoCode.IM: _localization.im_, - IsoCode.IN: _localization.in_, - IsoCode.IO: _localization.io_, - IsoCode.IQ: _localization.iq_, - IsoCode.IR: _localization.ir_, - IsoCode.IS: _localization.is_, - IsoCode.IT: _localization.it_, - IsoCode.JE: _localization.je_, - IsoCode.JM: _localization.jm_, - IsoCode.JO: _localization.jo_, - IsoCode.JP: _localization.jp_, - IsoCode.KE: _localization.ke_, - IsoCode.KG: _localization.kg_, - IsoCode.KH: _localization.kh_, - IsoCode.KI: _localization.ki_, - IsoCode.KM: _localization.km_, - IsoCode.KN: _localization.kn_, - IsoCode.KP: _localization.kp_, - IsoCode.KR: _localization.kr_, - IsoCode.KW: _localization.kw_, - IsoCode.KY: _localization.ky_, - IsoCode.KZ: _localization.kz_, - IsoCode.LA: _localization.la_, - IsoCode.LB: _localization.lb_, - IsoCode.LC: _localization.lc_, - IsoCode.LI: _localization.li_, - IsoCode.LK: _localization.lk_, - IsoCode.LR: _localization.lr_, - IsoCode.LS: _localization.ls_, - IsoCode.LT: _localization.lt_, - IsoCode.LU: _localization.lu_, - IsoCode.LV: _localization.lv_, - IsoCode.LY: _localization.ly_, - IsoCode.MA: _localization.ma_, - IsoCode.MC: _localization.mc_, - IsoCode.MD: _localization.md_, - IsoCode.ME: _localization.me_, - IsoCode.MF: _localization.mf_, - IsoCode.MG: _localization.mg_, - IsoCode.MH: _localization.mh_, - IsoCode.MK: _localization.mk_, - IsoCode.ML: _localization.ml_, - IsoCode.MM: _localization.mm_, - IsoCode.MN: _localization.mn_, - IsoCode.MO: _localization.mo_, - IsoCode.MP: _localization.mp_, - IsoCode.MQ: _localization.mq_, - IsoCode.MR: _localization.mr_, - IsoCode.MS: _localization.ms_, - IsoCode.MT: _localization.mt_, - IsoCode.MU: _localization.mu_, - IsoCode.MV: _localization.mv_, - IsoCode.MW: _localization.mw_, - IsoCode.MX: _localization.mx_, - IsoCode.MY: _localization.my_, - IsoCode.MZ: _localization.mz_, - IsoCode.NA: _localization.na_, - IsoCode.NC: _localization.nc_, - IsoCode.NE: _localization.ne_, - IsoCode.NF: _localization.nf_, - IsoCode.NG: _localization.ng_, - IsoCode.NI: _localization.ni_, - IsoCode.NL: _localization.nl_, - IsoCode.NO: _localization.no_, - IsoCode.NP: _localization.np_, - IsoCode.NR: _localization.nr_, - IsoCode.NU: _localization.nu_, - IsoCode.NZ: _localization.nz_, - IsoCode.OM: _localization.om_, - IsoCode.PA: _localization.pa_, - IsoCode.PE: _localization.pe_, - IsoCode.PF: _localization.pf_, - IsoCode.PG: _localization.pg_, - IsoCode.PH: _localization.ph_, - IsoCode.PK: _localization.pk_, - IsoCode.PL: _localization.pl_, - IsoCode.PM: _localization.pm_, - IsoCode.PR: _localization.pr_, - IsoCode.PS: _localization.ps_, - IsoCode.PT: _localization.pt_, - IsoCode.PW: _localization.pw_, - IsoCode.PY: _localization.py_, - IsoCode.QA: _localization.qa_, - IsoCode.RE: _localization.re_, - IsoCode.RO: _localization.ro_, - IsoCode.RS: _localization.rs_, - IsoCode.RU: _localization.ru_, - IsoCode.RW: _localization.rw_, - IsoCode.SA: _localization.sa_, - IsoCode.SB: _localization.sb_, - IsoCode.SC: _localization.sc_, - IsoCode.SD: _localization.sd_, - IsoCode.SE: _localization.se_, - IsoCode.SG: _localization.sg_, - IsoCode.SI: _localization.si_, - IsoCode.SK: _localization.sk_, - IsoCode.SL: _localization.sl_, - IsoCode.SM: _localization.sm_, - IsoCode.SN: _localization.sn_, - IsoCode.SO: _localization.so_, - IsoCode.SR: _localization.sr_, - IsoCode.SS: _localization.ss_, - IsoCode.ST: _localization.st_, - IsoCode.SV: _localization.sv_, - IsoCode.SY: _localization.sy_, - IsoCode.SZ: _localization.sz_, - IsoCode.TA: _localization.ta_, - IsoCode.TC: _localization.tc_, - IsoCode.TD: _localization.td_, - IsoCode.TG: _localization.tg_, - IsoCode.TH: _localization.th_, - IsoCode.TJ: _localization.tj_, - IsoCode.TK: _localization.tk_, - IsoCode.TL: _localization.tl_, - IsoCode.TM: _localization.tm_, - IsoCode.TN: _localization.tn_, - IsoCode.TO: _localization.to_, - IsoCode.TR: _localization.tr_, - IsoCode.TT: _localization.tt_, - IsoCode.TV: _localization.tv_, - IsoCode.TW: _localization.tw_, - IsoCode.TZ: _localization.tz_, - IsoCode.UA: _localization.ua_, - IsoCode.UG: _localization.ug_, - IsoCode.US: _localization.us_, - IsoCode.UY: _localization.uy_, - IsoCode.UZ: _localization.uz_, - IsoCode.VA: _localization.va_, - IsoCode.VC: _localization.vc_, - IsoCode.VE: _localization.ve_, - IsoCode.VG: _localization.vg_, - IsoCode.VI: _localization.vi_, - IsoCode.VN: _localization.vn_, - IsoCode.VU: _localization.vu_, - IsoCode.WF: _localization.wf_, - IsoCode.WS: _localization.ws_, - IsoCode.YE: _localization.ye_, - IsoCode.YT: _localization.yt_, - IsoCode.ZA: _localization.za_, - IsoCode.ZM: _localization.zm_, - IsoCode.ZW: _localization.zw_ - }; -} diff --git a/lib/src/country_selection/search_box.dart b/lib/src/country_selection/search_box.dart index 194748c0..80a789a3 100644 --- a/lib/src/country_selection/search_box.dart +++ b/lib/src/country_selection/search_box.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:phone_form_field/l10n/generated/phone_field_localization.dart'; class SearchBox extends StatefulWidget { final Function(String) onChanged; @@ -47,32 +48,27 @@ class _SearchBoxState extends State { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), - child: TextField( - autofocus: widget.autofocus, - onChanged: handleChange, - onSubmitted: (_) => widget.onSubmitted(), - cursorColor: widget.style?.color, - style: widget.style, - autofillHints: const [AutofillHints.countryName], - decoration: widget.decoration ?? - InputDecoration( - prefixIcon: Icon( - Icons.search, - color: widget.searchIconColor ?? - (Theme.of(context).brightness == Brightness.dark - ? Colors.white54 - : Colors.black38), - ), - filled: true, - isDense: true, - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(20), - ), + return TextField( + autofocus: widget.autofocus, + onChanged: handleChange, + onSubmitted: (_) => widget.onSubmitted(), + cursorColor: widget.style?.color, + style: widget.style ?? Theme.of(context).textTheme.titleLarge, + autofillHints: const [AutofillHints.countryName], + decoration: widget.decoration ?? + InputDecoration( + prefixIcon: const Icon( + Icons.search, + size: 24, ), - ), + filled: true, + isDense: true, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(20), + ), + hintText: PhoneFieldLocalization.of(context)?.search, + ), ); } } diff --git a/lib/src/phone_form_field.dart b/lib/src/phone_form_field.dart index 8f4d2c2f..d4f956ba 100644 --- a/lib/src/phone_form_field.dart +++ b/lib/src/phone_form_field.dart @@ -109,7 +109,7 @@ class PhoneFormField extends FormField { this.focusNode, bool showFlagInInput = true, CountrySelectorNavigator countrySelectorNavigator = - const CountrySelectorNavigator.searchDelegate(), + const CountrySelectorNavigator.page(), Function(PhoneNumber?)? super.onSaved, this.defaultCountry = IsoCode.US, InputDecoration decoration = From 0513504439ef1dd2f4529484baeb90df61c9b51b Mon Sep 17 00:00:00 2001 From: cedvdb Date: Fri, 2 Feb 2024 12:55:13 +0100 Subject: [PATCH 05/25] improve search ux --- lib/src/country_selection/country_finder.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/country_selection/country_finder.dart b/lib/src/country_selection/country_finder.dart index fa950df4..f75456b1 100644 --- a/lib/src/country_selection/country_finder.dart +++ b/lib/src/country_selection/country_finder.dart @@ -2,7 +2,6 @@ import 'package:diacritic/diacritic.dart'; import 'package:phone_form_field/phone_form_field.dart'; -import 'package:phone_form_field/src/validation/allowed_characters.dart'; class CountryFinder { List whereText({ @@ -10,7 +9,7 @@ class CountryFinder { required List countries, }) { // remove + if search text starts with + - if (text.startsWith(AllowedCharacters.plus)) { + if (text.startsWith('+')) { text = text.substring(1); } // reset search From 0bd93087c677930211456dd17a5347ce268db917 Mon Sep 17 00:00:00 2001 From: cedvdb Date: Fri, 2 Feb 2024 14:06:42 +0100 Subject: [PATCH 06/25] refactor --- CHANGELOG.md | 9 + example/lib/main.dart | 3 +- .../country_selection/country_list_view.dart | 26 +- .../country_selection/country_selector.dart | 2 + .../country_selector_controller.dart | 3 +- .../country_selector_navigator.dart | 4 +- .../country_selector_page.dart | 1 + lib/src/country_selection/no_result_view.dart | 20 ++ lib/src/country_selection/search_box.dart | 15 +- lib/src/phone_field_state.dart | 6 +- test/_country_selector_test.dart | 301 +++++------------- 11 files changed, 144 insertions(+), 246 deletions(-) create mode 100644 lib/src/country_selection/no_result_view.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 3524e354..b9b3477a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [9.0.0] +- Various fixes for country selection UX +- Improve accessibility +- [Breaking] : `SearchDelegateNavigator` changed into +`PageNavigator`. +- [Breaking] : `LocalizedCountryRegistry` removed. If you were using it to localize a country name, you should use `PhoneFieldLocalization.of(context).countryName(isoCode)`. +- Internal refactor in the hope of making contributions easier + + ## [8.1.1] - Upgraded phone_numbers_parser lib to 8.1.0 - Added norwegian language (PR #203) thanks @sidlatau diff --git a/example/lib/main.dart b/example/lib/main.dart index 67e74abb..1aeb855d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -210,7 +210,8 @@ class PhoneFormFieldScreenState extends State { }, items: const [ DropdownMenuItem( - value: CountrySelectorNavigator.bottomSheet(), + value: CountrySelectorNavigator.bottomSheet( + favorites: [IsoCode.GU, IsoCode.GY]), child: Text('Bottom sheet'), ), DropdownMenuItem( diff --git a/lib/src/country_selection/country_list_view.dart b/lib/src/country_selection/country_list_view.dart index a3b8e8ba..23dbfe9a 100644 --- a/lib/src/country_selection/country_list_view.dart +++ b/lib/src/country_selection/country_list_view.dart @@ -1,8 +1,9 @@ import 'package:circle_flags/circle_flags.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import '../../l10n/generated/phone_field_localization.dart'; import '../country/localized_country.dart'; +import 'no_result_view.dart'; class CountryListView extends StatelessWidget { /// Callback function triggered when user select a country @@ -46,24 +47,21 @@ class CountryListView extends StatelessWidget { this.subtitleStyle, this.titleStyle, }) { - _allListElement = [ - ...favorites, - if (favorites.isNotEmpty) null, // delimiter - ...countries, - ]; + if (listEquals(countries, favorites)) { + _allListElement = countries; + } else { + _allListElement = [ + ...favorites, + if (favorites.isNotEmpty) null, // delimiter + ...countries, + ]; + } } @override Widget build(BuildContext context) { if (_allListElement.isEmpty) { - return Center( - child: Text( - noResultMessage ?? - PhoneFieldLocalization.of(context)?.noResultMessage ?? - 'No result found', - key: const ValueKey('no-result'), - ), - ); + return NoResultView(title: noResultMessage); } return ListView.builder( physics: scrollPhysics, diff --git a/lib/src/country_selection/country_selector.dart b/lib/src/country_selection/country_selector.dart index 8acbdd4f..3582863e 100644 --- a/lib/src/country_selection/country_selector.dart +++ b/lib/src/country_selection/country_selector.dart @@ -8,6 +8,8 @@ import '../country/localized_country.dart'; import 'country_list_view.dart'; import 'search_box.dart'; +/// Displays a country selector with a search box at the top +/// and a list of countries underneath. class CountrySelector extends StatefulWidget { /// List of countries to display in the selector /// Value optional in constructor. diff --git a/lib/src/country_selection/country_selector_controller.dart b/lib/src/country_selection/country_selector_controller.dart index dc640f03..b95d7651 100644 --- a/lib/src/country_selection/country_selector_controller.dart +++ b/lib/src/country_selection/country_selector_controller.dart @@ -56,6 +56,7 @@ class CountrySelectorController with ChangeNotifier { return isoCodes .map((isoCode) => LocalizedCountry(isoCode, localization.countryName(isoCode))) - .toList(); + .toList() + ..sort((a, b) => a.name.compareTo(b.name)); } } diff --git a/lib/src/country_selection/country_selector_navigator.dart b/lib/src/country_selection/country_selector_navigator.dart index a03d6ccd..e13b9e98 100644 --- a/lib/src/country_selection/country_selector_navigator.dart +++ b/lib/src/country_selection/country_selector_navigator.dart @@ -273,7 +273,9 @@ class BottomSheetNavigator extends CountrySelectorNavigator { @override Future navigate( - BuildContext context, FlagCache flagCache) { + BuildContext context, + FlagCache flagCache, + ) { LocalizedCountry? selected; final ctrl = showBottomSheet( context: context, diff --git a/lib/src/country_selection/country_selector_page.dart b/lib/src/country_selection/country_selector_page.dart index 34607e94..61db6ffb 100644 --- a/lib/src/country_selection/country_selector_page.dart +++ b/lib/src/country_selection/country_selector_page.dart @@ -10,6 +10,7 @@ import '../country/localized_country.dart'; import 'country_list_view.dart'; import 'search_box.dart'; +/// Same as [CountrySelector] but designed as a full page class CountrySelectorPage extends StatefulWidget { /// List of countries to display in the selector /// Value optional in constructor. diff --git a/lib/src/country_selection/no_result_view.dart b/lib/src/country_selection/no_result_view.dart new file mode 100644 index 00000000..646ad8d7 --- /dev/null +++ b/lib/src/country_selection/no_result_view.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:phone_form_field/l10n/generated/phone_field_localization.dart'; +import 'package:phone_form_field/l10n/generated/phone_field_localization_en.dart'; + +class NoResultView extends StatelessWidget { + final String? title; + const NoResultView({super.key, this.title}); + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + title ?? + PhoneFieldLocalization.of(context)?.noResultMessage ?? + PhoneFieldLocalizationEn().noResultMessage, + key: const ValueKey('no-result'), + ), + ); + } +} diff --git a/lib/src/country_selection/search_box.dart b/lib/src/country_selection/search_box.dart index 80a789a3..6f5fa340 100644 --- a/lib/src/country_selection/search_box.dart +++ b/lib/src/country_selection/search_box.dart @@ -31,19 +31,14 @@ class _SearchBoxState extends State { super.initState(); } - void handleChange(e) { - widget.onChanged(e); + void handleChange(text) { + widget.onChanged(text); - // detect length difference - final diff = e.length - _previousValue.length; - if (diff > 3) { - // more than 3 characters added, probably a paste / autofill of country name + final isAutofill = text.length > 3 && _previousValue == ''; + if (isAutofill) { widget.onSubmitted(); } - - setState(() { - _previousValue = e; - }); + _previousValue = text; } @override diff --git a/lib/src/phone_field_state.dart b/lib/src/phone_field_state.dart index 9ce024cc..ec7c0e00 100644 --- a/lib/src/phone_field_state.dart +++ b/lib/src/phone_field_state.dart @@ -40,10 +40,10 @@ class PhoneFieldState extends State { SystemChannels.textInput.invokeMethod('TextInput.show'); } - // TODO: Would be cleaner if we could infer it from - // TextField._defaultContextMenuBuilder, but it's private static Widget _defaultContextMenuBuilder( - BuildContext context, EditableTextState editableTextState) { + BuildContext context, + EditableTextState editableTextState, + ) { return AdaptiveTextSelectionToolbar.editableText( editableTextState: editableTextState, ); diff --git a/test/_country_selector_test.dart b/test/_country_selector_test.dart index 4f7e4d9d..8c88b270 100644 --- a/test/_country_selector_test.dart +++ b/test/_country_selector_test.dart @@ -3,48 +3,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:phone_form_field/phone_form_field.dart'; +import 'package:phone_form_field/src/country_selection/no_result_view.dart'; import 'package:phone_form_field/src/country_selection/search_box.dart'; void main() { group('CountrySelector', () { - group('Without internationalization', () { - final app = MaterialApp( - home: Scaffold( - body: CountrySelector( - onCountrySelected: (c) {}, - flagCache: FlagCache(), - ), - ), - ); - - testWidgets('Should filter with text', (tester) async { - await tester.pumpWidget(app); - await tester.pumpAndSettle(); - final txtFound = find.byType(SearchBox); - expect(txtFound, findsOneWidget); - await tester.enterText(txtFound, 'sp'); - await tester.pumpAndSettle(); - final tiles = find.byType(ListTile); - expect(tiles, findsWidgets); - expect( - tester.widget(tiles.first).key, equals(const Key('ES'))); - // not the right language (we let english go through tho) - await tester.enterText(txtFound, 'Espagne'); - await tester.pumpAndSettle(); - expect(tiles, findsNothing); - await tester.pumpAndSettle(); - // country codes - await tester.enterText(txtFound, '33'); - await tester.pumpAndSettle(); - expect(tiles, findsWidgets); - expect( - tester.widget(tiles.first).key, equals(const Key('FR'))); - }); - }); - - group('With internationalization', () { - final app = MaterialApp( - locale: const Locale('es', ''), + Widget buildSelector({List favorites = const []}) { + return MaterialApp( + locale: const Locale('en', ''), localizationsDelegates: const [ ...GlobalMaterialLocalizations.delegates, PhoneFieldLocalization.delegate, @@ -54,198 +20,101 @@ void main() { body: CountrySelector( onCountrySelected: (c) {}, flagCache: FlagCache(), + favoriteCountries: favorites, ), ), ); - - testWidgets('Should filter with text', (tester) async { - await tester.pumpWidget(app); - await tester.pump(const Duration(seconds: 1)); - final txtFound = find.byType(SearchBox); - expect(txtFound, findsOneWidget); - await tester.enterText(txtFound, 'esp'); - await tester.pump(const Duration(seconds: 1)); - final tiles = find.byType(ListTile); - expect(tiles, findsWidgets); - expect( - tester.widget(tiles.first).key, equals(const Key('ES'))); - // not the right language (we let english go through tho) - await tester.enterText(txtFound, 'Espagne'); - await tester.pump(const Duration(seconds: 1)); - expect(tiles, findsNothing); - await tester.pump(const Duration(seconds: 1)); - // country codes - await tester.enterText(txtFound, '33'); - await tester.pump(const Duration(seconds: 1)); - expect(tiles, findsWidgets); - expect( - tester.widget(tiles.first).key, equals(const Key('FR'))); - }); + } + + testWidgets('Should filter with text', (tester) async { + await tester.pumpWidget(buildSelector()); + await tester.pump(const Duration(seconds: 1)); + final txtFound = find.byType(SearchBox); + expect(txtFound, findsOneWidget); + await tester.enterText(txtFound, 'esp'); + await tester.pump(const Duration(seconds: 1)); + final tiles = find.byType(ListTile); + expect(tiles, findsWidgets); + expect(tester.widget(tiles.first).key, equals(const Key('ES'))); + // not the right language (we let english go through tho) + await tester.enterText(txtFound, 'Espagne'); + await tester.pump(const Duration(seconds: 1)); + expect(tiles, findsNothing); + await tester.pump(const Duration(seconds: 1)); + // country codes + await tester.enterText(txtFound, '33'); + await tester.pump(const Duration(seconds: 1)); + expect(tiles, findsWidgets); + expect(tester.widget(tiles.first).key, equals(const Key('FR'))); }); - group('sorted countries with or without favorites', () { - Widget builder({ - List? favorites, - bool addFavoritesSeparator = false, - }) => - MaterialApp( - locale: const Locale('fr'), - localizationsDelegates: const [ - PhoneFieldLocalization.delegate, - ...GlobalMaterialLocalizations.delegates, - ], - supportedLocales: const [Locale('fr')], - home: Scaffold( - body: CountrySelector( - onCountrySelected: (c) {}, - addFavoritesSeparator: addFavoritesSeparator, - favoriteCountries: favorites ?? const [], - flagCache: FlagCache(), - ), - ), - ); - - testWidgets('should be properly sorted without favorites', - (tester) async { - await tester.pumpWidget(builder()); - await tester.pump(const Duration(seconds: 1)); - final allTiles = find.byType(ListTile); - expect(allTiles, findsWidgets); - // expect(tester.widget(allTiles.first).key, equals(Key('AF'))); - }); - - testWidgets('should be properly sorted with favorites', (tester) async { - await tester.pumpWidget(builder(favorites: [IsoCode.GU, IsoCode.GY])); - await tester.pump(const Duration(seconds: 1)); - final allTiles = find.byType(ListTile, skipOffstage: false); - expect(allTiles, findsWidgets); - expect(tester.widget(allTiles.at(0)).key, - equals(Key(IsoCode.GU.name))); - expect(tester.widget(allTiles.at(1)).key, - equals(Key(IsoCode.GY.name))); - - final txtFound = find.byType(SearchBox); - expect(txtFound, findsOneWidget); - await tester.enterText(txtFound, 'guy'); - await tester.pumpAndSettle(); - final filteredTiles = find.byType(ListTile); - expect(filteredTiles, findsWidgets); - expect(filteredTiles.evaluate().length, equals(2)); - }); - - testWidgets('should display/hide separator', (tester) async { - await tester.pumpWidget(builder( - favorites: [IsoCode.GU, IsoCode.GY], - addFavoritesSeparator: true, - )); - await tester.pump(const Duration(seconds: 1)); - final list = find.byType(ListView); - expect(list, findsOneWidget); - final allTiles = find.descendant( - of: list, - matching: find.byWidgetPredicate( - (Widget widget) => widget is ListTile || widget is Divider, - ), - ); + testWidgets('should show a divider between favorites and all countries', + (tester) async { + await tester.pumpWidget(buildSelector(favorites: const [IsoCode.BE])); + await tester.pump(const Duration(seconds: 1)); + final list = find.byType(ListView); + expect(list, findsOneWidget); + final allTiles = find.descendant( + of: list, + matching: find.byWidgetPredicate( + (Widget widget) => widget is ListTile || widget is Divider, + ), + ); - expect(allTiles, findsWidgets); - expect( - tester.widget(allTiles.at(2)), - isA(), - reason: 'separator should be visible after the favorites countries', - ); + expect(allTiles, findsWidgets); + expect( + tester.widget(allTiles.at(1)), + isA(), + reason: 'separator should be visible after the favorites countries', + ); + }); - final txtFound = find.byType(SearchBox); - expect(txtFound, findsOneWidget); - await tester.enterText(txtFound, 'guy'); - await tester.pump(const Duration(seconds: 1)); - final tiles = find.byType(ListTile); - expect(tiles, findsWidgets); - expect( - tiles.evaluate().length, - equals(2), - reason: 'Separator should be hidden as all elements' - 'found are in favorites', - ); - }); + testWidgets('should hide favorites when search has started', + (tester) async { + await tester.pumpWidget(buildSelector(favorites: const [IsoCode.BE])); + await tester.pump(const Duration(seconds: 1)); + final searchBox = find.byType(SearchBox); + expect(searchBox, findsOneWidget); + await tester.enterText(searchBox, 'belg'); + await tester.pump(const Duration(seconds: 1)); + final tiles = find.byType(ListTile); + expect(tiles, findsOneWidget); }); - group('Empty search result', () { - Widget builder({ - String? noResultMessage, - }) => - MaterialApp( - locale: const Locale('fr'), - localizationsDelegates: const [ - PhoneFieldLocalization.delegate, - ...GlobalMaterialLocalizations.delegates, - ], - supportedLocales: const [Locale('fr')], - home: Scaffold( - body: CountrySelector( - onCountrySelected: (c) {}, - noResultMessage: noResultMessage, - flagCache: FlagCache(), - ), - ), - ); + testWidgets('should sort countries', (tester) async { + await tester + .pumpWidget(buildSelector(favorites: const [IsoCode.SE, IsoCode.SG])); + await tester.pump(const Duration(seconds: 1)); + final allTiles = find.byType(ListTile, skipOffstage: false); + expect(allTiles, findsWidgets); + expect(tester.widget(allTiles.at(0)).key, + equals(Key(IsoCode.SG.name))); + expect(tester.widget(allTiles.at(1)).key, + equals(Key(IsoCode.SE.name))); + }); - testWidgets('should display default untranslated no result message', - (tester) async { - await tester.pumpWidget(MaterialApp( - home: Scaffold( - body: CountrySelector( - onCountrySelected: (c) {}, - flagCache: FlagCache(), - ), + testWidgets('should display no result when there is no result', + (tester) async { + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: CountrySelector( + onCountrySelected: (c) {}, + flagCache: FlagCache(), ), - )); - - final txtFound = find.byType(SearchBox); - expect(txtFound, findsOneWidget); - await tester.enterText(txtFound, 'fake search with no result'); - await tester.pumpAndSettle(); - - // no listitem should be displayed when no result found - final allTiles = find.byType(ListTile); - expect(allTiles, findsNothing); - - final noResultWidget = find.text('No result found'); - expect(noResultWidget, findsOneWidget); - }); - - testWidgets('should display default translated no result message', - (tester) async { - await tester.pumpWidget(builder()); - - final txtFound = find.byType(SearchBox); - expect(txtFound, findsOneWidget); - await tester.enterText(txtFound, 'fake search with no result'); - await tester.pumpAndSettle(); - - // no listitem should be displayed when no result found - final allTiles = find.byType(ListTile); - expect(allTiles, findsNothing); - - final noResultWidget = find.text('Aucun résultat'); - expect(noResultWidget, findsOneWidget); - }); - - testWidgets('should display custom no result message', (tester) async { - await tester.pumpWidget(builder(noResultMessage: 'Bad news !')); + ), + )); - final txtFound = find.byType(SearchBox); - expect(txtFound, findsOneWidget); - await tester.enterText(txtFound, 'fake search with no result'); - await tester.pumpAndSettle(); + final searchBox = find.byType(SearchBox); + expect(searchBox, findsOneWidget); + await tester.enterText(searchBox, 'fake search with no result'); + await tester.pumpAndSettle(); - // no listitem should be displayed when no result found - final allTiles = find.byType(ListTile); - expect(allTiles, findsNothing); + // no listitem should be displayed when no result found + final allTiles = find.byType(ListTile); + expect(allTiles, findsNothing); - final noResultWidget = find.text('Bad news !'); - expect(noResultWidget, findsOneWidget); - }); + final noResultWidget = find.byType(NoResultView); + expect(noResultWidget, findsOneWidget); }); }); } From a0a953e3dac70d37cd93bcbace549ae27e72e90c Mon Sep 17 00:00:00 2001 From: cedvdb Date: Fri, 2 Feb 2024 15:51:28 +0100 Subject: [PATCH 07/25] saving --- example/lib/main.dart | 14 ++-- lib/phone_form_field.dart | 2 +- lib/src/phone_field.dart | 4 +- lib/src/phone_field_controller.dart | 82 +++++++++--------- lib/src/phone_field_state.dart | 23 ++--- lib/src/phone_form_field.dart | 10 +-- lib/src/phone_form_field_state.dart | 106 +++--------------------- lib/src/validation/phone_validator.dart | 56 ------------- test/phone_form_field_test.dart | 10 +-- 9 files changed, 85 insertions(+), 222 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 1aeb855d..f26f517f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -13,6 +13,7 @@ void main() { class PhoneFieldView extends StatelessWidget { final Key inputKey; final PhoneController controller; + final FocusNode focusNode; final CountrySelectorNavigator selectorNavigator; final bool withLabel; final bool outlineBorder; @@ -25,6 +26,7 @@ class PhoneFieldView extends StatelessWidget { Key? key, required this.inputKey, required this.controller, + required this.focusNode, required this.selectorNavigator, required this.withLabel, required this.outlineBorder, @@ -123,6 +125,7 @@ class PhoneFormFieldScreen extends StatefulWidget { class PhoneFormFieldScreenState extends State { late PhoneController controller; + final FocusNode focusNode = FocusNode(); bool outlineBorder = true; bool mobileOnly = true; bool shouldFormat = true; @@ -137,7 +140,7 @@ class PhoneFormFieldScreenState extends State { @override initState() { super.initState(); - controller = PhoneController(null); + controller = PhoneController(); controller.addListener(() => setState(() {})); } @@ -245,6 +248,7 @@ class PhoneFormFieldScreenState extends State { child: PhoneFieldView( inputKey: phoneKey, controller: controller, + focusNode: focusNode, selectorNavigator: selectorNavigator, withLabel: withLabel, outlineBorder: outlineBorder, @@ -257,14 +261,12 @@ class PhoneFormFieldScreenState extends State { const SizedBox(height: 12), Text(controller.value.toString()), Text('is valid mobile number ' - '${controller.value?.isValid(type: PhoneNumberType.mobile) ?? 'false'}'), + '${controller.value.isValid(type: PhoneNumberType.mobile)}'), Text( - 'is valid fixed line number ${controller.value?.isValid(type: PhoneNumberType.fixedLine) ?? 'false'}'), + 'is valid fixed line number ${controller.value.isValid(type: PhoneNumberType.fixedLine)}'), const SizedBox(height: 12), ElevatedButton( - onPressed: controller.value == null - ? null - : () => controller.reset(), + onPressed: () => controller.reset(), child: const Text('reset'), ), const SizedBox(height: 12), diff --git a/lib/phone_form_field.dart b/lib/phone_form_field.dart index 6dd2ba7a..104ae298 100644 --- a/lib/phone_form_field.dart +++ b/lib/phone_form_field.dart @@ -9,7 +9,7 @@ export 'src/validation/phone_validator.dart'; export 'l10n/generated/phone_field_localization.dart'; -export 'src/phone_controller.dart'; +export 'src/phone_field_controller.dart'; export 'src/country/localized_country.dart'; export 'package:phone_numbers_parser/phone_numbers_parser.dart' diff --git a/lib/src/phone_field.dart b/lib/src/phone_field.dart index 8c000122..eac7bf9d 100644 --- a/lib/src/phone_field.dart +++ b/lib/src/phone_field.dart @@ -14,7 +14,8 @@ part 'phone_field_state.dart'; /// /// This deals with mostly UI and has no dependency on any phone parser library class PhoneField extends StatefulWidget { - final PhoneFieldController controller; + final FocusNode focusNode; + final PhoneController controller; final bool showFlagInInput; final bool showIsoCodeInInput; final bool showDialCode; @@ -72,6 +73,7 @@ class PhoneField extends StatefulWidget { // form field params super.key, required this.controller, + required this.focusNode, required this.showFlagInInput, required this.selectorNavigator, required this.flagSize, diff --git a/lib/src/phone_field_controller.dart b/lib/src/phone_field_controller.dart index 7ef9e85c..ccf7c937 100644 --- a/lib/src/phone_field_controller.dart +++ b/lib/src/phone_field_controller.dart @@ -1,50 +1,60 @@ import 'package:flutter/material.dart'; import 'package:phone_form_field/phone_form_field.dart'; +import 'package:phone_form_field/src/validation/allowed_characters.dart'; -class PhoneFieldController extends ChangeNotifier { - late final ValueNotifier isoCodeController; - late final TextEditingController nationalNumberController; +class PhoneController extends ChangeNotifier { + final bool shouldFormat; /// focus node of the national number - final FocusNode focusNode; + // final FocusNode focusNode; + final PhoneNumber initialValue; + PhoneNumber _value; + PhoneNumber get value => _value; + set value(PhoneNumber phoneNumber) { + _value = phoneNumber; + changeCountry(_value.isoCode); + changeText(_value.nsn); + } - IsoCode get isoCode => isoCodeController.value; - String? get national => nationalNumberController.text; + late final TextEditingController nationalNumberController; - set isoCode(IsoCode isoCode) => isoCodeController.value = isoCode; + PhoneController({ + this.initialValue = const PhoneNumber(isoCode: IsoCode.US, nsn: ''), + this.shouldFormat = true, + }) : _value = initialValue, + nationalNumberController = TextEditingController( + text: shouldFormat + ? initialValue.getFormattedNsn() + : initialValue.nsn); - set national(String? national) { - national = national ?? ''; - final currentSelectionOffset = - nationalNumberController.selection.extentOffset; - final isCursorAtEnd = - currentSelectionOffset == nationalNumberController.text.length; - var offset = national.length; + reset() { + _value = initialValue; + notifyListeners(); + } - if (isCursorAtEnd) { - offset = national.length; - } else if (currentSelectionOffset <= national.length) { - offset = currentSelectionOffset; - } - // when the cursor is at the end we need to preserve that - // since there is formatting going on we need to explicitely do it - nationalNumberController.value = TextEditingValue( - text: national, - selection: TextSelection.fromPosition( - TextPosition(offset: offset), - ), + changeCountry(IsoCode isoCode) { + _value = PhoneNumber.parse( + _value.nsn, + destinationCountry: isoCode, ); + notifyListeners(); } - PhoneFieldController({ - required String? national, - required IsoCode isoCode, - required this.focusNode, - }) { - isoCodeController = ValueNotifier(isoCode); - nationalNumberController = TextEditingController(text: national); - isoCodeController.addListener(notifyListeners); - nationalNumberController.addListener(notifyListeners); + changeText(String? text) { + text = text ?? ''; + // if starts with + then we parse the whole number + // to figure out the country code + if (text.startsWith(RegExp('[${AllowedCharacters.plus}]'))) { + _value = PhoneNumber.parse(text); + } else { + _value = PhoneNumber.parse( + text, + destinationCountry: _value.isoCode, + ); + } + nationalNumberController.text = + shouldFormat ? _value.getFormattedNsn() : _value.nsn; + notifyListeners(); } selectNationalNumber() { @@ -52,12 +62,10 @@ class PhoneFieldController extends ChangeNotifier { baseOffset: 0, extentOffset: nationalNumberController.value.text.length, ); - focusNode.requestFocus(); } @override void dispose() { - isoCodeController.dispose(); nationalNumberController.dispose(); super.dispose(); } diff --git a/lib/src/phone_field_state.dart b/lib/src/phone_field_state.dart index ec7c0e00..67c06441 100644 --- a/lib/src/phone_field_state.dart +++ b/lib/src/phone_field_state.dart @@ -1,13 +1,14 @@ part of 'phone_field.dart'; class PhoneFieldState extends State { - PhoneFieldController get controller => widget.controller; + PhoneController get controller => widget.controller; + FocusNode get focusNode => widget.focusNode; + final _flagCache = FlagCache(); PhoneFieldState(); @override void initState() { - controller.focusNode.addListener(onFocusChange); _preloadFlagsInMemory(); super.initState(); } @@ -20,12 +21,6 @@ class PhoneFieldState extends State { setState(() {}); } - @override - void dispose() { - controller.focusNode.removeListener(onFocusChange); - super.dispose(); - } - void selectCountry() async { if (!widget.isCountrySelectionEnabled) { return; @@ -34,9 +29,9 @@ class PhoneFieldState extends State { final selected = await widget.selectorNavigator.navigate(context, _flagCache); if (selected != null) { - controller.isoCode = selected.isoCode; + controller.changeCountry(selected.isoCode); } - controller.focusNode.requestFocus(); + focusNode.requestFocus(); SystemChannels.textInput.invokeMethod('TextInput.show'); } @@ -61,15 +56,15 @@ class PhoneFieldState extends State { return MouseRegion( cursor: SystemMouseCursors.text, child: GestureDetector( - onTap: controller.focusNode.requestFocus, + onTap: focusNode.requestFocus, // absorb pointer when the country chip is not shown, else flutter // still allows the country chip to be clicked even though it is not shown child: InputDecorator( decoration: _getOutterInputDecoration(), - isFocused: controller.focusNode.hasFocus, + isFocused: focusNode.hasFocus, isEmpty: _isEffectivelyEmpty(), child: TextField( - focusNode: controller.focusNode, + focusNode: focusNode, controller: controller.nationalNumberController, enabled: widget.enabled, decoration: _getInnerInputDecoration(), @@ -136,7 +131,7 @@ class PhoneFieldState extends State { : const EdgeInsetsDirectional.fromSTEB(8, 0, 8, 0), child: CountryChip( key: const ValueKey('country-code-chip'), - isoCode: controller.isoCode, + isoCode: controller.value.isoCode, showFlag: widget.showFlagInInput, showIsoCode: widget.showIsoCodeInInput, showDialCode: widget.showDialCode, diff --git a/lib/src/phone_form_field.dart b/lib/src/phone_form_field.dart index d4f956ba..53cfe0a7 100644 --- a/lib/src/phone_form_field.dart +++ b/lib/src/phone_form_field.dart @@ -1,17 +1,14 @@ -import 'dart:async'; import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:phone_numbers_parser/phone_numbers_parser.dart'; -import 'validation/allowed_characters.dart'; -import 'phone_controller.dart'; +import 'country_selection/country_selector_navigator.dart'; +import 'phone_field.dart'; import 'phone_field_controller.dart'; import 'validation/phone_validator.dart'; import 'validation/validator_translator.dart'; -import 'country_selection/country_selector_navigator.dart'; -import 'phone_field.dart'; part 'phone_form_field_state.dart'; @@ -172,7 +169,8 @@ class PhoneFormField extends FormField { builder: (state) { final field = state as PhoneFormFieldState; return PhoneField( - controller: field._childController, + controller: field.controller, + focusNode: field.focusNode, showFlagInInput: showFlagInInput, showIsoCodeInInput: showIsoCodeInInput, selectorNavigator: countrySelectorNavigator, diff --git a/lib/src/phone_form_field_state.dart b/lib/src/phone_form_field_state.dart index 84b36948..82a5c325 100644 --- a/lib/src/phone_form_field_state.dart +++ b/lib/src/phone_form_field_state.dart @@ -1,9 +1,8 @@ part of 'phone_form_field.dart'; class PhoneFormFieldState extends FormFieldState { - late final PhoneController _controller; - late final PhoneFieldController _childController; - late final StreamSubscription _selectionSubscription; + late final PhoneController controller; + late final FocusNode focusNode; @override PhoneFormField get widget => super.widget as PhoneFormField; @@ -11,104 +10,19 @@ class PhoneFormFieldState extends FormFieldState { @override void initState() { super.initState(); - _controller = widget.controller ?? PhoneController(value); - _childController = PhoneFieldController( - isoCode: _controller.value?.isoCode ?? widget.defaultCountry, - national: _getFormattedNsn(), - focusNode: widget.focusNode ?? FocusNode(), - ); - _controller.addListener(_onControllerChange); - _childController.addListener(() => _onChildControllerChange()); - // to expose text selection of national number - _selectionSubscription = _controller.selectionRequestStream - .listen((event) => _childController.selectNationalNumber()); - } - - @override - void dispose() { - super.dispose(); - _childController.dispose(); - _selectionSubscription.cancel(); - _controller.removeListener(_onControllerChange); - // dispose the controller only when it's initialised in this instance - // otherwise this should be done where instance is created - if (widget.controller == null) { - _controller.dispose(); - } - } - - @override - void reset() { - _controller.value = widget.initialValue; - super.reset(); - } - - /// when the controller changes this function will - /// update the childController so the [PhoneField] which - /// deals with the UI can display the correct value. - void _onControllerChange() { - final phone = _controller.value; - - widget.onChanged?.call(phone); - didChange(phone); - final formatted = _getFormattedNsn(); - if (_childController.national != formatted) { - _childController.national = formatted; - } - if (_childController.isoCode != phone?.isoCode) { - _childController.isoCode = phone?.isoCode ?? widget.defaultCountry; - } - } - - /// when the base controller changes (when the user manually input something) - /// then we need to update the local controller's value. - void _onChildControllerChange() { - if (_childController.national == _controller.value?.nsn && - _childController.isoCode == _controller.value?.isoCode) { - return; - } - - if (_childController.national == null) { - return _controller.value = null; - } - // we convert the multiple controllers from the child controller - // to a full blown PhoneNumber to access validation, formatting etc. - PhoneNumber phoneNumber; - // when the nsn input change we check if its not a whole number - // to allow for copy pasting and auto fill. If it is one then - // we parse it accordingly. - // we assume it's a whole phone number if it starts with + - final childNsn = _childController.national; - if (childNsn != null && - childNsn.startsWith(RegExp('[${AllowedCharacters.plus}]'))) { - // if starts with + then we parse the whole number - // to figure out the country code - final international = childNsn; - try { - phoneNumber = PhoneNumber.parse(international); - } on PhoneNumberException { - return; - } - } else { - phoneNumber = PhoneNumber.parse( - childNsn ?? '', - destinationCountry: _childController.isoCode, - ); - } - _controller.value = phoneNumber; - } - - String? _getFormattedNsn() { - if (widget.shouldFormat) { - return _controller.value?.getFormattedNsn(); - } - return _controller.value?.nsn; + controller = widget.controller ?? + PhoneController( + initialValue: widget.initialValue ?? + const PhoneNumber(isoCode: IsoCode.US, nsn: ''), + ); + focusNode = widget.focusNode ?? FocusNode(); } /// gets the localized error text if any String? getErrorText() { + final errorText = this.errorText; if (errorText != null) { - return ValidatorTranslator.message(context, errorText!); + return ValidatorTranslator.message(context, errorText); } return null; } diff --git a/lib/src/validation/phone_validator.dart b/lib/src/validation/phone_validator.dart index 2f62ff45..a596a97a 100644 --- a/lib/src/validation/phone_validator.dart +++ b/lib/src/validation/phone_validator.dart @@ -48,7 +48,6 @@ class PhoneValidator { /// determine whether a missing value should be reported as invalid bool allowEmpty = true, }) { - return (PhoneNumber? valueCandidate) { if (valueCandidate == null && !allowEmpty) { return errorText ?? 'invalidPhoneNumber'; @@ -62,22 +61,6 @@ class PhoneValidator { }; } - @Deprecated('use validType, invalid type naming was backward') - static PhoneNumberInputValidator invalidType( - /// expected phonetype - PhoneNumberType expectedType, { - /// custom error message - String? errorText, - - /// determine whether a missing value should be reported as invalid - bool allowEmpty = true, - }) => - validType( - expectedType, - errorText: errorText, - allowEmpty: allowEmpty, - ); - static PhoneNumberInputValidator validType( /// expected phonetype PhoneNumberType expectedType, { @@ -100,16 +83,6 @@ class PhoneValidator { }; } - @Deprecated('use validFixedLine, naming was backward') - static PhoneNumberInputValidator invalidFixedLine({ - /// custom error message - String? errorText, - - /// determine whether a missing value should be reported as invalid - bool allowEmpty = true, - }) => - validFixedLine(errorText: errorText, allowEmpty: allowEmpty); - /// convenience shortcut method for /// invalidType(context, PhoneNumberType.fixedLine, ...) static PhoneNumberInputValidator validFixedLine({ @@ -125,19 +98,6 @@ class PhoneValidator { allowEmpty: allowEmpty, ); - @Deprecated('Use validMobile, naming was backward') - static PhoneNumberInputValidator invalidMobile({ - /// custom error message - String? errorText, - - /// determine whether a missing value should be reported as invalid - bool allowEmpty = true, - }) => - validMobile( - errorText: errorText, - allowEmpty: allowEmpty, - ); - /// convenience shortcut method for /// invalidType(context, PhoneNumberType.mobile, ...) static PhoneNumberInputValidator validMobile({ @@ -153,22 +113,6 @@ class PhoneValidator { allowEmpty: allowEmpty, ); - @Deprecated('Use valid country, naming was backward') - static invalidCountry( - /// list of valid country isocode - List expectedCountries, { - /// custom error message - String? errorText, - - /// determine whether a missing value should be reported as invalid - bool allowEmpty = true, - }) => - validCountry( - expectedCountries, - errorText: errorText, - allowEmpty: allowEmpty, - ); - static PhoneNumberInputValidator validCountry( /// list of valid country isocode List expectedCountries, { diff --git a/test/phone_form_field_test.dart b/test/phone_form_field_test.dart index 91e0d7d7..64aa8501 100644 --- a/test/phone_form_field_test.dart +++ b/test/phone_form_field_test.dart @@ -157,7 +157,7 @@ void main() { }); testWidgets('Should change value of controller', (tester) async { - final controller = PhoneController(null); + final controller = PhoneController(); PhoneNumber? newValue; controller.addListener(() { newValue = controller.value; @@ -181,7 +181,7 @@ void main() { testWidgets('Should change value of input when controller changes', (tester) async { - final controller = PhoneController(null); + final controller = PhoneController(); // ignore: unused_local_variable PhoneNumber? newValue; controller.addListener(() { @@ -198,7 +198,7 @@ void main() { testWidgets( 'Should change value of country code chip when full number copy pasted', (tester) async { - final controller = PhoneController(null); + final controller = PhoneController(); // ignore: unused_local_variable PhoneNumber? newValue; controller.addListener(() { @@ -211,8 +211,8 @@ void main() { // non digits should not work await tester.enterText(phoneField, '+33 0488 99 77 22'); await tester.pump(); - expect(controller.value?.isoCode, equals(IsoCode.FR)); - expect(controller.value?.nsn, equals('488997722')); + expect(controller.value.isoCode, equals(IsoCode.FR)); + expect(controller.value.nsn, equals('488997722')); }); testWidgets('Should call onChange', (tester) async { From 72176d861934adccada842110f373e72a2ce54e0 Mon Sep 17 00:00:00 2001 From: cedvdb Date: Sat, 3 Feb 2024 01:53:51 +0100 Subject: [PATCH 08/25] refactor --- example/lib/main.dart | 6 +- example/pubspec.lock | 20 +++--- .../phone_field_localization_el.dart | 3 +- lib/src/phone_field_controller.dart | 67 ++++++++++++++++--- lib/src/phone_field_state.dart | 12 +++- pubspec.yaml | 6 +- 6 files changed, 86 insertions(+), 28 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index f26f517f..8a4a37a4 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -54,6 +54,7 @@ class PhoneFieldView extends StatelessWidget { child: PhoneFormField( key: inputKey, controller: controller, + focusNode: focusNode, shouldFormat: shouldFormat && !useRtl, autofocus: false, autofillHints: const [AutofillHints.telephoneNumber], @@ -271,7 +272,10 @@ class PhoneFormFieldScreenState extends State { ), const SizedBox(height: 12), ElevatedButton( - onPressed: () => controller.selectNationalNumber(), + onPressed: () { + controller.selectNationalNumber(); + focusNode.requestFocus(); + }, child: const Text('Select national number'), ), const SizedBox(height: 12), diff --git a/example/pubspec.lock b/example/pubspec.lock index eff4efaa..e056cced 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -61,10 +61,10 @@ packages: dependency: transitive description: name: diacritic - sha256: a84e03ec2779375fb86430dbe9d8fba62c68376f2499097a5f6e75556babe706 + sha256: "96db5db6149cbe4aa3cfcbfd170aca9b7648639be7e48025f9d458517f807fe4" url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.1.5" fake_async: dependency: transitive description: @@ -179,10 +179,10 @@ packages: dependency: transitive description: name: phone_numbers_parser - sha256: "17a6686350c574a08f7beb839c5f908cc19b9c0eabd6e97029b517527a49da02" + sha256: c36e71f611b5232eb917c9bcc11ffa5a3144e072241f97bda5b352cb5b9f3f80 url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.1.2" sky_engine: dependency: transitive description: flutter @@ -240,26 +240,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "0f0c746dd2d6254a0057218ff980fc7f5670fd0fcf5e4db38a490d31eed4ad43" + sha256: "18f6690295af52d081f6808f2f7c69f0eed6d7e23a71539d75f4aeb8f0062172" url: "https://pub.dev" source: hosted - version: "1.1.9+1" + version: "1.1.9+2" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "0edf6d630d1bfd5589114138ed8fada3234deacc37966bec033d3047c29248b7" + sha256: "531d20465c10dfac7f5cd90b60bbe4dd9921f1ec4ca54c83ebb176dbacb7bb2d" url: "https://pub.dev" source: hosted - version: "1.1.9+1" + version: "1.1.9+2" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: d24333727332d9bd20990f1483af4e09abdb9b1fc7c3db940b56ab5c42790c26 + sha256: "03012b0a33775c5530576b70240308080e1d5050f0faf000118c20e6463bc0ad" url: "https://pub.dev" source: hosted - version: "1.1.9+1" + version: "1.1.9+2" vector_math: dependency: transitive description: diff --git a/lib/l10n/generated/phone_field_localization_el.dart b/lib/l10n/generated/phone_field_localization_el.dart index 21563b9a..10d33e9a 100644 --- a/lib/l10n/generated/phone_field_localization_el.dart +++ b/lib/l10n/generated/phone_field_localization_el.dart @@ -14,8 +14,7 @@ class PhoneFieldLocalizationEl extends PhoneFieldLocalization { String get invalidMobilePhoneNumber => 'Μη έγκυρος αριθμός κινητού τηλεφώνου'; @override - String get invalidFixedLinePhoneNumber => - 'Μη έγκυρος αριθμός σταθερού τηλεφώνου'; + String get invalidFixedLinePhoneNumber => 'Μη έγκυρος αριθμός σταθερού τηλεφώνου'; @override String get requiredPhoneNumber => 'Απαιτούμενος αριθμός τηλεφώνου'; diff --git a/lib/src/phone_field_controller.dart b/lib/src/phone_field_controller.dart index ccf7c937..0c926324 100644 --- a/lib/src/phone_field_controller.dart +++ b/lib/src/phone_field_controller.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:phone_form_field/phone_form_field.dart'; import 'package:phone_form_field/src/validation/allowed_characters.dart'; +import 'package:phone_numbers_parser/phone_numbers_parser.dart'; class PhoneController extends ChangeNotifier { final bool shouldFormat; @@ -10,25 +10,27 @@ class PhoneController extends ChangeNotifier { final PhoneNumber initialValue; PhoneNumber _value; PhoneNumber get value => _value; + set value(PhoneNumber phoneNumber) { _value = phoneNumber; changeCountry(_value.isoCode); changeText(_value.nsn); } + /// text editing controller of the nsn ( where user types the phone number ) late final TextEditingController nationalNumberController; PhoneController({ this.initialValue = const PhoneNumber(isoCode: IsoCode.US, nsn: ''), this.shouldFormat = true, }) : _value = initialValue, - nationalNumberController = TextEditingController( - text: shouldFormat - ? initialValue.getFormattedNsn() - : initialValue.nsn); + nationalNumberController = + TextEditingController(text: initialValue.getFormattedNsn()); reset() { _value = initialValue; + changeCountry(_value.isoCode); + changeText(_value.nsn); notifyListeners(); } @@ -42,26 +44,69 @@ class PhoneController extends ChangeNotifier { changeText(String? text) { text = text ?? ''; + var newText = text; + // if starts with + then we parse the whole number - // to figure out the country code - if (text.startsWith(RegExp('[${AllowedCharacters.plus}]'))) { - _value = PhoneNumber.parse(text); + final startsWithPlus = + text.startsWith(RegExp('[${AllowedCharacters.plus}]')); + + if (startsWithPlus) { + final phoneNumber = _tryParseWithPlus(text); + // if we could parse the phone number we can change the value inside + // the national number field to remove the "+ country dial code" + if (phoneNumber != null) { + _value = phoneNumber; + newText = _value.getFormattedNsn(); + } } else { - _value = PhoneNumber.parse( + final phoneNumber = PhoneNumber.parse( text, destinationCountry: _value.isoCode, ); + _value = phoneNumber; + newText = phoneNumber.getFormattedNsn(); } - nationalNumberController.text = - shouldFormat ? _value.getFormattedNsn() : _value.nsn; + nationalNumberController.value = TextEditingValue( + text: newText, + selection: computeSelection(text, newText), + ); + notifyListeners(); } + /// When the cursor is at the end of the text we need to preserve that. + /// Since there is formatting going on we need to explicitely do it. + /// We don't want to do it in the middle because the user might have + /// used arrow keys to move inside the text. + TextSelection computeSelection(String originalText, String newText) { + final currentSelectionOffset = + nationalNumberController.selection.extentOffset; + final isCursorAtEnd = currentSelectionOffset == originalText.length; + var offset = currentSelectionOffset; + + if (isCursorAtEnd || currentSelectionOffset >= newText.length) { + offset = newText.length; + } + return TextSelection.fromPosition( + TextPosition(offset: offset), + ); + } + + PhoneNumber? _tryParseWithPlus(String text) { + try { + return PhoneNumber.parse(text); + // parsing "+", a country code won't be found + } on PhoneNumberException { + return null; + } + } + selectNationalNumber() { nationalNumberController.selection = TextSelection( baseOffset: 0, extentOffset: nationalNumberController.value.text.length, ); + notifyListeners(); } @override diff --git a/lib/src/phone_field_state.dart b/lib/src/phone_field_state.dart index 67c06441..82852162 100644 --- a/lib/src/phone_field_state.dart +++ b/lib/src/phone_field_state.dart @@ -10,14 +10,23 @@ class PhoneFieldState extends State { @override void initState() { _preloadFlagsInMemory(); + focusNode.addListener(_onFocusChange); super.initState(); } + @override + void dispose() { + focusNode.removeListener(_onFocusChange); + super.dispose(); + } + void _preloadFlagsInMemory() { _flagCache.preload(IsoCode.values.map((isoCode) => isoCode.name)); } - void onFocusChange() { + void _onFocusChange() { + // call setState when the text input has focus so + // the flag is shown when there is a label setState(() {}); } @@ -73,6 +82,7 @@ class PhoneFieldState extends State { FilteringTextInputFormatter.allow(RegExp( '[${AllowedCharacters.plus}${AllowedCharacters.digits}${AllowedCharacters.punctuation}]')), ], + onChanged: (txt) => controller.changeText(txt), autofillHints: widget.autofillHints, keyboardType: widget.keyboardType, textInputAction: widget.textInputAction, diff --git a/pubspec.yaml b/pubspec.yaml index 7b8cd3e8..ffe26dcc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,9 +14,9 @@ dependencies: sdk: flutter circle_flags: ^3.0.1 - phone_numbers_parser: ^8.1.0 - intl: ">=0.17.0 <=1.0.0" - diacritic: ^0.1.3 + phone_numbers_parser: ^8.1.2 + intl: ">=0.18.1 <=1.0.0" + diacritic: ^0.1.5 dev_dependencies: flutter_test: From 48bf65c316c9f8024695632b512e593cdcff44af Mon Sep 17 00:00:00 2001 From: cedvdb Date: Sat, 3 Feb 2024 12:34:21 +0100 Subject: [PATCH 09/25] rm --- .pubignore | 36 ---------- example/.pubignore | 76 -------------------- example/lib/main.dart | 35 ++++++--- lib/src/phone_field_state.dart | 125 ++++++++++++++++++++++++++++++++- 4 files changed, 148 insertions(+), 124 deletions(-) delete mode 100644 .pubignore delete mode 100644 example/.pubignore diff --git a/.pubignore b/.pubignore deleted file mode 100644 index d01dc04c..00000000 --- a/.pubignore +++ /dev/null @@ -1,36 +0,0 @@ -docs/ -example/build -example/windows -example/web -example/android -example/ios -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ -demo_image.png - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. -/pubspec.lock -**/doc/api/ -.dart_tool/ -.packages -build/ diff --git a/example/.pubignore b/example/.pubignore deleted file mode 100644 index b349b8a0..00000000 --- a/example/.pubignore +++ /dev/null @@ -1,76 +0,0 @@ -docs/ - -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.packages -.pub-cache/ -.pub/ -build/ - -# Android related -**/android/**/gradle-wrapper.jar -**/android/.gradle -**/android/captures/ -**/android/gradlew -**/android/gradlew.bat -**/android/local.properties -**/android/**/GeneratedPluginRegistrant.java - -# iOS/XCode related -**/ios/**/*.mode1v3 -**/ios/**/*.mode2v3 -**/ios/**/*.moved-aside -**/ios/**/*.pbxuser -**/ios/**/*.perspectivev3 -**/ios/**/*sync/ -**/ios/**/.sconsign.dblite -**/ios/**/.tags* -**/ios/**/.vagrant/ -**/ios/**/DerivedData/ -**/ios/**/Icon? -**/ios/**/Pods/ -**/ios/**/.symlinks/ -**/ios/**/profile -**/ios/**/xcuserdata -**/ios/.generated/ -**/ios/Flutter/App.framework -**/ios/Flutter/Flutter.framework -**/ios/Flutter/Flutter.podspec -**/ios/Flutter/Generated.xcconfig -**/ios/Flutter/app.flx -**/ios/Flutter/app.zip -**/ios/Flutter/flutter_assets/ -**/ios/Flutter/flutter_export_environment.sh -**/ios/ServiceDefinitions.json -**/ios/Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!**/ios/**/default.mode1v3 -!**/ios/**/default.mode2v3 -!**/ios/**/default.pbxuser -!**/ios/**/default.perspectivev3 diff --git a/example/lib/main.dart b/example/lib/main.dart index 8a4a37a4..15d69476 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -246,17 +246,30 @@ class PhoneFormFieldScreenState extends State { const SizedBox(height: 40), Form( key: formKey, - child: PhoneFieldView( - inputKey: phoneKey, - controller: controller, - focusNode: focusNode, - selectorNavigator: selectorNavigator, - withLabel: withLabel, - outlineBorder: outlineBorder, - isCountryChipPersistent: isCountryChipPersistent, - mobileOnly: mobileOnly, - shouldFormat: shouldFormat, - useRtl: useRtl, + child: Column( + children: [ + PhoneFieldView( + inputKey: phoneKey, + controller: controller, + focusNode: focusNode, + selectorNavigator: selectorNavigator, + withLabel: withLabel, + outlineBorder: outlineBorder, + isCountryChipPersistent: isCountryChipPersistent, + mobileOnly: mobileOnly, + shouldFormat: shouldFormat, + useRtl: useRtl, + ), + TextFormField( + decoration: InputDecoration( + label: withLabel ? const Text('Phone') : null, + border: outlineBorder + ? const OutlineInputBorder() + : const UnderlineInputBorder(), + hintText: withLabel ? '' : 'Phone', + ), + ), + ], ), ), const SizedBox(height: 12), diff --git a/lib/src/phone_field_state.dart b/lib/src/phone_field_state.dart index 82852162..098c1b45 100644 --- a/lib/src/phone_field_state.dart +++ b/lib/src/phone_field_state.dart @@ -55,6 +55,107 @@ class PhoneFieldState extends State { @override Widget build(BuildContext context) { + return InputDecorator( + decoration: _getOutterInputDecoration(), + isFocused: focusNode.hasFocus, + isEmpty: _isEffectivelyEmpty(), + child: TextField( + focusNode: focusNode, + controller: controller.nationalNumberController, + enabled: widget.enabled, + decoration: _getInnerInputDecoration(), + inputFormatters: widget.inputFormatters ?? + [ + FilteringTextInputFormatter.allow(RegExp( + '[${AllowedCharacters.plus}${AllowedCharacters.digits}${AllowedCharacters.punctuation}]')), + ], + onChanged: (txt) => controller.changeText(txt), + autofillHints: widget.autofillHints, + keyboardType: widget.keyboardType, + textInputAction: widget.textInputAction, + style: widget.style, + strutStyle: widget.strutStyle, + textAlign: widget.textAlign, + textAlignVertical: widget.textAlignVertical, + autofocus: widget.autofocus, + obscuringCharacter: widget.obscuringCharacter, + obscureText: widget.obscureText, + autocorrect: widget.autocorrect, + smartDashesType: widget.smartDashesType, + smartQuotesType: widget.smartQuotesType, + enableSuggestions: widget.enableSuggestions, + contextMenuBuilder: + widget.contextMenuBuilder ?? _defaultContextMenuBuilder, + showCursor: widget.showCursor, + onEditingComplete: widget.onEditingComplete, + onSubmitted: widget.onSubmitted, + onAppPrivateCommand: widget.onAppPrivateCommand, + cursorWidth: widget.cursorWidth, + cursorHeight: widget.cursorHeight, + cursorRadius: widget.cursorRadius, + cursorColor: widget.cursorColor, + onTapOutside: widget.onTapOutside, + selectionHeightStyle: widget.selectionHeightStyle, + selectionWidthStyle: widget.selectionWidthStyle, + keyboardAppearance: widget.keyboardAppearance, + scrollPadding: widget.scrollPadding, + enableInteractiveSelection: widget.enableInteractiveSelection, + selectionControls: widget.selectionControls, + mouseCursor: widget.mouseCursor, + scrollController: widget.scrollController, + scrollPhysics: widget.scrollPhysics, + restorationId: widget.restorationId, + enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, + ), + ); + return TextFormField( + decoration: _getOutterInputDecoration(), + focusNode: focusNode, + controller: controller.nationalNumberController, + enabled: widget.enabled, + // decoration: _getInnerInputDecoration(), + inputFormatters: widget.inputFormatters ?? + [ + FilteringTextInputFormatter.allow(RegExp( + '[${AllowedCharacters.plus}${AllowedCharacters.digits}${AllowedCharacters.punctuation}]')), + ], + onChanged: (txt) => controller.changeText(txt), + autofillHints: widget.autofillHints, + keyboardType: widget.keyboardType, + textInputAction: widget.textInputAction, + style: widget.style, + strutStyle: widget.strutStyle, + textAlign: widget.textAlign, + textAlignVertical: widget.textAlignVertical, + autofocus: widget.autofocus, + obscuringCharacter: widget.obscuringCharacter, + obscureText: widget.obscureText, + autocorrect: widget.autocorrect, + smartDashesType: widget.smartDashesType, + smartQuotesType: widget.smartQuotesType, + enableSuggestions: widget.enableSuggestions, + contextMenuBuilder: + widget.contextMenuBuilder ?? _defaultContextMenuBuilder, + showCursor: widget.showCursor, + onEditingComplete: widget.onEditingComplete, + onAppPrivateCommand: widget.onAppPrivateCommand, + cursorWidth: widget.cursorWidth, + cursorHeight: widget.cursorHeight, + cursorRadius: widget.cursorRadius, + cursorColor: widget.cursorColor, + onTapOutside: widget.onTapOutside, + selectionHeightStyle: widget.selectionHeightStyle, + selectionWidthStyle: widget.selectionWidthStyle, + keyboardAppearance: widget.keyboardAppearance, + scrollPadding: widget.scrollPadding, + enableInteractiveSelection: widget.enableInteractiveSelection, + selectionControls: widget.selectionControls, + mouseCursor: widget.mouseCursor, + scrollController: widget.scrollController, + scrollPhysics: widget.scrollPhysics, + restorationId: widget.restorationId, + enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, + ); // the idea here is to have a mouse region that surround the input which // contains a flag button and a text field. The text field is surrounded // by padding so we want to request focus even when clicking outside of the @@ -126,6 +227,27 @@ class PhoneFieldState extends State { } Widget _getCountryCodeChip() { + return InkWell( + onTap: () {}, + child: SizedBox( + height: 64, + child: CountryChip( + key: const ValueKey('country-code-chip'), + isoCode: controller.value.isoCode, + showFlag: widget.showFlagInInput, + showIsoCode: widget.showIsoCodeInInput, + showDialCode: widget.showDialCode, + textStyle: widget.countryCodeStyle ?? + widget.decoration.labelStyle ?? + TextStyle( + fontSize: 16, + color: Theme.of(context).textTheme.bodySmall?.color, + ), + flagSize: widget.flagSize, + enabled: widget.enabled, + ), + ), + ); return Directionality( textDirection: TextDirection.ltr, child: MouseRegion( @@ -162,7 +284,7 @@ class PhoneFieldState extends State { } InputDecoration _getInnerInputDecoration() { - return InputDecoration.collapsed( + return InputDecoration( hintText: widget.decoration.hintText, hintStyle: widget.decoration.hintStyle, ).copyWith( @@ -179,6 +301,7 @@ class PhoneFieldState extends State { return widget.decoration.copyWith( hintText: null, + isCollapsed: true, errorText: widget.errorText, prefix: directionality == TextDirection.ltr ? _getCountryCodeChip() : null, From e797434fe1b0a5d061481973d21d53ff1f7734a2 Mon Sep 17 00:00:00 2001 From: cedvdb Date: Sat, 3 Feb 2024 12:39:41 +0100 Subject: [PATCH 10/25] upgrade flag cache --- .../country_selection/country_list_view.dart | 3 --- .../country_selection/country_selector.dart | 4 ---- .../country_selector_navigator.dart | 21 +++---------------- .../country_selector_page.dart | 4 ---- pubspec.yaml | 2 +- 5 files changed, 4 insertions(+), 30 deletions(-) diff --git a/lib/src/country_selection/country_list_view.dart b/lib/src/country_selection/country_list_view.dart index 23dbfe9a..81ee23fa 100644 --- a/lib/src/country_selection/country_list_view.dart +++ b/lib/src/country_selection/country_list_view.dart @@ -31,7 +31,6 @@ class CountryListView extends StatelessWidget { final TextStyle? subtitleStyle; final TextStyle? titleStyle; - final FlagCache? flagCache; CountryListView({ super.key, @@ -39,7 +38,6 @@ class CountryListView extends StatelessWidget { required this.favorites, required this.onTap, required this.noResultMessage, - required this.flagCache, this.scrollController, this.scrollPhysics, this.showDialCode = true, @@ -79,7 +77,6 @@ class CountryListView extends StatelessWidget { country.isoCode.name, key: ValueKey('circle-flag-${country.isoCode.name}'), size: flagSize, - cache: flagCache, ), title: Align( alignment: AlignmentDirectional.centerStart, diff --git a/lib/src/country_selection/country_selector.dart b/lib/src/country_selection/country_selector.dart index 3582863e..d9238141 100644 --- a/lib/src/country_selection/country_selector.dart +++ b/lib/src/country_selection/country_selector.dart @@ -1,4 +1,3 @@ -import 'package:circle_flags/circle_flags.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:phone_form_field/src/country_selection/country_selector_controller.dart'; @@ -59,12 +58,10 @@ class CountrySelector extends StatefulWidget { /// The [Color] of the Search Icon in the Search Box final Color? searchBoxIconColor; final double flagSize; - final FlagCache flagCache; const CountrySelector({ super.key, required this.onCountrySelected, - required this.flagCache, this.scrollController, this.scrollPhysics, this.addFavoritesSeparator = true, @@ -158,7 +155,6 @@ class CountrySelectorState extends State { noResultMessage: widget.noResultMessage, titleStyle: widget.titleStyle, subtitleStyle: widget.subtitleStyle, - flagCache: widget.flagCache, ); }, ), diff --git a/lib/src/country_selection/country_selector_navigator.dart b/lib/src/country_selection/country_selector_navigator.dart index e13b9e98..19fd1e48 100644 --- a/lib/src/country_selection/country_selector_navigator.dart +++ b/lib/src/country_selection/country_selector_navigator.dart @@ -1,4 +1,3 @@ -import 'package:circle_flags/circle_flags.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:phone_form_field/phone_form_field.dart'; @@ -39,11 +38,10 @@ abstract class CountrySelectorNavigator { this.useRootNavigator = true, }); - Future navigate(BuildContext context, FlagCache flagCache); + Future navigate(BuildContext context); CountrySelector _getCountrySelector({ required ValueChanged onCountrySelected, - required FlagCache flagCache, ScrollController? scrollController, }) { return CountrySelector( @@ -62,7 +60,6 @@ abstract class CountrySelectorNavigator { searchBoxIconColor: searchBoxIconColor, scrollPhysics: scrollPhysics, flagSize: flagSize, - flagCache: flagCache, ); } @@ -179,8 +176,7 @@ class DialogNavigator extends CountrySelectorNavigator { }); @override - Future navigate( - BuildContext context, FlagCache flagCache) { + Future navigate(BuildContext context) { return showDialog( context: context, builder: (_) => Dialog( @@ -190,7 +186,6 @@ class DialogNavigator extends CountrySelectorNavigator { child: _getCountrySelector( onCountrySelected: (country) => Navigator.of(context, rootNavigator: true).pop(country), - flagCache: flagCache, ), ), ), @@ -220,7 +215,6 @@ class PageNavigator extends CountrySelectorNavigator { CountrySelectorPage _getCountrySelectorPage({ required ValueChanged onCountrySelected, - required FlagCache flagCache, ScrollController? scrollController, }) { return CountrySelectorPage( @@ -234,20 +228,17 @@ class PageNavigator extends CountrySelectorNavigator { showCountryCode: showCountryCode, titleStyle: titleStyle, subtitleStyle: subtitleStyle, - flagCache: flagCache, ); } @override Future navigate( BuildContext context, - FlagCache flagCache, ) { return Navigator.of(context).push( MaterialPageRoute( builder: (ctx) => _getCountrySelectorPage( onCountrySelected: (country) => Navigator.pop(context, country), - flagCache: flagCache, ), ), ); @@ -274,7 +265,6 @@ class BottomSheetNavigator extends CountrySelectorNavigator { @override Future navigate( BuildContext context, - FlagCache flagCache, ) { LocalizedCountry? selected; final ctrl = showBottomSheet( @@ -287,7 +277,6 @@ class BottomSheetNavigator extends CountrySelectorNavigator { selected = country; Navigator.pop(context, country); }, - flagCache: flagCache, ), ), ), @@ -319,7 +308,6 @@ class ModalBottomSheetNavigator extends CountrySelectorNavigator { @override Future navigate( BuildContext context, - FlagCache flagCache, ) { return showModalBottomSheet( context: context, @@ -327,7 +315,6 @@ class ModalBottomSheetNavigator extends CountrySelectorNavigator { height: height ?? MediaQuery.of(context).size.height - 90, child: _getCountrySelector( onCountrySelected: (country) => Navigator.pop(context, country), - flagCache: flagCache, ), ), isScrollControlled: true, @@ -364,8 +351,7 @@ class DraggableModalBottomSheetNavigator extends CountrySelectorNavigator { }); @override - Future navigate( - BuildContext context, FlagCache flagCache) { + Future navigate(BuildContext context) { final effectiveBorderRadius = borderRadius ?? const BorderRadius.only( topLeft: Radius.circular(16), @@ -389,7 +375,6 @@ class DraggableModalBottomSheetNavigator extends CountrySelectorNavigator { child: _getCountrySelector( onCountrySelected: (country) => Navigator.pop(context, country), scrollController: scrollController, - flagCache: flagCache, ), ); }, diff --git a/lib/src/country_selection/country_selector_page.dart b/lib/src/country_selection/country_selector_page.dart index 61db6ffb..b9491b6a 100644 --- a/lib/src/country_selection/country_selector_page.dart +++ b/lib/src/country_selection/country_selector_page.dart @@ -1,4 +1,3 @@ -import 'package:circle_flags/circle_flags.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:phone_form_field/l10n/generated/phone_field_localization.dart'; @@ -60,12 +59,10 @@ class CountrySelectorPage extends StatefulWidget { /// The [Color] of the Search Icon in the Search Box final Color? searchBoxIconColor; final double flagSize; - final FlagCache flagCache; const CountrySelectorPage({ super.key, required this.onCountrySelected, - required this.flagCache, this.scrollController, this.scrollPhysics, this.addFavoritesSeparator = true, @@ -147,7 +144,6 @@ class CountrySelectorPageState extends State { noResultMessage: widget.noResultMessage, titleStyle: widget.titleStyle, subtitleStyle: widget.subtitleStyle, - flagCache: widget.flagCache, ); }, ), diff --git a/pubspec.yaml b/pubspec.yaml index ffe26dcc..a7d93009 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: flutter_localizations: # Add this line sdk: flutter - circle_flags: ^3.0.1 + circle_flags: ^4.0.0 phone_numbers_parser: ^8.1.2 intl: ">=0.18.1 <=1.0.0" diacritic: ^0.1.5 From e5cb97b01a2e56b20d7922a888f13c7c1dea393e Mon Sep 17 00:00:00 2001 From: cedvdb Date: Sat, 3 Feb 2024 12:41:32 +0100 Subject: [PATCH 11/25] cache --- test/_country_selector_navigator_test.dart | 9 ++++----- test/_country_selector_test.dart | 3 --- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/test/_country_selector_navigator_test.dart b/test/_country_selector_navigator_test.dart index c1462e75..992b5269 100644 --- a/test/_country_selector_navigator_test.dart +++ b/test/_country_selector_navigator_test.dart @@ -1,4 +1,3 @@ -import 'package:circle_flags/circle_flags.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:phone_form_field/phone_form_field.dart'; @@ -18,7 +17,7 @@ void main() { testWidgets('should navigate to dialog', (tester) async { const nav = CountrySelectorNavigator.dialog(); - await tester.pumpWidget(getApp((ctx) => nav.navigate(ctx, FlagCache()))); + await tester.pumpWidget(getApp((ctx) => nav.navigate(ctx))); await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); expect(find.byType(CountrySelector), findsOneWidget); @@ -26,7 +25,7 @@ void main() { testWidgets('should navigate to modal bottom sheet', (tester) async { const nav = CountrySelectorNavigator.modalBottomSheet(); - await tester.pumpWidget(getApp((ctx) => nav.navigate(ctx, FlagCache()))); + await tester.pumpWidget(getApp((ctx) => nav.navigate(ctx))); await tester.tap(find.byType(ElevatedButton)); await tester.pump(const Duration(seconds: 1)); expect(find.byType(CountrySelector), findsOneWidget); @@ -34,7 +33,7 @@ void main() { testWidgets('should navigate to bottom sheet', (tester) async { const nav = CountrySelectorNavigator.bottomSheet(); - await tester.pumpWidget(getApp((ctx) => nav.navigate(ctx, FlagCache()))); + await tester.pumpWidget(getApp((ctx) => nav.navigate(ctx))); await tester.tap(find.byType(ElevatedButton)); await tester.pump(const Duration(seconds: 1)); expect(find.byType(CountrySelector), findsOneWidget); @@ -42,7 +41,7 @@ void main() { testWidgets('should navigate to draggable sheet', (tester) async { const nav = CountrySelectorNavigator.draggableBottomSheet(); - await tester.pumpWidget(getApp((ctx) => nav.navigate(ctx, FlagCache()))); + await tester.pumpWidget(getApp((ctx) => nav.navigate(ctx))); await tester.tap(find.byType(ElevatedButton)); await tester.pump(const Duration(seconds: 1)); expect(find.byType(CountrySelector), findsOneWidget); diff --git a/test/_country_selector_test.dart b/test/_country_selector_test.dart index 8c88b270..76b729f2 100644 --- a/test/_country_selector_test.dart +++ b/test/_country_selector_test.dart @@ -1,4 +1,3 @@ -import 'package:circle_flags/circle_flags.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -19,7 +18,6 @@ void main() { home: Scaffold( body: CountrySelector( onCountrySelected: (c) {}, - flagCache: FlagCache(), favoriteCountries: favorites, ), ), @@ -99,7 +97,6 @@ void main() { home: Scaffold( body: CountrySelector( onCountrySelected: (c) {}, - flagCache: FlagCache(), ), ), )); From aec50292b225487db9dd0374e8963c24e0adbd14 Mon Sep 17 00:00:00 2001 From: cedvdb Date: Sat, 3 Feb 2024 15:57:28 +0100 Subject: [PATCH 12/25] saving --- example/lib/main.dart | 33 +++- example/pubspec.lock | 4 +- lib/src/phone_field.dart | 5 +- lib/src/phone_field_state.dart | 277 ++++++--------------------------- lib/src/phone_form_field.dart | 14 +- 5 files changed, 97 insertions(+), 236 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 15d69476..db178fa1 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -56,6 +56,7 @@ class PhoneFieldView extends StatelessWidget { controller: controller, focusNode: focusNode, shouldFormat: shouldFormat && !useRtl, + isCountryChipPersistent: isCountryChipPersistent, autofocus: false, autofillHints: const [AutofillHints.telephoneNumber], countrySelectorNavigator: selectorNavigator, @@ -77,7 +78,6 @@ class PhoneFieldView extends StatelessWidget { onSaved: (p) => print('saved $p'), // ignore: avoid_print onChanged: (p) => print('changed $p'), - isCountryChipPersistent: isCountryChipPersistent, ), ), ); @@ -127,6 +127,7 @@ class PhoneFormFieldScreen extends StatefulWidget { class PhoneFormFieldScreenState extends State { late PhoneController controller; final FocusNode focusNode = FocusNode(); + bool outlineBorder = true; bool mobileOnly = true; bool shouldFormat = true; @@ -248,6 +249,26 @@ class PhoneFormFieldScreenState extends State { key: formKey, child: Column( children: [ + TextFormField( + decoration: InputDecoration( + label: withLabel ? const Text('Phone') : null, + prefix: Icon(Icons.house), + border: outlineBorder + ? const OutlineInputBorder() + : const UnderlineInputBorder(), + hintText: withLabel ? '' : 'Phone', + ), + ), + TextFormField( + decoration: InputDecoration( + label: withLabel ? const Text('Phone') : null, + prefix: Icon(Icons.house), + border: outlineBorder + ? const OutlineInputBorder() + : const UnderlineInputBorder(), + hintText: withLabel ? '' : 'Phone', + ), + ), PhoneFieldView( inputKey: phoneKey, controller: controller, @@ -269,6 +290,16 @@ class PhoneFormFieldScreenState extends State { hintText: withLabel ? '' : 'Phone', ), ), + TextFormField( + decoration: InputDecoration( + label: withLabel ? const Text('Phone') : null, + prefix: Icon(Icons.house), + border: outlineBorder + ? const OutlineInputBorder() + : const UnderlineInputBorder(), + hintText: withLabel ? '' : 'Phone', + ), + ), ], ), ), diff --git a/example/pubspec.lock b/example/pubspec.lock index e056cced..dd592d17 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: circle_flags - sha256: cac0fe72ad731cae5984e30be536814d7df37eeb7efc388ba76fdb84dab47ac4 + sha256: bf798dd8a651ee4301e35b85d8227b1171ee5d59cf1c1d003d5a9b5cfb256611 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "4.0.0" clock: dependency: transitive description: diff --git a/lib/src/phone_field.dart b/lib/src/phone_field.dart index eac7bf9d..3123e7c7 100644 --- a/lib/src/phone_field.dart +++ b/lib/src/phone_field.dart @@ -6,7 +6,6 @@ import 'package:flutter/services.dart'; import 'package:phone_form_field/src/validation/allowed_characters.dart'; import '../../phone_form_field.dart'; -import 'phone_field_controller.dart'; part 'phone_field_state.dart'; @@ -19,11 +18,12 @@ class PhoneField extends StatefulWidget { final bool showFlagInInput; final bool showIsoCodeInInput; final bool showDialCode; + final bool isCountryChipPersistent; final String? errorText; final double flagSize; final InputDecoration decoration; final bool isCountrySelectionEnabled; - final bool isCountryChipPersistent; + final EdgeInsets? countryButtonPadding; /// configures the way the country picker selector is shown final CountrySelectorNavigator selectorNavigator; @@ -81,6 +81,7 @@ class PhoneField extends StatefulWidget { required this.decoration, required this.isCountrySelectionEnabled, required this.isCountryChipPersistent, + required this.countryButtonPadding, // textfield inputs required this.keyboardType, required this.textInputAction, diff --git a/lib/src/phone_field_state.dart b/lib/src/phone_field_state.dart index 098c1b45..0bc788a1 100644 --- a/lib/src/phone_field_state.dart +++ b/lib/src/phone_field_state.dart @@ -3,31 +3,21 @@ part of 'phone_field.dart'; class PhoneFieldState extends State { PhoneController get controller => widget.controller; FocusNode get focusNode => widget.focusNode; - - final _flagCache = FlagCache(); PhoneFieldState(); @override void initState() { _preloadFlagsInMemory(); - focusNode.addListener(_onFocusChange); super.initState(); } @override void dispose() { - focusNode.removeListener(_onFocusChange); super.dispose(); } void _preloadFlagsInMemory() { - _flagCache.preload(IsoCode.values.map((isoCode) => isoCode.name)); - } - - void _onFocusChange() { - // call setState when the text input has focus so - // the flag is shown when there is a label - setState(() {}); + CircleFlag.preload(IsoCode.values.map((isoCode) => isoCode.name).toList()); } void selectCountry() async { @@ -35,8 +25,7 @@ class PhoneFieldState extends State { return; } SystemChannels.textInput.invokeMethod('TextInput.hide'); - final selected = - await widget.selectorNavigator.navigate(context, _flagCache); + final selected = await widget.selectorNavigator.navigate(context); if (selected != null) { controller.changeCountry(selected.isoCode); } @@ -55,129 +44,18 @@ class PhoneFieldState extends State { @override Widget build(BuildContext context) { - return InputDecorator( - decoration: _getOutterInputDecoration(), - isFocused: focusNode.hasFocus, - isEmpty: _isEffectivelyEmpty(), - child: TextField( - focusNode: focusNode, - controller: controller.nationalNumberController, - enabled: widget.enabled, - decoration: _getInnerInputDecoration(), - inputFormatters: widget.inputFormatters ?? - [ - FilteringTextInputFormatter.allow(RegExp( - '[${AllowedCharacters.plus}${AllowedCharacters.digits}${AllowedCharacters.punctuation}]')), - ], - onChanged: (txt) => controller.changeText(txt), - autofillHints: widget.autofillHints, - keyboardType: widget.keyboardType, - textInputAction: widget.textInputAction, - style: widget.style, - strutStyle: widget.strutStyle, - textAlign: widget.textAlign, - textAlignVertical: widget.textAlignVertical, - autofocus: widget.autofocus, - obscuringCharacter: widget.obscuringCharacter, - obscureText: widget.obscureText, - autocorrect: widget.autocorrect, - smartDashesType: widget.smartDashesType, - smartQuotesType: widget.smartQuotesType, - enableSuggestions: widget.enableSuggestions, - contextMenuBuilder: - widget.contextMenuBuilder ?? _defaultContextMenuBuilder, - showCursor: widget.showCursor, - onEditingComplete: widget.onEditingComplete, - onSubmitted: widget.onSubmitted, - onAppPrivateCommand: widget.onAppPrivateCommand, - cursorWidth: widget.cursorWidth, - cursorHeight: widget.cursorHeight, - cursorRadius: widget.cursorRadius, - cursorColor: widget.cursorColor, - onTapOutside: widget.onTapOutside, - selectionHeightStyle: widget.selectionHeightStyle, - selectionWidthStyle: widget.selectionWidthStyle, - keyboardAppearance: widget.keyboardAppearance, - scrollPadding: widget.scrollPadding, - enableInteractiveSelection: widget.enableInteractiveSelection, - selectionControls: widget.selectionControls, - mouseCursor: widget.mouseCursor, - scrollController: widget.scrollController, - scrollPhysics: widget.scrollPhysics, - restorationId: widget.restorationId, - enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, - ), - ); - return TextFormField( - decoration: _getOutterInputDecoration(), - focusNode: focusNode, - controller: controller.nationalNumberController, - enabled: widget.enabled, - // decoration: _getInnerInputDecoration(), - inputFormatters: widget.inputFormatters ?? - [ - FilteringTextInputFormatter.allow(RegExp( - '[${AllowedCharacters.plus}${AllowedCharacters.digits}${AllowedCharacters.punctuation}]')), - ], - onChanged: (txt) => controller.changeText(txt), - autofillHints: widget.autofillHints, - keyboardType: widget.keyboardType, - textInputAction: widget.textInputAction, - style: widget.style, - strutStyle: widget.strutStyle, - textAlign: widget.textAlign, - textAlignVertical: widget.textAlignVertical, - autofocus: widget.autofocus, - obscuringCharacter: widget.obscuringCharacter, - obscureText: widget.obscureText, - autocorrect: widget.autocorrect, - smartDashesType: widget.smartDashesType, - smartQuotesType: widget.smartQuotesType, - enableSuggestions: widget.enableSuggestions, - contextMenuBuilder: - widget.contextMenuBuilder ?? _defaultContextMenuBuilder, - showCursor: widget.showCursor, - onEditingComplete: widget.onEditingComplete, - onAppPrivateCommand: widget.onAppPrivateCommand, - cursorWidth: widget.cursorWidth, - cursorHeight: widget.cursorHeight, - cursorRadius: widget.cursorRadius, - cursorColor: widget.cursorColor, - onTapOutside: widget.onTapOutside, - selectionHeightStyle: widget.selectionHeightStyle, - selectionWidthStyle: widget.selectionWidthStyle, - keyboardAppearance: widget.keyboardAppearance, - scrollPadding: widget.scrollPadding, - enableInteractiveSelection: widget.enableInteractiveSelection, - selectionControls: widget.selectionControls, - mouseCursor: widget.mouseCursor, - scrollController: widget.scrollController, - scrollPhysics: widget.scrollPhysics, - restorationId: widget.restorationId, - enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, - ); - // the idea here is to have a mouse region that surround the input which - // contains a flag button and a text field. The text field is surrounded - // by padding so we want to request focus even when clicking outside of the - // inner field. - // When the country chip is not shown it request focus to the inner text - // field which doesn't span the whole input - // When the country chip is shown, clicking on it request country selection - return MouseRegion( - cursor: SystemMouseCursors.text, - child: GestureDetector( - onTap: focusNode.requestFocus, - // absorb pointer when the country chip is not shown, else flutter - // still allows the country chip to be clicked even though it is not shown - child: InputDecorator( - decoration: _getOutterInputDecoration(), - isFocused: focusNode.hasFocus, - isEmpty: _isEffectivelyEmpty(), - child: TextField( + return AnimatedBuilder( + animation: focusNode, + builder: (context, _) { + return TextFormField( + decoration: widget.decoration.copyWith( + errorText: widget.errorText, + prefixIcon: _getCountryCodeChip(), + ), focusNode: focusNode, controller: controller.nationalNumberController, enabled: widget.enabled, - decoration: _getInnerInputDecoration(), + // decoration: _getInnerInputDecoration(), inputFormatters: widget.inputFormatters ?? [ FilteringTextInputFormatter.allow(RegExp( @@ -202,7 +80,6 @@ class PhoneFieldState extends State { widget.contextMenuBuilder ?? _defaultContextMenuBuilder, showCursor: widget.showCursor, onEditingComplete: widget.onEditingComplete, - onSubmitted: widget.onSubmitted, onAppPrivateCommand: widget.onAppPrivateCommand, cursorWidth: widget.cursorWidth, cursorHeight: widget.cursorHeight, @@ -220,105 +97,51 @@ class PhoneFieldState extends State { scrollPhysics: widget.scrollPhysics, restorationId: widget.restorationId, enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, - ), - ), - ), - ); + ); + }); } - Widget _getCountryCodeChip() { - return InkWell( - onTap: () {}, - child: SizedBox( - height: 64, - child: CountryChip( - key: const ValueKey('country-code-chip'), - isoCode: controller.value.isoCode, - showFlag: widget.showFlagInInput, - showIsoCode: widget.showIsoCodeInInput, - showDialCode: widget.showDialCode, - textStyle: widget.countryCodeStyle ?? - widget.decoration.labelStyle ?? - TextStyle( - fontSize: 16, - color: Theme.of(context).textTheme.bodySmall?.color, - ), - flagSize: widget.flagSize, - enabled: widget.enabled, - ), - ), - ); - return Directionality( - textDirection: TextDirection.ltr, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: widget.enabled ? selectCountry : null, - // material here else the click pass through empty spaces - child: Material( - color: Colors.transparent, - child: Padding( - padding: !widget.showDialCode && !widget.showFlagInInput - ? EdgeInsets.zero - : const EdgeInsetsDirectional.fromSTEB(8, 0, 8, 0), - child: CountryChip( - key: const ValueKey('country-code-chip'), - isoCode: controller.value.isoCode, - showFlag: widget.showFlagInInput, - showIsoCode: widget.showIsoCodeInInput, - showDialCode: widget.showDialCode, - textStyle: widget.countryCodeStyle ?? - widget.decoration.labelStyle ?? - TextStyle( - fontSize: 16, - color: Theme.of(context).textTheme.bodySmall?.color, - ), - flagSize: widget.flagSize, - enabled: widget.enabled, - ), - ), + Widget? _getCountryCodeChip() { + if (widget.isCountryChipPersistent || focusNode.hasFocus) { + return InkWell( + onTap: widget.enabled ? selectCountry : null, + child: Padding( + padding: _computeCountryButtonPadding(), + child: CountryChip( + key: const ValueKey('country-code-chip'), + isoCode: controller.value.isoCode, + showFlag: widget.showFlagInInput, + showIsoCode: widget.showIsoCodeInInput, + showDialCode: widget.showDialCode, + textStyle: widget.countryCodeStyle ?? + widget.decoration.labelStyle ?? + TextStyle( + fontSize: 16, + color: Theme.of(context).textTheme.bodySmall?.color, + ), + flagSize: widget.flagSize, + enabled: widget.enabled, ), ), - ), - ); - } - - InputDecoration _getInnerInputDecoration() { - return InputDecoration( - hintText: widget.decoration.hintText, - hintStyle: widget.decoration.hintStyle, - ).copyWith( - focusedBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - enabledBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - ); - } - - InputDecoration _getOutterInputDecoration() { - final directionality = Directionality.of(context); - - return widget.decoration.copyWith( - hintText: null, - isCollapsed: true, - errorText: widget.errorText, - prefix: - directionality == TextDirection.ltr ? _getCountryCodeChip() : null, - suffix: - directionality == TextDirection.rtl ? _getCountryCodeChip() : null, - ); + ); + } + return null; } - bool _isEffectivelyEmpty() { - if (widget.isCountryChipPersistent) return false; - final outterDecoration = _getOutterInputDecoration(); - // when there is not label and an hint text we need to have - // isEmpty false so the country code is displayed along the - // hint text to not have the hint text in the middle - if (outterDecoration.label == null && outterDecoration.hintText != null) { - return false; + EdgeInsets _computeCountryButtonPadding() { + final countryButtonPadding = widget.countryButtonPadding; + EdgeInsets padding = + const EdgeInsets.symmetric(horizontal: 12, vertical: 16); + final isUnderline = widget.decoration.border is UnderlineInputBorder; + final hasLabel = + widget.decoration.label != null || widget.decoration.labelText != null; + if (countryButtonPadding != null) { + padding = countryButtonPadding; + } else if (isUnderline && hasLabel) { + padding = const EdgeInsets.fromLTRB(12, 25, 12, 7); + } else if (isUnderline && !hasLabel) { + padding = const EdgeInsets.fromLTRB(12, 2, 12, 0); } - return controller.nationalNumberController.text.isEmpty; + return padding; } } diff --git a/lib/src/phone_form_field.dart b/lib/src/phone_form_field.dart index 53cfe0a7..50f2d34d 100644 --- a/lib/src/phone_form_field.dart +++ b/lib/src/phone_form_field.dart @@ -98,6 +98,10 @@ class PhoneFormField extends FormField { /// show selected iso code or not final bool showIsoCodeInInput; + /// padding inside country button, + /// this can be used to align the country button with the phone number + final EdgeInsets? countryButtonPadding; + PhoneFormField({ super.key, this.controller, @@ -111,14 +115,17 @@ class PhoneFormField extends FormField { this.defaultCountry = IsoCode.US, InputDecoration decoration = const InputDecoration(border: UnderlineInputBorder()), - AutovalidateMode super.autovalidateMode = - AutovalidateMode.onUserInteraction, PhoneNumber? initialValue, double flagSize = 16, PhoneNumberInputValidator? validator, bool isCountrySelectionEnabled = true, bool isCountryChipPersistent = true, + this.showDialCode = true, + this.showIsoCodeInInput = false, + this.countryButtonPadding, // textfield inputs + AutovalidateMode super.autovalidateMode = + AutovalidateMode.onUserInteraction, TextInputType keyboardType = TextInputType.phone, TextInputAction? textInputAction, TextStyle? style, @@ -157,8 +164,6 @@ class PhoneFormField extends FormField { Iterable? autofillHints, super.restorationId, bool enableIMEPersonalizedLearning = true, - this.showDialCode = true, - this.showIsoCodeInInput = false, }) : assert( initialValue == null || controller == null, 'One of initialValue or controller can be specified at a time', @@ -181,6 +186,7 @@ class PhoneFormField extends FormField { enabled: enabled, isCountrySelectionEnabled: isCountrySelectionEnabled, isCountryChipPersistent: isCountryChipPersistent, + countryButtonPadding: countryButtonPadding, // textfield params autofillHints: autofillHints, keyboardType: keyboardType, From ba8461e3bf8c6a92e2dcb2a14814f2d8782e737f Mon Sep 17 00:00:00 2001 From: cedvdb Date: Sun, 4 Feb 2024 12:42:52 +0100 Subject: [PATCH 13/25] refa --- example/windows/flutter/CMakeLists.txt | 7 +- example/windows/runner/Runner.rc | 10 +- lib/phone_form_field.dart | 4 +- lib/src/country/country_button.dart | 80 +++++++++++ lib/src/country/country_chip.dart | 64 --------- lib/src/phone_controller.dart | 117 +++++++++++++++-- lib/src/phone_field_controller.dart | 117 ----------------- lib/src/phone_field_state.dart | 175 +++++++++++-------------- lib/src/phone_form_field.dart | 2 +- test/phone_form_field_test.dart | 8 +- 10 files changed, 278 insertions(+), 306 deletions(-) create mode 100644 lib/src/country/country_button.dart delete mode 100644 lib/src/country/country_chip.dart delete mode 100644 lib/src/phone_field_controller.dart diff --git a/example/windows/flutter/CMakeLists.txt b/example/windows/flutter/CMakeLists.txt index b2e4bd8d..4f2af69b 100644 --- a/example/windows/flutter/CMakeLists.txt +++ b/example/windows/flutter/CMakeLists.txt @@ -9,6 +9,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -91,7 +96,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/example/windows/runner/Runner.rc b/example/windows/runner/Runner.rc index 5fdea291..0f5c0857 100644 --- a/example/windows/runner/Runner.rc +++ b/example/windows/runner/Runner.rc @@ -60,14 +60,14 @@ IDI_APP_ICON ICON "resources\\app_icon.ico" // Version // -#ifdef FLUTTER_BUILD_NUMBER -#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else -#define VERSION_AS_NUMBER 1,0,0 +#define VERSION_AS_NUMBER 1,0,0,0 #endif -#ifdef FLUTTER_BUILD_NAME -#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION #else #define VERSION_AS_STRING "1.0.0" #endif diff --git a/lib/phone_form_field.dart b/lib/phone_form_field.dart index 104ae298..f5d951f2 100644 --- a/lib/phone_form_field.dart +++ b/lib/phone_form_field.dart @@ -3,13 +3,13 @@ library phone_number_input; export 'src/phone_form_field.dart'; export 'src/country_selection/country_selector_navigator.dart'; export 'src/country_selection/country_selector.dart'; -export 'src/country/country_chip.dart'; +export 'src/country/country_button.dart'; export 'src/validation/phone_validator.dart'; export 'l10n/generated/phone_field_localization.dart'; -export 'src/phone_field_controller.dart'; +export 'src/phone_controller.dart'; export 'src/country/localized_country.dart'; export 'package:phone_numbers_parser/phone_numbers_parser.dart' diff --git a/lib/src/country/country_button.dart b/lib/src/country/country_button.dart new file mode 100644 index 00000000..dcc5286c --- /dev/null +++ b/lib/src/country/country_button.dart @@ -0,0 +1,80 @@ +import 'package:circle_flags/circle_flags.dart'; +import 'package:flutter/material.dart'; +import 'package:phone_numbers_parser/phone_numbers_parser.dart'; + +import 'localized_country.dart'; + +@Deprecated('Use [CountryButton] instead') +typedef CountryChip = CountryButton; + +class CountryButton extends StatelessWidget { + final Function()? onTap; + final IsoCode isoCode; + final bool showFlag; + final bool showDialCode; + final TextStyle? textStyle; + final EdgeInsets padding; + final double flagSize; + final TextDirection? textDirection; + final bool showIsoCode; + final bool enabled; + + const CountryButton({ + super.key, + required this.isoCode, + required this.onTap, + this.textStyle, + this.showFlag = true, + this.showDialCode = true, + this.padding = const EdgeInsets.fromLTRB(12, 16, 4, 16), + this.flagSize = 20, + this.textDirection, + this.showIsoCode = false, + this.enabled = true, + }); + + @override + Widget build(BuildContext context) { + final textStyle = this.textStyle ?? + Theme.of(context).textTheme.labelMedium ?? + const TextStyle(); + final country = LocalizedCountry.fromContext(context, isoCode); + return InkWell( + onTap: onTap, + child: Padding( + padding: padding, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (showIsoCode) ...[ + Text( + country.isoCode.name, + style: textStyle.copyWith( + color: enabled ? null : Theme.of(context).disabledColor, + ), + ), + const SizedBox(width: 8), + ], + if (showFlag) ...[ + CircleFlag( + country.isoCode.name, + size: flagSize, + ), + const SizedBox(width: 8), + ], + if (showDialCode) ...[ + Text( + country.formattedCountryDialingCode, + style: textStyle.copyWith( + color: enabled ? null : Theme.of(context).disabledColor, + ), + textDirection: textDirection, + ), + ], + const Icon(Icons.arrow_drop_down), + ], + ), + ), + ); + } +} diff --git a/lib/src/country/country_chip.dart b/lib/src/country/country_chip.dart deleted file mode 100644 index fdcec7d5..00000000 --- a/lib/src/country/country_chip.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:circle_flags/circle_flags.dart'; -import 'package:flutter/material.dart'; -import 'package:phone_numbers_parser/phone_numbers_parser.dart'; - -import 'localized_country.dart'; - -class CountryChip extends StatelessWidget { - final IsoCode isoCode; - final bool showFlag; - final bool showDialCode; - final TextStyle textStyle; - final EdgeInsets padding; - final double flagSize; - final TextDirection? textDirection; - final bool showIsoCode; - final bool enabled; - - const CountryChip({ - super.key, - required this.isoCode, - this.textStyle = const TextStyle(), - this.showFlag = true, - this.showDialCode = true, - this.padding = const EdgeInsets.all(20), - this.flagSize = 20, - this.textDirection, - this.showIsoCode = false, - this.enabled = true, - }); - - @override - Widget build(BuildContext context) { - final country = LocalizedCountry.fromContext(context, isoCode); - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (showIsoCode) ...[ - Text( - country.isoCode.name, - style: textStyle.copyWith( - color: enabled ? null : Theme.of(context).disabledColor, - ), - ), - const SizedBox(width: 8), - ], - if (showFlag) ...[ - CircleFlag( - country.isoCode.name, - size: flagSize, - ), - const SizedBox(width: 8), - ], - if (showDialCode) - Text( - country.formattedCountryDialingCode, - style: textStyle.copyWith( - color: enabled ? null : Theme.of(context).disabledColor, - ), - textDirection: textDirection, - ), - ], - ); - } -} diff --git a/lib/src/phone_controller.dart b/lib/src/phone_controller.dart index 94c95ecc..0c926324 100644 --- a/lib/src/phone_controller.dart +++ b/lib/src/phone_controller.dart @@ -1,28 +1,117 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; -import 'package:phone_form_field/phone_form_field.dart'; +import 'package:phone_form_field/src/validation/allowed_characters.dart'; +import 'package:phone_numbers_parser/phone_numbers_parser.dart'; -class PhoneController extends ValueNotifier { - final PhoneNumber? initialValue; - // when we want to select the national number - final StreamController _selectionRequestController = - StreamController.broadcast(); - Stream get selectionRequestStream => _selectionRequestController.stream; +class PhoneController extends ChangeNotifier { + final bool shouldFormat; - PhoneController(this.initialValue) : super(initialValue); + /// focus node of the national number + // final FocusNode focusNode; + final PhoneNumber initialValue; + PhoneNumber _value; + PhoneNumber get value => _value; - selectNationalNumber() { - _selectionRequestController.add(null); + set value(PhoneNumber phoneNumber) { + _value = phoneNumber; + changeCountry(_value.isoCode); + changeText(_value.nsn); } + /// text editing controller of the nsn ( where user types the phone number ) + late final TextEditingController nationalNumberController; + + PhoneController({ + this.initialValue = const PhoneNumber(isoCode: IsoCode.US, nsn: ''), + this.shouldFormat = true, + }) : _value = initialValue, + nationalNumberController = + TextEditingController(text: initialValue.getFormattedNsn()); + reset() { - value = null; + _value = initialValue; + changeCountry(_value.isoCode); + changeText(_value.nsn); + notifyListeners(); + } + + changeCountry(IsoCode isoCode) { + _value = PhoneNumber.parse( + _value.nsn, + destinationCountry: isoCode, + ); + notifyListeners(); + } + + changeText(String? text) { + text = text ?? ''; + var newText = text; + + // if starts with + then we parse the whole number + final startsWithPlus = + text.startsWith(RegExp('[${AllowedCharacters.plus}]')); + + if (startsWithPlus) { + final phoneNumber = _tryParseWithPlus(text); + // if we could parse the phone number we can change the value inside + // the national number field to remove the "+ country dial code" + if (phoneNumber != null) { + _value = phoneNumber; + newText = _value.getFormattedNsn(); + } + } else { + final phoneNumber = PhoneNumber.parse( + text, + destinationCountry: _value.isoCode, + ); + _value = phoneNumber; + newText = phoneNumber.getFormattedNsn(); + } + nationalNumberController.value = TextEditingValue( + text: newText, + selection: computeSelection(text, newText), + ); + + notifyListeners(); + } + + /// When the cursor is at the end of the text we need to preserve that. + /// Since there is formatting going on we need to explicitely do it. + /// We don't want to do it in the middle because the user might have + /// used arrow keys to move inside the text. + TextSelection computeSelection(String originalText, String newText) { + final currentSelectionOffset = + nationalNumberController.selection.extentOffset; + final isCursorAtEnd = currentSelectionOffset == originalText.length; + var offset = currentSelectionOffset; + + if (isCursorAtEnd || currentSelectionOffset >= newText.length) { + offset = newText.length; + } + return TextSelection.fromPosition( + TextPosition(offset: offset), + ); + } + + PhoneNumber? _tryParseWithPlus(String text) { + try { + return PhoneNumber.parse(text); + // parsing "+", a country code won't be found + } on PhoneNumberException { + return null; + } + } + + selectNationalNumber() { + nationalNumberController.selection = TextSelection( + baseOffset: 0, + extentOffset: nationalNumberController.value.text.length, + ); + notifyListeners(); } @override void dispose() { - _selectionRequestController.close(); + nationalNumberController.dispose(); super.dispose(); } } diff --git a/lib/src/phone_field_controller.dart b/lib/src/phone_field_controller.dart deleted file mode 100644 index 0c926324..00000000 --- a/lib/src/phone_field_controller.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:phone_form_field/src/validation/allowed_characters.dart'; -import 'package:phone_numbers_parser/phone_numbers_parser.dart'; - -class PhoneController extends ChangeNotifier { - final bool shouldFormat; - - /// focus node of the national number - // final FocusNode focusNode; - final PhoneNumber initialValue; - PhoneNumber _value; - PhoneNumber get value => _value; - - set value(PhoneNumber phoneNumber) { - _value = phoneNumber; - changeCountry(_value.isoCode); - changeText(_value.nsn); - } - - /// text editing controller of the nsn ( where user types the phone number ) - late final TextEditingController nationalNumberController; - - PhoneController({ - this.initialValue = const PhoneNumber(isoCode: IsoCode.US, nsn: ''), - this.shouldFormat = true, - }) : _value = initialValue, - nationalNumberController = - TextEditingController(text: initialValue.getFormattedNsn()); - - reset() { - _value = initialValue; - changeCountry(_value.isoCode); - changeText(_value.nsn); - notifyListeners(); - } - - changeCountry(IsoCode isoCode) { - _value = PhoneNumber.parse( - _value.nsn, - destinationCountry: isoCode, - ); - notifyListeners(); - } - - changeText(String? text) { - text = text ?? ''; - var newText = text; - - // if starts with + then we parse the whole number - final startsWithPlus = - text.startsWith(RegExp('[${AllowedCharacters.plus}]')); - - if (startsWithPlus) { - final phoneNumber = _tryParseWithPlus(text); - // if we could parse the phone number we can change the value inside - // the national number field to remove the "+ country dial code" - if (phoneNumber != null) { - _value = phoneNumber; - newText = _value.getFormattedNsn(); - } - } else { - final phoneNumber = PhoneNumber.parse( - text, - destinationCountry: _value.isoCode, - ); - _value = phoneNumber; - newText = phoneNumber.getFormattedNsn(); - } - nationalNumberController.value = TextEditingValue( - text: newText, - selection: computeSelection(text, newText), - ); - - notifyListeners(); - } - - /// When the cursor is at the end of the text we need to preserve that. - /// Since there is formatting going on we need to explicitely do it. - /// We don't want to do it in the middle because the user might have - /// used arrow keys to move inside the text. - TextSelection computeSelection(String originalText, String newText) { - final currentSelectionOffset = - nationalNumberController.selection.extentOffset; - final isCursorAtEnd = currentSelectionOffset == originalText.length; - var offset = currentSelectionOffset; - - if (isCursorAtEnd || currentSelectionOffset >= newText.length) { - offset = newText.length; - } - return TextSelection.fromPosition( - TextPosition(offset: offset), - ); - } - - PhoneNumber? _tryParseWithPlus(String text) { - try { - return PhoneNumber.parse(text); - // parsing "+", a country code won't be found - } on PhoneNumberException { - return null; - } - } - - selectNationalNumber() { - nationalNumberController.selection = TextSelection( - baseOffset: 0, - extentOffset: nationalNumberController.value.text.length, - ); - notifyListeners(); - } - - @override - void dispose() { - nationalNumberController.dispose(); - super.dispose(); - } -} diff --git a/lib/src/phone_field_state.dart b/lib/src/phone_field_state.dart index 0bc788a1..7da70143 100644 --- a/lib/src/phone_field_state.dart +++ b/lib/src/phone_field_state.dart @@ -11,11 +11,6 @@ class PhoneFieldState extends State { super.initState(); } - @override - void dispose() { - super.dispose(); - } - void _preloadFlagsInMemory() { CircleFlag.preload(IsoCode.values.map((isoCode) => isoCode.name).toList()); } @@ -24,123 +19,107 @@ class PhoneFieldState extends State { if (!widget.isCountrySelectionEnabled) { return; } - SystemChannels.textInput.invokeMethod('TextInput.hide'); final selected = await widget.selectorNavigator.navigate(context); if (selected != null) { controller.changeCountry(selected.isoCode); } focusNode.requestFocus(); - SystemChannels.textInput.invokeMethod('TextInput.show'); - } - - static Widget _defaultContextMenuBuilder( - BuildContext context, - EditableTextState editableTextState, - ) { - return AdaptiveTextSelectionToolbar.editableText( - editableTextState: editableTextState, - ); } @override Widget build(BuildContext context) { return AnimatedBuilder( - animation: focusNode, - builder: (context, _) { - return TextFormField( - decoration: widget.decoration.copyWith( - errorText: widget.errorText, - prefixIcon: _getCountryCodeChip(), - ), - focusNode: focusNode, - controller: controller.nationalNumberController, - enabled: widget.enabled, - // decoration: _getInnerInputDecoration(), - inputFormatters: widget.inputFormatters ?? - [ - FilteringTextInputFormatter.allow(RegExp( - '[${AllowedCharacters.plus}${AllowedCharacters.digits}${AllowedCharacters.punctuation}]')), - ], - onChanged: (txt) => controller.changeText(txt), - autofillHints: widget.autofillHints, - keyboardType: widget.keyboardType, - textInputAction: widget.textInputAction, - style: widget.style, - strutStyle: widget.strutStyle, - textAlign: widget.textAlign, - textAlignVertical: widget.textAlignVertical, - autofocus: widget.autofocus, - obscuringCharacter: widget.obscuringCharacter, - obscureText: widget.obscureText, - autocorrect: widget.autocorrect, - smartDashesType: widget.smartDashesType, - smartQuotesType: widget.smartQuotesType, - enableSuggestions: widget.enableSuggestions, - contextMenuBuilder: - widget.contextMenuBuilder ?? _defaultContextMenuBuilder, - showCursor: widget.showCursor, - onEditingComplete: widget.onEditingComplete, - onAppPrivateCommand: widget.onAppPrivateCommand, - cursorWidth: widget.cursorWidth, - cursorHeight: widget.cursorHeight, - cursorRadius: widget.cursorRadius, - cursorColor: widget.cursorColor, - onTapOutside: widget.onTapOutside, - selectionHeightStyle: widget.selectionHeightStyle, - selectionWidthStyle: widget.selectionWidthStyle, - keyboardAppearance: widget.keyboardAppearance, - scrollPadding: widget.scrollPadding, - enableInteractiveSelection: widget.enableInteractiveSelection, - selectionControls: widget.selectionControls, - mouseCursor: widget.mouseCursor, - scrollController: widget.scrollController, - scrollPhysics: widget.scrollPhysics, - restorationId: widget.restorationId, - enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, - ); - }); + animation: focusNode, + builder: (context, _) { + return TextField( + decoration: widget.decoration.copyWith( + errorText: widget.errorText, + prefixIcon: + widget.isCountryChipPersistent ? _getCountryCodeChip() : null, + prefix: + widget.isCountryChipPersistent ? null : _getCountryCodeChip(), + ), + focusNode: focusNode, + controller: controller.nationalNumberController, + enabled: widget.enabled, + inputFormatters: widget.inputFormatters ?? + [ + FilteringTextInputFormatter.allow(RegExp( + '[${AllowedCharacters.plus}${AllowedCharacters.digits}${AllowedCharacters.punctuation}]')), + ], + onChanged: (txt) => controller.changeText(txt), + autofillHints: widget.autofillHints, + keyboardType: widget.keyboardType, + textInputAction: widget.textInputAction, + style: widget.style, + strutStyle: widget.strutStyle, + textAlign: widget.textAlign, + textAlignVertical: widget.textAlignVertical, + autofocus: widget.autofocus, + obscuringCharacter: widget.obscuringCharacter, + obscureText: widget.obscureText, + autocorrect: widget.autocorrect, + smartDashesType: widget.smartDashesType, + smartQuotesType: widget.smartQuotesType, + enableSuggestions: widget.enableSuggestions, + showCursor: widget.showCursor, + onEditingComplete: widget.onEditingComplete, + onAppPrivateCommand: widget.onAppPrivateCommand, + cursorWidth: widget.cursorWidth, + cursorHeight: widget.cursorHeight, + cursorRadius: widget.cursorRadius, + cursorColor: widget.cursorColor, + onTapOutside: widget.onTapOutside, + selectionHeightStyle: widget.selectionHeightStyle, + selectionWidthStyle: widget.selectionWidthStyle, + keyboardAppearance: widget.keyboardAppearance, + scrollPadding: widget.scrollPadding, + enableInteractiveSelection: widget.enableInteractiveSelection, + selectionControls: widget.selectionControls, + mouseCursor: widget.mouseCursor, + scrollController: widget.scrollController, + scrollPhysics: widget.scrollPhysics, + restorationId: widget.restorationId, + enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, + ); + }, + ); } - Widget? _getCountryCodeChip() { - if (widget.isCountryChipPersistent || focusNode.hasFocus) { - return InkWell( - onTap: widget.enabled ? selectCountry : null, - child: Padding( - padding: _computeCountryButtonPadding(), - child: CountryChip( - key: const ValueKey('country-code-chip'), - isoCode: controller.value.isoCode, - showFlag: widget.showFlagInInput, - showIsoCode: widget.showIsoCodeInInput, - showDialCode: widget.showDialCode, - textStyle: widget.countryCodeStyle ?? - widget.decoration.labelStyle ?? - TextStyle( - fontSize: 16, - color: Theme.of(context).textTheme.bodySmall?.color, - ), - flagSize: widget.flagSize, - enabled: widget.enabled, + Widget _getCountryCodeChip() { + return CountryButton( + key: const ValueKey('country-code-chip'), + isoCode: controller.value.isoCode, + onTap: widget.enabled ? selectCountry : null, + padding: _computeCountryButtonPadding(), + showFlag: widget.showFlagInInput, + showIsoCode: widget.showIsoCodeInInput, + showDialCode: widget.showDialCode, + textStyle: widget.countryCodeStyle ?? + widget.decoration.labelStyle ?? + TextStyle( + fontSize: 16, + color: Theme.of(context).textTheme.bodySmall?.color, ), - ), - ); - } - return null; + flagSize: widget.flagSize, + enabled: widget.enabled, + ); } EdgeInsets _computeCountryButtonPadding() { final countryButtonPadding = widget.countryButtonPadding; - EdgeInsets padding = - const EdgeInsets.symmetric(horizontal: 12, vertical: 16); + EdgeInsets padding = const EdgeInsets.fromLTRB(12, 16, 4, 16); final isUnderline = widget.decoration.border is UnderlineInputBorder; final hasLabel = widget.decoration.label != null || widget.decoration.labelText != null; if (countryButtonPadding != null) { padding = countryButtonPadding; + } else if (!widget.isCountryChipPersistent) { + padding = const EdgeInsets.only(right: 4, left: 12); } else if (isUnderline && hasLabel) { - padding = const EdgeInsets.fromLTRB(12, 25, 12, 7); + padding = const EdgeInsets.fromLTRB(12, 25, 4, 7); } else if (isUnderline && !hasLabel) { - padding = const EdgeInsets.fromLTRB(12, 2, 12, 0); + padding = const EdgeInsets.fromLTRB(12, 2, 4, 0); } return padding; } diff --git a/lib/src/phone_form_field.dart b/lib/src/phone_form_field.dart index 50f2d34d..3390fe7e 100644 --- a/lib/src/phone_form_field.dart +++ b/lib/src/phone_form_field.dart @@ -6,7 +6,7 @@ import 'package:phone_numbers_parser/phone_numbers_parser.dart'; import 'country_selection/country_selector_navigator.dart'; import 'phone_field.dart'; -import 'phone_field_controller.dart'; +import 'phone_controller.dart'; import 'validation/phone_validator.dart'; import 'validation/validator_translator.dart'; diff --git a/test/phone_form_field_test.dart b/test/phone_form_field_test.dart index 64aa8501..0dd2385e 100644 --- a/test/phone_form_field_test.dart +++ b/test/phone_form_field_test.dart @@ -57,7 +57,7 @@ void main() { testWidgets('Should display country code', (tester) async { await tester.pumpWidget(getWidget()); - expect(find.byType(CountryChip), findsWidgets); + expect(find.byType(CountryButton), findsWidgets); }); testWidgets('Should display flag', (tester) async { @@ -70,10 +70,10 @@ void main() { (tester) async { await tester.pumpWidget(getWidget(enabled: false)); final countryChip = - tester.widget(find.byType(CountryChip)); + tester.widget(find.byType(CountryButton)); expect(countryChip.enabled, false); - await tester.tap(find.byType(CountryChip)); + await tester.tap(find.byType(CountryButton)); await tester.pumpAndSettle(); expect(find.byType(CountryListView), findsNothing); @@ -87,7 +87,7 @@ void main() { expect(find.byType(CountryListView), findsNothing); await tester.tap(find.byType(PhoneFormField)); await tester.pump(const Duration(seconds: 1)); - await tester.tap(find.byType(CountryChip)); + await tester.tap(find.byType(CountryButton)); await tester.pumpAndSettle(); expect(find.byType(CountryListView), findsOneWidget); }); From 047989733584efcb578cbddda34e96064b0d87af Mon Sep 17 00:00:00 2001 From: cedvdb Date: Sun, 4 Feb 2024 12:56:45 +0100 Subject: [PATCH 14/25] refactor --- example/lib/main.dart | 39 ------------------------------- lib/src/phone_field_state.dart | 42 +++++++++++++++++++++++++--------- 2 files changed, 31 insertions(+), 50 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index db178fa1..ffbe991e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -249,26 +249,6 @@ class PhoneFormFieldScreenState extends State { key: formKey, child: Column( children: [ - TextFormField( - decoration: InputDecoration( - label: withLabel ? const Text('Phone') : null, - prefix: Icon(Icons.house), - border: outlineBorder - ? const OutlineInputBorder() - : const UnderlineInputBorder(), - hintText: withLabel ? '' : 'Phone', - ), - ), - TextFormField( - decoration: InputDecoration( - label: withLabel ? const Text('Phone') : null, - prefix: Icon(Icons.house), - border: outlineBorder - ? const OutlineInputBorder() - : const UnderlineInputBorder(), - hintText: withLabel ? '' : 'Phone', - ), - ), PhoneFieldView( inputKey: phoneKey, controller: controller, @@ -281,25 +261,6 @@ class PhoneFormFieldScreenState extends State { shouldFormat: shouldFormat, useRtl: useRtl, ), - TextFormField( - decoration: InputDecoration( - label: withLabel ? const Text('Phone') : null, - border: outlineBorder - ? const OutlineInputBorder() - : const UnderlineInputBorder(), - hintText: withLabel ? '' : 'Phone', - ), - ), - TextFormField( - decoration: InputDecoration( - label: withLabel ? const Text('Phone') : null, - prefix: Icon(Icons.house), - border: outlineBorder - ? const OutlineInputBorder() - : const UnderlineInputBorder(), - hintText: withLabel ? '' : 'Phone', - ), - ), ], ), ), diff --git a/lib/src/phone_field_state.dart b/lib/src/phone_field_state.dart index 7da70143..fe95d7f2 100644 --- a/lib/src/phone_field_state.dart +++ b/lib/src/phone_field_state.dart @@ -34,10 +34,12 @@ class PhoneFieldState extends State { return TextField( decoration: widget.decoration.copyWith( errorText: widget.errorText, - prefixIcon: - widget.isCountryChipPersistent ? _getCountryCodeChip() : null, - prefix: - widget.isCountryChipPersistent ? null : _getCountryCodeChip(), + prefixIcon: widget.isCountryChipPersistent + ? _getCountryCodeChip(context) + : null, + prefix: widget.isCountryChipPersistent + ? null + : _getCountryCodeChip(context), ), focusNode: focusNode, controller: controller.nationalNumberController, @@ -86,12 +88,12 @@ class PhoneFieldState extends State { ); } - Widget _getCountryCodeChip() { + Widget _getCountryCodeChip(BuildContext context) { return CountryButton( key: const ValueKey('country-code-chip'), isoCode: controller.value.isoCode, onTap: widget.enabled ? selectCountry : null, - padding: _computeCountryButtonPadding(), + padding: _computeCountryButtonPadding(context), showFlag: widget.showFlagInInput, showIsoCode: widget.showIsoCodeInInput, showDialCode: widget.showDialCode, @@ -106,20 +108,38 @@ class PhoneFieldState extends State { ); } - EdgeInsets _computeCountryButtonPadding() { + /// computes the padding inside the country button + /// this is used to align the flag and dial code with the rest + /// of the phone number. + /// The padding must work for this matrix: + /// - has label or not + /// - is border underline or outline + /// - is country button shown as a prefix or prefixIcon (isCountryChipPersistent) + /// - text direction + EdgeInsets _computeCountryButtonPadding(BuildContext context) { final countryButtonPadding = widget.countryButtonPadding; - EdgeInsets padding = const EdgeInsets.fromLTRB(12, 16, 4, 16); final isUnderline = widget.decoration.border is UnderlineInputBorder; final hasLabel = widget.decoration.label != null || widget.decoration.labelText != null; + final isLtr = Directionality.of(context) == TextDirection.ltr; + + EdgeInsets padding = isLtr + ? const EdgeInsets.fromLTRB(12, 16, 4, 16) + : const EdgeInsets.fromLTRB(4, 16, 12, 16); if (countryButtonPadding != null) { padding = countryButtonPadding; } else if (!widget.isCountryChipPersistent) { - padding = const EdgeInsets.only(right: 4, left: 12); + padding = isLtr + ? const EdgeInsets.only(right: 4, left: 12) + : const EdgeInsets.only(left: 4, right: 12); } else if (isUnderline && hasLabel) { - padding = const EdgeInsets.fromLTRB(12, 25, 4, 7); + padding = isLtr + ? const EdgeInsets.fromLTRB(12, 25, 4, 7) + : const EdgeInsets.fromLTRB(4, 25, 12, 7); } else if (isUnderline && !hasLabel) { - padding = const EdgeInsets.fromLTRB(12, 2, 4, 0); + padding = isLtr + ? const EdgeInsets.fromLTRB(12, 2, 4, 0) + : const EdgeInsets.fromLTRB(4, 2, 12, 0); } return padding; } From 788425885eee8586cf56fd75232d68f1b5b8ea9d Mon Sep 17 00:00:00 2001 From: cedvdb Date: Sun, 4 Feb 2024 13:35:01 +0100 Subject: [PATCH 15/25] comment --- lib/src/phone_form_field.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/phone_form_field.dart b/lib/src/phone_form_field.dart index 3390fe7e..966c38e1 100644 --- a/lib/src/phone_form_field.dart +++ b/lib/src/phone_form_field.dart @@ -100,6 +100,7 @@ class PhoneFormField extends FormField { /// padding inside country button, /// this can be used to align the country button with the phone number + /// and is mostly useful when using [isCountryChipPersistent] as true. final EdgeInsets? countryButtonPadding; PhoneFormField({ From 16b495ff86d0dd7e50ae5d642e672e0df6eb28af Mon Sep 17 00:00:00 2001 From: cedvdb Date: Sun, 4 Feb 2024 14:59:09 +0100 Subject: [PATCH 16/25] validation --- README.md | 4 +- .../country_selector_navigator.dart | 4 + lib/src/phone_field.dart | 130 -------- lib/src/phone_field_state.dart | 146 --------- lib/src/phone_form_field.dart | 287 +++++++++--------- lib/src/phone_form_field_state.dart | 148 ++++++++- lib/src/validation/phone_validator.dart | 67 ++-- test/phone_validator_test.dart | 13 +- 8 files changed, 326 insertions(+), 473 deletions(-) delete mode 100644 lib/src/phone_field.dart delete mode 100644 lib/src/phone_field_state.dart diff --git a/README.md b/README.md index 9ea08993..f3b2a338 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,8 @@ Here are the list of the parameters available for all built-in country selector ### Built-in country selector -* **CountrySelectorNavigator.searchDelegate** - Open a dialog to select the country. +* **CountrySelectorNavigator.page** + Open a page to select the country. No extra parameters * **CountrySelectorNavigator.dialog** diff --git a/lib/src/country_selection/country_selector_navigator.dart b/lib/src/country_selection/country_selector_navigator.dart index 19fd1e48..88b42340 100644 --- a/lib/src/country_selection/country_selector_navigator.dart +++ b/lib/src/country_selection/country_selector_navigator.dart @@ -81,6 +81,10 @@ abstract class CountrySelectorNavigator { ScrollPhysics? scrollPhysics, }) = DialogNavigator._; + @Deprecated('Use [CountrySelectorNavigator.page] instead') + const factory CountrySelectorNavigator.searchDelegate() = + CountrySelectorNavigator.page; + const factory CountrySelectorNavigator.page({ List? countries, List? favorites, diff --git a/lib/src/phone_field.dart b/lib/src/phone_field.dart deleted file mode 100644 index 3123e7c7..00000000 --- a/lib/src/phone_field.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; - -import 'package:circle_flags/circle_flags.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:phone_form_field/src/validation/allowed_characters.dart'; - -import '../../phone_form_field.dart'; - -part 'phone_field_state.dart'; - -/// Phone field -/// -/// This deals with mostly UI and has no dependency on any phone parser library -class PhoneField extends StatefulWidget { - final FocusNode focusNode; - final PhoneController controller; - final bool showFlagInInput; - final bool showIsoCodeInInput; - final bool showDialCode; - final bool isCountryChipPersistent; - final String? errorText; - final double flagSize; - final InputDecoration decoration; - final bool isCountrySelectionEnabled; - final EdgeInsets? countryButtonPadding; - - /// configures the way the country picker selector is shown - final CountrySelectorNavigator selectorNavigator; - - // textfield inputs - final TextInputType keyboardType; - final TextInputAction? textInputAction; - final TextStyle? style; - final TextStyle? countryCodeStyle; - final StrutStyle? strutStyle; - final TextAlign textAlign; - final TextAlignVertical? textAlignVertical; - final bool autofocus; - final String obscuringCharacter; - final bool obscureText; - final bool autocorrect; - final SmartDashesType? smartDashesType; - final SmartQuotesType? smartQuotesType; - final bool enableSuggestions; - final Widget Function(BuildContext, EditableTextState)? contextMenuBuilder; - final bool? showCursor; - final VoidCallback? onEditingComplete; - final ValueChanged? onSubmitted; - final AppPrivateCommandCallback? onAppPrivateCommand; - final Function(PointerDownEvent)? onTapOutside; - final bool enabled; - final double cursorWidth; - final double? cursorHeight; - final Radius? cursorRadius; - final Color? cursorColor; - final ui.BoxHeightStyle selectionHeightStyle; - final ui.BoxWidthStyle selectionWidthStyle; - final Brightness? keyboardAppearance; - final EdgeInsets scrollPadding; - final bool enableInteractiveSelection; - final TextSelectionControls? selectionControls; - bool get selectionEnabled => enableInteractiveSelection; - final MouseCursor? mouseCursor; - final ScrollPhysics? scrollPhysics; - final ScrollController? scrollController; - final Iterable? autofillHints; - final String? restorationId; - final bool enableIMEPersonalizedLearning; - final List? inputFormatters; - - const PhoneField({ - // form field params - super.key, - required this.controller, - required this.focusNode, - required this.showFlagInInput, - required this.selectorNavigator, - required this.flagSize, - required this.errorText, - required this.decoration, - required this.isCountrySelectionEnabled, - required this.isCountryChipPersistent, - required this.countryButtonPadding, - // textfield inputs - required this.keyboardType, - required this.textInputAction, - required this.style, - required this.countryCodeStyle, - required this.strutStyle, - required this.textAlign, - required this.textAlignVertical, - required this.autofocus, - required this.obscuringCharacter, - required this.obscureText, - required this.autocorrect, - required this.smartDashesType, - required this.smartQuotesType, - required this.enableSuggestions, - required this.contextMenuBuilder, - required this.showCursor, - required this.onEditingComplete, - required this.onSubmitted, - required this.onAppPrivateCommand, - required this.enabled, - required this.cursorWidth, - required this.cursorHeight, - required this.cursorRadius, - required this.cursorColor, - required this.selectionHeightStyle, - required this.selectionWidthStyle, - required this.keyboardAppearance, - required this.scrollPadding, - required this.enableInteractiveSelection, - required this.selectionControls, - required this.mouseCursor, - required this.scrollPhysics, - required this.scrollController, - required this.autofillHints, - required this.restorationId, - required this.enableIMEPersonalizedLearning, - required this.inputFormatters, - required this.showDialCode, - required this.showIsoCodeInInput, - required this.onTapOutside, - }); - - @override - PhoneFieldState createState() => PhoneFieldState(); -} diff --git a/lib/src/phone_field_state.dart b/lib/src/phone_field_state.dart deleted file mode 100644 index fe95d7f2..00000000 --- a/lib/src/phone_field_state.dart +++ /dev/null @@ -1,146 +0,0 @@ -part of 'phone_field.dart'; - -class PhoneFieldState extends State { - PhoneController get controller => widget.controller; - FocusNode get focusNode => widget.focusNode; - PhoneFieldState(); - - @override - void initState() { - _preloadFlagsInMemory(); - super.initState(); - } - - void _preloadFlagsInMemory() { - CircleFlag.preload(IsoCode.values.map((isoCode) => isoCode.name).toList()); - } - - void selectCountry() async { - if (!widget.isCountrySelectionEnabled) { - return; - } - final selected = await widget.selectorNavigator.navigate(context); - if (selected != null) { - controller.changeCountry(selected.isoCode); - } - focusNode.requestFocus(); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: focusNode, - builder: (context, _) { - return TextField( - decoration: widget.decoration.copyWith( - errorText: widget.errorText, - prefixIcon: widget.isCountryChipPersistent - ? _getCountryCodeChip(context) - : null, - prefix: widget.isCountryChipPersistent - ? null - : _getCountryCodeChip(context), - ), - focusNode: focusNode, - controller: controller.nationalNumberController, - enabled: widget.enabled, - inputFormatters: widget.inputFormatters ?? - [ - FilteringTextInputFormatter.allow(RegExp( - '[${AllowedCharacters.plus}${AllowedCharacters.digits}${AllowedCharacters.punctuation}]')), - ], - onChanged: (txt) => controller.changeText(txt), - autofillHints: widget.autofillHints, - keyboardType: widget.keyboardType, - textInputAction: widget.textInputAction, - style: widget.style, - strutStyle: widget.strutStyle, - textAlign: widget.textAlign, - textAlignVertical: widget.textAlignVertical, - autofocus: widget.autofocus, - obscuringCharacter: widget.obscuringCharacter, - obscureText: widget.obscureText, - autocorrect: widget.autocorrect, - smartDashesType: widget.smartDashesType, - smartQuotesType: widget.smartQuotesType, - enableSuggestions: widget.enableSuggestions, - showCursor: widget.showCursor, - onEditingComplete: widget.onEditingComplete, - onAppPrivateCommand: widget.onAppPrivateCommand, - cursorWidth: widget.cursorWidth, - cursorHeight: widget.cursorHeight, - cursorRadius: widget.cursorRadius, - cursorColor: widget.cursorColor, - onTapOutside: widget.onTapOutside, - selectionHeightStyle: widget.selectionHeightStyle, - selectionWidthStyle: widget.selectionWidthStyle, - keyboardAppearance: widget.keyboardAppearance, - scrollPadding: widget.scrollPadding, - enableInteractiveSelection: widget.enableInteractiveSelection, - selectionControls: widget.selectionControls, - mouseCursor: widget.mouseCursor, - scrollController: widget.scrollController, - scrollPhysics: widget.scrollPhysics, - restorationId: widget.restorationId, - enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, - ); - }, - ); - } - - Widget _getCountryCodeChip(BuildContext context) { - return CountryButton( - key: const ValueKey('country-code-chip'), - isoCode: controller.value.isoCode, - onTap: widget.enabled ? selectCountry : null, - padding: _computeCountryButtonPadding(context), - showFlag: widget.showFlagInInput, - showIsoCode: widget.showIsoCodeInInput, - showDialCode: widget.showDialCode, - textStyle: widget.countryCodeStyle ?? - widget.decoration.labelStyle ?? - TextStyle( - fontSize: 16, - color: Theme.of(context).textTheme.bodySmall?.color, - ), - flagSize: widget.flagSize, - enabled: widget.enabled, - ); - } - - /// computes the padding inside the country button - /// this is used to align the flag and dial code with the rest - /// of the phone number. - /// The padding must work for this matrix: - /// - has label or not - /// - is border underline or outline - /// - is country button shown as a prefix or prefixIcon (isCountryChipPersistent) - /// - text direction - EdgeInsets _computeCountryButtonPadding(BuildContext context) { - final countryButtonPadding = widget.countryButtonPadding; - final isUnderline = widget.decoration.border is UnderlineInputBorder; - final hasLabel = - widget.decoration.label != null || widget.decoration.labelText != null; - final isLtr = Directionality.of(context) == TextDirection.ltr; - - EdgeInsets padding = isLtr - ? const EdgeInsets.fromLTRB(12, 16, 4, 16) - : const EdgeInsets.fromLTRB(4, 16, 12, 16); - if (countryButtonPadding != null) { - padding = countryButtonPadding; - } else if (!widget.isCountryChipPersistent) { - padding = isLtr - ? const EdgeInsets.only(right: 4, left: 12) - : const EdgeInsets.only(left: 4, right: 12); - } else if (isUnderline && hasLabel) { - padding = isLtr - ? const EdgeInsets.fromLTRB(12, 25, 4, 7) - : const EdgeInsets.fromLTRB(4, 25, 12, 7); - } else if (isUnderline && !hasLabel) { - padding = isLtr - ? const EdgeInsets.fromLTRB(12, 2, 4, 0) - : const EdgeInsets.fromLTRB(4, 2, 12, 0); - } - return padding; - } -} diff --git a/lib/src/phone_form_field.dart b/lib/src/phone_form_field.dart index 966c38e1..fcddbf24 100644 --- a/lib/src/phone_form_field.dart +++ b/lib/src/phone_form_field.dart @@ -1,14 +1,15 @@ import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; +import 'package:circle_flags/circle_flags.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:phone_form_field/src/validation/allowed_characters.dart'; import 'package:phone_numbers_parser/phone_numbers_parser.dart'; +import 'country/country_button.dart'; import 'country_selection/country_selector_navigator.dart'; -import 'phone_field.dart'; import 'phone_controller.dart'; import 'validation/phone_validator.dart'; -import 'validation/validator_translator.dart'; part 'phone_form_field_state.dart'; @@ -43,48 +44,25 @@ part 'phone_form_field_state.dart'; /// /// /// This does not affect the output value, only the display. -/// Therefor [onSizeFound] will still return a [PhoneNumber] +/// Therefor [onChanged] will still return a [PhoneNumber] /// with nsn of 677784455. /// {@endtemplate} -/// -/// ### phoneNumberType: -/// {@template phoneNumberType} -/// specify the type of phone number with [phoneNumberType]. -/// -/// accepted values are: -/// - null (can be mobile or fixedLine) -/// - mobile -/// - fixedLine -/// {@endtemplate} -/// -/// -/// ### Country picker: -/// -/// {@template selectorNavigator} -/// specify which type of country selector will be shown with [selectorNavigator]. -/// -/// Uses one of: -/// - const BottomSheetNavigator() -/// - const DraggableModalBottomSheetNavigator() -/// - const ModalBottomSheetNavigator() -/// - const DialogNavigator() -/// {@endtemplate} -/// -/// ### Country Code visibility: -/// -/// The country dial code will be visible when: -/// - the field is focussed. -/// - the field has a value for national number. -/// - the field has no label obstructing the view. -class PhoneFormField extends FormField { +class PhoneFormField extends StatefulWidget { /// {@macro controller} final PhoneController? controller; + /// {@macro initialValue} + final PhoneNumber? initialValue; + + /// Validator for the phone number. + /// example: PhoneValidator.validType(expectedType: PhoneNumberType.mobile) + final PhoneNumberInputValidator validator; + /// {@macro shouldFormat} final bool shouldFormat; /// callback called when the input value changes - final ValueChanged? onChanged; + final Function(PhoneNumber)? onChanged; /// country that is displayed when there is no value final IsoCode defaultCountry; @@ -92,143 +70,152 @@ class PhoneFormField extends FormField { /// the focusNode of the national number final FocusNode? focusNode; - /// show Dial Code or not - final bool showDialCode; + /// whether the input is enabled + final bool enabled; - /// show selected iso code or not - final bool showIsoCodeInInput; + /// how to display the country selection + final CountrySelectorNavigator countrySelectorNavigator; /// padding inside country button, /// this can be used to align the country button with the phone number - /// and is mostly useful when using [isCountryChipPersistent] as true. + /// and is mostly useful when using [isCountryButtonPersistent] as true. final EdgeInsets? countryButtonPadding; + /// whether the user can select a new country when pressing the country button + final bool isCountrySelectionEnabled; + + /// This controls wether the country button is alway shown or is hidden by + /// the label when the national number is empty. In flutter terms this controls + /// whether the country button is shown as a prefix or prefixIcon inside + /// the text field. + final bool isCountryButtonPersistent; + + /// show Dial Code or not in the country button + final bool showDialCode; + + /// show selected iso code or not in the country button + final bool showIsoCodeInInput; + + /// The size of the flag inside the country button + final double flagSize; + + /// whether the flag is shown inside the country button + final bool showFlagInInput; + + // form field inputs + final AutovalidateMode autovalidateMode; + final Function(PhoneNumber?)? onSaved; + + // textfield inputs + final InputDecoration decoration; + final TextInputType keyboardType; + final TextInputAction? textInputAction; + final TextStyle? style; + final TextStyle? countryCodeStyle; + final StrutStyle? strutStyle; + final TextAlign textAlign; + final TextAlignVertical? textAlignVertical; + final bool autofocus; + final String obscuringCharacter; + final bool obscureText; + final bool autocorrect; + final SmartDashesType? smartDashesType; + final SmartQuotesType? smartQuotesType; + final bool enableSuggestions; + final Widget Function(BuildContext, EditableTextState)? contextMenuBuilder; + final bool? showCursor; + final VoidCallback? onEditingComplete; + final ValueChanged? onSubmitted; + final AppPrivateCommandCallback? onAppPrivateCommand; + final Function(PointerDownEvent)? onTapOutside; + final double cursorWidth; + final double? cursorHeight; + final Radius? cursorRadius; + final Color? cursorColor; + final ui.BoxHeightStyle selectionHeightStyle; + final ui.BoxWidthStyle selectionWidthStyle; + final Brightness? keyboardAppearance; + final EdgeInsets scrollPadding; + final bool enableInteractiveSelection; + final TextSelectionControls? selectionControls; + bool get selectionEnabled => enableInteractiveSelection; + final MouseCursor? mouseCursor; + final ScrollPhysics? scrollPhysics; + final ScrollController? scrollController; + final Iterable? autofillHints; + final String? restorationId; + final bool enableIMEPersonalizedLearning; + final List? inputFormatters; + PhoneFormField({ super.key, this.controller, this.shouldFormat = true, this.onChanged, this.focusNode, - bool showFlagInInput = true, - CountrySelectorNavigator countrySelectorNavigator = - const CountrySelectorNavigator.page(), - Function(PhoneNumber?)? super.onSaved, + this.showFlagInInput = true, + this.countrySelectorNavigator = const CountrySelectorNavigator.page(), this.defaultCountry = IsoCode.US, - InputDecoration decoration = - const InputDecoration(border: UnderlineInputBorder()), - PhoneNumber? initialValue, - double flagSize = 16, + this.initialValue, + this.flagSize = 16, PhoneNumberInputValidator? validator, - bool isCountrySelectionEnabled = true, - bool isCountryChipPersistent = true, + this.isCountrySelectionEnabled = true, + bool? isCountryButtonPersistent, + @Deprecated('Use [isCountryButtonPersistent]') + bool? isCountryChipPersistent, this.showDialCode = true, this.showIsoCodeInInput = false, this.countryButtonPadding, + // form field inputs + this.onSaved, + this.autovalidateMode = AutovalidateMode.onUserInteraction, // textfield inputs - AutovalidateMode super.autovalidateMode = - AutovalidateMode.onUserInteraction, - TextInputType keyboardType = TextInputType.phone, - TextInputAction? textInputAction, - TextStyle? style, - TextStyle? countryCodeStyle, - StrutStyle? strutStyle, - TextAlign textAlign = TextAlign.start, - TextAlignVertical? textAlignVertical, - bool autofocus = false, - String obscuringCharacter = '*', - bool obscureText = false, - bool autocorrect = true, - SmartDashesType? smartDashesType, - SmartQuotesType? smartQuotesType, - bool enableSuggestions = true, - Widget Function(BuildContext, EditableTextState)? contextMenuBuilder, - bool? showCursor, - VoidCallback? onEditingComplete, - ValueChanged? onSubmitted, - AppPrivateCommandCallback? onAppPrivateCommand, - Function(PointerDownEvent)? onTapOutside, - List? inputFormatters, - super.enabled, - double cursorWidth = 2.0, - double? cursorHeight, - Radius? cursorRadius, - Color? cursorColor, - ui.BoxHeightStyle selectionHeightStyle = ui.BoxHeightStyle.tight, - ui.BoxWidthStyle selectionWidthStyle = ui.BoxWidthStyle.tight, - Brightness? keyboardAppearance, - EdgeInsets scrollPadding = const EdgeInsets.all(20.0), - bool enableInteractiveSelection = true, - TextSelectionControls? selectionControls, - MouseCursor? mouseCursor, - ScrollPhysics? scrollPhysics, - ScrollController? scrollController, - Iterable? autofillHints, - super.restorationId, - bool enableIMEPersonalizedLearning = true, + this.decoration = const InputDecoration(), + this.keyboardType = TextInputType.phone, + this.textInputAction, + this.style, + this.countryCodeStyle, + this.strutStyle, + this.textAlign = TextAlign.start, + this.textAlignVertical, + this.autofocus = false, + this.obscuringCharacter = '*', + this.obscureText = false, + this.autocorrect = true, + this.smartDashesType, + this.smartQuotesType, + this.enableSuggestions = true, + this.contextMenuBuilder, + this.showCursor, + this.onEditingComplete, + this.onSubmitted, + this.onAppPrivateCommand, + this.onTapOutside, + this.inputFormatters, + this.enabled = true, + this.cursorWidth = 2.0, + this.cursorHeight, + this.cursorRadius, + this.cursorColor, + this.selectionHeightStyle = ui.BoxHeightStyle.tight, + this.selectionWidthStyle = ui.BoxWidthStyle.tight, + this.keyboardAppearance, + this.scrollPadding = const EdgeInsets.all(20.0), + this.enableInteractiveSelection = true, + this.selectionControls, + this.mouseCursor, + this.scrollPhysics, + this.scrollController, + this.autofillHints, + this.restorationId, + this.enableIMEPersonalizedLearning = true, }) : assert( initialValue == null || controller == null, 'One of initialValue or controller can be specified at a time', ), - super( - initialValue: controller != null ? controller.value : initialValue, - validator: validator ?? PhoneValidator.valid(), - builder: (state) { - final field = state as PhoneFormFieldState; - return PhoneField( - controller: field.controller, - focusNode: field.focusNode, - showFlagInInput: showFlagInInput, - showIsoCodeInInput: showIsoCodeInInput, - selectorNavigator: countrySelectorNavigator, - errorText: field.getErrorText(), - showDialCode: showDialCode, - flagSize: flagSize, - decoration: decoration, - enabled: enabled, - isCountrySelectionEnabled: isCountrySelectionEnabled, - isCountryChipPersistent: isCountryChipPersistent, - countryButtonPadding: countryButtonPadding, - // textfield params - autofillHints: autofillHints, - keyboardType: keyboardType, - textInputAction: textInputAction, - style: style, - countryCodeStyle: countryCodeStyle, - strutStyle: strutStyle, - textAlign: textAlign, - textAlignVertical: textAlignVertical, - autofocus: autofocus, - obscuringCharacter: obscuringCharacter, - obscureText: obscureText, - autocorrect: autocorrect, - smartDashesType: smartDashesType, - smartQuotesType: smartQuotesType, - enableSuggestions: enableSuggestions, - contextMenuBuilder: contextMenuBuilder, - showCursor: showCursor, - onEditingComplete: onEditingComplete, - onSubmitted: onSubmitted, - onAppPrivateCommand: onAppPrivateCommand, - onTapOutside: onTapOutside, - cursorWidth: cursorWidth, - cursorHeight: cursorHeight, - cursorRadius: cursorRadius, - cursorColor: cursorColor, - selectionHeightStyle: selectionHeightStyle, - selectionWidthStyle: selectionWidthStyle, - keyboardAppearance: keyboardAppearance, - scrollPadding: scrollPadding, - enableInteractiveSelection: enableInteractiveSelection, - selectionControls: selectionControls, - mouseCursor: mouseCursor, - scrollController: scrollController, - scrollPhysics: scrollPhysics, - restorationId: restorationId, - enableIMEPersonalizedLearning: enableIMEPersonalizedLearning, - inputFormatters: inputFormatters, - ); - }, - ); + validator = validator ?? PhoneValidator.valid(), + isCountryButtonPersistent = + isCountryButtonPersistent ?? isCountryChipPersistent ?? true; @override PhoneFormFieldState createState() => PhoneFormFieldState(); diff --git a/lib/src/phone_form_field_state.dart b/lib/src/phone_form_field_state.dart index 82a5c325..baa130d6 100644 --- a/lib/src/phone_form_field_state.dart +++ b/lib/src/phone_form_field_state.dart @@ -1,12 +1,9 @@ part of 'phone_form_field.dart'; -class PhoneFormFieldState extends FormFieldState { +class PhoneFormFieldState extends State { late final PhoneController controller; late final FocusNode focusNode; - @override - PhoneFormField get widget => super.widget as PhoneFormField; - @override void initState() { super.initState(); @@ -16,14 +13,145 @@ class PhoneFormFieldState extends FormFieldState { const PhoneNumber(isoCode: IsoCode.US, nsn: ''), ); focusNode = widget.focusNode ?? FocusNode(); + _preloadFlagsInMemory(); + } + + void _preloadFlagsInMemory() { + CircleFlag.preload(IsoCode.values.map((isoCode) => isoCode.name).toList()); + } + + void selectCountry() async { + if (!widget.isCountrySelectionEnabled) { + return; + } + final selected = await widget.countrySelectorNavigator.navigate(context); + if (selected != null) { + controller.changeCountry(selected.isoCode); + } + focusNode.requestFocus(); + } + + @override + Widget build(BuildContext context) { + return FormField( + autovalidateMode: widget.autovalidateMode, + enabled: widget.enabled, + initialValue: widget.initialValue, + onSaved: widget.onSaved, + restorationId: widget.restorationId, + validator: (phoneNumber) => widget.validator(phoneNumber, context), + builder: (formFieldState) => AnimatedBuilder( + animation: focusNode, + builder: (context, _) => TextField( + decoration: widget.decoration.copyWith( + errorText: formFieldState.errorText, + prefixIcon: widget.isCountryButtonPersistent + ? _getCountryCodeChip(context) + : null, + prefix: widget.isCountryButtonPersistent + ? null + : _getCountryCodeChip(context), + ), + focusNode: focusNode, + controller: controller.nationalNumberController, + enabled: widget.enabled, + inputFormatters: widget.inputFormatters ?? + [ + FilteringTextInputFormatter.allow(RegExp( + '[${AllowedCharacters.plus}${AllowedCharacters.digits}${AllowedCharacters.punctuation}]')), + ], + onChanged: (txt) => controller.changeText(txt), + autofillHints: widget.autofillHints, + keyboardType: widget.keyboardType, + textInputAction: widget.textInputAction, + style: widget.style, + strutStyle: widget.strutStyle, + textAlign: widget.textAlign, + textAlignVertical: widget.textAlignVertical, + autofocus: widget.autofocus, + obscuringCharacter: widget.obscuringCharacter, + obscureText: widget.obscureText, + autocorrect: widget.autocorrect, + smartDashesType: widget.smartDashesType, + smartQuotesType: widget.smartQuotesType, + enableSuggestions: widget.enableSuggestions, + showCursor: widget.showCursor, + onEditingComplete: widget.onEditingComplete, + onAppPrivateCommand: widget.onAppPrivateCommand, + cursorWidth: widget.cursorWidth, + cursorHeight: widget.cursorHeight, + cursorRadius: widget.cursorRadius, + cursorColor: widget.cursorColor, + onTapOutside: widget.onTapOutside, + selectionHeightStyle: widget.selectionHeightStyle, + selectionWidthStyle: widget.selectionWidthStyle, + keyboardAppearance: widget.keyboardAppearance, + scrollPadding: widget.scrollPadding, + enableInteractiveSelection: widget.enableInteractiveSelection, + selectionControls: widget.selectionControls, + mouseCursor: widget.mouseCursor, + scrollController: widget.scrollController, + scrollPhysics: widget.scrollPhysics, + restorationId: widget.restorationId, + enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, + ), + ), + ); } - /// gets the localized error text if any - String? getErrorText() { - final errorText = this.errorText; - if (errorText != null) { - return ValidatorTranslator.message(context, errorText); + Widget _getCountryCodeChip(BuildContext context) { + return CountryButton( + key: const ValueKey('country-code-chip'), + isoCode: controller.value.isoCode, + onTap: widget.enabled ? selectCountry : null, + padding: _computeCountryButtonPadding(context), + showFlag: widget.showFlagInInput, + showIsoCode: widget.showIsoCodeInInput, + showDialCode: widget.showDialCode, + textStyle: widget.countryCodeStyle ?? + widget.decoration.labelStyle ?? + TextStyle( + fontSize: 16, + color: Theme.of(context).textTheme.bodySmall?.color, + ), + flagSize: widget.flagSize, + enabled: widget.enabled, + ); + } + + /// computes the padding inside the country button + /// this is used to align the flag and dial code with the rest + /// of the phone number. + /// The padding must work for this matrix: + /// - has label or not + /// - is border underline or outline + /// - is country button shown as a prefix or prefixIcon (isCountryChipPersistent) + /// - text direction + EdgeInsets _computeCountryButtonPadding(BuildContext context) { + final countryButtonPadding = widget.countryButtonPadding; + final isUnderline = widget.decoration.border is UnderlineInputBorder; + final hasLabel = + widget.decoration.label != null || widget.decoration.labelText != null; + final isLtr = Directionality.of(context) == TextDirection.ltr; + + EdgeInsets padding = isLtr + ? const EdgeInsets.fromLTRB(12, 16, 4, 16) + : const EdgeInsets.fromLTRB(4, 16, 12, 16); + if (countryButtonPadding != null) { + padding = countryButtonPadding; + } else if (!widget.isCountryButtonPersistent) { + padding = isLtr + ? const EdgeInsets.only(right: 4, left: 12) + : const EdgeInsets.only(left: 4, right: 12); + } else if (isUnderline && hasLabel) { + padding = isLtr + ? const EdgeInsets.fromLTRB(12, 25, 4, 7) + : const EdgeInsets.fromLTRB(4, 25, 12, 7); + } else if (isUnderline && !hasLabel) { + padding = isLtr + ? const EdgeInsets.fromLTRB(12, 2, 4, 0) + : const EdgeInsets.fromLTRB(4, 2, 12, 0); } - return null; + return padding; } } diff --git a/lib/src/validation/phone_validator.dart b/lib/src/validation/phone_validator.dart index a596a97a..c77e1ad8 100644 --- a/lib/src/validation/phone_validator.dart +++ b/lib/src/validation/phone_validator.dart @@ -1,6 +1,9 @@ +import 'package:flutter/material.dart'; +import 'package:phone_form_field/l10n/generated/phone_field_localization_en.dart'; import 'package:phone_form_field/phone_form_field.dart'; -typedef PhoneNumberInputValidator = String? Function(PhoneNumber? phoneNumber); +typedef PhoneNumberInputValidator = String? Function( + PhoneNumber? phoneNumber, BuildContext context); class PhoneValidator { /// allow to compose several validators @@ -9,9 +12,9 @@ class PhoneValidator { static PhoneNumberInputValidator compose( List validators, ) { - return (valueCandidate) { + return (valueCandidate, context) { for (var validator in validators) { - final validatorResult = validator.call(valueCandidate); + final validatorResult = validator.call(valueCandidate, context); if (validatorResult != null) { return validatorResult; } @@ -24,9 +27,11 @@ class PhoneValidator { /// custom error message String? errorText, }) { - return (PhoneNumber? valueCandidate) { + return (PhoneNumber? valueCandidate, BuildContext context) { if (valueCandidate == null || (valueCandidate.nsn.trim().isEmpty)) { - return errorText ?? 'requiredPhoneNumber'; + return errorText ?? + PhoneFieldLocalization.of(context)?.requiredPhoneNumber ?? + PhoneFieldLocalizationEn().requiredPhoneNumber; } return null; }; @@ -48,14 +53,18 @@ class PhoneValidator { /// determine whether a missing value should be reported as invalid bool allowEmpty = true, }) { - return (PhoneNumber? valueCandidate) { + return (PhoneNumber? valueCandidate, BuildContext context) { if (valueCandidate == null && !allowEmpty) { - return errorText ?? 'invalidPhoneNumber'; + return errorText ?? + PhoneFieldLocalization.of(context)?.invalidPhoneNumber ?? + PhoneFieldLocalizationEn().invalidPhoneNumber; } if (valueCandidate != null && (!allowEmpty || valueCandidate.nsn.isNotEmpty) && !valueCandidate.isValid()) { - return errorText ?? 'invalidPhoneNumber'; + return errorText ?? + PhoneFieldLocalization.of(context)?.invalidPhoneNumber ?? + PhoneFieldLocalizationEn().invalidPhoneNumber; } return null; }; @@ -66,18 +75,23 @@ class PhoneValidator { PhoneNumberType expectedType, { /// custom error message String? errorText, - - /// determine whether a missing value should be reported as invalid - bool allowEmpty = true, }) { - final defaultMessage = expectedType == PhoneNumberType.mobile - ? 'invalidMobilePhoneNumber' - : 'invalidFixedLinePhoneNumber'; - return (PhoneNumber? valueCandidate) { + return (PhoneNumber? valueCandidate, BuildContext context) { if (valueCandidate != null && - (!allowEmpty || valueCandidate.nsn.isNotEmpty) && + valueCandidate.nsn.isNotEmpty && !valueCandidate.isValid(type: expectedType)) { - return errorText ?? defaultMessage; + if (expectedType == PhoneNumberType.mobile) { + return errorText ?? + PhoneFieldLocalization.of(context)?.invalidMobilePhoneNumber ?? + PhoneFieldLocalizationEn().invalidMobilePhoneNumber; + } else if (expectedType == PhoneNumberType.fixedLine) { + return errorText ?? + PhoneFieldLocalization.of(context)?.invalidFixedLinePhoneNumber ?? + PhoneFieldLocalizationEn().invalidFixedLinePhoneNumber; + } + return errorText ?? + PhoneFieldLocalization.of(context)?.invalidPhoneNumber ?? + PhoneFieldLocalizationEn().invalidPhoneNumber; } return null; }; @@ -88,14 +102,10 @@ class PhoneValidator { static PhoneNumberInputValidator validFixedLine({ /// custom error message String? errorText, - - /// determine whether a missing value should be reported as invalid - bool allowEmpty = true, }) => validType( PhoneNumberType.fixedLine, errorText: errorText, - allowEmpty: allowEmpty, ); /// convenience shortcut method for @@ -110,7 +120,6 @@ class PhoneValidator { validType( PhoneNumberType.mobile, errorText: errorText, - allowEmpty: allowEmpty, ); static PhoneNumberInputValidator validCountry( @@ -118,21 +127,21 @@ class PhoneValidator { List expectedCountries, { /// custom error message String? errorText, - - /// determine whether a missing value should be reported as invalid - bool allowEmpty = true, }) { - return (PhoneNumber? valueCandidate) { + return (PhoneNumber? valueCandidate, BuildContext context) { if (valueCandidate != null && - (!allowEmpty || valueCandidate.nsn.isNotEmpty) && + (valueCandidate.nsn.isNotEmpty) && !expectedCountries.contains(valueCandidate.isoCode)) { - return errorText ?? 'invalidCountry'; + return errorText ?? + PhoneFieldLocalization.of(context)?.invalidCountry ?? + PhoneFieldLocalizationEn().invalidCountry; } return null; }; } - static PhoneNumberInputValidator get none => (PhoneNumber? valueCandidate) { + static PhoneNumberInputValidator get none => + (PhoneNumber? valueCandidate, BuildContext context) { return null; }; } diff --git a/test/phone_validator_test.dart b/test/phone_validator_test.dart index c246e9e5..ae68886b 100644 --- a/test/phone_validator_test.dart +++ b/test/phone_validator_test.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:phone_form_field/phone_form_field.dart'; @@ -10,15 +11,15 @@ void main() async { bool last = false; final validator = PhoneValidator.compose([ - (PhoneNumber? p) { + (PhoneNumber? p, BuildContext context) { first = true; return null; }, - (PhoneNumber? p) { + (PhoneNumber? p, BuildContext context) { second = true; return null; }, - (PhoneNumber? p) { + (PhoneNumber? p, BuildContext context) { last = true; return null; }, @@ -36,14 +37,14 @@ void main() async { bool firstValidationDone = false; bool lastValidationDone = false; final validator = PhoneValidator.compose([ - (PhoneNumber? p) { + (PhoneNumber? p, BuildContext context) { firstValidationDone = true; return null; }, - (PhoneNumber? p) { + (PhoneNumber? p, BuildContext context) { return 'validation failed'; }, - (PhoneNumber? p) { + (PhoneNumber? p, BuildContext context) { lastValidationDone = true; return null; }, From 4896d87b5bee52c235e84230cef713e92bec71b9 Mon Sep 17 00:00:00 2001 From: cedvdb Date: Sun, 4 Feb 2024 15:35:28 +0100 Subject: [PATCH 17/25] moving tests --- lib/src/phone_form_field.dart | 2 + lib/src/validation/validator_translator.dart | 45 ---- test/phone_form_field_test.dart | 250 ++++++++++++++----- test/phone_validator_test.dart | 204 --------------- 4 files changed, 183 insertions(+), 318 deletions(-) delete mode 100644 lib/src/validation/validator_translator.dart delete mode 100644 test/phone_validator_test.dart diff --git a/lib/src/phone_form_field.dart b/lib/src/phone_form_field.dart index fcddbf24..4f52bf57 100644 --- a/lib/src/phone_form_field.dart +++ b/lib/src/phone_form_field.dart @@ -155,6 +155,8 @@ class PhoneFormField extends StatefulWidget { this.focusNode, this.showFlagInInput = true, this.countrySelectorNavigator = const CountrySelectorNavigator.page(), + @Deprecated( + 'Use [initialValue] or [controller] to set the initial phone number') this.defaultCountry = IsoCode.US, this.initialValue, this.flagSize = 16, diff --git a/lib/src/validation/validator_translator.dart b/lib/src/validation/validator_translator.dart deleted file mode 100644 index a8ee7af4..00000000 --- a/lib/src/validation/validator_translator.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:phone_form_field/l10n/generated/phone_field_localization.dart'; - -typedef _PhoneValidatorMessageDelegate = String? Function(BuildContext context); - -class ValidatorTranslator { - static final Map _validatorMessages = - { - 'invalidPhoneNumber': (ctx) => - PhoneFieldLocalization.of(ctx)?.invalidPhoneNumber, - 'invalidCountry': (ctx) => PhoneFieldLocalization.of(ctx)?.invalidCountry, - 'invalidMobilePhoneNumber': (ctx) => - PhoneFieldLocalization.of(ctx)?.invalidMobilePhoneNumber, - 'invalidFixedLinePhoneNumber': (ctx) => - PhoneFieldLocalization.of(ctx)?.invalidFixedLinePhoneNumber, - 'requiredPhoneNumber': (ctx) => - PhoneFieldLocalization.of(ctx)?.requiredPhoneNumber, - }; - - static final Map _defaults = { - 'invalidPhoneNumber': 'Invalid phone number', - 'invalidCountry': 'Invalid country', - 'invalidMobilePhoneNumber': 'Invalid mobile phone number', - 'invalidFixedLinePhoneNumber': 'Invalid fixedline phone number', - 'requiredPhoneNumber': 'required phone number', - }; - - /// Localised name depending on the current application locale - /// If you have many LocalisedName to handle in a same context, consider - /// supplying the second optional PhoneFieldLocalization to avoid - /// walking up the widget to get [PhoneFieldLocalization] instance - /// for each call. - static String message( - BuildContext context, - String key, - ) { - String? name = getMessageFromKey(context, key); - return name ?? _defaults[key] ?? key; - } - - static String? getMessageFromKey(BuildContext ctx, String key) { - final _PhoneValidatorMessageDelegate? translateFn = _validatorMessages[key]; - return translateFn?.call(ctx); - } -} diff --git a/test/phone_form_field_test.dart b/test/phone_form_field_test.dart index 0dd2385e..2108c44c 100644 --- a/test/phone_form_field_test.dart +++ b/test/phone_form_field_test.dart @@ -2,6 +2,7 @@ import 'package:circle_flags/circle_flags.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:phone_form_field/l10n/generated/phone_field_localization_en.dart'; import 'package:phone_form_field/phone_form_field.dart'; import 'package:phone_form_field/src/country_selection/country_list_view.dart'; @@ -9,6 +10,7 @@ void main() { group('PhoneFormField', () { final formKey = GlobalKey(); final phoneKey = GlobalKey>(); + Widget getWidget({ Function(PhoneNumber?)? onChanged, Function(PhoneNumber?)? onSaved, @@ -17,7 +19,6 @@ void main() { PhoneController? controller, bool showFlagInInput = true, bool showDialCode = true, - IsoCode defaultCountry = IsoCode.US, bool shouldFormat = false, PhoneNumberInputValidator? validator, bool enabled = true, @@ -40,7 +41,6 @@ void main() { showFlagInInput: showFlagInInput, showDialCode: showDialCode, controller: controller, - defaultCountry: defaultCountry, shouldFormat: shouldFormat, validator: validator, enabled: enabled, @@ -51,8 +51,12 @@ void main() { group('display', () { testWidgets('Should display input', (tester) async { - await tester.pumpWidget(getWidget()); - expect(find.byType(TextField), findsOneWidget); + await tester.pumpWidget( + getWidget( + initialValue: PhoneNumber.parse('+33'), + ), + ); + expect(find.byType(PhoneFormField), findsOneWidget); }); testWidgets('Should display country code', (tester) async { @@ -66,7 +70,7 @@ void main() { }); testWidgets( - 'disabled, tap on country chip - country list dialog is not shown', + 'Should not show country selection when disabled and country chip is tapped', (tester) async { await tester.pumpWidget(getWidget(enabled: false)); final countryChip = @@ -91,11 +95,6 @@ void main() { await tester.pumpAndSettle(); expect(find.byType(CountryListView), findsOneWidget); }); - testWidgets('Should have a default country', (tester) async { - await tester.pumpWidget(getWidget(defaultCountry: IsoCode.FR)); - expect(find.text('+ 33'), findsWidgets); - }); - testWidgets('Should hide flag', (tester) async { await tester.pumpWidget(getWidget(showFlagInInput: false)); expect(find.byType(CircleFlag), findsNothing); @@ -118,30 +117,28 @@ void main() { testWidgets('Should show dial code when showDialCode is true', (tester) async { - PhoneNumber? phoneNumber = PhoneNumber.parse( - '', - destinationCountry: IsoCode.FR, - ); + PhoneNumber phoneNumber = PhoneNumber.parse('+33'); - await tester.pumpWidget(getWidget( + await tester.pumpWidget( + getWidget( initialValue: phoneNumber, showDialCode: true, - defaultCountry: IsoCode.FR)); + ), + ); await tester.pump(const Duration(seconds: 1)); expect(find.text('+ 33'), findsOneWidget); }); testWidgets('Should hide dial code when showDialCode is false', (tester) async { - PhoneNumber? phoneNumber = PhoneNumber.parse( - '', - destinationCountry: IsoCode.FR, - ); + PhoneNumber phoneNumber = PhoneNumber.parse('+33'); - await tester.pumpWidget(getWidget( + await tester.pumpWidget( + getWidget( initialValue: phoneNumber, showDialCode: false, - defaultCountry: IsoCode.FR)); + ), + ); await tester.pump(const Duration(seconds: 1)); expect(find.text('+ 33'), findsNothing); }); @@ -149,21 +146,24 @@ void main() { group('value changes', () { testWidgets('Should display initial value', (tester) async { - await tester.pumpWidget(getWidget( - initialValue: PhoneNumber.parse('478787827', - destinationCountry: IsoCode.FR))); + await tester.pumpWidget( + getWidget( + initialValue: PhoneNumber.parse('+33478787827'), + ), + ); expect(find.text('+ 33'), findsWidgets); expect(find.text('478787827'), findsOneWidget); }); testWidgets('Should change value of controller', (tester) async { - final controller = PhoneController(); + final controller = PhoneController( + initialValue: PhoneNumber.parse('+1'), + ); PhoneNumber? newValue; controller.addListener(() { newValue = controller.value; }); - await tester.pumpWidget( - getWidget(controller: controller, defaultCountry: IsoCode.US)); + await tester.pumpWidget(getWidget(controller: controller)); final phoneField = find.byType(PhoneFormField); await tester.tap(phoneField); // non digits should not work @@ -187,10 +187,8 @@ void main() { controller.addListener(() { newValue = controller.value; }); - await tester.pumpWidget( - getWidget(controller: controller, defaultCountry: IsoCode.US)); - controller.value = - PhoneNumber.parse('488997722', destinationCountry: IsoCode.FR); + await tester.pumpWidget(getWidget(controller: controller)); + controller.value = PhoneNumber.parse('+33488997722'); await tester.pump(const Duration(seconds: 1)); expect(find.text('+ 33'), findsWidgets); expect(find.text('488997722'), findsOneWidget); @@ -204,8 +202,7 @@ void main() { controller.addListener(() { newValue = controller.value; }); - await tester.pumpWidget( - getWidget(controller: controller, defaultCountry: IsoCode.US)); + await tester.pumpWidget(getWidget(controller: controller)); final phoneField = find.byType(PhoneFormField); await tester.tap(phoneField); // non digits should not work @@ -244,61 +241,172 @@ void main() { }); }); - group('validity', () { - testWidgets('Should tell when a phone number is not valid', - (tester) async { - PhoneNumber? phoneNumber = PhoneNumber.parse( - '', - destinationCountry: IsoCode.FR, - ); + group('validator', () { + testWidgets( + 'Should display invalid message when no validator is specified and ' + 'the phone number is invalid', (tester) async { + PhoneNumber? phoneNumber = PhoneNumber.parse('+33'); await tester.pumpWidget(getWidget(initialValue: phoneNumber)); final phoneField = find.byType(PhoneFormField); await tester.enterText(phoneField, '9984'); - await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(const Duration(seconds: 1)); - expect(find.text('Invalid phone number'), findsOneWidget); + expect( + find.text(PhoneFieldLocalizationEn().invalidPhoneNumber), + findsOneWidget, + ); }); testWidgets( - 'Should tell when a phone number is not valid for a given phone number type', + 'Should display invalid mobile phone when PhoneValidator.validMobile' + ' is used and the phone number is not a mobile phone number', (tester) async { - PhoneNumber? phoneNumber = PhoneNumber.parse( - '', - destinationCountry: IsoCode.BE, - ); - // valid fixed line + PhoneNumber? phoneNumber = PhoneNumber.parse('+32'); await tester.pumpWidget(getWidget( initialValue: phoneNumber, - validator: PhoneValidator.validFixedLine(), + validator: PhoneValidator.validMobile(), )); final phoneField = find.byType(PhoneFormField); await tester.enterText(phoneField, '77777777'); await tester.pumpAndSettle(); - expect(find.text('Invalid'), findsNothing); - // invalid mobile + expect( + find.text(PhoneFieldLocalizationEn().invalidMobilePhoneNumber), + findsNothing, + ); + await tester.enterText(phoneField, '777'); + await tester.pumpAndSettle(); + expect( + find.text(PhoneFieldLocalizationEn().invalidMobilePhoneNumber), + findsOneWidget, + ); + }); + + testWidgets( + 'Should display invalid fixed line phone when PhoneValidator.validFixedLine' + ' is used and the phone number is not a fixed line phone number', + (tester) async { + PhoneNumber? phoneNumber = PhoneNumber.parse('+32'); await tester.pumpWidget(getWidget( initialValue: phoneNumber, - validator: PhoneValidator.validMobile( - errorText: 'Invalid phone number', - ), + validator: PhoneValidator.validFixedLine(), )); - final phoneField2 = find.byType(PhoneFormField); + final phoneField = find.byType(PhoneFormField); + await tester.enterText(phoneField, '77777777'); await tester.pumpAndSettle(); - await tester.enterText(phoneField2, '77777777'); + expect( + find.text(PhoneFieldLocalizationEn().invalidFixedLinePhoneNumber), + findsNothing, + ); + await tester.enterText(phoneField, '777'); await tester.pumpAndSettle(); - expect(find.text('Invalid phone number'), findsOneWidget); + expect( + find.text(PhoneFieldLocalizationEn().invalidFixedLinePhoneNumber), + findsOneWidget, + ); + }); - // valid mobile + testWidgets( + 'should display error when PhoneValidator.required is used and the nsn is empty', + (WidgetTester tester) async { + final controller = + PhoneController(initialValue: PhoneNumber.parse('+32 444')); await tester.pumpWidget(getWidget( - initialValue: phoneNumber, - validator: PhoneValidator.validMobile( - errorText: 'Invalid phone number', - ), + controller: controller, + validator: PhoneValidator.required(), )); - final phoneField3 = find.byType(PhoneFormField); - await tester.enterText(phoneField3, '477668899'); + controller.changeText(''); await tester.pumpAndSettle(); - expect(find.text('Invalid'), findsNothing); + + expect( + find.text(PhoneFieldLocalizationEn().requiredPhoneNumber), + findsOneWidget, + ); + }); + + testWidgets( + 'should show error message when PhoneValidator.validCountry ' + 'is used and the current country is invalid', + (WidgetTester tester) async { + final controller = + PhoneController(initialValue: PhoneNumber.parse('+32 444')); + await tester.pumpWidget(getWidget( + controller: controller, + validator: PhoneValidator.validCountry([IsoCode.FR, IsoCode.BE]), + )); + controller.changeCountry(IsoCode.US); + await tester.pumpAndSettle(); + expect( + find.text(PhoneFieldLocalizationEn().invalidCountry), + findsOneWidget, + ); + }); + + testWidgets('should validate against all validators when compose is used', + (WidgetTester tester) async { + bool first = false; + bool second = false; + bool last = false; + + final validator = PhoneValidator.compose([ + (PhoneNumber? p, BuildContext context) { + first = true; + return null; + }, + (PhoneNumber? p, BuildContext context) { + second = true; + return null; + }, + (PhoneNumber? p, BuildContext context) { + last = true; + return null; + }, + ]); + + await tester.pumpWidget( + getWidget( + initialValue: PhoneNumber.parse('+33'), + validator: validator, + ), + ); + final phoneField = find.byType(PhoneFormField); + await tester.enterText(phoneField, '9999'); + await tester.pumpAndSettle(); + expect(first, isTrue); + expect(second, isTrue); + expect(last, isTrue); + }); + + testWidgets( + 'should stop and return first validator failure when compose is used', + (WidgetTester tester) async { + bool firstValidationDone = false; + bool lastValidationDone = false; + final validator = PhoneValidator.compose([ + (PhoneNumber? p, BuildContext context) { + firstValidationDone = true; + return null; + }, + (PhoneNumber? p, BuildContext context) { + return 'validation failed'; + }, + (PhoneNumber? p, BuildContext context) { + lastValidationDone = true; + return null; + }, + ]); + await tester.pumpWidget( + getWidget( + initialValue: PhoneNumber.parse('+33'), + validator: validator, + ), + ); + final phoneField = find.byType(PhoneFormField); + await tester.enterText(phoneField, '9999'); + await tester.pumpAndSettle(); + + expect(find.text('validation failed'), findsOneWidget); + expect(firstValidationDone, isTrue); + expect(lastValidationDone, isFalse); }); }); @@ -357,12 +465,16 @@ void main() { await tester.pump(const Duration(seconds: 1)); expect(saved, isTrue); expect( - phoneNumber, - equals(PhoneNumber.parse( + phoneNumber, + equals( + PhoneNumber.parse( '479281938', destinationCountry: IsoCode.FR, - ))); + ), + ), + ); }); + testWidgets('Should call onTapOutside', (tester) async { PhoneNumber? phoneNumber = PhoneNumber.parse( '', diff --git a/test/phone_validator_test.dart b/test/phone_validator_test.dart deleted file mode 100644 index ae68886b..00000000 --- a/test/phone_validator_test.dart +++ /dev/null @@ -1,204 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:phone_form_field/phone_form_field.dart'; - -void main() async { - group('PhoneValidator.compose', () { - testWidgets('compose should test each validator', - (WidgetTester tester) async { - bool first = false; - bool second = false; - bool last = false; - - final validator = PhoneValidator.compose([ - (PhoneNumber? p, BuildContext context) { - first = true; - return null; - }, - (PhoneNumber? p, BuildContext context) { - second = true; - return null; - }, - (PhoneNumber? p, BuildContext context) { - last = true; - return null; - }, - ]); - - expect( - validator(const PhoneNumber(isoCode: IsoCode.FR, nsn: '')), isNull); - expect(first, isTrue); - expect(second, isTrue); - expect(last, isTrue); - }); - - testWidgets('compose should stop and return first validator failure', - (WidgetTester tester) async { - bool firstValidationDone = false; - bool lastValidationDone = false; - final validator = PhoneValidator.compose([ - (PhoneNumber? p, BuildContext context) { - firstValidationDone = true; - return null; - }, - (PhoneNumber? p, BuildContext context) { - return 'validation failed'; - }, - (PhoneNumber? p, BuildContext context) { - lastValidationDone = true; - return null; - }, - ]); - expect(validator(const PhoneNumber(isoCode: IsoCode.FR, nsn: '')), - equals('validation failed')); - expect(firstValidationDone, isTrue); - expect(lastValidationDone, isFalse); - }); - }); - - group('PhoneValidator.required', () { - testWidgets('should be required value', (WidgetTester tester) async { - final validator = PhoneValidator.required(); - expect( - validator(const PhoneNumber(isoCode: IsoCode.US, nsn: '')), - equals('requiredPhoneNumber'), - ); - - final validatorWithText = PhoneValidator.required( - errorText: 'custom message', - ); - expect( - validatorWithText(const PhoneNumber(isoCode: IsoCode.US, nsn: '')), - equals('custom message'), - ); - }); - }); - - group('PhoneValidator.invalid', () { - testWidgets('should be invalid', (WidgetTester tester) async { - final validator = PhoneValidator.valid(); - expect( - validator(const PhoneNumber(isoCode: IsoCode.FR, nsn: '123')), - equals('invalidPhoneNumber'), - ); - - final validatorWithText = PhoneValidator.valid( - errorText: 'custom message', - ); - expect( - validatorWithText(const PhoneNumber(isoCode: IsoCode.FR, nsn: '123')), - equals('custom message'), - ); - }); - - testWidgets('should (not) be invalid when (no) value entered', - (WidgetTester tester) async { - final validator = PhoneValidator.valid(); - expect( - validator(const PhoneNumber(isoCode: IsoCode.FR, nsn: '')), - isNull, - ); - - final validatorNotEmpty = PhoneValidator.valid(allowEmpty: false); - expect( - validatorNotEmpty(const PhoneNumber(isoCode: IsoCode.FR, nsn: '')), - equals('invalidPhoneNumber'), - ); - }); - }); - - group('PhoneValidator.type', () { - testWidgets('should be invalid mobile type', (WidgetTester tester) async { - final validator = PhoneValidator.validMobile(); - expect( - validator(const PhoneNumber(isoCode: IsoCode.FR, nsn: '412345678')), - equals('invalidMobilePhoneNumber'), - ); - - final validatorWithText = PhoneValidator.validMobile( - errorText: 'custom type message', - ); - expect( - validatorWithText( - const PhoneNumber(isoCode: IsoCode.FR, nsn: '412345678')), - equals('custom type message'), - ); - }); - - testWidgets('should (not) be invalid mobile type when (no) value entered', - (WidgetTester tester) async { - final validator = PhoneValidator.validMobile(); - expect( - validator(const PhoneNumber(isoCode: IsoCode.FR, nsn: '')), - isNull, - ); - - final validatorNotEmpty = PhoneValidator.validMobile(allowEmpty: false); - expect( - validatorNotEmpty(const PhoneNumber(isoCode: IsoCode.FR, nsn: '')), - equals('invalidMobilePhoneNumber'), - ); - }); - - testWidgets('should be invalid fixed line type', - (WidgetTester tester) async { - final validator = PhoneValidator.validFixedLine(); - expect( - validator(const PhoneNumber(isoCode: IsoCode.FR, nsn: '612345678')), - equals('invalidFixedLinePhoneNumber'), - ); - - final validatorWithText = PhoneValidator.validFixedLine( - errorText: 'custom fixed type message', - ); - expect( - validatorWithText( - const PhoneNumber(isoCode: IsoCode.FR, nsn: '612345678')), - equals('custom fixed type message'), - ); - }); - - testWidgets( - 'should (not) be invalid fixed line type when (no) value entered', - (WidgetTester tester) async { - final validator = PhoneValidator.validFixedLine(); - expect( - validator(const PhoneNumber(isoCode: IsoCode.FR, nsn: '')), isNull); - - final validatorNotEmpty = - PhoneValidator.validFixedLine(allowEmpty: false); - expect( - validatorNotEmpty(const PhoneNumber(isoCode: IsoCode.FR, nsn: '')), - equals('invalidFixedLinePhoneNumber'), - ); - }); - }); - - group('PhoneValidator.country', () { - testWidgets('should be invalid country', (WidgetTester tester) async { - final validator = PhoneValidator.validCountry([IsoCode.FR, IsoCode.BE]); - expect( - validator(const PhoneNumber(isoCode: IsoCode.US, nsn: '112')), - equals('invalidCountry'), - ); - }); - - testWidgets('should (not) be invalid country when (no) value entered', - (WidgetTester tester) async { - final validator = PhoneValidator.validCountry([IsoCode.US, IsoCode.BE]); - expect( - validator(const PhoneNumber(isoCode: IsoCode.FR, nsn: '')), - isNull, - ); - - final validatorNotEmpty = PhoneValidator.validCountry( - [IsoCode.US, IsoCode.BE], - allowEmpty: false, - ); - expect( - validatorNotEmpty(const PhoneNumber(isoCode: IsoCode.FR, nsn: '')), - equals('invalidCountry'), - ); - }); - }); -} From 9a44ab266a9adf4b75e079fe5d31609720891360 Mon Sep 17 00:00:00 2001 From: cedvdb Date: Sun, 4 Feb 2024 16:20:54 +0100 Subject: [PATCH 18/25] saving --- README.md | 78 +++---- example/lib/main.dart | 18 +- lib/src/phone_controller.dart | 16 +- lib/src/phone_form_field_state.dart | 4 +- lib/src/validation/phone_validator.dart | 3 - test/phone_form_field_test.dart | 262 ++++++++++++------------ 6 files changed, 182 insertions(+), 199 deletions(-) diff --git a/README.md b/README.md index f3b2a338..cd176039 100644 --- a/README.md +++ b/README.md @@ -5,52 +5,42 @@ Flutter phone input integrated with flutter internationalization ## Features - Totally cross platform, this is a dart only package / dependencies -- Internationalization +- Internationalization: many languages supported +- Semantics - Phone formatting localized by region - Phone number validation (built-in validators included for main use cases) -- Support autofill and copy paste -- Extends Flutter's FormField +- Support auto fill and copy paste +- Form field - Uses dart phone_numbers_parser for parsing - ## Demo Demo available at https://cedvdb.github.io/phone_form_field/ - ## Usage ```dart - -// works without any param PhoneFormField(); -// all params +/// params PhoneFormField( - key: Key('phone-field') - controller: null, // controller & initialValue value - initialValue: null, // can't be supplied simultaneously - shouldFormat: true // default - defaultCountry: IsoCode.US, // default - decoration: InputDecoration( - labelText: 'Phone', // default to null - border: OutlineInputBorder() // default to UnderlineInputBorder(), - // ... - ), - validator: PhoneValidator.validMobile(), // default PhoneValidator.valid() - isCountryChipPersistent: false, // default - isCountrySelectionEnabled: true, // default - countrySelectorNavigator: CountrySelectorNavigator.bottomSheet(), - showFlagInInput: true, // default - flagSize: 16, // default - autofillHints: [AutofillHints.telephoneNumber], // default to null - enabled: true, // default - autofocus: false, // default - onSaved: (PhoneNumber p) => print('saved $p'), // default null - onChanged: (PhoneNumber p) => print('saved $p'), // default null - // ... + other textfield params -) - + initialValue: PhoneNumber.parse('+33'), // or use the controller + validator: PhoneValidator.compose( + [PhoneValidator.required(), PhoneValidator.validMobile()]), + countrySelectorNavigator: const CountrySelectorNavigator.page(), + onChanged: (phoneNumber) => print('changed into $phoneNumber'), + enabled: true, + countryButtonPadding: null, + isCountrySelectionEnabled: true, + isCountryButtonPersistent: true, + showDialCode: true, + showIsoCodeInInput: true, + showFlagInInput: true, + flagSize: 16 + // + all parameters of TextField + // + all parameters of FormField + // ... +); ``` ## Validation @@ -68,7 +58,6 @@ PhoneFormField( ### Validators details * Each validator has an optional `errorText` property to override built-in translated text -* Most of them have an optional `allowEmpty` (default is true) preventing to flag an empty field as valid. Consider using a composed validator with a first `PhoneValidator.required` when a different text is needed for empty field. ### Composing validators @@ -134,11 +123,11 @@ Here are the list of the parameters available for all built-in country selector ### Custom Country Selector Navigator You can use your own country selector by creating a class that implements `CountrySelectorNavigator` -It has one required method `navigate` expected to return the selected country: +It has one required method `show` expected to return the selected country: ```dart class CustomCountrySelectorNavigator implements CountrySelectorNavigator { - Future navigate(BuildContext context) { + Future show(BuildContext context) { // ask user for a country and return related `Country` class } } @@ -179,22 +168,23 @@ PhoneFormField( - 'ar', - 'de', + - 'el', - 'en', - - 'el' - 'es', + - 'fa', - 'fr', - - 'hin', + - 'hi', - 'it', + - 'ku', - 'nb', - 'nl', - 'pt', - 'ru', - - 'uz', - - 'uk', - - 'tr', - - 'zh', - 'sv', + - 'tr', + - 'uk', + - 'uz', + - 'zh', - - If one of the language you target is not supported you can submit a - pull request with the translated file in src/l10n +If one of the language you target is not supported you can submit a + pull request diff --git a/example/lib/main.dart b/example/lib/main.dart index ffbe991e..e46b5a80 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -18,7 +18,7 @@ class PhoneFieldView extends StatelessWidget { final bool withLabel; final bool outlineBorder; final bool shouldFormat; - final bool isCountryChipPersistent; + final bool isCountryButtonPersistant; final bool mobileOnly; final bool useRtl; @@ -31,7 +31,7 @@ class PhoneFieldView extends StatelessWidget { required this.withLabel, required this.outlineBorder, required this.shouldFormat, - required this.isCountryChipPersistent, + required this.isCountryButtonPersistant, required this.mobileOnly, required this.useRtl, }) : super(key: key); @@ -53,14 +53,13 @@ class PhoneFieldView extends StatelessWidget { textDirection: useRtl ? TextDirection.rtl : TextDirection.ltr, child: PhoneFormField( key: inputKey, - controller: controller, + initialValue: PhoneNumber.parse('+33478787827'), focusNode: focusNode, shouldFormat: shouldFormat && !useRtl, - isCountryChipPersistent: isCountryChipPersistent, + isCountryButtonPersistent: isCountryButtonPersistant, autofocus: false, autofillHints: const [AutofillHints.telephoneNumber], countrySelectorNavigator: selectorNavigator, - defaultCountry: IsoCode.US, decoration: InputDecoration( label: withLabel ? const Text('Phone') : null, border: outlineBorder @@ -131,7 +130,7 @@ class PhoneFormFieldScreenState extends State { bool outlineBorder = true; bool mobileOnly = true; bool shouldFormat = true; - bool isCountryChipPersistent = false; + bool isCountryButtonPersistent = true; bool withLabel = true; bool useRtl = false; CountrySelectorNavigator selectorNavigator = @@ -178,9 +177,9 @@ class PhoneFormFieldScreenState extends State { title: const Text('Label'), ), SwitchListTile( - value: isCountryChipPersistent, + value: isCountryButtonPersistent, onChanged: (v) => - setState(() => isCountryChipPersistent = v), + setState(() => isCountryButtonPersistent = v), title: const Text('Persistent country chip'), ), SwitchListTile( @@ -256,7 +255,8 @@ class PhoneFormFieldScreenState extends State { selectorNavigator: selectorNavigator, withLabel: withLabel, outlineBorder: outlineBorder, - isCountryChipPersistent: isCountryChipPersistent, + isCountryButtonPersistant: + isCountryButtonPersistent, mobileOnly: mobileOnly, shouldFormat: shouldFormat, useRtl: useRtl, diff --git a/lib/src/phone_controller.dart b/lib/src/phone_controller.dart index 0c926324..93a1e734 100644 --- a/lib/src/phone_controller.dart +++ b/lib/src/phone_controller.dart @@ -18,13 +18,14 @@ class PhoneController extends ChangeNotifier { } /// text editing controller of the nsn ( where user types the phone number ) - late final TextEditingController nationalNumberController; + /// when shouldFormat is true + late final TextEditingController formattedNationalNumberController; PhoneController({ this.initialValue = const PhoneNumber(isoCode: IsoCode.US, nsn: ''), this.shouldFormat = true, }) : _value = initialValue, - nationalNumberController = + formattedNationalNumberController = TextEditingController(text: initialValue.getFormattedNsn()); reset() { @@ -66,11 +67,10 @@ class PhoneController extends ChangeNotifier { _value = phoneNumber; newText = phoneNumber.getFormattedNsn(); } - nationalNumberController.value = TextEditingValue( + formattedNationalNumberController.value = TextEditingValue( text: newText, selection: computeSelection(text, newText), ); - notifyListeners(); } @@ -80,7 +80,7 @@ class PhoneController extends ChangeNotifier { /// used arrow keys to move inside the text. TextSelection computeSelection(String originalText, String newText) { final currentSelectionOffset = - nationalNumberController.selection.extentOffset; + formattedNationalNumberController.selection.extentOffset; final isCursorAtEnd = currentSelectionOffset == originalText.length; var offset = currentSelectionOffset; @@ -102,16 +102,16 @@ class PhoneController extends ChangeNotifier { } selectNationalNumber() { - nationalNumberController.selection = TextSelection( + formattedNationalNumberController.selection = TextSelection( baseOffset: 0, - extentOffset: nationalNumberController.value.text.length, + extentOffset: formattedNationalNumberController.value.text.length, ); notifyListeners(); } @override void dispose() { - nationalNumberController.dispose(); + formattedNationalNumberController.dispose(); super.dispose(); } } diff --git a/lib/src/phone_form_field_state.dart b/lib/src/phone_form_field_state.dart index baa130d6..9c9dce80 100644 --- a/lib/src/phone_form_field_state.dart +++ b/lib/src/phone_form_field_state.dart @@ -10,7 +10,7 @@ class PhoneFormFieldState extends State { controller = widget.controller ?? PhoneController( initialValue: widget.initialValue ?? - const PhoneNumber(isoCode: IsoCode.US, nsn: ''), + PhoneNumber(isoCode: widget.defaultCountry, nsn: ''), ); focusNode = widget.focusNode ?? FocusNode(); _preloadFlagsInMemory(); @@ -53,7 +53,7 @@ class PhoneFormFieldState extends State { : _getCountryCodeChip(context), ), focusNode: focusNode, - controller: controller.nationalNumberController, + controller: controller.formattedNationalNumberController, enabled: widget.enabled, inputFormatters: widget.inputFormatters ?? [ diff --git a/lib/src/validation/phone_validator.dart b/lib/src/validation/phone_validator.dart index c77e1ad8..4ebe2485 100644 --- a/lib/src/validation/phone_validator.dart +++ b/lib/src/validation/phone_validator.dart @@ -113,9 +113,6 @@ class PhoneValidator { static PhoneNumberInputValidator validMobile({ /// custom error message String? errorText, - - /// determine whether a missing value should be reported as invalid - bool allowEmpty = true, }) => validType( PhoneNumberType.mobile, diff --git a/test/phone_form_field_test.dart b/test/phone_form_field_test.dart index 2108c44c..d2b3418d 100644 --- a/test/phone_form_field_test.dart +++ b/test/phone_form_field_test.dart @@ -49,39 +49,35 @@ void main() { ), ); - group('display', () { - testWidgets('Should display input', (tester) async { - await tester.pumpWidget( - getWidget( - initialValue: PhoneNumber.parse('+33'), - ), - ); - expect(find.byType(PhoneFormField), findsOneWidget); - }); + testWidgets('Should display input', (tester) async { + await tester.pumpWidget( + getWidget(initialValue: PhoneNumber.parse('+33')), + ); + expect(find.byType(PhoneFormField), findsOneWidget); + }); - testWidgets('Should display country code', (tester) async { - await tester.pumpWidget(getWidget()); - expect(find.byType(CountryButton), findsWidgets); - }); + testWidgets('Should display country code', (tester) async { + await tester.pumpWidget(getWidget()); + expect(find.byType(CountryButton), findsWidgets); + }); - testWidgets('Should display flag', (tester) async { - await tester.pumpWidget(getWidget()); - expect(find.byType(CircleFlag), findsWidgets); - }); + testWidgets('Should display flag', (tester) async { + await tester.pumpWidget(getWidget()); + expect(find.byType(CircleFlag), findsWidgets); + }); - testWidgets( - 'Should not show country selection when disabled and country chip is tapped', - (tester) async { - await tester.pumpWidget(getWidget(enabled: false)); - final countryChip = - tester.widget(find.byType(CountryButton)); - expect(countryChip.enabled, false); + testWidgets( + 'Should not show country selection when disabled and country chip is tapped', + (tester) async { + await tester.pumpWidget(getWidget(enabled: false)); + final countryChip = + tester.widget(find.byType(CountryButton)); + expect(countryChip.enabled, false); - await tester.tap(find.byType(CountryButton)); - await tester.pumpAndSettle(); + await tester.tap(find.byType(CountryButton)); + await tester.pumpAndSettle(); - expect(find.byType(CountryListView), findsNothing); - }); + expect(find.byType(CountryListView), findsNothing); }); group('Country code', () { @@ -144,101 +140,100 @@ void main() { }); }); - group('value changes', () { - testWidgets('Should display initial value', (tester) async { - await tester.pumpWidget( - getWidget( - initialValue: PhoneNumber.parse('+33478787827'), - ), - ); - expect(find.text('+ 33'), findsWidgets); - expect(find.text('478787827'), findsOneWidget); - }); + testWidgets('Should display initial value', (tester) async { + await tester.pumpWidget( + getWidget( + initialValue: PhoneNumber.parse('+33478787827'), + ), + ); + expect(find.text('+ 33'), findsWidgets); + expect(find.text('4 78 78 78 27'), findsOneWidget); + }); - testWidgets('Should change value of controller', (tester) async { - final controller = PhoneController( - initialValue: PhoneNumber.parse('+1'), - ); - PhoneNumber? newValue; - controller.addListener(() { - newValue = controller.value; - }); - await tester.pumpWidget(getWidget(controller: controller)); - final phoneField = find.byType(PhoneFormField); - await tester.tap(phoneField); - // non digits should not work - await tester.enterText(phoneField, '123456789'); - expect( - newValue, - equals( - PhoneNumber.parse( - '123456789', - destinationCountry: IsoCode.US, - ), - ), - ); + testWidgets('Should change value of controller', (tester) async { + final controller = PhoneController( + initialValue: PhoneNumber.parse('+1'), + ); + PhoneNumber? newValue; + controller.addListener(() { + newValue = controller.value; }); + await tester.pumpWidget(getWidget(controller: controller)); + final phoneField = find.byType(PhoneFormField); + await tester.tap(phoneField); + // non digits should not work + await tester.enterText(phoneField, '123456789'); + expect( + newValue, + equals( + PhoneNumber.parse( + '123456789', + destinationCountry: IsoCode.US, + ), + ), + ); + }); - testWidgets('Should change value of input when controller changes', - (tester) async { - final controller = PhoneController(); - // ignore: unused_local_variable - PhoneNumber? newValue; - controller.addListener(() { - newValue = controller.value; - }); - await tester.pumpWidget(getWidget(controller: controller)); - controller.value = PhoneNumber.parse('+33488997722'); - await tester.pump(const Duration(seconds: 1)); - expect(find.text('+ 33'), findsWidgets); - expect(find.text('488997722'), findsOneWidget); + testWidgets('Should change value of input when controller changes', + (tester) async { + final controller = PhoneController(); + // ignore: unused_local_variable + PhoneNumber? newValue; + controller.addListener(() { + newValue = controller.value; }); - testWidgets( - 'Should change value of country code chip when full number copy pasted', - (tester) async { - final controller = PhoneController(); - // ignore: unused_local_variable - PhoneNumber? newValue; - controller.addListener(() { - newValue = controller.value; - }); - await tester.pumpWidget(getWidget(controller: controller)); - final phoneField = find.byType(PhoneFormField); - await tester.tap(phoneField); - // non digits should not work - await tester.enterText(phoneField, '+33 0488 99 77 22'); - await tester.pump(); - expect(controller.value.isoCode, equals(IsoCode.FR)); - expect(controller.value.nsn, equals('488997722')); - }); - - testWidgets('Should call onChange', (tester) async { - bool changed = false; - PhoneNumber? phoneNumber = - PhoneNumber.parse('', destinationCountry: IsoCode.FR); - void onChanged(PhoneNumber? p) { - changed = true; - phoneNumber = p; - } + await tester.pumpWidget(getWidget(controller: controller)); + controller.value = PhoneNumber.parse('+33488997722'); + await tester.pump(const Duration(seconds: 1)); + expect(find.text('+ 33'), findsWidgets); + expect(find.text('488997722'), findsOneWidget); + }); - await tester.pumpWidget( - getWidget( - initialValue: phoneNumber, - onChanged: onChanged, - ), - ); - final phoneField = find.byType(PhoneFormField); - await tester.tap(phoneField); - // non digits should not work - await tester.enterText(phoneField, 'aaa'); - await tester.pump(const Duration(seconds: 1)); - expect(changed, equals(false)); - await tester.enterText(phoneField, '123'); - await tester.pump(const Duration(seconds: 1)); - expect(changed, equals(true)); - expect(phoneNumber, - equals(PhoneNumber.parse('123', destinationCountry: IsoCode.FR))); + testWidgets( + 'Should change value of country code chip when full number copy pasted', + (tester) async { + final controller = PhoneController(); + // ignore: unused_local_variable + PhoneNumber? newValue; + controller.addListener(() { + newValue = controller.value; }); + await tester.pumpWidget(getWidget(controller: controller)); + final phoneField = find.byType(PhoneFormField); + await tester.tap(phoneField); + // non digits should not work + await tester.enterText(phoneField, '+33 0488 99 77 22'); + await tester.pump(); + expect(controller.value.isoCode, equals(IsoCode.FR)); + expect(controller.value.nsn, equals('488997722')); + }); + + testWidgets('Should call onChange', (tester) async { + bool changed = false; + PhoneNumber? phoneNumber = + PhoneNumber.parse('', destinationCountry: IsoCode.FR); + void onChanged(PhoneNumber? p) { + changed = true; + phoneNumber = p; + } + + await tester.pumpWidget( + getWidget( + initialValue: phoneNumber, + onChanged: onChanged, + ), + ); + final phoneField = find.byType(PhoneFormField); + await tester.tap(phoneField); + // non digits should not work + await tester.enterText(phoneField, 'aaa'); + await tester.pump(const Duration(seconds: 1)); + expect(changed, equals(false)); + await tester.enterText(phoneField, '123'); + await tester.pump(const Duration(seconds: 1)); + expect(changed, equals(true)); + expect(phoneNumber, + equals(PhoneNumber.parse('123', destinationCountry: IsoCode.FR))); }); group('validator', () { @@ -324,22 +319,23 @@ void main() { }); testWidgets( - 'should show error message when PhoneValidator.validCountry ' - 'is used and the current country is invalid', - (WidgetTester tester) async { - final controller = - PhoneController(initialValue: PhoneNumber.parse('+32 444')); - await tester.pumpWidget(getWidget( - controller: controller, - validator: PhoneValidator.validCountry([IsoCode.FR, IsoCode.BE]), - )); - controller.changeCountry(IsoCode.US); - await tester.pumpAndSettle(); - expect( - find.text(PhoneFieldLocalizationEn().invalidCountry), - findsOneWidget, - ); - }); + 'should show error message when PhoneValidator.validCountry ' + 'is used and the current country is invalid', + (WidgetTester tester) async { + final controller = + PhoneController(initialValue: PhoneNumber.parse('+32 444')); + await tester.pumpWidget(getWidget( + controller: controller, + validator: PhoneValidator.validCountry([IsoCode.FR, IsoCode.BE]), + )); + controller.changeCountry(IsoCode.US); + await tester.pumpAndSettle(); + expect( + find.text(PhoneFieldLocalizationEn().invalidCountry), + findsOneWidget, + ); + }, + ); testWidgets('should validate against all validators when compose is used', (WidgetTester tester) async { From 16db2d42409aab34aab5ca3abc9d1a6b4625a209 Mon Sep 17 00:00:00 2001 From: cedvdb Date: Sun, 4 Feb 2024 17:30:02 +0100 Subject: [PATCH 19/25] language --- example/lib/main.dart | 2 +- l10n.yaml | 5 +- {lib/l10n => l10n}/ar.arb | 0 {lib/l10n => l10n}/ckb.arb | 0 {lib/l10n => l10n}/de.arb | 0 {lib/l10n => l10n}/el.arb | 0 {lib/l10n => l10n}/en.arb | 0 {lib/l10n => l10n}/es.arb | 0 {lib/l10n => l10n}/fa.arb | 0 {lib/l10n => l10n}/fr.arb | 0 {lib/l10n => l10n}/hi.arb | 0 {lib/l10n => l10n}/it.arb | 0 {lib/l10n => l10n}/ku.arb | 0 {lib/l10n => l10n}/nb.arb | 0 {lib/l10n => l10n}/nl.arb | 0 {lib/l10n => l10n}/pt.arb | 0 {lib/l10n => l10n}/ru.arb | 0 {lib/l10n => l10n}/sv.arb | 0 {lib/l10n => l10n}/tr.arb | 0 {lib/l10n => l10n}/uk.arb | 0 {lib/l10n => l10n}/uz.arb | 0 {lib/l10n => l10n}/zh.arb | 0 lib/phone_form_field.dart | 3 +- lib/src/country/localized_country.dart | 3 +- .../country_selector_controller.dart | 3 +- .../country_selector_page.dart | 3 +- lib/src/country_selection/no_result_view.dart | 4 +- lib/src/country_selection/search_box.dart | 3 +- .../generated/phone_field_localization.dart | 112 ++++++++++++------ .../phone_field_localization_ar.dart | 2 +- .../phone_field_localization_ckb.dart | 8 +- .../phone_field_localization_de.dart | 2 +- .../phone_field_localization_el.dart | 5 +- .../phone_field_localization_en.dart | 2 +- .../phone_field_localization_es.dart | 2 +- .../phone_field_localization_fa.dart | 2 +- .../phone_field_localization_fr.dart | 5 +- .../phone_field_localization_hi.dart | 2 +- .../phone_field_localization_it.dart | 2 +- .../phone_field_localization_ku.dart | 8 +- .../phone_field_localization_nb.dart | 2 +- .../phone_field_localization_nl.dart | 2 +- .../phone_field_localization_pt.dart | 2 +- .../phone_field_localization_ru.dart | 5 +- .../phone_field_localization_sv.dart | 2 +- .../phone_field_localization_tr.dart | 5 +- .../phone_field_localization_uk.dart | 5 +- .../phone_field_localization_uz.dart | 5 +- .../phone_field_localization_zh.dart | 2 +- .../localization.dart} | 6 +- lib/src/phone_controller.dart | 29 +++-- lib/src/phone_form_field_state.dart | 4 +- lib/src/validation/phone_validator.dart | 2 +- pubspec.yaml | 3 + test/phone_form_field_test.dart | 1 - 55 files changed, 160 insertions(+), 93 deletions(-) rename {lib/l10n => l10n}/ar.arb (100%) rename {lib/l10n => l10n}/ckb.arb (100%) rename {lib/l10n => l10n}/de.arb (100%) rename {lib/l10n => l10n}/el.arb (100%) rename {lib/l10n => l10n}/en.arb (100%) rename {lib/l10n => l10n}/es.arb (100%) rename {lib/l10n => l10n}/fa.arb (100%) rename {lib/l10n => l10n}/fr.arb (100%) rename {lib/l10n => l10n}/hi.arb (100%) rename {lib/l10n => l10n}/it.arb (100%) rename {lib/l10n => l10n}/ku.arb (100%) rename {lib/l10n => l10n}/nb.arb (100%) rename {lib/l10n => l10n}/nl.arb (100%) rename {lib/l10n => l10n}/pt.arb (100%) rename {lib/l10n => l10n}/ru.arb (100%) rename {lib/l10n => l10n}/sv.arb (100%) rename {lib/l10n => l10n}/tr.arb (100%) rename {lib/l10n => l10n}/uk.arb (100%) rename {lib/l10n => l10n}/uz.arb (100%) rename {lib/l10n => l10n}/zh.arb (100%) rename lib/{l10n => src/localization}/generated/phone_field_localization.dart (94%) rename lib/{l10n => src/localization}/generated/phone_field_localization_ar.dart (99%) rename lib/{l10n => src/localization}/generated/phone_field_localization_ckb.dart (97%) rename lib/{l10n => src/localization}/generated/phone_field_localization_de.dart (99%) rename lib/{l10n => src/localization}/generated/phone_field_localization_el.dart (98%) rename lib/{l10n => src/localization}/generated/phone_field_localization_en.dart (99%) rename lib/{l10n => src/localization}/generated/phone_field_localization_es.dart (99%) rename lib/{l10n => src/localization}/generated/phone_field_localization_fa.dart (99%) rename lib/{l10n => src/localization}/generated/phone_field_localization_fr.dart (98%) rename lib/{l10n => src/localization}/generated/phone_field_localization_hi.dart (99%) rename lib/{l10n => src/localization}/generated/phone_field_localization_it.dart (99%) rename lib/{l10n => src/localization}/generated/phone_field_localization_ku.dart (97%) rename lib/{l10n => src/localization}/generated/phone_field_localization_nb.dart (99%) rename lib/{l10n => src/localization}/generated/phone_field_localization_nl.dart (99%) rename lib/{l10n => src/localization}/generated/phone_field_localization_pt.dart (99%) rename lib/{l10n => src/localization}/generated/phone_field_localization_ru.dart (98%) rename lib/{l10n => src/localization}/generated/phone_field_localization_sv.dart (99%) rename lib/{l10n => src/localization}/generated/phone_field_localization_tr.dart (98%) rename lib/{l10n => src/localization}/generated/phone_field_localization_uk.dart (98%) rename lib/{l10n => src/localization}/generated/phone_field_localization_uz.dart (98%) rename lib/{l10n => src/localization}/generated/phone_field_localization_zh.dart (99%) rename lib/src/{country/localize_country.dart => localization/localization.dart} (97%) diff --git a/example/lib/main.dart b/example/lib/main.dart index e46b5a80..51b75353 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -89,7 +89,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - localizationsDelegates: const [ + localizationsDelegates: [ ...GlobalMaterialLocalizations.delegates, PhoneFieldLocalization.delegate ], diff --git a/l10n.yaml b/l10n.yaml index 5482d2f7..02604888 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -1,6 +1,7 @@ -arb-dir: lib/l10n +arb-dir: l10n template-arb-file: en.arb output-localization-file: phone_field_localization.dart output-class: PhoneFieldLocalization -output-dir: lib/l10n/generated +output-dir: lib/src/localization/generated synthetic-package: false +format: true diff --git a/lib/l10n/ar.arb b/l10n/ar.arb similarity index 100% rename from lib/l10n/ar.arb rename to l10n/ar.arb diff --git a/lib/l10n/ckb.arb b/l10n/ckb.arb similarity index 100% rename from lib/l10n/ckb.arb rename to l10n/ckb.arb diff --git a/lib/l10n/de.arb b/l10n/de.arb similarity index 100% rename from lib/l10n/de.arb rename to l10n/de.arb diff --git a/lib/l10n/el.arb b/l10n/el.arb similarity index 100% rename from lib/l10n/el.arb rename to l10n/el.arb diff --git a/lib/l10n/en.arb b/l10n/en.arb similarity index 100% rename from lib/l10n/en.arb rename to l10n/en.arb diff --git a/lib/l10n/es.arb b/l10n/es.arb similarity index 100% rename from lib/l10n/es.arb rename to l10n/es.arb diff --git a/lib/l10n/fa.arb b/l10n/fa.arb similarity index 100% rename from lib/l10n/fa.arb rename to l10n/fa.arb diff --git a/lib/l10n/fr.arb b/l10n/fr.arb similarity index 100% rename from lib/l10n/fr.arb rename to l10n/fr.arb diff --git a/lib/l10n/hi.arb b/l10n/hi.arb similarity index 100% rename from lib/l10n/hi.arb rename to l10n/hi.arb diff --git a/lib/l10n/it.arb b/l10n/it.arb similarity index 100% rename from lib/l10n/it.arb rename to l10n/it.arb diff --git a/lib/l10n/ku.arb b/l10n/ku.arb similarity index 100% rename from lib/l10n/ku.arb rename to l10n/ku.arb diff --git a/lib/l10n/nb.arb b/l10n/nb.arb similarity index 100% rename from lib/l10n/nb.arb rename to l10n/nb.arb diff --git a/lib/l10n/nl.arb b/l10n/nl.arb similarity index 100% rename from lib/l10n/nl.arb rename to l10n/nl.arb diff --git a/lib/l10n/pt.arb b/l10n/pt.arb similarity index 100% rename from lib/l10n/pt.arb rename to l10n/pt.arb diff --git a/lib/l10n/ru.arb b/l10n/ru.arb similarity index 100% rename from lib/l10n/ru.arb rename to l10n/ru.arb diff --git a/lib/l10n/sv.arb b/l10n/sv.arb similarity index 100% rename from lib/l10n/sv.arb rename to l10n/sv.arb diff --git a/lib/l10n/tr.arb b/l10n/tr.arb similarity index 100% rename from lib/l10n/tr.arb rename to l10n/tr.arb diff --git a/lib/l10n/uk.arb b/l10n/uk.arb similarity index 100% rename from lib/l10n/uk.arb rename to l10n/uk.arb diff --git a/lib/l10n/uz.arb b/l10n/uz.arb similarity index 100% rename from lib/l10n/uz.arb rename to l10n/uz.arb diff --git a/lib/l10n/zh.arb b/l10n/zh.arb similarity index 100% rename from lib/l10n/zh.arb rename to l10n/zh.arb diff --git a/lib/phone_form_field.dart b/lib/phone_form_field.dart index f5d951f2..dfd54695 100644 --- a/lib/phone_form_field.dart +++ b/lib/phone_form_field.dart @@ -6,8 +6,7 @@ export 'src/country_selection/country_selector.dart'; export 'src/country/country_button.dart'; export 'src/validation/phone_validator.dart'; - -export 'l10n/generated/phone_field_localization.dart'; +export 'src/localization/localization.dart'; export 'src/phone_controller.dart'; export 'src/country/localized_country.dart'; diff --git a/lib/src/country/localized_country.dart b/lib/src/country/localized_country.dart index b525814b..5d2837cf 100644 --- a/lib/src/country/localized_country.dart +++ b/lib/src/country/localized_country.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:phone_form_field/l10n/generated/phone_field_localization_en.dart'; import 'package:phone_form_field/phone_form_field.dart'; -import 'package:phone_form_field/src/country/localize_country.dart'; import 'package:phone_numbers_parser/metadata.dart'; + /// Country regroup informations for displaying a list of countries class LocalizedCountry { /// Country alpha-2 iso code diff --git a/lib/src/country_selection/country_selector_controller.dart b/lib/src/country_selection/country_selector_controller.dart index b95d7651..9714013a 100644 --- a/lib/src/country_selection/country_selector_controller.dart +++ b/lib/src/country_selection/country_selector_controller.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:phone_form_field/l10n/generated/phone_field_localization_en.dart'; import 'package:phone_form_field/phone_form_field.dart'; -import 'package:phone_form_field/src/country/localize_country.dart'; import 'package:phone_form_field/src/country_selection/country_finder.dart'; + class CountrySelectorController with ChangeNotifier { final _finder = CountryFinder(); List _countries = []; diff --git a/lib/src/country_selection/country_selector_page.dart b/lib/src/country_selection/country_selector_page.dart index b9491b6a..5b45ceb7 100644 --- a/lib/src/country_selection/country_selector_page.dart +++ b/lib/src/country_selection/country_selector_page.dart @@ -1,11 +1,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:phone_form_field/l10n/generated/phone_field_localization.dart'; -import 'package:phone_form_field/l10n/generated/phone_field_localization_en.dart'; import 'package:phone_form_field/src/country_selection/country_selector_controller.dart'; import 'package:phone_numbers_parser/phone_numbers_parser.dart'; import '../country/localized_country.dart'; +import '../localization/localization.dart'; import 'country_list_view.dart'; import 'search_box.dart'; diff --git a/lib/src/country_selection/no_result_view.dart b/lib/src/country_selection/no_result_view.dart index 646ad8d7..1af8603c 100644 --- a/lib/src/country_selection/no_result_view.dart +++ b/lib/src/country_selection/no_result_view.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:phone_form_field/l10n/generated/phone_field_localization.dart'; -import 'package:phone_form_field/l10n/generated/phone_field_localization_en.dart'; + +import '../localization/localization.dart'; class NoResultView extends StatelessWidget { final String? title; diff --git a/lib/src/country_selection/search_box.dart b/lib/src/country_selection/search_box.dart index 6f5fa340..c7a61036 100644 --- a/lib/src/country_selection/search_box.dart +++ b/lib/src/country_selection/search_box.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:phone_form_field/l10n/generated/phone_field_localization.dart'; + +import '../localization/localization.dart'; class SearchBox extends StatefulWidget { final Function(String) onChanged; diff --git a/lib/l10n/generated/phone_field_localization.dart b/lib/src/localization/generated/phone_field_localization.dart similarity index 94% rename from lib/l10n/generated/phone_field_localization.dart rename to lib/src/localization/generated/phone_field_localization.dart index 63900696..52fb27ce 100644 --- a/lib/l10n/generated/phone_field_localization.dart +++ b/lib/src/localization/generated/phone_field_localization.dart @@ -78,15 +78,18 @@ import 'phone_field_localization_zh.dart'; /// be consistent with the languages listed in the PhoneFieldLocalization.supportedLocales /// property. abstract class PhoneFieldLocalization { - PhoneFieldLocalization(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + PhoneFieldLocalization(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); final String localeName; static PhoneFieldLocalization? of(BuildContext context) { - return Localizations.of(context, PhoneFieldLocalization); + return Localizations.of( + context, PhoneFieldLocalization); } - static const LocalizationsDelegate delegate = _PhoneFieldLocalizationDelegate(); + static const LocalizationsDelegate delegate = + _PhoneFieldLocalizationDelegate(); /// A list of this localizations delegate along with the default localizations /// delegates. @@ -98,7 +101,8 @@ abstract class PhoneFieldLocalization { /// Additional delegates can be added by appending to this list in /// MaterialApp. This list does not have to be used at all if a custom list /// of delegates is preferred or required. - static const List> localizationsDelegates = >[ + static const List> localizationsDelegates = + >[ delegate, GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, @@ -1630,52 +1634,92 @@ abstract class PhoneFieldLocalization { String get zw_; } -class _PhoneFieldLocalizationDelegate extends LocalizationsDelegate { +class _PhoneFieldLocalizationDelegate + extends LocalizationsDelegate { const _PhoneFieldLocalizationDelegate(); @override Future load(Locale locale) { - return SynchronousFuture(lookupPhoneFieldLocalization(locale)); + return SynchronousFuture( + lookupPhoneFieldLocalization(locale)); } @override - bool isSupported(Locale locale) => ['ar', 'ckb', 'de', 'el', 'en', 'es', 'fa', 'fr', 'hi', 'it', 'ku', 'nb', 'nl', 'pt', 'ru', 'sv', 'tr', 'uk', 'uz', 'zh'].contains(locale.languageCode); + bool isSupported(Locale locale) => [ + 'ar', + 'ckb', + 'de', + 'el', + 'en', + 'es', + 'fa', + 'fr', + 'hi', + 'it', + 'ku', + 'nb', + 'nl', + 'pt', + 'ru', + 'sv', + 'tr', + 'uk', + 'uz', + 'zh' + ].contains(locale.languageCode); @override bool shouldReload(_PhoneFieldLocalizationDelegate old) => false; } PhoneFieldLocalization lookupPhoneFieldLocalization(Locale locale) { - - // Lookup logic when only language code is specified. switch (locale.languageCode) { - case 'ar': return PhoneFieldLocalizationAr(); - case 'ckb': return PhoneFieldLocalizationCkb(); - case 'de': return PhoneFieldLocalizationDe(); - case 'el': return PhoneFieldLocalizationEl(); - case 'en': return PhoneFieldLocalizationEn(); - case 'es': return PhoneFieldLocalizationEs(); - case 'fa': return PhoneFieldLocalizationFa(); - case 'fr': return PhoneFieldLocalizationFr(); - case 'hi': return PhoneFieldLocalizationHi(); - case 'it': return PhoneFieldLocalizationIt(); - case 'ku': return PhoneFieldLocalizationKu(); - case 'nb': return PhoneFieldLocalizationNb(); - case 'nl': return PhoneFieldLocalizationNl(); - case 'pt': return PhoneFieldLocalizationPt(); - case 'ru': return PhoneFieldLocalizationRu(); - case 'sv': return PhoneFieldLocalizationSv(); - case 'tr': return PhoneFieldLocalizationTr(); - case 'uk': return PhoneFieldLocalizationUk(); - case 'uz': return PhoneFieldLocalizationUz(); - case 'zh': return PhoneFieldLocalizationZh(); + case 'ar': + return PhoneFieldLocalizationAr(); + case 'ckb': + return PhoneFieldLocalizationCkb(); + case 'de': + return PhoneFieldLocalizationDe(); + case 'el': + return PhoneFieldLocalizationEl(); + case 'en': + return PhoneFieldLocalizationEn(); + case 'es': + return PhoneFieldLocalizationEs(); + case 'fa': + return PhoneFieldLocalizationFa(); + case 'fr': + return PhoneFieldLocalizationFr(); + case 'hi': + return PhoneFieldLocalizationHi(); + case 'it': + return PhoneFieldLocalizationIt(); + case 'ku': + return PhoneFieldLocalizationKu(); + case 'nb': + return PhoneFieldLocalizationNb(); + case 'nl': + return PhoneFieldLocalizationNl(); + case 'pt': + return PhoneFieldLocalizationPt(); + case 'ru': + return PhoneFieldLocalizationRu(); + case 'sv': + return PhoneFieldLocalizationSv(); + case 'tr': + return PhoneFieldLocalizationTr(); + case 'uk': + return PhoneFieldLocalizationUk(); + case 'uz': + return PhoneFieldLocalizationUz(); + case 'zh': + return PhoneFieldLocalizationZh(); } throw FlutterError( - 'PhoneFieldLocalization.delegate failed to load unsupported locale "$locale". This is likely ' - 'an issue with the localizations generation tool. Please file an issue ' - 'on GitHub with a reproducible sample app and the gen-l10n configuration ' - 'that was used.' - ); + 'PhoneFieldLocalization.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.'); } diff --git a/lib/l10n/generated/phone_field_localization_ar.dart b/lib/src/localization/generated/phone_field_localization_ar.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_ar.dart rename to lib/src/localization/generated/phone_field_localization_ar.dart index 9635806c..3ba060df 100644 --- a/lib/l10n/generated/phone_field_localization_ar.dart +++ b/lib/src/localization/generated/phone_field_localization_ar.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Arabic (`ar`). class PhoneFieldLocalizationAr extends PhoneFieldLocalization { - PhoneFieldLocalizationAr([String locale = 'ar']) : super(locale); + PhoneFieldLocalizationAr([super.locale = 'ar']); @override String get invalidPhoneNumber => 'رقم الهاتف غير صحيح'; diff --git a/lib/l10n/generated/phone_field_localization_ckb.dart b/lib/src/localization/generated/phone_field_localization_ckb.dart similarity index 97% rename from lib/l10n/generated/phone_field_localization_ckb.dart rename to lib/src/localization/generated/phone_field_localization_ckb.dart index 78d1e0ac..40c07678 100644 --- a/lib/l10n/generated/phone_field_localization_ckb.dart +++ b/lib/src/localization/generated/phone_field_localization_ckb.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Central Kurdish (`ckb`). class PhoneFieldLocalizationCkb extends PhoneFieldLocalization { - PhoneFieldLocalizationCkb([String locale = 'ckb']) : super(locale); + PhoneFieldLocalizationCkb([super.locale = 'ckb']); @override String get invalidPhoneNumber => 'ژمارەی تەلەفۆنی نادروست'; @@ -14,7 +14,8 @@ class PhoneFieldLocalizationCkb extends PhoneFieldLocalization { String get invalidMobilePhoneNumber => 'ژمارەی مۆبایل نادروستە'; @override - String get invalidFixedLinePhoneNumber => 'ژمارەی تەلەفۆنی هێڵی جێگیر نادروستە'; + String get invalidFixedLinePhoneNumber => + 'ژمارەی تەلەفۆنی هێڵی جێگیر نادروستە'; @override String get requiredPhoneNumber => 'ژمارەی تەلەفۆنی پێویست'; @@ -260,7 +261,8 @@ class PhoneFieldLocalizationCkb extends PhoneFieldLocalization { String get ge_ => 'جۆرجیا'; @override - String get gf_ => 'دورگەیەکی فەڕەنسایە کە دەکەوێتە باکوری خۆرهەڵاتی ئەمەریکای باشور'; + String get gf_ => + 'دورگەیەکی فەڕەنسایە کە دەکەوێتە باکوری خۆرهەڵاتی ئەمەریکای باشور'; @override String get gg_ => 'گێرنسی'; diff --git a/lib/l10n/generated/phone_field_localization_de.dart b/lib/src/localization/generated/phone_field_localization_de.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_de.dart rename to lib/src/localization/generated/phone_field_localization_de.dart index 289eb198..d80a0082 100644 --- a/lib/l10n/generated/phone_field_localization_de.dart +++ b/lib/src/localization/generated/phone_field_localization_de.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for German (`de`). class PhoneFieldLocalizationDe extends PhoneFieldLocalization { - PhoneFieldLocalizationDe([String locale = 'de']) : super(locale); + PhoneFieldLocalizationDe([super.locale = 'de']); @override String get invalidPhoneNumber => 'Ungültige Telefonnummer'; diff --git a/lib/l10n/generated/phone_field_localization_el.dart b/lib/src/localization/generated/phone_field_localization_el.dart similarity index 98% rename from lib/l10n/generated/phone_field_localization_el.dart rename to lib/src/localization/generated/phone_field_localization_el.dart index 10d33e9a..f55eee66 100644 --- a/lib/l10n/generated/phone_field_localization_el.dart +++ b/lib/src/localization/generated/phone_field_localization_el.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Modern Greek (`el`). class PhoneFieldLocalizationEl extends PhoneFieldLocalization { - PhoneFieldLocalizationEl([String locale = 'el']) : super(locale); + PhoneFieldLocalizationEl([super.locale = 'el']); @override String get invalidPhoneNumber => 'Μη έγκυρος αριθμός τηλεφώνου'; @@ -14,7 +14,8 @@ class PhoneFieldLocalizationEl extends PhoneFieldLocalization { String get invalidMobilePhoneNumber => 'Μη έγκυρος αριθμός κινητού τηλεφώνου'; @override - String get invalidFixedLinePhoneNumber => 'Μη έγκυρος αριθμός σταθερού τηλεφώνου'; + String get invalidFixedLinePhoneNumber => + 'Μη έγκυρος αριθμός σταθερού τηλεφώνου'; @override String get requiredPhoneNumber => 'Απαιτούμενος αριθμός τηλεφώνου'; diff --git a/lib/l10n/generated/phone_field_localization_en.dart b/lib/src/localization/generated/phone_field_localization_en.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_en.dart rename to lib/src/localization/generated/phone_field_localization_en.dart index 62a23426..7ff77099 100644 --- a/lib/l10n/generated/phone_field_localization_en.dart +++ b/lib/src/localization/generated/phone_field_localization_en.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for English (`en`). class PhoneFieldLocalizationEn extends PhoneFieldLocalization { - PhoneFieldLocalizationEn([String locale = 'en']) : super(locale); + PhoneFieldLocalizationEn([super.locale = 'en']); @override String get invalidPhoneNumber => 'Invalid phone number'; diff --git a/lib/l10n/generated/phone_field_localization_es.dart b/lib/src/localization/generated/phone_field_localization_es.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_es.dart rename to lib/src/localization/generated/phone_field_localization_es.dart index 4aa34f26..ccedb32e 100644 --- a/lib/l10n/generated/phone_field_localization_es.dart +++ b/lib/src/localization/generated/phone_field_localization_es.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Spanish Castilian (`es`). class PhoneFieldLocalizationEs extends PhoneFieldLocalization { - PhoneFieldLocalizationEs([String locale = 'es']) : super(locale); + PhoneFieldLocalizationEs([super.locale = 'es']); @override String get invalidPhoneNumber => 'Numero de telefono invalido'; diff --git a/lib/l10n/generated/phone_field_localization_fa.dart b/lib/src/localization/generated/phone_field_localization_fa.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_fa.dart rename to lib/src/localization/generated/phone_field_localization_fa.dart index 71dadad9..fef4b6d9 100644 --- a/lib/l10n/generated/phone_field_localization_fa.dart +++ b/lib/src/localization/generated/phone_field_localization_fa.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Persian (`fa`). class PhoneFieldLocalizationFa extends PhoneFieldLocalization { - PhoneFieldLocalizationFa([String locale = 'fa']) : super(locale); + PhoneFieldLocalizationFa([super.locale = 'fa']); @override String get invalidPhoneNumber => 'شماره تلفن نامعتبر است'; diff --git a/lib/l10n/generated/phone_field_localization_fr.dart b/lib/src/localization/generated/phone_field_localization_fr.dart similarity index 98% rename from lib/l10n/generated/phone_field_localization_fr.dart rename to lib/src/localization/generated/phone_field_localization_fr.dart index d5548a85..a680a3b4 100644 --- a/lib/l10n/generated/phone_field_localization_fr.dart +++ b/lib/src/localization/generated/phone_field_localization_fr.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for French (`fr`). class PhoneFieldLocalizationFr extends PhoneFieldLocalization { - PhoneFieldLocalizationFr([String locale = 'fr']) : super(locale); + PhoneFieldLocalizationFr([super.locale = 'fr']); @override String get invalidPhoneNumber => 'Numéro de téléphone invalide'; @@ -11,7 +11,8 @@ class PhoneFieldLocalizationFr extends PhoneFieldLocalization { String get invalidCountry => 'Pays invalide'; @override - String get invalidMobilePhoneNumber => 'Numéro de téléphone portable invalide'; + String get invalidMobilePhoneNumber => + 'Numéro de téléphone portable invalide'; @override String get invalidFixedLinePhoneNumber => 'Numéro de téléphone fixe invalide'; diff --git a/lib/l10n/generated/phone_field_localization_hi.dart b/lib/src/localization/generated/phone_field_localization_hi.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_hi.dart rename to lib/src/localization/generated/phone_field_localization_hi.dart index a30c442b..94e8f1ee 100644 --- a/lib/l10n/generated/phone_field_localization_hi.dart +++ b/lib/src/localization/generated/phone_field_localization_hi.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Hindi (`hi`). class PhoneFieldLocalizationHi extends PhoneFieldLocalization { - PhoneFieldLocalizationHi([String locale = 'hi']) : super(locale); + PhoneFieldLocalizationHi([super.locale = 'hi']); @override String get invalidPhoneNumber => 'अवैध फोन नंबर'; diff --git a/lib/l10n/generated/phone_field_localization_it.dart b/lib/src/localization/generated/phone_field_localization_it.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_it.dart rename to lib/src/localization/generated/phone_field_localization_it.dart index f6bf00ff..70a33276 100644 --- a/lib/l10n/generated/phone_field_localization_it.dart +++ b/lib/src/localization/generated/phone_field_localization_it.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Italian (`it`). class PhoneFieldLocalizationIt extends PhoneFieldLocalization { - PhoneFieldLocalizationIt([String locale = 'it']) : super(locale); + PhoneFieldLocalizationIt([super.locale = 'it']); @override String get invalidPhoneNumber => 'Numero di telefono invalido'; diff --git a/lib/l10n/generated/phone_field_localization_ku.dart b/lib/src/localization/generated/phone_field_localization_ku.dart similarity index 97% rename from lib/l10n/generated/phone_field_localization_ku.dart rename to lib/src/localization/generated/phone_field_localization_ku.dart index 1cf39550..e789ace9 100644 --- a/lib/l10n/generated/phone_field_localization_ku.dart +++ b/lib/src/localization/generated/phone_field_localization_ku.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Kurdish (`ku`). class PhoneFieldLocalizationKu extends PhoneFieldLocalization { - PhoneFieldLocalizationKu([String locale = 'ku']) : super(locale); + PhoneFieldLocalizationKu([super.locale = 'ku']); @override String get invalidPhoneNumber => 'ژمارەی تەلەفۆنی نادروست'; @@ -14,7 +14,8 @@ class PhoneFieldLocalizationKu extends PhoneFieldLocalization { String get invalidMobilePhoneNumber => 'ژمارەی مۆبایل نادروستە'; @override - String get invalidFixedLinePhoneNumber => 'ژمارەی تەلەفۆنی هێڵی جێگیر نادروستە'; + String get invalidFixedLinePhoneNumber => + 'ژمارەی تەلەفۆنی هێڵی جێگیر نادروستە'; @override String get requiredPhoneNumber => 'ژمارەی تەلەفۆنی پێویست'; @@ -260,7 +261,8 @@ class PhoneFieldLocalizationKu extends PhoneFieldLocalization { String get ge_ => 'جۆرجیا'; @override - String get gf_ => 'دورگەیەکی فەڕەنسایە کە دەکەوێتە باکوری خۆرهەڵاتی ئەمەریکای باشور'; + String get gf_ => + 'دورگەیەکی فەڕەنسایە کە دەکەوێتە باکوری خۆرهەڵاتی ئەمەریکای باشور'; @override String get gg_ => 'گێرنسی'; diff --git a/lib/l10n/generated/phone_field_localization_nb.dart b/lib/src/localization/generated/phone_field_localization_nb.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_nb.dart rename to lib/src/localization/generated/phone_field_localization_nb.dart index 2fbdc576..f7448c8f 100644 --- a/lib/l10n/generated/phone_field_localization_nb.dart +++ b/lib/src/localization/generated/phone_field_localization_nb.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Norwegian Bokmål (`nb`). class PhoneFieldLocalizationNb extends PhoneFieldLocalization { - PhoneFieldLocalizationNb([String locale = 'nb']) : super(locale); + PhoneFieldLocalizationNb([super.locale = 'nb']); @override String get invalidPhoneNumber => 'Ugyldig telefonnummer'; diff --git a/lib/l10n/generated/phone_field_localization_nl.dart b/lib/src/localization/generated/phone_field_localization_nl.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_nl.dart rename to lib/src/localization/generated/phone_field_localization_nl.dart index efa85fb7..7eba73e7 100644 --- a/lib/l10n/generated/phone_field_localization_nl.dart +++ b/lib/src/localization/generated/phone_field_localization_nl.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Dutch Flemish (`nl`). class PhoneFieldLocalizationNl extends PhoneFieldLocalization { - PhoneFieldLocalizationNl([String locale = 'nl']) : super(locale); + PhoneFieldLocalizationNl([super.locale = 'nl']); @override String get invalidPhoneNumber => 'Ongeldig telefoonnummer'; diff --git a/lib/l10n/generated/phone_field_localization_pt.dart b/lib/src/localization/generated/phone_field_localization_pt.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_pt.dart rename to lib/src/localization/generated/phone_field_localization_pt.dart index c6ab94cf..41bda7f7 100644 --- a/lib/l10n/generated/phone_field_localization_pt.dart +++ b/lib/src/localization/generated/phone_field_localization_pt.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Portuguese (`pt`). class PhoneFieldLocalizationPt extends PhoneFieldLocalization { - PhoneFieldLocalizationPt([String locale = 'pt']) : super(locale); + PhoneFieldLocalizationPt([super.locale = 'pt']); @override String get invalidPhoneNumber => 'Número de telefone inválido'; diff --git a/lib/l10n/generated/phone_field_localization_ru.dart b/lib/src/localization/generated/phone_field_localization_ru.dart similarity index 98% rename from lib/l10n/generated/phone_field_localization_ru.dart rename to lib/src/localization/generated/phone_field_localization_ru.dart index badd55c0..a74ed943 100644 --- a/lib/l10n/generated/phone_field_localization_ru.dart +++ b/lib/src/localization/generated/phone_field_localization_ru.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Russian (`ru`). class PhoneFieldLocalizationRu extends PhoneFieldLocalization { - PhoneFieldLocalizationRu([String locale = 'ru']) : super(locale); + PhoneFieldLocalizationRu([super.locale = 'ru']); @override String get invalidPhoneNumber => 'Неправильный номер телефона'; @@ -14,7 +14,8 @@ class PhoneFieldLocalizationRu extends PhoneFieldLocalization { String get invalidMobilePhoneNumber => 'Неверный номер мобильного телефона'; @override - String get invalidFixedLinePhoneNumber => 'Недействительный номер стационарного телефона'; + String get invalidFixedLinePhoneNumber => + 'Недействительный номер стационарного телефона'; @override String get requiredPhoneNumber => 'Требуется номер телефона'; diff --git a/lib/l10n/generated/phone_field_localization_sv.dart b/lib/src/localization/generated/phone_field_localization_sv.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_sv.dart rename to lib/src/localization/generated/phone_field_localization_sv.dart index 3195ba51..1d8b8595 100644 --- a/lib/l10n/generated/phone_field_localization_sv.dart +++ b/lib/src/localization/generated/phone_field_localization_sv.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Swedish (`sv`). class PhoneFieldLocalizationSv extends PhoneFieldLocalization { - PhoneFieldLocalizationSv([String locale = 'sv']) : super(locale); + PhoneFieldLocalizationSv([super.locale = 'sv']); @override String get invalidPhoneNumber => 'Ogiltigt telefonnummer'; diff --git a/lib/l10n/generated/phone_field_localization_tr.dart b/lib/src/localization/generated/phone_field_localization_tr.dart similarity index 98% rename from lib/l10n/generated/phone_field_localization_tr.dart rename to lib/src/localization/generated/phone_field_localization_tr.dart index 65507e4a..cbbd2995 100644 --- a/lib/l10n/generated/phone_field_localization_tr.dart +++ b/lib/src/localization/generated/phone_field_localization_tr.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Turkish (`tr`). class PhoneFieldLocalizationTr extends PhoneFieldLocalization { - PhoneFieldLocalizationTr([String locale = 'tr']) : super(locale); + PhoneFieldLocalizationTr([super.locale = 'tr']); @override String get invalidPhoneNumber => 'Geçersiz telefon numarası'; @@ -14,7 +14,8 @@ class PhoneFieldLocalizationTr extends PhoneFieldLocalization { String get invalidMobilePhoneNumber => 'Geçersiz cep telefonu numarası'; @override - String get invalidFixedLinePhoneNumber => 'Geçersiz sabit hat telefon numarası'; + String get invalidFixedLinePhoneNumber => + 'Geçersiz sabit hat telefon numarası'; @override String get requiredPhoneNumber => 'Telefon numarası gerekli'; diff --git a/lib/l10n/generated/phone_field_localization_uk.dart b/lib/src/localization/generated/phone_field_localization_uk.dart similarity index 98% rename from lib/l10n/generated/phone_field_localization_uk.dart rename to lib/src/localization/generated/phone_field_localization_uk.dart index d40b7261..08d56b90 100644 --- a/lib/l10n/generated/phone_field_localization_uk.dart +++ b/lib/src/localization/generated/phone_field_localization_uk.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Ukrainian (`uk`). class PhoneFieldLocalizationUk extends PhoneFieldLocalization { - PhoneFieldLocalizationUk([String locale = 'uk']) : super(locale); + PhoneFieldLocalizationUk([super.locale = 'uk']); @override String get invalidPhoneNumber => 'Невірний номер телефону'; @@ -14,7 +14,8 @@ class PhoneFieldLocalizationUk extends PhoneFieldLocalization { String get invalidMobilePhoneNumber => 'Невірний номер мобільного телефону'; @override - String get invalidFixedLinePhoneNumber => 'Невірний номер стаціонарного телефону'; + String get invalidFixedLinePhoneNumber => + 'Невірний номер стаціонарного телефону'; @override String get requiredPhoneNumber => 'Необхідний номер телефону'; diff --git a/lib/l10n/generated/phone_field_localization_uz.dart b/lib/src/localization/generated/phone_field_localization_uz.dart similarity index 98% rename from lib/l10n/generated/phone_field_localization_uz.dart rename to lib/src/localization/generated/phone_field_localization_uz.dart index b59e7450..26e54aa8 100644 --- a/lib/l10n/generated/phone_field_localization_uz.dart +++ b/lib/src/localization/generated/phone_field_localization_uz.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Uzbek (`uz`). class PhoneFieldLocalizationUz extends PhoneFieldLocalization { - PhoneFieldLocalizationUz([String locale = 'uz']) : super(locale); + PhoneFieldLocalizationUz([super.locale = 'uz']); @override String get invalidPhoneNumber => 'Telefon raqami noto‘g‘ri'; @@ -14,7 +14,8 @@ class PhoneFieldLocalizationUz extends PhoneFieldLocalization { String get invalidMobilePhoneNumber => 'Telfon raqami noto‘g‘ri'; @override - String get invalidFixedLinePhoneNumber => 'Ruxsat etilgan telefon raqami yaroqsiz'; + String get invalidFixedLinePhoneNumber => + 'Ruxsat etilgan telefon raqami yaroqsiz'; @override String get requiredPhoneNumber => 'Telfon raqami majburiy'; diff --git a/lib/l10n/generated/phone_field_localization_zh.dart b/lib/src/localization/generated/phone_field_localization_zh.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_zh.dart rename to lib/src/localization/generated/phone_field_localization_zh.dart index eeaa18aa..0e56aa22 100644 --- a/lib/l10n/generated/phone_field_localization_zh.dart +++ b/lib/src/localization/generated/phone_field_localization_zh.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Chinese (`zh`). class PhoneFieldLocalizationZh extends PhoneFieldLocalization { - PhoneFieldLocalizationZh([String locale = 'zh']) : super(locale); + PhoneFieldLocalizationZh([super.locale = 'zh']); @override String get invalidPhoneNumber => '无效的电话号码'; diff --git a/lib/src/country/localize_country.dart b/lib/src/localization/localization.dart similarity index 97% rename from lib/src/country/localize_country.dart rename to lib/src/localization/localization.dart index a8061be0..ed37c65f 100644 --- a/lib/src/country/localize_country.dart +++ b/lib/src/localization/localization.dart @@ -1,5 +1,7 @@ -import 'package:phone_form_field/l10n/generated/phone_field_localization.dart'; -import 'package:phone_numbers_parser/phone_numbers_parser.dart'; +import 'package:phone_form_field/phone_form_field.dart'; + +export 'generated/phone_field_localization.dart'; +export 'generated/phone_field_localization_en.dart'; extension DynamicLocalization on PhoneFieldLocalization { countryName(IsoCode isoCode) { diff --git a/lib/src/phone_controller.dart b/lib/src/phone_controller.dart index 93a1e734..50e0be87 100644 --- a/lib/src/phone_controller.dart +++ b/lib/src/phone_controller.dart @@ -3,8 +3,6 @@ import 'package:phone_form_field/src/validation/allowed_characters.dart'; import 'package:phone_numbers_parser/phone_numbers_parser.dart'; class PhoneController extends ChangeNotifier { - final bool shouldFormat; - /// focus node of the national number // final FocusNode focusNode; final PhoneNumber initialValue; @@ -21,12 +19,17 @@ class PhoneController extends ChangeNotifier { /// when shouldFormat is true late final TextEditingController formattedNationalNumberController; + /// text editing controller of the nsn ( where user types the phone number ) + /// when shouldFormat is false + late final TextEditingController nationalNumberController; + PhoneController({ this.initialValue = const PhoneNumber(isoCode: IsoCode.US, nsn: ''), - this.shouldFormat = true, }) : _value = initialValue, formattedNationalNumberController = - TextEditingController(text: initialValue.getFormattedNsn()); + TextEditingController(text: initialValue.getFormattedNsn()), + nationalNumberController = + TextEditingController(text: initialValue.nsn); reset() { _value = initialValue; @@ -43,9 +46,11 @@ class PhoneController extends ChangeNotifier { notifyListeners(); } - changeText(String? text) { + changeText( + String? text, + ) { text = text ?? ''; - var newText = text; + var newFormattedText = text; // if starts with + then we parse the whole number final startsWithPlus = @@ -57,7 +62,7 @@ class PhoneController extends ChangeNotifier { // the national number field to remove the "+ country dial code" if (phoneNumber != null) { _value = phoneNumber; - newText = _value.getFormattedNsn(); + newFormattedText = _value.getFormattedNsn(); } } else { final phoneNumber = PhoneNumber.parse( @@ -65,11 +70,15 @@ class PhoneController extends ChangeNotifier { destinationCountry: _value.isoCode, ); _value = phoneNumber; - newText = phoneNumber.getFormattedNsn(); + newFormattedText = phoneNumber.getFormattedNsn(); } formattedNationalNumberController.value = TextEditingValue( - text: newText, - selection: computeSelection(text, newText), + text: newFormattedText, + selection: computeSelection(text, newFormattedText), + ); + nationalNumberController.value = TextEditingValue( + text: text, + selection: computeSelection(text, text), ); notifyListeners(); } diff --git a/lib/src/phone_form_field_state.dart b/lib/src/phone_form_field_state.dart index 9c9dce80..5d3cf870 100644 --- a/lib/src/phone_form_field_state.dart +++ b/lib/src/phone_form_field_state.dart @@ -53,7 +53,9 @@ class PhoneFormFieldState extends State { : _getCountryCodeChip(context), ), focusNode: focusNode, - controller: controller.formattedNationalNumberController, + controller: widget.shouldFormat + ? controller.formattedNationalNumberController + : controller.nationalNumberController, enabled: widget.enabled, inputFormatters: widget.inputFormatters ?? [ diff --git a/lib/src/validation/phone_validator.dart b/lib/src/validation/phone_validator.dart index 4ebe2485..9337ee29 100644 --- a/lib/src/validation/phone_validator.dart +++ b/lib/src/validation/phone_validator.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:phone_form_field/l10n/generated/phone_field_localization_en.dart'; import 'package:phone_form_field/phone_form_field.dart'; + typedef PhoneNumberInputValidator = String? Function( PhoneNumber? phoneNumber, BuildContext context); diff --git a/pubspec.yaml b/pubspec.yaml index a7d93009..a92ce79f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,3 +22,6 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^3.0.1 + +flutter: + generate: true diff --git a/test/phone_form_field_test.dart b/test/phone_form_field_test.dart index d2b3418d..a87cba1f 100644 --- a/test/phone_form_field_test.dart +++ b/test/phone_form_field_test.dart @@ -2,7 +2,6 @@ import 'package:circle_flags/circle_flags.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:phone_form_field/l10n/generated/phone_field_localization_en.dart'; import 'package:phone_form_field/phone_form_field.dart'; import 'package:phone_form_field/src/country_selection/country_list_view.dart'; From 604629bc080e676260c5bf8d9cb2525ac437db5b Mon Sep 17 00:00:00 2001 From: cedvdb Date: Sun, 4 Feb 2024 19:58:27 +0100 Subject: [PATCH 20/25] fixing tests --- example/lib/main.dart | 20 ++------- example/pubspec.lock | 4 +- lib/phone_form_field.dart | 1 - lib/src/phone_controller.dart | 58 ++++++++++-------------- lib/src/phone_form_field.dart | 18 +------- lib/src/phone_form_field_state.dart | 68 +++++++++++++++++------------ pubspec.yaml | 2 +- test/phone_form_field_test.dart | 55 +++++++---------------- 8 files changed, 87 insertions(+), 139 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 51b75353..f740a7af 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -17,7 +17,6 @@ class PhoneFieldView extends StatelessWidget { final CountrySelectorNavigator selectorNavigator; final bool withLabel; final bool outlineBorder; - final bool shouldFormat; final bool isCountryButtonPersistant; final bool mobileOnly; final bool useRtl; @@ -30,7 +29,6 @@ class PhoneFieldView extends StatelessWidget { required this.selectorNavigator, required this.withLabel, required this.outlineBorder, - required this.shouldFormat, required this.isCountryButtonPersistant, required this.mobileOnly, required this.useRtl, @@ -53,9 +51,8 @@ class PhoneFieldView extends StatelessWidget { textDirection: useRtl ? TextDirection.rtl : TextDirection.ltr, child: PhoneFormField( key: inputKey, - initialValue: PhoneNumber.parse('+33478787827'), focusNode: focusNode, - shouldFormat: shouldFormat && !useRtl, + controller: controller, isCountryButtonPersistent: isCountryButtonPersistant, autofocus: false, autofillHints: const [AutofillHints.telephoneNumber], @@ -89,7 +86,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - localizationsDelegates: [ + localizationsDelegates: const [ ...GlobalMaterialLocalizations.delegates, PhoneFieldLocalization.delegate ], @@ -129,7 +126,6 @@ class PhoneFormFieldScreenState extends State { bool outlineBorder = true; bool mobileOnly = true; - bool shouldFormat = true; bool isCountryButtonPersistent = true; bool withLabel = true; bool useRtl = false; @@ -187,11 +183,6 @@ class PhoneFormFieldScreenState extends State { onChanged: (v) => setState(() => mobileOnly = v), title: const Text('Mobile phone number only'), ), - SwitchListTile( - value: shouldFormat, - onChanged: (v) => setState(() => shouldFormat = v), - title: const Text('Should format'), - ), SwitchListTile( value: useRtl, onChanged: (v) { @@ -258,7 +249,6 @@ class PhoneFormFieldScreenState extends State { isCountryButtonPersistant: isCountryButtonPersistent, mobileOnly: mobileOnly, - shouldFormat: shouldFormat, useRtl: useRtl, ), ], @@ -285,10 +275,8 @@ class PhoneFormFieldScreenState extends State { ), const SizedBox(height: 12), ElevatedButton( - onPressed: () => controller.value = PhoneNumber.parse( - '699999999', - destinationCountry: IsoCode.FR, - ), + onPressed: () => controller.value = + PhoneNumber.parse('+33 699 999 999'), child: const Text('Set +33 699 999 999'), ), ], diff --git a/example/pubspec.lock b/example/pubspec.lock index dd592d17..8656b817 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -179,10 +179,10 @@ packages: dependency: transitive description: name: phone_numbers_parser - sha256: c36e71f611b5232eb917c9bcc11ffa5a3144e072241f97bda5b352cb5b9f3f80 + sha256: d0dad4f5b61c3d959b069df088ef7242ffed42a3cf74c7549fd7c324e1eb964e url: "https://pub.dev" source: hosted - version: "8.1.2" + version: "8.1.3" sky_engine: dependency: transitive description: flutter diff --git a/lib/phone_form_field.dart b/lib/phone_form_field.dart index dfd54695..b99ef406 100644 --- a/lib/phone_form_field.dart +++ b/lib/phone_form_field.dart @@ -8,7 +8,6 @@ export 'src/country/country_button.dart'; export 'src/validation/phone_validator.dart'; export 'src/localization/localization.dart'; -export 'src/phone_controller.dart'; export 'src/country/localized_country.dart'; export 'package:phone_numbers_parser/phone_numbers_parser.dart' diff --git a/lib/src/phone_controller.dart b/lib/src/phone_controller.dart index 50e0be87..9276426f 100644 --- a/lib/src/phone_controller.dart +++ b/lib/src/phone_controller.dart @@ -1,6 +1,4 @@ -import 'package:flutter/material.dart'; -import 'package:phone_form_field/src/validation/allowed_characters.dart'; -import 'package:phone_numbers_parser/phone_numbers_parser.dart'; +part of 'phone_form_field.dart'; class PhoneController extends ChangeNotifier { /// focus node of the national number @@ -11,31 +9,25 @@ class PhoneController extends ChangeNotifier { set value(PhoneNumber phoneNumber) { _value = phoneNumber; - changeCountry(_value.isoCode); - changeText(_value.nsn); + final formattedNsn = _value.formatNsn(); + if (_formattedNationalNumberController.text != formattedNsn) { + changeNationalNumber(formattedNsn); + } else { + notifyListeners(); + } } /// text editing controller of the nsn ( where user types the phone number ) - /// when shouldFormat is true - late final TextEditingController formattedNationalNumberController; - - /// text editing controller of the nsn ( where user types the phone number ) - /// when shouldFormat is false - late final TextEditingController nationalNumberController; - + late final TextEditingController _formattedNationalNumberController; PhoneController({ this.initialValue = const PhoneNumber(isoCode: IsoCode.US, nsn: ''), }) : _value = initialValue, - formattedNationalNumberController = - TextEditingController(text: initialValue.getFormattedNsn()), - nationalNumberController = - TextEditingController(text: initialValue.nsn); + _formattedNationalNumberController = TextEditingController( + text: initialValue.formatNsn(), + ); reset() { - _value = initialValue; - changeCountry(_value.isoCode); - changeText(_value.nsn); - notifyListeners(); + value = initialValue; } changeCountry(IsoCode isoCode) { @@ -46,9 +38,7 @@ class PhoneController extends ChangeNotifier { notifyListeners(); } - changeText( - String? text, - ) { + changeNationalNumber(String? text) { text = text ?? ''; var newFormattedText = text; @@ -62,7 +52,7 @@ class PhoneController extends ChangeNotifier { // the national number field to remove the "+ country dial code" if (phoneNumber != null) { _value = phoneNumber; - newFormattedText = _value.getFormattedNsn(); + newFormattedText = _value.formatNsn(); } } else { final phoneNumber = PhoneNumber.parse( @@ -70,15 +60,11 @@ class PhoneController extends ChangeNotifier { destinationCountry: _value.isoCode, ); _value = phoneNumber; - newFormattedText = phoneNumber.getFormattedNsn(); + newFormattedText = phoneNumber.formatNsn(); } - formattedNationalNumberController.value = TextEditingValue( + _formattedNationalNumberController.value = TextEditingValue( text: newFormattedText, - selection: computeSelection(text, newFormattedText), - ); - nationalNumberController.value = TextEditingValue( - text: text, - selection: computeSelection(text, text), + selection: _computeSelection(text, newFormattedText), ); notifyListeners(); } @@ -87,9 +73,9 @@ class PhoneController extends ChangeNotifier { /// Since there is formatting going on we need to explicitely do it. /// We don't want to do it in the middle because the user might have /// used arrow keys to move inside the text. - TextSelection computeSelection(String originalText, String newText) { + TextSelection _computeSelection(String originalText, String newText) { final currentSelectionOffset = - formattedNationalNumberController.selection.extentOffset; + _formattedNationalNumberController.selection.extentOffset; final isCursorAtEnd = currentSelectionOffset == originalText.length; var offset = currentSelectionOffset; @@ -111,16 +97,16 @@ class PhoneController extends ChangeNotifier { } selectNationalNumber() { - formattedNationalNumberController.selection = TextSelection( + _formattedNationalNumberController.selection = TextSelection( baseOffset: 0, - extentOffset: formattedNationalNumberController.value.text.length, + extentOffset: _formattedNationalNumberController.value.text.length, ); notifyListeners(); } @override void dispose() { - formattedNationalNumberController.dispose(); + _formattedNationalNumberController.dispose(); super.dispose(); } } diff --git a/lib/src/phone_form_field.dart b/lib/src/phone_form_field.dart index 4f52bf57..27bf3e45 100644 --- a/lib/src/phone_form_field.dart +++ b/lib/src/phone_form_field.dart @@ -8,9 +8,9 @@ import 'package:phone_numbers_parser/phone_numbers_parser.dart'; import 'country/country_button.dart'; import 'country_selection/country_selector_navigator.dart'; -import 'phone_controller.dart'; import 'validation/phone_validator.dart'; +part 'phone_controller.dart'; part 'phone_form_field_state.dart'; /// Phone input extending form field. @@ -33,20 +33,6 @@ part 'phone_form_field_state.dart'; /// If [controller] is specified the [initialValue] will be /// the first value of the [PhoneController] /// {@endtemplate} -/// -/// ### formatting: -/// {@template shouldFormat} -/// Specify whether the field will format the national number with [shouldFormat] = true (default) -/// eg: +33677784455 will be displayed as +33 6 77 78 44 55. -/// -/// The formats are localized for the country code. -/// eg: +1 677-784-455 & +33 6 77 78 44 55 -/// -/// -/// This does not affect the output value, only the display. -/// Therefor [onChanged] will still return a [PhoneNumber] -/// with nsn of 677784455. -/// {@endtemplate} class PhoneFormField extends StatefulWidget { /// {@macro controller} final PhoneController? controller; @@ -58,7 +44,6 @@ class PhoneFormField extends StatefulWidget { /// example: PhoneValidator.validType(expectedType: PhoneNumberType.mobile) final PhoneNumberInputValidator validator; - /// {@macro shouldFormat} final bool shouldFormat; /// callback called when the input value changes @@ -150,6 +135,7 @@ class PhoneFormField extends StatefulWidget { PhoneFormField({ super.key, this.controller, + @Deprecated('This is now always true and has no effect anymore') this.shouldFormat = true, this.onChanged, this.focusNode, diff --git a/lib/src/phone_form_field_state.dart b/lib/src/phone_form_field_state.dart index 5d3cf870..d2f71d59 100644 --- a/lib/src/phone_form_field_state.dart +++ b/lib/src/phone_form_field_state.dart @@ -7,20 +7,34 @@ class PhoneFormFieldState extends State { @override void initState() { super.initState(); + controller = widget.controller ?? PhoneController( initialValue: widget.initialValue ?? + // remove this line when defaultCountry is removed + // and just use the US default country if no initialValue is set PhoneNumber(isoCode: widget.defaultCountry, nsn: ''), ); + controller.addListener(_onValueChanged); focusNode = widget.focusNode ?? FocusNode(); _preloadFlagsInMemory(); } + @override + void dispose() { + controller.removeListener(_onValueChanged); + super.dispose(); + } + + void _onValueChanged() { + widget.onChanged?.call(controller.value); + } + void _preloadFlagsInMemory() { CircleFlag.preload(IsoCode.values.map((isoCode) => isoCode.name).toList()); } - void selectCountry() async { + void _selectCountry() async { if (!widget.isCountrySelectionEnabled) { return; } @@ -42,27 +56,21 @@ class PhoneFormFieldState extends State { validator: (phoneNumber) => widget.validator(phoneNumber, context), builder: (formFieldState) => AnimatedBuilder( animation: focusNode, - builder: (context, _) => TextField( + builder: (context, countryButton) => TextField( decoration: widget.decoration.copyWith( errorText: formFieldState.errorText, - prefixIcon: widget.isCountryButtonPersistent - ? _getCountryCodeChip(context) - : null, - prefix: widget.isCountryButtonPersistent - ? null - : _getCountryCodeChip(context), + prefixIcon: widget.isCountryButtonPersistent ? countryButton : null, + prefix: widget.isCountryButtonPersistent ? null : countryButton, ), focusNode: focusNode, - controller: widget.shouldFormat - ? controller.formattedNationalNumberController - : controller.nationalNumberController, + controller: controller._formattedNationalNumberController, enabled: widget.enabled, inputFormatters: widget.inputFormatters ?? [ FilteringTextInputFormatter.allow(RegExp( '[${AllowedCharacters.plus}${AllowedCharacters.digits}${AllowedCharacters.punctuation}]')), ], - onChanged: (txt) => controller.changeText(txt), + onChanged: (txt) => controller.changeNationalNumber(txt), autofillHints: widget.autofillHints, keyboardType: widget.keyboardType, textInputAction: widget.textInputAction, @@ -97,27 +105,31 @@ class PhoneFormFieldState extends State { restorationId: widget.restorationId, enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, ), + child: _getCountryCodeChip(context), ), ); } Widget _getCountryCodeChip(BuildContext context) { - return CountryButton( - key: const ValueKey('country-code-chip'), - isoCode: controller.value.isoCode, - onTap: widget.enabled ? selectCountry : null, - padding: _computeCountryButtonPadding(context), - showFlag: widget.showFlagInInput, - showIsoCode: widget.showIsoCodeInInput, - showDialCode: widget.showDialCode, - textStyle: widget.countryCodeStyle ?? - widget.decoration.labelStyle ?? - TextStyle( - fontSize: 16, - color: Theme.of(context).textTheme.bodySmall?.color, - ), - flagSize: widget.flagSize, - enabled: widget.enabled, + return AnimatedBuilder( + animation: controller, + builder: (context, _) => CountryButton( + key: const ValueKey('country-code-chip'), + isoCode: controller.value.isoCode, + onTap: widget.enabled ? _selectCountry : null, + padding: _computeCountryButtonPadding(context), + showFlag: widget.showFlagInInput, + showIsoCode: widget.showIsoCodeInInput, + showDialCode: widget.showDialCode, + textStyle: widget.countryCodeStyle ?? + widget.decoration.labelStyle ?? + TextStyle( + fontSize: 16, + color: Theme.of(context).textTheme.bodySmall?.color, + ), + flagSize: widget.flagSize, + enabled: widget.enabled, + ), ); } diff --git a/pubspec.yaml b/pubspec.yaml index a92ce79f..722364c3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: sdk: flutter circle_flags: ^4.0.0 - phone_numbers_parser: ^8.1.2 + phone_numbers_parser: ^8.1.3 intl: ">=0.18.1 <=1.0.0" diacritic: ^0.1.5 diff --git a/test/phone_form_field_test.dart b/test/phone_form_field_test.dart index a87cba1f..a67cf782 100644 --- a/test/phone_form_field_test.dart +++ b/test/phone_form_field_test.dart @@ -18,7 +18,6 @@ void main() { PhoneController? controller, bool showFlagInInput = true, bool showDialCode = true, - bool shouldFormat = false, PhoneNumberInputValidator? validator, bool enabled = true, }) => @@ -40,9 +39,9 @@ void main() { showFlagInInput: showFlagInInput, showDialCode: showDialCode, controller: controller, - shouldFormat: shouldFormat, validator: validator, enabled: enabled, + autovalidateMode: AutovalidateMode.onUserInteraction, ), ), ), @@ -73,7 +72,7 @@ void main() { tester.widget(find.byType(CountryButton)); expect(countryChip.enabled, false); - await tester.tap(find.byType(CountryButton)); + await tester.tap(find.byType(CountryButton), warnIfMissed: false); await tester.pumpAndSettle(); expect(find.byType(CountryListView), findsNothing); @@ -95,14 +94,13 @@ void main() { expect(find.byType(CircleFlag), findsNothing); }); - testWidgets('Should format when shouldFormat is true', (tester) async { + testWidgets('Should format phone number', (tester) async { PhoneNumber? phoneNumber = PhoneNumber.parse( '', destinationCountry: IsoCode.FR, ); - await tester.pumpWidget( - getWidget(initialValue: phoneNumber, shouldFormat: true)); + await tester.pumpWidget(getWidget(initialValue: phoneNumber)); await tester.pump(const Duration(seconds: 1)); final phoneField = find.byType(PhoneFormField); await tester.enterText(phoneField, '677777777'); @@ -145,6 +143,7 @@ void main() { initialValue: PhoneNumber.parse('+33478787827'), ), ); + await tester.pumpAndSettle(); expect(find.text('+ 33'), findsWidgets); expect(find.text('4 78 78 78 27'), findsOneWidget); }); @@ -164,28 +163,20 @@ void main() { await tester.enterText(phoneField, '123456789'); expect( newValue, - equals( - PhoneNumber.parse( - '123456789', - destinationCountry: IsoCode.US, - ), - ), + equals(PhoneNumber.parse('+1 123456789')), ); }); testWidgets('Should change value of input when controller changes', (tester) async { final controller = PhoneController(); - // ignore: unused_local_variable - PhoneNumber? newValue; - controller.addListener(() { - newValue = controller.value; - }); await tester.pumpWidget(getWidget(controller: controller)); controller.value = PhoneNumber.parse('+33488997722'); - await tester.pump(const Duration(seconds: 1)); + + await tester.pumpAndSettle(); + expect(find.text('+ 33'), findsWidgets); - expect(find.text('488997722'), findsOneWidget); + expect(find.text(controller.value.formatNsn()), findsOneWidget); }); testWidgets( @@ -231,8 +222,10 @@ void main() { await tester.enterText(phoneField, '123'); await tester.pump(const Duration(seconds: 1)); expect(changed, equals(true)); - expect(phoneNumber, - equals(PhoneNumber.parse('123', destinationCountry: IsoCode.FR))); + expect( + phoneNumber, + equals(PhoneNumber.parse('123', destinationCountry: IsoCode.FR)), + ); }); group('validator', () { @@ -308,7 +301,7 @@ void main() { controller: controller, validator: PhoneValidator.required(), )); - controller.changeText(''); + controller.changeNationalNumber(''); await tester.pumpAndSettle(); expect( @@ -412,29 +405,13 @@ void main() { destinationCountry: IsoCode.FR, ); - await tester.pumpWidget( - getWidget(initialValue: phoneNumber, shouldFormat: true)); + await tester.pumpWidget(getWidget(initialValue: phoneNumber)); await tester.pump(const Duration(seconds: 1)); final phoneField = find.byType(PhoneFormField); await tester.enterText(phoneField, '677777777'); await tester.pump(const Duration(seconds: 1)); expect(find.text('6 77 77 77 77'), findsOneWidget); }); - testWidgets('Should not format when shouldFormat is false', - (tester) async { - PhoneNumber? phoneNumber = PhoneNumber.parse( - '', - destinationCountry: IsoCode.FR, - ); - - await tester.pumpWidget( - getWidget(initialValue: phoneNumber, shouldFormat: false)); - await tester.pump(const Duration(seconds: 1)); - final phoneField = find.byType(PhoneFormField); - await tester.enterText(phoneField, '677777777'); - await tester.pump(const Duration(seconds: 1)); - expect(find.text('677777777'), findsOneWidget); - }); }); group('form field', () { From 257f03221ed000795c617bb5cac5df6d1122d083 Mon Sep 17 00:00:00 2001 From: cedvdb Date: Sun, 4 Feb 2024 20:20:06 +0100 Subject: [PATCH 21/25] fix tests --- lib/src/phone_form_field_state.dart | 105 ++++++++++++++-------------- test/phone_form_field_test.dart | 13 ++-- 2 files changed, 59 insertions(+), 59 deletions(-) diff --git a/lib/src/phone_form_field_state.dart b/lib/src/phone_form_field_state.dart index d2f71d59..ad92fe19 100644 --- a/lib/src/phone_form_field_state.dart +++ b/lib/src/phone_form_field_state.dart @@ -3,6 +3,7 @@ part of 'phone_form_field.dart'; class PhoneFormFieldState extends State { late final PhoneController controller; late final FocusNode focusNode; + final GlobalKey _formFieldKey = GlobalKey(); @override void initState() { @@ -27,6 +28,7 @@ class PhoneFormFieldState extends State { } void _onValueChanged() { + _formFieldKey.currentState?.didChange(controller.value); widget.onChanged?.call(controller.value); } @@ -48,64 +50,65 @@ class PhoneFormFieldState extends State { @override Widget build(BuildContext context) { return FormField( + key: _formFieldKey, autovalidateMode: widget.autovalidateMode, enabled: widget.enabled, initialValue: widget.initialValue, onSaved: widget.onSaved, restorationId: widget.restorationId, validator: (phoneNumber) => widget.validator(phoneNumber, context), - builder: (formFieldState) => AnimatedBuilder( - animation: focusNode, - builder: (context, countryButton) => TextField( - decoration: widget.decoration.copyWith( - errorText: formFieldState.errorText, - prefixIcon: widget.isCountryButtonPersistent ? countryButton : null, - prefix: widget.isCountryButtonPersistent ? null : countryButton, - ), - focusNode: focusNode, - controller: controller._formattedNationalNumberController, - enabled: widget.enabled, - inputFormatters: widget.inputFormatters ?? - [ - FilteringTextInputFormatter.allow(RegExp( - '[${AllowedCharacters.plus}${AllowedCharacters.digits}${AllowedCharacters.punctuation}]')), - ], - onChanged: (txt) => controller.changeNationalNumber(txt), - autofillHints: widget.autofillHints, - keyboardType: widget.keyboardType, - textInputAction: widget.textInputAction, - style: widget.style, - strutStyle: widget.strutStyle, - textAlign: widget.textAlign, - textAlignVertical: widget.textAlignVertical, - autofocus: widget.autofocus, - obscuringCharacter: widget.obscuringCharacter, - obscureText: widget.obscureText, - autocorrect: widget.autocorrect, - smartDashesType: widget.smartDashesType, - smartQuotesType: widget.smartQuotesType, - enableSuggestions: widget.enableSuggestions, - showCursor: widget.showCursor, - onEditingComplete: widget.onEditingComplete, - onAppPrivateCommand: widget.onAppPrivateCommand, - cursorWidth: widget.cursorWidth, - cursorHeight: widget.cursorHeight, - cursorRadius: widget.cursorRadius, - cursorColor: widget.cursorColor, - onTapOutside: widget.onTapOutside, - selectionHeightStyle: widget.selectionHeightStyle, - selectionWidthStyle: widget.selectionWidthStyle, - keyboardAppearance: widget.keyboardAppearance, - scrollPadding: widget.scrollPadding, - enableInteractiveSelection: widget.enableInteractiveSelection, - selectionControls: widget.selectionControls, - mouseCursor: widget.mouseCursor, - scrollController: widget.scrollController, - scrollPhysics: widget.scrollPhysics, - restorationId: widget.restorationId, - enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, + builder: (formFieldState) => TextField( + decoration: widget.decoration.copyWith( + errorText: formFieldState.errorText, + prefixIcon: widget.isCountryButtonPersistent + ? _getCountryCodeChip(context) + : null, + prefix: widget.isCountryButtonPersistent + ? null + : _getCountryCodeChip(context), ), - child: _getCountryCodeChip(context), + focusNode: focusNode, + controller: controller._formattedNationalNumberController, + enabled: widget.enabled, + inputFormatters: widget.inputFormatters ?? + [ + FilteringTextInputFormatter.allow(RegExp( + '[${AllowedCharacters.plus}${AllowedCharacters.digits}${AllowedCharacters.punctuation}]')), + ], + onChanged: (txt) => controller.changeNationalNumber(txt), + autofillHints: widget.autofillHints, + keyboardType: widget.keyboardType, + textInputAction: widget.textInputAction, + style: widget.style, + strutStyle: widget.strutStyle, + textAlign: widget.textAlign, + textAlignVertical: widget.textAlignVertical, + autofocus: widget.autofocus, + obscuringCharacter: widget.obscuringCharacter, + obscureText: widget.obscureText, + autocorrect: widget.autocorrect, + smartDashesType: widget.smartDashesType, + smartQuotesType: widget.smartQuotesType, + enableSuggestions: widget.enableSuggestions, + showCursor: widget.showCursor, + onEditingComplete: widget.onEditingComplete, + onAppPrivateCommand: widget.onAppPrivateCommand, + cursorWidth: widget.cursorWidth, + cursorHeight: widget.cursorHeight, + cursorRadius: widget.cursorRadius, + cursorColor: widget.cursorColor, + onTapOutside: widget.onTapOutside, + selectionHeightStyle: widget.selectionHeightStyle, + selectionWidthStyle: widget.selectionWidthStyle, + keyboardAppearance: widget.keyboardAppearance, + scrollPadding: widget.scrollPadding, + enableInteractiveSelection: widget.enableInteractiveSelection, + selectionControls: widget.selectionControls, + mouseCursor: widget.mouseCursor, + scrollController: widget.scrollController, + scrollPhysics: widget.scrollPhysics, + restorationId: widget.restorationId, + enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, ), ); } diff --git a/test/phone_form_field_test.dart b/test/phone_form_field_test.dart index a67cf782..83ecddf8 100644 --- a/test/phone_form_field_test.dart +++ b/test/phone_form_field_test.dart @@ -248,13 +248,13 @@ void main() { 'Should display invalid mobile phone when PhoneValidator.validMobile' ' is used and the phone number is not a mobile phone number', (tester) async { - PhoneNumber? phoneNumber = PhoneNumber.parse('+32'); + PhoneNumber? phoneNumber = PhoneNumber.parse('+33'); await tester.pumpWidget(getWidget( initialValue: phoneNumber, validator: PhoneValidator.validMobile(), )); final phoneField = find.byType(PhoneFormField); - await tester.enterText(phoneField, '77777777'); + await tester.enterText(phoneField, '6 99 99 99 99'); await tester.pumpAndSettle(); expect( find.text(PhoneFieldLocalizationEn().invalidMobilePhoneNumber), @@ -278,7 +278,7 @@ void main() { validator: PhoneValidator.validFixedLine(), )); final phoneField = find.byType(PhoneFormField); - await tester.enterText(phoneField, '77777777'); + await tester.enterText(phoneField, '67777777'); await tester.pumpAndSettle(); expect( find.text(PhoneFieldLocalizationEn().invalidFixedLinePhoneNumber), @@ -516,11 +516,8 @@ void main() { ); }); - testWidgets('Should reset', (tester) async { - PhoneNumber? phoneNumber = PhoneNumber.parse( - '', - destinationCountry: IsoCode.FR, - ); + testWidgets('Should reset with form state', (tester) async { + PhoneNumber? phoneNumber = PhoneNumber.parse('+33'); await tester.pumpWidget(getWidget(initialValue: phoneNumber)); await tester.pump(const Duration(seconds: 1)); From deb362726474796de33308658d767ac611b3c012 Mon Sep 17 00:00:00 2001 From: cedvdb Date: Sun, 4 Feb 2024 20:52:12 +0100 Subject: [PATCH 22/25] saving --- .../country_selector_navigator.dart | 15 ++- lib/src/phone_form_field_state.dart | 114 +++++++++--------- test/_country_selector_navigator_test.dart | 8 +- test/phone_form_field_test.dart | 3 +- 4 files changed, 75 insertions(+), 65 deletions(-) diff --git a/lib/src/country_selection/country_selector_navigator.dart b/lib/src/country_selection/country_selector_navigator.dart index 88b42340..0cf4b29a 100644 --- a/lib/src/country_selection/country_selector_navigator.dart +++ b/lib/src/country_selection/country_selector_navigator.dart @@ -38,7 +38,10 @@ abstract class CountrySelectorNavigator { this.useRootNavigator = true, }); - Future navigate(BuildContext context); + @Deprecated('Use [show] instead') + Future navigate(BuildContext context) => show(context); + + Future show(BuildContext context); CountrySelector _getCountrySelector({ required ValueChanged onCountrySelected, @@ -180,7 +183,7 @@ class DialogNavigator extends CountrySelectorNavigator { }); @override - Future navigate(BuildContext context) { + Future show(BuildContext context) { return showDialog( context: context, builder: (_) => Dialog( @@ -236,7 +239,7 @@ class PageNavigator extends CountrySelectorNavigator { } @override - Future navigate( + Future show( BuildContext context, ) { return Navigator.of(context).push( @@ -267,7 +270,7 @@ class BottomSheetNavigator extends CountrySelectorNavigator { }); @override - Future navigate( + Future show( BuildContext context, ) { LocalizedCountry? selected; @@ -310,7 +313,7 @@ class ModalBottomSheetNavigator extends CountrySelectorNavigator { }); @override - Future navigate( + Future show( BuildContext context, ) { return showModalBottomSheet( @@ -355,7 +358,7 @@ class DraggableModalBottomSheetNavigator extends CountrySelectorNavigator { }); @override - Future navigate(BuildContext context) { + Future show(BuildContext context) { final effectiveBorderRadius = borderRadius ?? const BorderRadius.only( topLeft: Radius.circular(16), diff --git a/lib/src/phone_form_field_state.dart b/lib/src/phone_form_field_state.dart index ad92fe19..51967937 100644 --- a/lib/src/phone_form_field_state.dart +++ b/lib/src/phone_form_field_state.dart @@ -40,7 +40,7 @@ class PhoneFormFieldState extends State { if (!widget.isCountrySelectionEnabled) { return; } - final selected = await widget.countrySelectorNavigator.navigate(context); + final selected = await widget.countrySelectorNavigator.show(context); if (selected != null) { controller.changeCountry(selected.isoCode); } @@ -57,59 +57,65 @@ class PhoneFormFieldState extends State { onSaved: widget.onSaved, restorationId: widget.restorationId, validator: (phoneNumber) => widget.validator(phoneNumber, context), - builder: (formFieldState) => TextField( - decoration: widget.decoration.copyWith( - errorText: formFieldState.errorText, - prefixIcon: widget.isCountryButtonPersistent - ? _getCountryCodeChip(context) - : null, - prefix: widget.isCountryButtonPersistent - ? null - : _getCountryCodeChip(context), - ), - focusNode: focusNode, - controller: controller._formattedNationalNumberController, - enabled: widget.enabled, - inputFormatters: widget.inputFormatters ?? - [ - FilteringTextInputFormatter.allow(RegExp( - '[${AllowedCharacters.plus}${AllowedCharacters.digits}${AllowedCharacters.punctuation}]')), - ], - onChanged: (txt) => controller.changeNationalNumber(txt), - autofillHints: widget.autofillHints, - keyboardType: widget.keyboardType, - textInputAction: widget.textInputAction, - style: widget.style, - strutStyle: widget.strutStyle, - textAlign: widget.textAlign, - textAlignVertical: widget.textAlignVertical, - autofocus: widget.autofocus, - obscuringCharacter: widget.obscuringCharacter, - obscureText: widget.obscureText, - autocorrect: widget.autocorrect, - smartDashesType: widget.smartDashesType, - smartQuotesType: widget.smartQuotesType, - enableSuggestions: widget.enableSuggestions, - showCursor: widget.showCursor, - onEditingComplete: widget.onEditingComplete, - onAppPrivateCommand: widget.onAppPrivateCommand, - cursorWidth: widget.cursorWidth, - cursorHeight: widget.cursorHeight, - cursorRadius: widget.cursorRadius, - cursorColor: widget.cursorColor, - onTapOutside: widget.onTapOutside, - selectionHeightStyle: widget.selectionHeightStyle, - selectionWidthStyle: widget.selectionWidthStyle, - keyboardAppearance: widget.keyboardAppearance, - scrollPadding: widget.scrollPadding, - enableInteractiveSelection: widget.enableInteractiveSelection, - selectionControls: widget.selectionControls, - mouseCursor: widget.mouseCursor, - scrollController: widget.scrollController, - scrollPhysics: widget.scrollPhysics, - restorationId: widget.restorationId, - enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, - ), + builder: (formFieldState) { + final fieldStateValue = formFieldState.value; + if (fieldStateValue != controller.value && fieldStateValue != null) { + controller.value = fieldStateValue; + } + return TextField( + decoration: widget.decoration.copyWith( + errorText: formFieldState.errorText, + prefixIcon: widget.isCountryButtonPersistent + ? _getCountryCodeChip(context) + : null, + prefix: widget.isCountryButtonPersistent + ? null + : _getCountryCodeChip(context), + ), + focusNode: focusNode, + controller: controller._formattedNationalNumberController, + enabled: widget.enabled, + inputFormatters: widget.inputFormatters ?? + [ + FilteringTextInputFormatter.allow(RegExp( + '[${AllowedCharacters.plus}${AllowedCharacters.digits}${AllowedCharacters.punctuation}]')), + ], + onChanged: (txt) => controller.changeNationalNumber(txt), + autofillHints: widget.autofillHints, + keyboardType: widget.keyboardType, + textInputAction: widget.textInputAction, + style: widget.style, + strutStyle: widget.strutStyle, + textAlign: widget.textAlign, + textAlignVertical: widget.textAlignVertical, + autofocus: widget.autofocus, + obscuringCharacter: widget.obscuringCharacter, + obscureText: widget.obscureText, + autocorrect: widget.autocorrect, + smartDashesType: widget.smartDashesType, + smartQuotesType: widget.smartQuotesType, + enableSuggestions: widget.enableSuggestions, + showCursor: widget.showCursor, + onEditingComplete: widget.onEditingComplete, + onAppPrivateCommand: widget.onAppPrivateCommand, + cursorWidth: widget.cursorWidth, + cursorHeight: widget.cursorHeight, + cursorRadius: widget.cursorRadius, + cursorColor: widget.cursorColor, + onTapOutside: widget.onTapOutside, + selectionHeightStyle: widget.selectionHeightStyle, + selectionWidthStyle: widget.selectionWidthStyle, + keyboardAppearance: widget.keyboardAppearance, + scrollPadding: widget.scrollPadding, + enableInteractiveSelection: widget.enableInteractiveSelection, + selectionControls: widget.selectionControls, + mouseCursor: widget.mouseCursor, + scrollController: widget.scrollController, + scrollPhysics: widget.scrollPhysics, + restorationId: widget.restorationId, + enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, + ); + }, ); } diff --git a/test/_country_selector_navigator_test.dart b/test/_country_selector_navigator_test.dart index 992b5269..a2df3824 100644 --- a/test/_country_selector_navigator_test.dart +++ b/test/_country_selector_navigator_test.dart @@ -17,7 +17,7 @@ void main() { testWidgets('should navigate to dialog', (tester) async { const nav = CountrySelectorNavigator.dialog(); - await tester.pumpWidget(getApp((ctx) => nav.navigate(ctx))); + await tester.pumpWidget(getApp((ctx) => nav.show(ctx))); await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); expect(find.byType(CountrySelector), findsOneWidget); @@ -25,7 +25,7 @@ void main() { testWidgets('should navigate to modal bottom sheet', (tester) async { const nav = CountrySelectorNavigator.modalBottomSheet(); - await tester.pumpWidget(getApp((ctx) => nav.navigate(ctx))); + await tester.pumpWidget(getApp((ctx) => nav.show(ctx))); await tester.tap(find.byType(ElevatedButton)); await tester.pump(const Duration(seconds: 1)); expect(find.byType(CountrySelector), findsOneWidget); @@ -33,7 +33,7 @@ void main() { testWidgets('should navigate to bottom sheet', (tester) async { const nav = CountrySelectorNavigator.bottomSheet(); - await tester.pumpWidget(getApp((ctx) => nav.navigate(ctx))); + await tester.pumpWidget(getApp((ctx) => nav.show(ctx))); await tester.tap(find.byType(ElevatedButton)); await tester.pump(const Duration(seconds: 1)); expect(find.byType(CountrySelector), findsOneWidget); @@ -41,7 +41,7 @@ void main() { testWidgets('should navigate to draggable sheet', (tester) async { const nav = CountrySelectorNavigator.draggableBottomSheet(); - await tester.pumpWidget(getApp((ctx) => nav.navigate(ctx))); + await tester.pumpWidget(getApp((ctx) => nav.show(ctx))); await tester.tap(find.byType(ElevatedButton)); await tester.pump(const Duration(seconds: 1)); expect(find.byType(CountrySelector), findsOneWidget); diff --git a/test/phone_form_field_test.dart b/test/phone_form_field_test.dart index 83ecddf8..b24aea33 100644 --- a/test/phone_form_field_test.dart +++ b/test/phone_form_field_test.dart @@ -524,9 +524,10 @@ void main() { const national = '123456'; final phoneField = find.byType(PhoneFormField); await tester.enterText(phoneField, national); + await tester.pumpAndSettle(); expect(find.text(national), findsOneWidget); formKey.currentState?.reset(); - await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); expect(find.text(national), findsNothing); }); }); From 8dda4e8fb227eb833d869b9b77cdbf87336a24ec Mon Sep 17 00:00:00 2001 From: cedvdb Date: Mon, 5 Feb 2024 00:07:00 +0100 Subject: [PATCH 23/25] saving --- CHANGELOG.md | 16 ++- example/lib/main.dart | 15 +-- lib/src/phone_controller.dart | 4 - lib/src/phone_form_field.dart | 36 ++--- lib/src/phone_form_field_state.dart | 167 +++++++++++++----------- lib/src/validation/phone_validator.dart | 57 ++++---- pubspec.yaml | 4 +- test/phone_form_field_test.dart | 12 +- 8 files changed, 151 insertions(+), 160 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9b3477a..8e9210ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,18 @@ ## [9.0.0] + +- Big Internal refactor in the hope of making contribution easier - Various fixes for country selection UX -- Improve accessibility -- [Breaking] : `SearchDelegateNavigator` changed into -`PageNavigator`. +- Various fixes for input cursor issues +- Improve accessibility touches surfaces +- Improve accessibility labels +- Some visual tweaks +- Added some missing countries +- [Breaking] : no validation done by default, provided validators now require a context parameter - [Breaking] : `LocalizedCountryRegistry` removed. If you were using it to localize a country name, you should use `PhoneFieldLocalization.of(context).countryName(isoCode)`. -- Internal refactor in the hope of making contributions easier +- [Deprecated] : `isCountryChipPersistent` in favor of `isCountryButtonPersistent`. +- [Deprecated] : `shouldFormat`, it is now always ON by default +- [Deprecated] : `defaultCountry`, you should now use either `initialValue` or provide a controller with an initial value. +- [Deprecated] : `CountrySelectorNavigator.searchDelegate()` changed into `CountrySelectorNavigator.PageNavigator()`. ## [8.1.1] diff --git a/example/lib/main.dart b/example/lib/main.dart index f740a7af..1cb7192d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -11,7 +11,6 @@ void main() { // For a simpler example see the README class PhoneFieldView extends StatelessWidget { - final Key inputKey; final PhoneController controller; final FocusNode focusNode; final CountrySelectorNavigator selectorNavigator; @@ -23,7 +22,6 @@ class PhoneFieldView extends StatelessWidget { const PhoneFieldView({ Key? key, - required this.inputKey, required this.controller, required this.focusNode, required this.selectorNavigator, @@ -34,12 +32,12 @@ class PhoneFieldView extends StatelessWidget { required this.useRtl, }) : super(key: key); - PhoneNumberInputValidator? _getValidator() { + PhoneNumberInputValidator? _getValidator(BuildContext context) { List validators = []; if (mobileOnly) { - validators.add(PhoneValidator.validMobile()); + validators.add(PhoneValidator.validMobile(context)); } else { - validators.add(PhoneValidator.valid()); + validators.add(PhoneValidator.valid(context)); } return validators.isNotEmpty ? PhoneValidator.compose(validators) : null; } @@ -50,7 +48,6 @@ class PhoneFieldView extends StatelessWidget { child: Directionality( textDirection: useRtl ? TextDirection.rtl : TextDirection.ltr, child: PhoneFormField( - key: inputKey, focusNode: focusNode, controller: controller, isCountryButtonPersistent: isCountryButtonPersistant, @@ -67,7 +64,7 @@ class PhoneFieldView extends StatelessWidget { enabled: true, showIsoCodeInInput: false, showFlagInInput: true, - validator: _getValidator(), + validator: _getValidator(context), autovalidateMode: AutovalidateMode.onUserInteraction, cursorColor: Theme.of(context).colorScheme.primary, // ignore: avoid_print @@ -132,7 +129,6 @@ class PhoneFormFieldScreenState extends State { CountrySelectorNavigator selectorNavigator = const CountrySelectorNavigator.page(); final formKey = GlobalKey(); - final phoneKey = GlobalKey>(); @override initState() { @@ -240,7 +236,6 @@ class PhoneFormFieldScreenState extends State { child: Column( children: [ PhoneFieldView( - inputKey: phoneKey, controller: controller, focusNode: focusNode, selectorNavigator: selectorNavigator, @@ -262,7 +257,7 @@ class PhoneFormFieldScreenState extends State { 'is valid fixed line number ${controller.value.isValid(type: PhoneNumberType.fixedLine)}'), const SizedBox(height: 12), ElevatedButton( - onPressed: () => controller.reset(), + onPressed: () => formKey.currentState?.reset(), child: const Text('reset'), ), const SizedBox(height: 12), diff --git a/lib/src/phone_controller.dart b/lib/src/phone_controller.dart index 9276426f..747897dd 100644 --- a/lib/src/phone_controller.dart +++ b/lib/src/phone_controller.dart @@ -26,10 +26,6 @@ class PhoneController extends ChangeNotifier { text: initialValue.formatNsn(), ); - reset() { - value = initialValue; - } - changeCountry(IsoCode isoCode) { _value = PhoneNumber.parse( _value.nsn, diff --git a/lib/src/phone_form_field.dart b/lib/src/phone_form_field.dart index 27bf3e45..c58701c4 100644 --- a/lib/src/phone_form_field.dart +++ b/lib/src/phone_form_field.dart @@ -8,7 +8,6 @@ import 'package:phone_numbers_parser/phone_numbers_parser.dart'; import 'country/country_button.dart'; import 'country_selection/country_selector_navigator.dart'; -import 'validation/phone_validator.dart'; part 'phone_controller.dart'; part 'phone_form_field_state.dart'; @@ -33,17 +32,10 @@ part 'phone_form_field_state.dart'; /// If [controller] is specified the [initialValue] will be /// the first value of the [PhoneController] /// {@endtemplate} -class PhoneFormField extends StatefulWidget { +class PhoneFormField extends FormField { /// {@macro controller} final PhoneController? controller; - /// {@macro initialValue} - final PhoneNumber? initialValue; - - /// Validator for the phone number. - /// example: PhoneValidator.validType(expectedType: PhoneNumberType.mobile) - final PhoneNumberInputValidator validator; - final bool shouldFormat; /// callback called when the input value changes @@ -55,9 +47,6 @@ class PhoneFormField extends StatefulWidget { /// the focusNode of the national number final FocusNode? focusNode; - /// whether the input is enabled - final bool enabled; - /// how to display the country selection final CountrySelectorNavigator countrySelectorNavigator; @@ -87,10 +76,6 @@ class PhoneFormField extends StatefulWidget { /// whether the flag is shown inside the country button final bool showFlagInInput; - // form field inputs - final AutovalidateMode autovalidateMode; - final Function(PhoneNumber?)? onSaved; - // textfield inputs final InputDecoration decoration; final TextInputType keyboardType; @@ -128,7 +113,6 @@ class PhoneFormField extends StatefulWidget { final ScrollPhysics? scrollPhysics; final ScrollController? scrollController; final Iterable? autofillHints; - final String? restorationId; final bool enableIMEPersonalizedLearning; final List? inputFormatters; @@ -144,9 +128,7 @@ class PhoneFormField extends StatefulWidget { @Deprecated( 'Use [initialValue] or [controller] to set the initial phone number') this.defaultCountry = IsoCode.US, - this.initialValue, this.flagSize = 16, - PhoneNumberInputValidator? validator, this.isCountrySelectionEnabled = true, bool? isCountryButtonPersistent, @Deprecated('Use [isCountryButtonPersistent]') @@ -155,8 +137,12 @@ class PhoneFormField extends StatefulWidget { this.showIsoCodeInInput = false, this.countryButtonPadding, // form field inputs - this.onSaved, - this.autovalidateMode = AutovalidateMode.onUserInteraction, + super.validator, + super.initialValue, + super.onSaved, + super.autovalidateMode = AutovalidateMode.onUserInteraction, + super.restorationId, + super.enabled = true, // textfield inputs this.decoration = const InputDecoration(), this.keyboardType = TextInputType.phone, @@ -180,7 +166,6 @@ class PhoneFormField extends StatefulWidget { this.onAppPrivateCommand, this.onTapOutside, this.inputFormatters, - this.enabled = true, this.cursorWidth = 2.0, this.cursorHeight, this.cursorRadius, @@ -195,15 +180,16 @@ class PhoneFormField extends StatefulWidget { this.scrollPhysics, this.scrollController, this.autofillHints, - this.restorationId, this.enableIMEPersonalizedLearning = true, }) : assert( initialValue == null || controller == null, 'One of initialValue or controller can be specified at a time', ), - validator = validator ?? PhoneValidator.valid(), isCountryButtonPersistent = - isCountryButtonPersistent ?? isCountryChipPersistent ?? true; + isCountryButtonPersistent ?? isCountryChipPersistent ?? true, + super( + builder: (state) => (state as PhoneFormFieldState).builder(), + ); @override PhoneFormFieldState createState() => PhoneFormFieldState(); diff --git a/lib/src/phone_form_field_state.dart b/lib/src/phone_form_field_state.dart index 51967937..6b0d18e2 100644 --- a/lib/src/phone_form_field_state.dart +++ b/lib/src/phone_form_field_state.dart @@ -1,9 +1,11 @@ part of 'phone_form_field.dart'; -class PhoneFormFieldState extends State { +class PhoneFormFieldState extends FormFieldState { late final PhoneController controller; late final FocusNode focusNode; - final GlobalKey _formFieldKey = GlobalKey(); + + @override + PhoneFormField get widget => super.widget as PhoneFormField; @override void initState() { @@ -16,24 +18,54 @@ class PhoneFormFieldState extends State { // and just use the US default country if no initialValue is set PhoneNumber(isoCode: widget.defaultCountry, nsn: ''), ); - controller.addListener(_onValueChanged); + controller.addListener(_onControllerValueChanged); focusNode = widget.focusNode ?? FocusNode(); _preloadFlagsInMemory(); } + void _preloadFlagsInMemory() { + CircleFlag.preload(IsoCode.values.map((isoCode) => isoCode.name).toList()); + } + @override void dispose() { - controller.removeListener(_onValueChanged); + controller.removeListener(_onControllerValueChanged); super.dispose(); } - void _onValueChanged() { - _formFieldKey.currentState?.didChange(controller.value); - widget.onChanged?.call(controller.value); + // overriding method from FormFieldState + @override + void didChange(PhoneNumber? value) { + if (value == null) { + return; + } + super.didChange(value); + + if (controller.value != value) { + controller.value = value; + } } - void _preloadFlagsInMemory() { - CircleFlag.preload(IsoCode.values.map((isoCode) => isoCode.name).toList()); + // overriding method of form field, so when the user resets a form, + // and subsequently every form field descendant, the controller is updated + @override + void reset() { + controller.value = controller.initialValue; + super.reset(); + } + + void _onControllerValueChanged() { + /// when the controller changes because the user called + /// controller.value = x we need to change the value of the form field + if (controller.value != value) { + didChange(controller.value); + } + } + + void onTextfieldChangedHandler(String value) { + controller.changeNationalNumber(value); + didChange(controller.value); + widget.onChanged?.call(controller.value); } void _selectCountry() async { @@ -47,75 +79,58 @@ class PhoneFormFieldState extends State { focusNode.requestFocus(); } - @override - Widget build(BuildContext context) { - return FormField( - key: _formFieldKey, - autovalidateMode: widget.autovalidateMode, + Widget builder() { + return TextField( + decoration: widget.decoration.copyWith( + errorText: errorText, + prefixIcon: widget.isCountryButtonPersistent + ? _getCountryCodeChip(context) + : null, + prefix: widget.isCountryButtonPersistent + ? null + : _getCountryCodeChip(context), + ), + focusNode: focusNode, enabled: widget.enabled, - initialValue: widget.initialValue, - onSaved: widget.onSaved, + inputFormatters: widget.inputFormatters ?? + [ + FilteringTextInputFormatter.allow(RegExp( + '[${AllowedCharacters.plus}${AllowedCharacters.digits}${AllowedCharacters.punctuation}]')), + ], + onChanged: (txt) => controller.changeNationalNumber(txt), + autofillHints: widget.autofillHints, + keyboardType: widget.keyboardType, + textInputAction: widget.textInputAction, + style: widget.style, + strutStyle: widget.strutStyle, + textAlign: widget.textAlign, + textAlignVertical: widget.textAlignVertical, + autofocus: widget.autofocus, + obscuringCharacter: widget.obscuringCharacter, + obscureText: widget.obscureText, + autocorrect: widget.autocorrect, + smartDashesType: widget.smartDashesType, + smartQuotesType: widget.smartQuotesType, + enableSuggestions: widget.enableSuggestions, + showCursor: widget.showCursor, + onEditingComplete: widget.onEditingComplete, + onAppPrivateCommand: widget.onAppPrivateCommand, + cursorWidth: widget.cursorWidth, + cursorHeight: widget.cursorHeight, + cursorRadius: widget.cursorRadius, + cursorColor: widget.cursorColor, + onTapOutside: widget.onTapOutside, + selectionHeightStyle: widget.selectionHeightStyle, + selectionWidthStyle: widget.selectionWidthStyle, + keyboardAppearance: widget.keyboardAppearance, + scrollPadding: widget.scrollPadding, + enableInteractiveSelection: widget.enableInteractiveSelection, + selectionControls: widget.selectionControls, + mouseCursor: widget.mouseCursor, + scrollController: widget.scrollController, + scrollPhysics: widget.scrollPhysics, restorationId: widget.restorationId, - validator: (phoneNumber) => widget.validator(phoneNumber, context), - builder: (formFieldState) { - final fieldStateValue = formFieldState.value; - if (fieldStateValue != controller.value && fieldStateValue != null) { - controller.value = fieldStateValue; - } - return TextField( - decoration: widget.decoration.copyWith( - errorText: formFieldState.errorText, - prefixIcon: widget.isCountryButtonPersistent - ? _getCountryCodeChip(context) - : null, - prefix: widget.isCountryButtonPersistent - ? null - : _getCountryCodeChip(context), - ), - focusNode: focusNode, - controller: controller._formattedNationalNumberController, - enabled: widget.enabled, - inputFormatters: widget.inputFormatters ?? - [ - FilteringTextInputFormatter.allow(RegExp( - '[${AllowedCharacters.plus}${AllowedCharacters.digits}${AllowedCharacters.punctuation}]')), - ], - onChanged: (txt) => controller.changeNationalNumber(txt), - autofillHints: widget.autofillHints, - keyboardType: widget.keyboardType, - textInputAction: widget.textInputAction, - style: widget.style, - strutStyle: widget.strutStyle, - textAlign: widget.textAlign, - textAlignVertical: widget.textAlignVertical, - autofocus: widget.autofocus, - obscuringCharacter: widget.obscuringCharacter, - obscureText: widget.obscureText, - autocorrect: widget.autocorrect, - smartDashesType: widget.smartDashesType, - smartQuotesType: widget.smartQuotesType, - enableSuggestions: widget.enableSuggestions, - showCursor: widget.showCursor, - onEditingComplete: widget.onEditingComplete, - onAppPrivateCommand: widget.onAppPrivateCommand, - cursorWidth: widget.cursorWidth, - cursorHeight: widget.cursorHeight, - cursorRadius: widget.cursorRadius, - cursorColor: widget.cursorColor, - onTapOutside: widget.onTapOutside, - selectionHeightStyle: widget.selectionHeightStyle, - selectionWidthStyle: widget.selectionWidthStyle, - keyboardAppearance: widget.keyboardAppearance, - scrollPadding: widget.scrollPadding, - enableInteractiveSelection: widget.enableInteractiveSelection, - selectionControls: widget.selectionControls, - mouseCursor: widget.mouseCursor, - scrollController: widget.scrollController, - scrollPhysics: widget.scrollPhysics, - restorationId: widget.restorationId, - enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, - ); - }, + enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, ); } diff --git a/lib/src/validation/phone_validator.dart b/lib/src/validation/phone_validator.dart index 9337ee29..5c53464c 100644 --- a/lib/src/validation/phone_validator.dart +++ b/lib/src/validation/phone_validator.dart @@ -1,9 +1,7 @@ import 'package:flutter/material.dart'; import 'package:phone_form_field/phone_form_field.dart'; - -typedef PhoneNumberInputValidator = String? Function( - PhoneNumber? phoneNumber, BuildContext context); +typedef PhoneNumberInputValidator = String? Function(PhoneNumber? phoneNumber); class PhoneValidator { /// allow to compose several validators @@ -12,9 +10,9 @@ class PhoneValidator { static PhoneNumberInputValidator compose( List validators, ) { - return (valueCandidate, context) { + return (valueCandidate) { for (var validator in validators) { - final validatorResult = validator.call(valueCandidate, context); + final validatorResult = validator.call(valueCandidate); if (validatorResult != null) { return validatorResult; } @@ -23,11 +21,12 @@ class PhoneValidator { }; } - static PhoneNumberInputValidator required({ + static PhoneNumberInputValidator required( + BuildContext context, { /// custom error message String? errorText, }) { - return (PhoneNumber? valueCandidate, BuildContext context) { + return (PhoneNumber? valueCandidate) { if (valueCandidate == null || (valueCandidate.nsn.trim().isEmpty)) { return errorText ?? PhoneFieldLocalization.of(context)?.requiredPhoneNumber ?? @@ -37,30 +36,14 @@ class PhoneValidator { }; } - static PhoneNumberInputValidator invalid({ + static PhoneNumberInputValidator valid( + BuildContext context, { /// custom error message String? errorText, - - /// determine whether a missing value should be reported as invalid - bool allowEmpty = true, - }) => - valid(errorText: errorText, allowEmpty: allowEmpty); - - static PhoneNumberInputValidator valid({ - /// custom error message - String? errorText, - - /// determine whether a missing value should be reported as invalid - bool allowEmpty = true, }) { - return (PhoneNumber? valueCandidate, BuildContext context) { - if (valueCandidate == null && !allowEmpty) { - return errorText ?? - PhoneFieldLocalization.of(context)?.invalidPhoneNumber ?? - PhoneFieldLocalizationEn().invalidPhoneNumber; - } + return (PhoneNumber? valueCandidate) { if (valueCandidate != null && - (!allowEmpty || valueCandidate.nsn.isNotEmpty) && + valueCandidate.nsn.isNotEmpty && !valueCandidate.isValid()) { return errorText ?? PhoneFieldLocalization.of(context)?.invalidPhoneNumber ?? @@ -71,12 +54,14 @@ class PhoneValidator { } static PhoneNumberInputValidator validType( + BuildContext context, + /// expected phonetype PhoneNumberType expectedType, { /// custom error message String? errorText, }) { - return (PhoneNumber? valueCandidate, BuildContext context) { + return (PhoneNumber? valueCandidate) { if (valueCandidate != null && valueCandidate.nsn.isNotEmpty && !valueCandidate.isValid(type: expectedType)) { @@ -99,33 +84,39 @@ class PhoneValidator { /// convenience shortcut method for /// invalidType(context, PhoneNumberType.fixedLine, ...) - static PhoneNumberInputValidator validFixedLine({ + static PhoneNumberInputValidator validFixedLine( + BuildContext context, { /// custom error message String? errorText, }) => validType( + context, PhoneNumberType.fixedLine, errorText: errorText, ); /// convenience shortcut method for /// invalidType(context, PhoneNumberType.mobile, ...) - static PhoneNumberInputValidator validMobile({ + static PhoneNumberInputValidator validMobile( + BuildContext context, { /// custom error message String? errorText, }) => validType( + context, PhoneNumberType.mobile, errorText: errorText, ); static PhoneNumberInputValidator validCountry( + BuildContext context, + /// list of valid country isocode List expectedCountries, { /// custom error message String? errorText, }) { - return (PhoneNumber? valueCandidate, BuildContext context) { + return (PhoneNumber? valueCandidate) { if (valueCandidate != null && (valueCandidate.nsn.isNotEmpty) && !expectedCountries.contains(valueCandidate.isoCode)) { @@ -137,8 +128,8 @@ class PhoneValidator { }; } - static PhoneNumberInputValidator get none => - (PhoneNumber? valueCandidate, BuildContext context) { + @Deprecated('Use null instead') + static PhoneNumberInputValidator get none => (PhoneNumber? valueCandidate) { return null; }; } diff --git a/pubspec.yaml b/pubspec.yaml index 722364c3..07c99166 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,12 +10,12 @@ environment: dependencies: flutter: sdk: flutter - flutter_localizations: # Add this line + flutter_localizations: sdk: flutter + intl: ">=0.18.1 <=1.0.0" circle_flags: ^4.0.0 phone_numbers_parser: ^8.1.3 - intl: ">=0.18.1 <=1.0.0" diacritic: ^0.1.5 dev_dependencies: diff --git a/test/phone_form_field_test.dart b/test/phone_form_field_test.dart index b24aea33..a0134d39 100644 --- a/test/phone_form_field_test.dart +++ b/test/phone_form_field_test.dart @@ -336,15 +336,15 @@ void main() { bool last = false; final validator = PhoneValidator.compose([ - (PhoneNumber? p, BuildContext context) { + (PhoneNumber? p) { first = true; return null; }, - (PhoneNumber? p, BuildContext context) { + (PhoneNumber? p) { second = true; return null; }, - (PhoneNumber? p, BuildContext context) { + (PhoneNumber? p) { last = true; return null; }, @@ -370,14 +370,14 @@ void main() { bool firstValidationDone = false; bool lastValidationDone = false; final validator = PhoneValidator.compose([ - (PhoneNumber? p, BuildContext context) { + (PhoneNumber? p) { firstValidationDone = true; return null; }, - (PhoneNumber? p, BuildContext context) { + (PhoneNumber? p) { return 'validation failed'; }, - (PhoneNumber? p, BuildContext context) { + (PhoneNumber? p) { lastValidationDone = true; return null; }, From e8f57e23405e429ef9c7e011504a07753cdad2a8 Mon Sep 17 00:00:00 2001 From: cedvdb Date: Mon, 5 Feb 2024 00:25:50 +0100 Subject: [PATCH 24/25] saving --- CHANGELOG.md | 3 +- lib/src/phone_form_field_state.dart | 20 ++++---- .../localized_validation_error.dart | 3 ++ test/phone_form_field_test.dart | 49 ++++++++++--------- 4 files changed, 41 insertions(+), 34 deletions(-) create mode 100644 lib/src/validation/localized_validation_error.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e9210ab..aa938b90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ - Improve accessibility labels - Some visual tweaks - Added some missing countries -- [Breaking] : no validation done by default, provided validators now require a context parameter +- [Breaking] : no validation done by default +- [Breaking] : provided validators now require a context parameter - [Breaking] : `LocalizedCountryRegistry` removed. If you were using it to localize a country name, you should use `PhoneFieldLocalization.of(context).countryName(isoCode)`. - [Deprecated] : `isCountryChipPersistent` in favor of `isCountryButtonPersistent`. - [Deprecated] : `shouldFormat`, it is now always ON by default diff --git a/lib/src/phone_form_field_state.dart b/lib/src/phone_form_field_state.dart index 6b0d18e2..36e74dc5 100644 --- a/lib/src/phone_form_field_state.dart +++ b/lib/src/phone_form_field_state.dart @@ -46,14 +46,6 @@ class PhoneFormFieldState extends FormFieldState { } } - // overriding method of form field, so when the user resets a form, - // and subsequently every form field descendant, the controller is updated - @override - void reset() { - controller.value = controller.initialValue; - super.reset(); - } - void _onControllerValueChanged() { /// when the controller changes because the user called /// controller.value = x we need to change the value of the form field @@ -62,12 +54,20 @@ class PhoneFormFieldState extends FormFieldState { } } - void onTextfieldChangedHandler(String value) { + void _onTextfieldChanged(String value) { controller.changeNationalNumber(value); didChange(controller.value); widget.onChanged?.call(controller.value); } + // overriding method of form field, so when the user resets a form, + // and subsequently every form field descendant, the controller is updated + @override + void reset() { + controller.value = controller.initialValue; + super.reset(); + } + void _selectCountry() async { if (!widget.isCountrySelectionEnabled) { return; @@ -97,7 +97,7 @@ class PhoneFormFieldState extends FormFieldState { FilteringTextInputFormatter.allow(RegExp( '[${AllowedCharacters.plus}${AllowedCharacters.digits}${AllowedCharacters.punctuation}]')), ], - onChanged: (txt) => controller.changeNationalNumber(txt), + onChanged: _onTextfieldChanged, autofillHints: widget.autofillHints, keyboardType: widget.keyboardType, textInputAction: widget.textInputAction, diff --git a/lib/src/validation/localized_validation_error.dart b/lib/src/validation/localized_validation_error.dart new file mode 100644 index 00000000..7da57585 --- /dev/null +++ b/lib/src/validation/localized_validation_error.dart @@ -0,0 +1,3 @@ +// we do not have access to context inside a validator, +// thus we return an error code, and localize that code in the view +// where it is easier to access the context \ No newline at end of file diff --git a/test/phone_form_field_test.dart b/test/phone_form_field_test.dart index a0134d39..1cc04d2d 100644 --- a/test/phone_form_field_test.dart +++ b/test/phone_form_field_test.dart @@ -18,7 +18,7 @@ void main() { PhoneController? controller, bool showFlagInInput = true, bool showDialCode = true, - PhoneNumberInputValidator? validator, + PhoneNumberInputValidator Function(BuildContext)? validatorBuilder, bool enabled = true, }) => MaterialApp( @@ -28,22 +28,24 @@ void main() { ], supportedLocales: const [Locale('en')], home: Scaffold( - body: Form( - key: formKey, - child: PhoneFormField( - key: phoneKey, - initialValue: initialValue, - onChanged: onChanged, - onSaved: onSaved, - onTapOutside: onTapOutside, - showFlagInInput: showFlagInInput, - showDialCode: showDialCode, - controller: controller, - validator: validator, - enabled: enabled, - autovalidateMode: AutovalidateMode.onUserInteraction, - ), - ), + body: Builder(builder: (context) { + return Form( + key: formKey, + child: PhoneFormField( + key: phoneKey, + initialValue: initialValue, + onChanged: onChanged, + onSaved: onSaved, + onTapOutside: onTapOutside, + showFlagInInput: showFlagInInput, + showDialCode: showDialCode, + controller: controller, + validator: validatorBuilder?.call(context), + enabled: enabled, + autovalidateMode: AutovalidateMode.onUserInteraction, + ), + ); + }), ), ); @@ -251,7 +253,7 @@ void main() { PhoneNumber? phoneNumber = PhoneNumber.parse('+33'); await tester.pumpWidget(getWidget( initialValue: phoneNumber, - validator: PhoneValidator.validMobile(), + validatorBuilder: (context) => PhoneValidator.validMobile(context), )); final phoneField = find.byType(PhoneFormField); await tester.enterText(phoneField, '6 99 99 99 99'); @@ -275,7 +277,7 @@ void main() { PhoneNumber? phoneNumber = PhoneNumber.parse('+32'); await tester.pumpWidget(getWidget( initialValue: phoneNumber, - validator: PhoneValidator.validFixedLine(), + validatorBuilder: (context) => PhoneValidator.validFixedLine(context), )); final phoneField = find.byType(PhoneFormField); await tester.enterText(phoneField, '67777777'); @@ -299,7 +301,7 @@ void main() { PhoneController(initialValue: PhoneNumber.parse('+32 444')); await tester.pumpWidget(getWidget( controller: controller, - validator: PhoneValidator.required(), + validatorBuilder: (context) => PhoneValidator.required(context), )); controller.changeNationalNumber(''); await tester.pumpAndSettle(); @@ -318,7 +320,8 @@ void main() { PhoneController(initialValue: PhoneNumber.parse('+32 444')); await tester.pumpWidget(getWidget( controller: controller, - validator: PhoneValidator.validCountry([IsoCode.FR, IsoCode.BE]), + validatorBuilder: (context) => + PhoneValidator.validCountry(context, [IsoCode.FR, IsoCode.BE]), )); controller.changeCountry(IsoCode.US); await tester.pumpAndSettle(); @@ -353,7 +356,7 @@ void main() { await tester.pumpWidget( getWidget( initialValue: PhoneNumber.parse('+33'), - validator: validator, + validatorBuilder: (context) => validator, ), ); final phoneField = find.byType(PhoneFormField); @@ -385,7 +388,7 @@ void main() { await tester.pumpWidget( getWidget( initialValue: PhoneNumber.parse('+33'), - validator: validator, + validatorBuilder: (context) => validator, ), ); final phoneField = find.byType(PhoneFormField); From 08c64c44246b34912686b8f5c7f3725e58b0b459 Mon Sep 17 00:00:00 2001 From: cedvdb Date: Mon, 5 Feb 2024 00:54:02 +0100 Subject: [PATCH 25/25] fix all tests --- lib/src/phone_form_field_state.dart | 3 ++- lib/src/validation/phone_validator.dart | 2 +- test/phone_form_field_test.dart | 21 +++++++++++++-------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/src/phone_form_field_state.dart b/lib/src/phone_form_field_state.dart index 36e74dc5..9d3464ab 100644 --- a/lib/src/phone_form_field_state.dart +++ b/lib/src/phone_form_field_state.dart @@ -14,7 +14,7 @@ class PhoneFormFieldState extends FormFieldState { controller = widget.controller ?? PhoneController( initialValue: widget.initialValue ?? - // remove this line when defaultCountry is removed + // remove this line when defaultCountry is removed (now deprecated) // and just use the US default country if no initialValue is set PhoneNumber(isoCode: widget.defaultCountry, nsn: ''), ); @@ -90,6 +90,7 @@ class PhoneFormFieldState extends FormFieldState { ? null : _getCountryCodeChip(context), ), + controller: controller._formattedNationalNumberController, focusNode: focusNode, enabled: widget.enabled, inputFormatters: widget.inputFormatters ?? diff --git a/lib/src/validation/phone_validator.dart b/lib/src/validation/phone_validator.dart index 5c53464c..1e44f870 100644 --- a/lib/src/validation/phone_validator.dart +++ b/lib/src/validation/phone_validator.dart @@ -3,7 +3,7 @@ import 'package:phone_form_field/phone_form_field.dart'; typedef PhoneNumberInputValidator = String? Function(PhoneNumber? phoneNumber); -class PhoneValidator { +abstract class PhoneValidator { /// allow to compose several validators /// Note that validator list order is important as first /// validator failing will return according message. diff --git a/test/phone_form_field_test.dart b/test/phone_form_field_test.dart index 1cc04d2d..90413428 100644 --- a/test/phone_form_field_test.dart +++ b/test/phone_form_field_test.dart @@ -232,10 +232,15 @@ void main() { group('validator', () { testWidgets( - 'Should display invalid message when no validator is specified and ' - 'the phone number is invalid', (tester) async { + 'Should display invalid message when PhoneValidator.valid is used ' + 'and the phone number is invalid', (tester) async { PhoneNumber? phoneNumber = PhoneNumber.parse('+33'); - await tester.pumpWidget(getWidget(initialValue: phoneNumber)); + await tester.pumpWidget( + getWidget( + initialValue: phoneNumber, + validatorBuilder: (context) => PhoneValidator.valid(context), + ), + ); final phoneField = find.byType(PhoneFormField); await tester.enterText(phoneField, '9984'); await tester.pumpAndSettle(const Duration(seconds: 1)); @@ -434,7 +439,7 @@ void main() { onSaved: onSaved, )); final phoneField = find.byType(PhoneFormField); - await tester.enterText(phoneField, '479281938'); + await tester.enterText(phoneField, '477889922'); await tester.pump(const Duration(seconds: 1)); formKey.currentState?.save(); await tester.pump(const Duration(seconds: 1)); @@ -443,7 +448,7 @@ void main() { phoneNumber, equals( PhoneNumber.parse( - '479281938', + '477 88 99 22', destinationCountry: IsoCode.FR, ), ), @@ -473,7 +478,7 @@ void main() { // Tap on the PhoneFormField to focus it final phoneField = find.byType(PhoneFormField); - await tester.enterText(phoneField, '479281938'); + await tester.enterText(phoneField, '488 22 33 44'); await tester.pump(const Duration(seconds: 1)); // Verify that the PhoneFormField has focus @@ -520,11 +525,11 @@ void main() { }); testWidgets('Should reset with form state', (tester) async { - PhoneNumber? phoneNumber = PhoneNumber.parse('+33'); + PhoneNumber? phoneNumber = PhoneNumber.parse('+32'); await tester.pumpWidget(getWidget(initialValue: phoneNumber)); await tester.pump(const Duration(seconds: 1)); - const national = '123456'; + const national = '477 88 99 22'; final phoneField = find.byType(PhoneFormField); await tester.enterText(phoneField, national); await tester.pumpAndSettle();