Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ SelectorConfig({
this.countryComparator,
this.setSelectorButtonAsPrefixIcon = false,
this.useBottomSheetSafeArea = false,
this.useRootNavigator = false,
});
```

Expand Down Expand Up @@ -480,6 +481,18 @@ SelectorConfig(
)
```

#### Issue: Bottom sheet not showing when inside nested navigator
**Solution:**
- Set `useRootNavigator: true` to display the bottom sheet using the root navigator
- This is useful when the phone number input is inside a nested navigator or another modal

```dart
SelectorConfig(
selectorType: PhoneInputSelectorType.BOTTOM_SHEET,
useRootNavigator: true,
)
```

#### Issue: Performance issues with large country lists
**Solution:**
- Filter countries using the `countries` parameter
Expand Down
2 changes: 1 addition & 1 deletion example/test_driver/app/app_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ main() {

tearDownAll(() async {
driver.close();
});
});

test('Tap On TextField and enter text', () async {
await driver.tap(inputTextFieldFinder);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ main() {

tearDownAll(() async {
driver.close();
});
});

test('Tap On TextField and enter text', () async {
await driver.tap(inputTextFieldFinder);
Expand Down
14 changes: 14 additions & 0 deletions lib/src/utils/selector_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,19 @@ class SelectorConfig {
/// Defaults to false.
final bool useBottomSheetSafeArea;

/// Whether to use the root navigator for the bottom sheet selector.
///
/// When true and [selectorType] is [PhoneInputSelectorType.BOTTOM_SHEET],
/// the bottom sheet will be displayed using the root navigator instead of
/// the nearest navigator. This is useful when you need the bottom sheet to
/// display above all other navigation contexts (e.g., when used inside a
/// nested navigator or inside another modal).
///
/// Only applies to bottom sheet selector type.
///
/// Defaults to false.
final bool useRootNavigator;

/// Creates a new [SelectorConfig] with the specified options.
///
/// All parameters have sensible defaults and can be omitted if not needed.
Expand All @@ -144,5 +157,6 @@ class SelectorConfig {
this.leadingPadding,
this.trailingSpace = true,
this.useBottomSheetSafeArea = false,
this.useRootNavigator = false,
});
}
74 changes: 25 additions & 49 deletions lib/src/widgets/input_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -434,14 +434,10 @@ class _InputWidgetState extends State<InternationalPhoneNumberInput> {
if (widget.initialValue!.phoneNumber != null &&
widget.initialValue!.phoneNumber!.isNotEmpty &&
(await PhoneNumberUtil.isValidNumber(
phoneNumber: widget.initialValue!.phoneNumber!,
isoCode: widget.initialValue!.isoCode!))!) {
String phoneNumber =
await PhoneNumber.getParsableNumber(widget.initialValue!);
phoneNumber: widget.initialValue!.phoneNumber!, isoCode: widget.initialValue!.isoCode!))!) {
String phoneNumber = await PhoneNumber.getParsableNumber(widget.initialValue!);

controller!.text = widget.formatInput
? phoneNumber
: phoneNumber.replaceAll(RegExp(r'[^\d+]'), '');
controller!.text = widget.formatInput ? phoneNumber : phoneNumber.replaceAll(RegExp(r'[^\d+]'), '');

phoneNumberControllerListener();
}
Expand All @@ -451,8 +447,7 @@ class _InputWidgetState extends State<InternationalPhoneNumberInput> {
/// loads countries from [Countries.countryList] and selected Country
void loadCountries({Country? previouslySelectedCountry}) {
if (this.mounted) {
List<Country> countries =
CountryProvider.getCountriesData(countries: widget.countries);
List<Country> countries = CountryProvider.getCountriesData(countries: widget.countries);

Country country = previouslySelectedCountry ??
Utils.getInitialSelectedCountry(
Expand All @@ -463,8 +458,7 @@ class _InputWidgetState extends State<InternationalPhoneNumberInput> {
// Remove potential duplicates
countries = countries.toSet().toList();

final CountryComparator? countryComparator =
widget.selectorConfig.countryComparator;
final CountryComparator? countryComparator = widget.selectorConfig.countryComparator;
if (countryComparator != null) {
countries.sort(countryComparator);
}
Expand All @@ -480,20 +474,15 @@ class _InputWidgetState extends State<InternationalPhoneNumberInput> {
/// the `ValueCallback` [widget.onInputValidated]
void phoneNumberControllerListener() {
if (this.mounted) {
String parsedPhoneNumberString =
controller!.text.replaceAll(RegExp(r'[^\d+]'), '');
String parsedPhoneNumberString = controller!.text.replaceAll(RegExp(r'[^\d+]'), '');

getParsedPhoneNumber(parsedPhoneNumberString, this.country?.alpha2Code)
.then((phoneNumber) {
getParsedPhoneNumber(parsedPhoneNumberString, this.country?.alpha2Code).then((phoneNumber) {
if (phoneNumber == null) {
String phoneNumber =
'${this.country?.dialCode}$parsedPhoneNumberString';
String phoneNumber = '${this.country?.dialCode}$parsedPhoneNumberString';

if (widget.onInputChanged != null) {
widget.onInputChanged!(PhoneNumber(
phoneNumber: phoneNumber,
isoCode: this.country?.alpha2Code,
dialCode: this.country?.dialCode));
widget.onInputChanged!(
PhoneNumber(phoneNumber: phoneNumber, isoCode: this.country?.alpha2Code, dialCode: this.country?.dialCode));
}

if (widget.onInputValidated != null) {
Expand All @@ -502,10 +491,8 @@ class _InputWidgetState extends State<InternationalPhoneNumberInput> {
this.isNotValid = true;
} else {
if (widget.onInputChanged != null) {
widget.onInputChanged!(PhoneNumber(
phoneNumber: phoneNumber,
isoCode: this.country?.alpha2Code,
dialCode: this.country?.dialCode));
widget.onInputChanged!(
PhoneNumber(phoneNumber: phoneNumber, isoCode: this.country?.alpha2Code, dialCode: this.country?.dialCode));
}

if (widget.onInputValidated != null) {
Expand All @@ -519,16 +506,13 @@ class _InputWidgetState extends State<InternationalPhoneNumberInput> {

/// Returns a formatted String of [phoneNumber] with [isoCode], returns `null`
/// if [phoneNumber] is not valid or if an [Exception] is caught.
Future<String?> getParsedPhoneNumber(
String phoneNumber, String? isoCode) async {
Future<String?> getParsedPhoneNumber(String phoneNumber, String? isoCode) async {
if (phoneNumber.isNotEmpty && isoCode != null) {
try {
bool? isValidPhoneNumber = await PhoneNumberUtil.isValidNumber(
phoneNumber: phoneNumber, isoCode: isoCode);
bool? isValidPhoneNumber = await PhoneNumberUtil.isValidNumber(phoneNumber: phoneNumber, isoCode: isoCode);

if (isValidPhoneNumber!) {
return await PhoneNumberUtil.normalizePhoneNumber(
phoneNumber: phoneNumber, isoCode: isoCode);
return await PhoneNumberUtil.normalizePhoneNumber(phoneNumber: phoneNumber, isoCode: isoCode);
}
} on Exception {
return null;
Expand Down Expand Up @@ -558,6 +542,7 @@ class _InputWidgetState extends State<InternationalPhoneNumberInput> {
isEnabled: widget.isEnabled,
autoFocusSearchField: widget.autoFocusSearch,
isScrollControlled: widget.countrySelectorScrollControlled,
useRootNavigator: widget.selectorConfig.useRootNavigator,
));
}

Expand All @@ -573,13 +558,11 @@ class _InputWidgetState extends State<InternationalPhoneNumberInput> {
///
/// Also updates [selectorButtonBottomPadding]
String? validator(String? value) {
bool isValid =
this.isNotValid && (value!.isNotEmpty || widget.ignoreBlank == false);
bool isValid = this.isNotValid && (value!.isNotEmpty || widget.ignoreBlank == false);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (isValid && widget.errorMessage != null) {
setState(() {
this.selectorButtonBottomPadding =
widget.selectorButtonOnErrorPadding;
this.selectorButtonBottomPadding = widget.selectorButtonOnErrorPadding;
});
} else {
setState(() {
Expand All @@ -601,17 +584,12 @@ class _InputWidgetState extends State<InternationalPhoneNumberInput> {

void _phoneNumberSaved() {
if (this.mounted) {
String parsedPhoneNumberString =
controller!.text.replaceAll(RegExp(r'[^\d+]'), '');
String parsedPhoneNumberString = controller!.text.replaceAll(RegExp(r'[^\d+]'), '');

String phoneNumber =
'${this.country?.dialCode ?? ''}' + parsedPhoneNumberString;
String phoneNumber = '${this.country?.dialCode ?? ''}' + parsedPhoneNumberString;

widget.onSaved?.call(
PhoneNumber(
phoneNumber: phoneNumber,
isoCode: this.country?.alpha2Code,
dialCode: this.country?.dialCode),
PhoneNumber(phoneNumber: phoneNumber, isoCode: this.country?.alpha2Code, dialCode: this.country?.dialCode),
);
}
}
Expand All @@ -625,20 +603,17 @@ class _InputWidgetState extends State<InternationalPhoneNumberInput> {
String? get locale {
if (widget.locale == null) return null;

if (widget.locale!.toLowerCase() == 'nb' ||
widget.locale!.toLowerCase() == 'nn') {
if (widget.locale!.toLowerCase() == 'nb' || widget.locale!.toLowerCase() == 'nn') {
return 'no';
}
return widget.locale;
}
}

class _InputWidgetView
extends WidgetView<InternationalPhoneNumberInput, _InputWidgetState> {
class _InputWidgetView extends WidgetView<InternationalPhoneNumberInput, _InputWidgetState> {
final _InputWidgetState state;

_InputWidgetView({Key? key, required this.state})
: super(key: key, state: state);
_InputWidgetView({Key? key, required this.state}) : super(key: key, state: state);

@override
Widget build(BuildContext context) {
Expand All @@ -665,6 +640,7 @@ class _InputWidgetView
isEnabled: widget.isEnabled,
autoFocusSearchField: widget.autoFocusSearch,
isScrollControlled: widget.countrySelectorScrollControlled,
useRootNavigator: widget.selectorConfig.useRootNavigator,
),
SizedBox(
height: state.selectorButtonBottomPadding,
Expand Down
28 changes: 11 additions & 17 deletions lib/src/widgets/selector_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class SelectorButton extends StatelessWidget {
final String? locale;
final bool isEnabled;
final bool isScrollControlled;
final bool useRootNavigator;

final ValueChanged<Country?> onCountryChanged;

Expand All @@ -32,6 +33,7 @@ class SelectorButton extends StatelessWidget {
required this.onCountryChanged,
required this.isEnabled,
required this.isScrollControlled,
required this.useRootNavigator,
}) : super(key: key);

@override
Expand Down Expand Up @@ -69,13 +71,10 @@ class SelectorButton extends StatelessWidget {
onPressed: countries.isNotEmpty && countries.length > 1 && isEnabled
? () async {
Country? selected;
if (selectorConfig.selectorType ==
PhoneInputSelectorType.BOTTOM_SHEET) {
selected = await showCountrySelectorBottomSheet(
context, countries);
if (selectorConfig.selectorType == PhoneInputSelectorType.BOTTOM_SHEET) {
selected = await showCountrySelectorBottomSheet(context, countries);
} else {
selected =
await showCountrySelectorDialog(context, countries);
selected = await showCountrySelectorBottomSheet(context, countries);
}

if (selected != null) {
Expand All @@ -98,8 +97,7 @@ class SelectorButton extends StatelessWidget {
}

/// Converts the list [countries] to `DropdownMenuItem`
List<DropdownMenuItem<Country>> mapCountryToDropdownItem(
List<Country> countries) {
List<DropdownMenuItem<Country>> mapCountryToDropdownItem(List<Country> countries) {
return countries.map((country) {
return DropdownMenuItem<Country>(
value: country,
Expand All @@ -117,8 +115,7 @@ class SelectorButton extends StatelessWidget {
}

/// shows a Dialog with list [countries] if the [PhoneInputSelectorType.DIALOG] is selected
Future<Country?> showCountrySelectorDialog(
BuildContext inheritedContext, List<Country> countries) {
Future<Country?> showCountrySelectorDialog(BuildContext inheritedContext, List<Country> countries) {
return showDialog(
context: inheritedContext,
barrierDismissible: true,
Expand All @@ -142,25 +139,22 @@ class SelectorButton extends StatelessWidget {
}

/// shows a Dialog with list [countries] if the [PhoneInputSelectorType.BOTTOM_SHEET] is selected
Future<Country?> showCountrySelectorBottomSheet(
BuildContext inheritedContext, List<Country> countries) {
Future<Country?> showCountrySelectorBottomSheet(BuildContext inheritedContext, List<Country> countries) {
return showModalBottomSheet(
context: inheritedContext,
clipBehavior: Clip.hardEdge,
isScrollControlled: isScrollControlled,
backgroundColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12), topRight: Radius.circular(12))),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12))),
useSafeArea: selectorConfig.useBottomSheetSafeArea,
useRootNavigator: useRootNavigator,
builder: (BuildContext context) {
return Stack(children: [
GestureDetector(
onTap: () => Navigator.pop(context),
),
Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom),
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: DraggableScrollableSheet(
builder: (BuildContext context, ScrollController controller) {
return Directionality(
Expand Down