UNPKG

@artmajeur/react-native-paper-phone-number-input

Version:

A performant phone number input component for react-native-paper with country picker

351 lines (335 loc) 12.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PhoneNumberInput = void 0; var _react = _interopRequireWildcard(require("react")); var _reactNative = require("react-native"); var _reactNativePaper = require("react-native-paper"); var _reactNativeSafeAreaContext = require("react-native-safe-area-context"); var _constants = require("./constants"); var _translatedCountries = _interopRequireDefault(require("./data/translatedCountries")); var _hooks = require("./hooks"); var _useDebouncedValue = require("./use-debounced-value"); var _useThemeWithFlagsFont = _interopRequireDefault(require("./useThemeWithFlagsFont")); var _utils = require("./utils"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } 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); } // Memoized country row component to prevent unnecessary re-renders const CountryRow = /*#__PURE__*/(0, _react.memo)(({ item, onPress, theme, themeWithFlagsFont, lang }) => /*#__PURE__*/_react.default.createElement(_reactNativePaper.DataTable.Row, { onPress: () => onPress(item), theme: theme }, /*#__PURE__*/_react.default.createElement(_reactNativePaper.DataTable.Cell, { theme: themeWithFlagsFont }, `${item.flag} ${_translatedCountries.default.getName(item.code, lang)}`), /*#__PURE__*/_react.default.createElement(_reactNativePaper.DataTable.Cell, { numeric: true, theme: theme }, item.dialCode))); CountryRow.displayName = 'CountryRow'; const PhoneNumberInput = exports.PhoneNumberInput = /*#__PURE__*/(0, _react.memo)(/*#__PURE__*/(0, _react.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 = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)(); const themeWithFlagsFont = (0, _useThemeWithFlagsFont.default)(theme); // States for the modal const [visible, setVisible] = (0, _react.useState)(false); // States for the searchbar const [searchQuery, setSearchQuery] = (0, _react.useState)(''); const debouncedSearchQuery = (0, _useDebouncedValue.useDebouncedValue)(searchQuery, 300); // Memoize country calculation const country = (0, _react.useMemo)(() => (0, _utils.getCountryByCode)(code), [code]); const textInputRef = (0, _react.useRef)(null); const searchbarRef = (0, _react.useRef)(null); // Memoize phone number change handler const onChangePhoneNumber = (0, _react.useCallback)(text => { const phoneNumber = text.split(' ').slice(2).join(' '); setPhoneNumber(phoneNumber); }, [setPhoneNumber]); const openModal = (0, _react.useCallback)(() => { setVisible(true); }, []); const closeModal = (0, _react.useCallback)(() => { setVisible(false); }, []); // Memoize search query change handler const onChangeSearchQuery = (0, _react.useCallback)(text => { setSearchQuery(text); }, []); // Memoize country selection handler const onSelectCountry = (0, _react.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 = (0, _react.useCallback)(({ nativeEvent }) => { if (nativeEvent.key === 'Escape') { closeModal(); } }, [closeModal]); // Focus the search bar when the modal becomes visible (0, _react.useEffect)(() => { if (visible) { setTimeout(() => { searchbarRef.current?.focus(); }, 100); } }, [visible]); (0, _react.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 = (0, _hooks.useCountriesList)({ showFirstOnList, includeCountries, excludeCountries }); const searchResult = (0, _hooks.useCountrySearch)({ searchQuery: debouncedSearchQuery, countriesList, lang }); // Dynamic styles based on theme const dynamicStyles = (0, _react.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 } = (0, _react.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 = (0, _react.useMemo)(() => `${country.flag} ${country.dialCode} ${(0, _utils.liveFormatPhoneNumber)(phoneNumber, code)}`, [country.flag, country.dialCode, phoneNumber, code]); // Memoize the ripple style const rippleStyle = (0, _react.useMemo)(() => [styles.ripple, { width }], [width]); // Memoize the modal container style const modalContainerStyles = (0, _react.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 = (0, _react.useCallback)(({ item }) => /*#__PURE__*/_react.default.createElement(CountryRow, { item: item, onPress: onSelectCountry, theme: theme, themeWithFlagsFont: themeWithFlagsFont, lang: lang }), [onSelectCountry, theme, themeWithFlagsFont, lang]); // Memoize FlatList keyExtractor const keyExtractor = (0, _react.useCallback)(item => item.code, []); return /*#__PURE__*/_react.default.createElement(_reactNative.View, null, /*#__PURE__*/_react.default.createElement(_reactNativePaper.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: _reactNative.Platform.select({ ios: (0, _reactNative.PlatformColor)('systemBlue'), android: (0, _reactNative.PlatformColor)('@android:color/holo_blue_light') }), cursorColor: _reactNative.Platform.select({ android: (0, _reactNative.PlatformColor)('@android:color/holo_blue_light') }), right: error && /*#__PURE__*/_react.default.createElement(_reactNativePaper.TextInput.Icon, { icon: () => errorIcon, disabled: true }) })), /*#__PURE__*/_react.default.createElement(_reactNativePaper.TouchableRipple, { disabled: disabled || !editable, style: rippleStyle, onPress: openModal, theme: theme }, /*#__PURE__*/_react.default.createElement(_reactNativePaper.Text, null, " ")), /*#__PURE__*/_react.default.createElement(_reactNativePaper.Portal, { theme: theme }, /*#__PURE__*/_react.default.createElement(_reactNativePaper.Modal, { style: [styles.modal, modalStyle], contentContainerStyle: modalContainerStyles, visible: visible, onDismiss: closeModal, theme: theme }, /*#__PURE__*/_react.default.createElement(_reactNative.View, { style: styles.searchbox }, /*#__PURE__*/_react.default.createElement(_reactNativePaper.IconButton, { icon: "arrow-left", onPress: closeModal, theme: theme }), /*#__PURE__*/_react.default.createElement(_reactNativePaper.TextInput, { style: [styles.searchbar, dynamicStyles.searchbar], placeholder: searchLabel, onChangeText: onChangeSearchQuery, value: searchQuery, ref: searchbarRef, mode: "outlined", dense: true, theme: theme, onKeyPress: onKeyPress, selectionColor: _reactNative.Platform.select({ ios: (0, _reactNative.PlatformColor)('systemBlue'), android: (0, _reactNative.PlatformColor)('@android:color/holo_blue_light') }), cursorColor: _reactNative.Platform.select({ android: (0, _reactNative.PlatformColor)('@android:color/holo_blue_light') }), left: /*#__PURE__*/_react.default.createElement(_reactNativePaper.TextInput.Icon, { icon: "magnify", size: 20, style: styles.searchIcon }), underlineStyle: styles.searchbarUnderline, contentStyle: [styles.searchbarContent, dynamicStyles.searchbarContent], outlineStyle: [styles.outlined, dynamicStyles.outlined] })), /*#__PURE__*/_react.default.createElement(_reactNativePaper.DataTable, { style: styles.flex1 }, /*#__PURE__*/_react.default.createElement(_reactNativePaper.DataTable.Header, { theme: theme }, /*#__PURE__*/_react.default.createElement(_reactNativePaper.DataTable.Title, { theme: theme }, countryLabel), /*#__PURE__*/_react.default.createElement(_reactNativePaper.DataTable.Title, { numeric: true, theme: theme }, dialCodeLabel)), /*#__PURE__*/_react.default.createElement(_reactNative.FlatList, { keyboardShouldPersistTaps: "handled", data: searchResult, keyExtractor: keyExtractor, renderItem: renderItem, removeClippedSubviews: true, maxToRenderPerBatch: 10, windowSize: 10, initialNumToRender: 10, getItemLayout: undefined }))))); })); PhoneNumberInput.displayName = 'PhoneNumberInput'; const styles = _reactNative.StyleSheet.create({ ripple: { position: 'absolute', top: 0, bottom: 0, left: 0 }, flex1: { flex: _constants.isIOS ? undefined : 1 }, modal: { marginTop: undefined, marginBottom: undefined, justifyContent: undefined }, countries: { paddingHorizontal: 16, flex: _constants.isIOS ? undefined : 1, marginBottom: _constants.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