From da13199645872f820025547861e606e3a62d72a6 Mon Sep 17 00:00:00 2001 From: atanasyordanov21 <63714308+atanasyordanov21@users.noreply.github.com> Date: Tue, 23 Apr 2024 11:11:41 +0300 Subject: [PATCH] Component phone input (#28) * chore: update contributing * fix: Fix button group immutability (#1) * Fix errors * fix copywith function * [automated commit] lint format and import sort --------- Co-authored-by: Osman Co-authored-by: github-actions * [automated commit] lint format and import sort * update on-main to push to firebase (#3) * ci: move firebase to flutter main host for qa (#4) * feat: Add List Item (#5) * feat: Add List Item * [automated commit] lint format and import sort --------- Co-authored-by: Simeon Dimitrov Co-authored-by: github-actions * fix(main): ListItem disabled color (#8) * fix(main): ListItem disabled color * [automated commit] lint format and import sort --------- Co-authored-by: github-actions * feat : Dropdown menu (#7) * Create dropdown * Add sizes * create stoyrybook and add size * Fix errrs and respond to comments * Fix issues * [automated commit] lint format and import sort * Alter isLarge * Fix spacing * [automated commit] lint format and import sort * Alter leading styles * [automated commit] lint format and import sort --------- Co-authored-by: Osman Co-authored-by: github-actions * Component ZetaSwitch (#6) * create ZetaSwitch * ZetaSwitch using MaterialSwitch * widgetbook for ZetaSwitch * remove hover; fix initState * add showHover parameter * add comments 'Zeta change' in material_switch.dart * remove size parameter and factory constructors * fix example and widgetbook * Component Zeta Radio Button (#9) * create component Zeta Radio Button * remove hover color * fix label line height * feat(main): SnackBar (#10) * add snackbar example * Add snackbar widgetbook * feat(main): SnackBar * [automated commit] lint format and import sort * remove view icon * Add view icon * Add widgetbook icon helper * [automated commit] lint format and import sort * fix alphabetical imports * Fix delete and error background color --------- Co-authored-by: github-actions * feat(main): Tabs (#11) * feat(main): Tabs * [automated commit] lint format and import sort --------- Co-authored-by: github-actions * chore: Update text styles (#13) * fix: switch on web (#14) * Component date input (#12) * create ZetaDateInput * create different ZetaDateInput variants * fix show error style * date validation and input mask; documentation for ZetaDateInput properties * create widgetbook * changes according to comments * Component date input (#16) * create ZetaDateInput * create different ZetaDateInput variants * fix show error style * date validation and input mask; documentation for ZetaDateInput properties * create widgetbook * changes according to comments * fix Typography of Date Input * restore * remove text line height * ZetaPhoneInput initial commit * complete ZetaPhoneInput; add flags * create phoneInputUseCase in Widgetbook * refactor phone input to use native alert dialog * don't use root navigator in widgetbook * pass parameter useRootNavigator * restore some missing countries in the list * countries search * add searchHint * fix comments --------- Co-authored-by: Luke Co-authored-by: ahmed-osman3 <99483750+ahmed-osman3@users.noreply.github.com> Co-authored-by: Osman Co-authored-by: github-actions Co-authored-by: Luke Walton Co-authored-by: Simeon Dimitrov Co-authored-by: sd-athlon <163880004+sd-athlon@users.noreply.github.com> --- .../phone_input/countries_dialog.dart | 165 +++++++++++++++--- .../components/phone_input/phone_input.dart | 13 +- 2 files changed, 148 insertions(+), 30 deletions(-) diff --git a/lib/src/components/phone_input/countries_dialog.dart b/lib/src/components/phone_input/countries_dialog.dart index 92919ad9..2f7876db 100644 --- a/lib/src/components/phone_input/countries_dialog.dart +++ b/lib/src/components/phone_input/countries_dialog.dart @@ -1,9 +1,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../../../zeta_flutter.dart'; +import 'countries.dart'; /// Class for [CountriesDialog] -class CountriesDialog extends StatefulWidget { +class CountriesDialog extends StatefulWidget { ///Constructor of [CountriesDialog] const CountriesDialog({ super.key, @@ -12,6 +13,7 @@ class CountriesDialog extends StatefulWidget { required this.items, required this.onChanged, this.enabled = true, + this.searchHint, this.useRootNavigator = true, }); @@ -21,46 +23,52 @@ class CountriesDialog extends StatefulWidget { /// The button, which opens the dialog. final Widget button; - /// List of [DropdownMenuItem] - final List> items; + /// List of [CountriesMenuItem] + final List items; /// Called when an item is selected. - final ValueSetter onChanged; + final ValueSetter onChanged; /// Determines if the button should be enabled (default) or disabled. final bool enabled; + /// The hint to be shown inside the country search input field. + /// Default is `Search by name or dial code`. + final String? searchHint; + /// Determines if the root navigator should be used. final bool useRootNavigator; @override - State> createState() => _CountriesDialogState(); + State createState() => _CountriesDialogState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(IterableProperty>('items', items)) - ..add(ObjectFlagProperty>.has('onChanged', onChanged)) + ..add(IterableProperty('items', items)) + ..add(ObjectFlagProperty>.has('onChanged', onChanged)) ..add(DiagnosticsProperty('enabled', enabled)) - ..add(DiagnosticsProperty('useRootNavigator', useRootNavigator)); + ..add(DiagnosticsProperty('useRootNavigator', useRootNavigator)) + ..add(StringProperty('searchHint', searchHint)); } } -class _CountriesDialogState extends State> { - Future _showCountriesDialog( +class _CountriesDialogState extends State { + Future _showCountriesDialog( BuildContext context, { Zeta? zeta, - required List> items, + required List items, bool barrierDismissible = true, bool useRootNavigator = true, }) => - showDialog( + showDialog( context: context, barrierDismissible: barrierDismissible, useRootNavigator: useRootNavigator, builder: (_) => _CountriesList( items: items, + searchHint: widget.searchHint, zeta: zeta, ), ); @@ -84,40 +92,143 @@ class _CountriesDialogState extends State> { } } -class _CountriesList extends StatelessWidget { +class _CountriesList extends StatefulWidget { const _CountriesList({ required this.items, + this.searchHint, this.zeta, }); final Zeta? zeta; - final List> items; + final List items; + final String? searchHint; + + @override + State<_CountriesList> createState() => _CountriesListState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(IterableProperty('items', items)) + ..add(StringProperty('searchHint', searchHint)); + } +} + +class _CountriesListState extends State<_CountriesList> { + late final bool _enableSearch = widget.items.length > 20; + final _controller = TextEditingController(); + List _items = []; + + @override + void initState() { + super.initState(); + _items = List.from(widget.items); + } + + void _search(String value) { + setState(() { + _items = widget.items.where((item) { + return item.value.name.toLowerCase().contains(value.toLowerCase()) || + (RegExp(r'^\d+$').hasMatch(value) && item.value.dialCode.indexOf('+$value') == 0); + }).toList(); + }); + } + + void _clearSearch() { + _controller.clear(); + setState(() { + _items = List.from(widget.items); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { - final zeta = this.zeta ?? Zeta.of(context); + final zeta = widget.zeta ?? Zeta.of(context); return AlertDialog( surfaceTintColor: zeta.colors.surfacePrimary, shape: const RoundedRectangleBorder(borderRadius: ZetaRadius.large), content: SizedBox( width: double.maxFinite, - child: ListView.builder( - shrinkWrap: true, - itemCount: items.length, - itemBuilder: (_, index) => InkWell( - onTap: () { - Navigator.of(context).pop(items[index].value); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: ZetaSpacing.xs, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_enableSearch) + Padding( + padding: const EdgeInsets.only(bottom: ZetaSpacing.b), + child: TextField( + controller: _controller, + onChanged: _search, + decoration: InputDecoration( + hintText: widget.searchHint ?? 'Search by name or dial code', + prefixIcon: const Icon(ZetaIcons.search_round), + suffixIcon: _controller.text.isEmpty + ? null + : IconButton( + onPressed: _clearSearch, + icon: Icon( + ZetaIcons.cancel_round, + color: zeta.colors.cool.shade70, + ), + ), + ), + ), + ), + if (_enableSearch) + Expanded( + child: _listView(context), + ) + else + _listView(context), + Padding( + padding: const EdgeInsets.only(top: ZetaSpacing.b), + child: TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), ), - child: items[index].child, ), - ), + ], ), ), ); } + + Widget _listView(BuildContext context) => ListView.builder( + shrinkWrap: true, + itemCount: _items.length, + itemBuilder: (_, index) => InkWell( + onTap: () { + Navigator.of(context).pop(_items[index].value); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: ZetaSpacing.xs, + ), + child: _items[index].child, + ), + ), + ); +} + +/// [CountriesMenuItem] +/// Item for the country selection dialog. +class CountriesMenuItem { + /// Constructor for [CountriesMenuItem]. + const CountriesMenuItem({ + required this.value, + required this.child, + }); + + /// The selected value from the list. + final Country value; + + /// The widget which will represent each item in the list. + final Widget child; } diff --git a/lib/src/components/phone_input/phone_input.dart b/lib/src/components/phone_input/phone_input.dart index e853e99a..ac347360 100644 --- a/lib/src/components/phone_input/phone_input.dart +++ b/lib/src/components/phone_input/phone_input.dart @@ -20,6 +20,7 @@ class ZetaPhoneInput extends StatefulWidget { this.countryDialCode, this.phoneNumber, this.countries, + this.countrySearchHint, this.useRootNavigator = true, }); @@ -56,6 +57,10 @@ class ZetaPhoneInput extends StatefulWidget { /// List of countries ISO 3166-1 alpha-2 codes final List? countries; + /// The hint to be shown inside the country search input field. + /// Default is `Search by name or dial code`. + final String? countrySearchHint; + /// Determines if the root navigator should be used in the [CountriesDialog]. final bool useRootNavigator; @@ -75,7 +80,8 @@ class ZetaPhoneInput extends StatefulWidget { ..add(StringProperty('countryDialCode', countryDialCode)) ..add(StringProperty('phoneNumber', phoneNumber)) ..add(IterableProperty('countries', countries)) - ..add(DiagnosticsProperty('useRootNavigator', useRootNavigator)); + ..add(DiagnosticsProperty('useRootNavigator', useRootNavigator)) + ..add(StringProperty('countrySearchHint', countrySearchHint)); } } @@ -173,10 +179,11 @@ class _ZetaPhoneInputState extends State { left: BorderSide(color: zeta.colors.cool.shade40), ), ), - child: CountriesDialog( + child: CountriesDialog( zeta: zeta, useRootNavigator: widget.useRootNavigator, enabled: widget.enabled, + searchHint: widget.countrySearchHint, button: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -201,7 +208,7 @@ class _ZetaPhoneInputState extends State { ), items: _countries .map( - (country) => DropdownMenuItem( + (country) => CountriesMenuItem( value: country, child: Row( children: [