@artmajeur/react-native-paper-phone-number-input
Version:
A performant phone number input component for react-native-paper with country picker
343 lines (327 loc) • 10.6 kB
JavaScript
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
import React, { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { FlatList, Platform, PlatformColor, StyleSheet, View } from 'react-native';
import { DataTable, IconButton, Modal, Portal, Text, TextInput, TouchableRipple } from 'react-native-paper';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { isIOS } from './constants';
import translatedCountries from './data/translatedCountries';
import { useCountriesList, useCountrySearch } from './hooks';
import { useDebouncedValue } from './use-debounced-value';
import useThemeWithFlagsFont from './useThemeWithFlagsFont';
import { getCountryByCode, liveFormatPhoneNumber } from './utils';
// Memoized country row component to prevent unnecessary re-renders
const CountryRow = /*#__PURE__*/memo(({
item,
onPress,
theme,
themeWithFlagsFont,
lang
}) => /*#__PURE__*/React.createElement(DataTable.Row, {
onPress: () => onPress(item),
theme: theme
}, /*#__PURE__*/React.createElement(DataTable.Cell, {
theme: themeWithFlagsFont
}, `${item.flag} ${translatedCountries.getName(item.code, lang)}`), /*#__PURE__*/React.createElement(DataTable.Cell, {
numeric: true,
theme: theme
}, item.dialCode)));
CountryRow.displayName = 'CountryRow';
export const PhoneNumberInput = /*#__PURE__*/memo(/*#__PURE__*/forwardRef(({
code = '##',
setCode,
phoneNumber = '',
setPhoneNumber,
showFirstOnList,
modalStyle,
modalContainerStyle,
includeCountries,
excludeCountries,
limitMaxLength,
// Props from TextInput that needs special handling
disabled,
editable = true,
keyboardType,
theme,
lang = 'fr',
searchLabel = '',
countryLabel = '',
dialCodeLabel = '',
error = false,
errorIcon = undefined,
// rest of the props
...rest
}, ref) => {
const insets = useSafeAreaInsets();
const themeWithFlagsFont = useThemeWithFlagsFont(theme);
// States for the modal
const [visible, setVisible] = useState(false);
// States for the searchbar
const [searchQuery, setSearchQuery] = useState('');
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
// Memoize country calculation
const country = useMemo(() => getCountryByCode(code), [code]);
const textInputRef = useRef(null);
const searchbarRef = useRef(null);
// Memoize phone number change handler
const onChangePhoneNumber = useCallback(text => {
const phoneNumber = text.split(' ').slice(2).join(' ');
setPhoneNumber(phoneNumber);
}, [setPhoneNumber]);
const openModal = useCallback(() => {
setVisible(true);
}, []);
const closeModal = useCallback(() => {
setVisible(false);
}, []);
// Memoize search query change handler
const onChangeSearchQuery = useCallback(text => {
setSearchQuery(text);
}, []);
// Memoize country selection handler
const onSelectCountry = useCallback(item => {
setCode(item.code);
closeModal();
if (limitMaxLength && item.length < phoneNumber.length) {
setPhoneNumber('');
}
}, [setCode, closeModal, limitMaxLength, phoneNumber.length, setPhoneNumber]);
// Memoize key press handler
const onKeyPress = useCallback(({
nativeEvent
}) => {
if (nativeEvent.key === 'Escape') {
closeModal();
}
}, [closeModal]);
// Focus the search bar when the modal becomes visible
useEffect(() => {
if (visible) {
setTimeout(() => {
searchbarRef.current?.focus();
}, 100);
}
}, [visible]);
useImperativeHandle(ref, () => ({
focus: () => textInputRef.current?.focus(),
clear: () => textInputRef.current?.clear(),
blur: () => textInputRef.current?.blur(),
isFocused: () => textInputRef.current?.isFocused() ?? false,
setNativeProps: props => textInputRef.current?.setNativeProps(props),
openCountryPicker: openModal,
closeCountryPicker: closeModal
}), [openModal, closeModal]);
const countriesList = useCountriesList({
showFirstOnList,
includeCountries,
excludeCountries
});
const searchResult = useCountrySearch({
searchQuery: debouncedSearchQuery,
countriesList,
lang
});
// Dynamic styles based on theme
const dynamicStyles = useMemo(() => ({
searchbar: {
flex: 1
},
searchbarContent: {
backgroundColor: 'transparent',
fontSize: 16,
color: theme?.colors?.onSurface || (theme?.dark ? '#ffffff' : '#000000')
},
outlined: {
borderRadius: 30,
borderColor: theme?.dark ? '#343740' : '#CBD5E1'
}
}), [theme]);
// Memoize width and baseline length calculations
const {
width,
baselineLength
} = useMemo(() => {
let width = 62;
let baselineLength = 8;
switch (country.dialCode.length) {
case 1:
case 2:
width = 62;
baselineLength = 8;
break;
case 3:
width = 71;
baselineLength = 9;
break;
case 4:
width = 80;
baselineLength = 10;
break;
case 5:
width = 89;
baselineLength = 11;
break;
default:
width = 98;
baselineLength = 12;
break;
}
return {
width,
baselineLength
};
}, [country.dialCode.length]);
// Memoize the text input value
const textInputValue = useMemo(() => `${country.flag} ${country.dialCode} ${liveFormatPhoneNumber(phoneNumber, code)}`, [country.flag, country.dialCode, phoneNumber, code]);
// Memoize the ripple style
const rippleStyle = useMemo(() => [styles.ripple, {
width
}], [width]);
// Memoize the modal container style
const modalContainerStyles = useMemo(() => [styles.countries, {
backgroundColor: themeWithFlagsFont.colors.background,
paddingTop: insets.top + 16,
paddingBottom: insets.bottom + 16
}, modalContainerStyle], [themeWithFlagsFont.colors.background, insets.top, insets.bottom, modalContainerStyle]);
// Memoize FlatList renderItem function
const renderItem = useCallback(({
item
}) => /*#__PURE__*/React.createElement(CountryRow, {
item: item,
onPress: onSelectCountry,
theme: theme,
themeWithFlagsFont: themeWithFlagsFont,
lang: lang
}), [onSelectCountry, theme, themeWithFlagsFont, lang]);
// Memoize FlatList keyExtractor
const keyExtractor = useCallback(item => item.code, []);
return /*#__PURE__*/React.createElement(View, null, /*#__PURE__*/React.createElement(TextInput
// @ts-ignore -- This type is wrong, it does not forward all the ref methods from native text input.
, _extends({
ref: textInputRef
}, rest, {
disabled: disabled,
editable: editable,
onChangeText: onChangePhoneNumber,
value: textInputValue,
keyboardType: keyboardType || 'phone-pad',
theme: themeWithFlagsFont,
maxLength: limitMaxLength ? baselineLength + country.length : undefined,
selectionColor: Platform.select({
ios: PlatformColor('systemBlue'),
android: PlatformColor('@android:color/holo_blue_light')
}),
cursorColor: Platform.select({
android: PlatformColor('@android:color/holo_blue_light')
}),
right: error && /*#__PURE__*/React.createElement(TextInput.Icon, {
icon: () => errorIcon,
disabled: true
})
})), /*#__PURE__*/React.createElement(TouchableRipple, {
disabled: disabled || !editable,
style: rippleStyle,
onPress: openModal,
theme: theme
}, /*#__PURE__*/React.createElement(Text, null, " ")), /*#__PURE__*/React.createElement(Portal, {
theme: theme
}, /*#__PURE__*/React.createElement(Modal, {
style: [styles.modal, modalStyle],
contentContainerStyle: modalContainerStyles,
visible: visible,
onDismiss: closeModal,
theme: theme
}, /*#__PURE__*/React.createElement(View, {
style: styles.searchbox
}, /*#__PURE__*/React.createElement(IconButton, {
icon: "arrow-left",
onPress: closeModal,
theme: theme
}), /*#__PURE__*/React.createElement(TextInput, {
style: [styles.searchbar, dynamicStyles.searchbar],
placeholder: searchLabel,
onChangeText: onChangeSearchQuery,
value: searchQuery,
ref: searchbarRef,
mode: "outlined",
dense: true,
theme: theme,
onKeyPress: onKeyPress,
selectionColor: Platform.select({
ios: PlatformColor('systemBlue'),
android: PlatformColor('@android:color/holo_blue_light')
}),
cursorColor: Platform.select({
android: PlatformColor('@android:color/holo_blue_light')
}),
left: /*#__PURE__*/React.createElement(TextInput.Icon, {
icon: "magnify",
size: 20,
style: styles.searchIcon
}),
underlineStyle: styles.searchbarUnderline,
contentStyle: [styles.searchbarContent, dynamicStyles.searchbarContent],
outlineStyle: [styles.outlined, dynamicStyles.outlined]
})), /*#__PURE__*/React.createElement(DataTable, {
style: styles.flex1
}, /*#__PURE__*/React.createElement(DataTable.Header, {
theme: theme
}, /*#__PURE__*/React.createElement(DataTable.Title, {
theme: theme
}, countryLabel), /*#__PURE__*/React.createElement(DataTable.Title, {
numeric: true,
theme: theme
}, dialCodeLabel)), /*#__PURE__*/React.createElement(FlatList, {
keyboardShouldPersistTaps: "handled",
data: searchResult,
keyExtractor: keyExtractor,
renderItem: renderItem,
removeClippedSubviews: true,
maxToRenderPerBatch: 10,
windowSize: 10,
initialNumToRender: 10,
getItemLayout: undefined
})))));
}));
PhoneNumberInput.displayName = 'PhoneNumberInput';
const styles = StyleSheet.create({
ripple: {
position: 'absolute',
top: 0,
bottom: 0,
left: 0
},
flex1: {
flex: isIOS ? undefined : 1
},
modal: {
marginTop: undefined,
marginBottom: undefined,
justifyContent: undefined
},
countries: {
paddingHorizontal: 16,
flex: isIOS ? undefined : 1,
marginBottom: isIOS ? 270 : undefined,
justifyContent: undefined
},
searchbox: {
flexDirection: 'row'
},
searchbar: {
flex: 1
},
searchbarContent: {
backgroundColor: 'transparent'
},
searchbarUnderline: {
display: 'none'
},
outlined: {
borderRadius: 30
},
searchIcon: {
alignSelf: 'center',
marginTop: 15
}
});
//# sourceMappingURL=PhoneNumberInput.js.map