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