UNPKG

@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
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