diff --git a/README.md b/README.md index 552f951..3a92454 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,7 @@ Below are the props you can pass to the React Component. | searchMessage | string | | searchMessage={'Some search message here'} | If you want to customize search message just use this prop. | | lang | string | 'en' | lang={'pl'} | If you need to change the lang. just put one of supported lang. Or if you didn't find required lang just add them and make a PR :) | | enableModalAvoiding | boolean | false | enableModalAvoiding={true} | Is modal should avoid keyboard ? On android to work required to use with androidWindowSoftInputMode with value pan, by default android will avoid keyboard by itself | +| openKeyboardMargin | number | keyboardHeight | openKeyboardMargin={50} | In case you have set enableModalAvoiding = {true} you can adjust how much you want the modal to rise above keyboard the default value for this will be the keyboard height itself | | androidWindowSoftInputMode | string | | androidWindowSoftInputMode={'pan'} | Basicaly android avoid keyboard by itself, if you want to use custom avoiding you may use this prop | | itemTemplate | ReactNode | CountryButton | itemTemplate={YourTemplateComponentsHere} | This parameter gets a React Node element to render it as a template for each item of the list. These properties are sent to the item: key, item, style, name, and onPress | | style | Object | | style={{yoursStylesHere}} | If you want to change styles for component you probably need this props. You can check the styling part below. | diff --git a/index.tsx b/index.tsx index da5b226..ebeac8b 100644 --- a/index.tsx +++ b/index.tsx @@ -1,30 +1,39 @@ -import React from 'react'; +import React from "react"; import { - FlatList, - TextInput, - View, - Text, - Animated, - Dimensions, - Easing, - Platform, - Keyboard, - ViewStyle, - Modal, - TextStyle -} from 'react-native'; -import { CountryItem, ItemTemplateProps, Style, ListHeaderComponentProps } from "./types/Types"; + FlatList, + TextInput, + View, + Text, + Animated, + Dimensions, + Easing, + Platform, + Keyboard, + ViewStyle, + Modal, + TextStyle, +} from "react-native"; +import { + CountryItem, + ItemTemplateProps, + Style, + ListHeaderComponentProps, +} from "./types/Types"; import { useKeyboardStatus } from "./helpers/useKeyboardStatus"; import { CountryButton } from "./components/CountryButton"; import { countriesRemover } from "./helpers/countriesRemover"; -import { removeDiacritics } from './helpers/diacriticsRemover'; +import { removeDiacritics } from "./helpers/diacriticsRemover"; -export { countryCodes } from './constants/countryCodes' +export { countryCodes } from "./constants/countryCodes"; export { CountryButton } from "./components/CountryButton"; -export type { CountryItem, ItemTemplateProps, Style, ListHeaderComponentProps } from "./types/Types"; - +export type { + CountryItem, + ItemTemplateProps, + Style, + ListHeaderComponentProps, +} from "./types/Types"; -const height = Dimensions.get('window').height; +const height = Dimensions.get("window").height; /** * Country picker component @@ -46,452 +55,498 @@ const height = Dimensions.get('window').height; */ interface Props { - excludedCountries?: string[], - showOnly?: string[], - popularCountries?: string[], - - style?: Style, - - show: boolean, - enableModalAvoiding?: boolean, - disableBackdrop?: boolean, - - onBackdropPress?: (...args: any) => any, - pickerButtonOnPress: (item: CountryItem) => any, - itemTemplate?: (props: ItemTemplateProps) => JSX.Element, - ListHeaderComponent?: (props: ListHeaderComponentProps) => JSX.Element, - onRequestClose?: (...args: any) => any, - - lang: string, - inputPlaceholder?: string, - inputPlaceholderTextColor?: TextStyle['color'], - searchMessage?: string, - androidWindowSoftInputMode?: string, - initialState?: string, + excludedCountries?: string[]; + showOnly?: string[]; + popularCountries?: string[]; + + style?: Style; + + show: boolean; + enableModalAvoiding?: boolean; + disableBackdrop?: boolean; + + onBackdropPress?: (...args: any) => any; + pickerButtonOnPress: (item: CountryItem) => any; + itemTemplate?: (props: ItemTemplateProps) => JSX.Element; + ListHeaderComponent?: (props: ListHeaderComponentProps) => JSX.Element; + onRequestClose?: (...args: any) => any; + + lang: string; + inputPlaceholder?: string; + inputPlaceholderTextColor?: TextStyle["color"]; + searchMessage?: string; + androidWindowSoftInputMode?: string; + initialState?: string; + openKeyboardMargin?: number; } export const CountryPicker = ({ - show, - popularCountries, - pickerButtonOnPress, - inputPlaceholder, - inputPlaceholderTextColor, - searchMessage, - lang = 'en', - style, - enableModalAvoiding, - androidWindowSoftInputMode, - onBackdropPress, - disableBackdrop, - excludedCountries, - initialState, - onRequestClose, - showOnly, - ListHeaderComponent, - itemTemplate: ItemTemplate = CountryButton, - ...rest + show, + popularCountries, + pickerButtonOnPress, + inputPlaceholder, + inputPlaceholderTextColor, + searchMessage, + lang = "en", + style, + enableModalAvoiding, + androidWindowSoftInputMode, + onBackdropPress, + disableBackdrop, + excludedCountries, + initialState, + onRequestClose, + showOnly, + ListHeaderComponent, + itemTemplate: ItemTemplate = CountryButton, + openKeyboardMargin, + ...rest }: Props) => { - // ToDo refactor exclude and showOnly props to objects - let filteredCodes = countriesRemover(excludedCountries); - const keyboardStatus = useKeyboardStatus(); - const animationDriver = React.useRef(new Animated.Value(0)).current; - const animatedMargin = React.useRef(new Animated.Value(0)).current; - const [searchValue, setSearchValue] = React.useState(initialState || ''); - const [showModal, setShowModal] = React.useState(false); - - React.useEffect(() => { - if (show) { - setShowModal(true); - } else { - closeModal(); - } - }, [show]); - - React.useEffect(() => { - if ( - enableModalAvoiding && - ( - keyboardStatus.keyboardPlatform === 'ios' || - (keyboardStatus.keyboardPlatform === 'android' && - androidWindowSoftInputMode === 'pan') - ) - ) { - if (keyboardStatus.isOpen) - Animated.timing(animatedMargin, { - toValue: keyboardStatus.keyboardHeight, - duration: 190, - easing: Easing.ease, - useNativeDriver: false, - }).start(); - - if (!keyboardStatus.isOpen) - Animated.timing(animatedMargin, { - toValue: 0, - duration: 190, - easing: Easing.ease, - useNativeDriver: false, - }).start(); - } - }, [keyboardStatus.isOpen]); - - const preparedPopularCountries = React.useMemo(() => { - return filteredCodes?.filter(country => { - return (popularCountries?.find(short => country?.code === short?.toUpperCase())); - }); - }, [popularCountries]); - - const codes = React.useMemo(() => { - let newCodes = filteredCodes; - - if (showOnly?.length) { - newCodes = filteredCodes?.filter(country => { - return (showOnly?.find(short => country?.code === short?.toUpperCase())); - }); - } - newCodes.sort((a, b) => (a?.name[lang || 'en'].localeCompare(b?.name[lang || 'en']))); - - return newCodes; - }, [showOnly, excludedCountries, lang]); - - const resultCountries = React.useMemo(() => { - const lowerSearchValue = searchValue.toLowerCase(); - - return codes.filter((country) => { - if (country?.dial_code.includes(searchValue) || - country?.name[lang || 'en'].toLowerCase().includes(lowerSearchValue) || - removeDiacritics(country?.name[lang || 'en'].toLowerCase()).includes(lowerSearchValue) - ) { - return country; - } - }); - }, [searchValue]); - - const modalPosition = animationDriver.interpolate({ - inputRange: [0, 1], - outputRange: [height, 0], - extrapolate: 'clamp', - }); + // ToDo refactor exclude and showOnly props to objects + let filteredCodes = countriesRemover(excludedCountries); + const keyboardStatus = useKeyboardStatus(); + const animationDriver = React.useRef(new Animated.Value(0)).current; + const animatedMargin = React.useRef(new Animated.Value(0)).current; + const [searchValue, setSearchValue] = React.useState( + initialState || "" + ); + const [showModal, setShowModal] = React.useState(false); + + React.useEffect(() => { + if (show) { + setShowModal(true); + } else { + closeModal(); + } + }, [show]); + + React.useEffect(() => { + if ( + enableModalAvoiding && + (keyboardStatus.keyboardPlatform === "ios" || + (keyboardStatus.keyboardPlatform === "android" && + androidWindowSoftInputMode === "pan")) + ) { + if (keyboardStatus.isOpen) + Animated.timing(animatedMargin, { + toValue: openKeyboardMargin ?? keyboardStatus.keyboardHeight, + duration: 190, + easing: Easing.ease, + useNativeDriver: false, + }).start(); - const modalBackdropFade = animationDriver.interpolate({ - inputRange: [0, 0.5, 1], - outputRange: [0, 0.5, 1], - extrapolate: 'clamp' + if (!keyboardStatus.isOpen) + Animated.timing(animatedMargin, { + toValue: 0, + duration: 190, + easing: Easing.ease, + useNativeDriver: false, + }).start(); + } + }, [keyboardStatus.isOpen]); + + const preparedPopularCountries = React.useMemo(() => { + return filteredCodes?.filter((country) => { + return popularCountries?.find( + (short) => country?.code === short?.toUpperCase() + ); }); + }, [popularCountries]); - const openModal = () => { - Animated.timing(animationDriver, { - toValue: 1, - duration: 400, - useNativeDriver: true, - }).start(); - }; - - const closeModal = () => { - Animated.timing(animationDriver, { - toValue: 0, - duration: 400, - useNativeDriver: true, - }).start(() => setShowModal(false)); - }; - - const renderItem = ({ item, index }: { item: CountryItem, index: number }) => { - let itemName = item?.name[lang]; - let checkName = itemName?.length ? itemName : item?.name['en']; - - return ( - { - Keyboard.dismiss(); - typeof pickerButtonOnPress === 'function' && pickerButtonOnPress(item); - }} - /> - ); - }; + const codes = React.useMemo(() => { + let newCodes = filteredCodes; - const onStartShouldSetResponder = () => { - onBackdropPress?.(); - return false; - }; + if (showOnly?.length) { + newCodes = filteredCodes?.filter((country) => { + return showOnly?.find( + (short) => country?.code === short?.toUpperCase() + ); + }); + } + newCodes.sort((a, b) => + a?.name[lang || "en"].localeCompare(b?.name[lang || "en"]) + ); + + return newCodes; + }, [showOnly, excludedCountries, lang]); + + const resultCountries = React.useMemo(() => { + const lowerSearchValue = searchValue.toLowerCase(); + + return codes.filter((country) => { + if ( + country?.dial_code.includes(searchValue) || + country?.name[lang || "en"].toLowerCase().includes(lowerSearchValue) || + removeDiacritics(country?.name[lang || "en"].toLowerCase()).includes( + lowerSearchValue + ) + ) { + return country; + } + }); + }, [searchValue]); + + const modalPosition = animationDriver.interpolate({ + inputRange: [0, 1], + outputRange: [height, 0], + extrapolate: "clamp", + }); + + const modalBackdropFade = animationDriver.interpolate({ + inputRange: [0, 0.5, 1], + outputRange: [0, 0.5, 1], + extrapolate: "clamp", + }); + + const openModal = () => { + Animated.timing(animationDriver, { + toValue: 1, + duration: 400, + useNativeDriver: true, + }).start(); + }; + + const closeModal = () => { + Animated.timing(animationDriver, { + toValue: 0, + duration: 400, + useNativeDriver: true, + }).start(() => setShowModal(false)); + }; + + const renderItem = ({ + item, + index, + }: { + item: CountryItem; + index: number; + }) => { + let itemName = item?.name[lang]; + let checkName = itemName?.length ? itemName : item?.name["en"]; return ( - { + Keyboard.dismiss(); + typeof pickerButtonOnPress === "function" && + pickerButtonOnPress(item); + }} + /> + ); + }; + + const onStartShouldSetResponder = () => { + onBackdropPress?.(); + return false; + }; + + return ( + + + {!disableBackdrop && ( + + )} + + + + + + {resultCountries.length === 0 ? ( - {!disableBackdrop && ( - - )} - - - - - - {resultCountries.length === 0 ? ( - - - {searchMessage || 'Sorry we cant find your country :('} - - - ) : ( - '' + item + index} - initialNumToRender={10} - maxToRenderPerBatch={10} - style={[style?.itemsList]} - keyboardShouldPersistTaps={'handled'} - renderItem={renderItem} - testID='countryCodesPickerFlatList' - ListHeaderComponent={(popularCountries && ListHeaderComponent && !searchValue) ? - { - Keyboard.dismiss(); - typeof pickerButtonOnPress === 'function' && pickerButtonOnPress(item); - }} - /> - : null - } - {...rest} - /> - )} - - + + {searchMessage || "Sorry we cant find your country :("} + - - ) + ) : ( + "" + item + index} + initialNumToRender={10} + maxToRenderPerBatch={10} + style={[style?.itemsList]} + keyboardShouldPersistTaps={"handled"} + renderItem={renderItem} + testID="countryCodesPickerFlatList" + ListHeaderComponent={ + popularCountries && ListHeaderComponent && !searchValue ? ( + { + Keyboard.dismiss(); + typeof pickerButtonOnPress === "function" && + pickerButtonOnPress(item); + }} + /> + ) : null + } + {...rest} + /> + )} + + + + + ); }; interface CountryListProps { - lang: string, - searchValue?: string, - excludedCountries?: string[], - popularCountries?: string[], - showOnly?: string[], + lang: string; + searchValue?: string; + excludedCountries?: string[]; + popularCountries?: string[]; + showOnly?: string[]; - ListHeaderComponent?: (props: ListHeaderComponentProps) => JSX.Element, - itemTemplate?: (props: ItemTemplateProps) => JSX.Element, - pickerButtonOnPress: (item: CountryItem) => any, + ListHeaderComponent?: (props: ListHeaderComponentProps) => JSX.Element; + itemTemplate?: (props: ItemTemplateProps) => JSX.Element; + pickerButtonOnPress: (item: CountryItem) => any; - style?: Style, + style?: Style; } export const CountryList = ({ - showOnly, - popularCountries, - lang = 'en', - searchValue = '', - excludedCountries, - style, - pickerButtonOnPress, - ListHeaderComponent, - itemTemplate: ItemTemplate = CountryButton, - ...rest + showOnly, + popularCountries, + lang = "en", + searchValue = "", + excludedCountries, + style, + pickerButtonOnPress, + ListHeaderComponent, + itemTemplate: ItemTemplate = CountryButton, + ...rest }: CountryListProps) => { - // ToDo refactor exclude and showOnly props to objects - let filteredCodes = countriesRemover(excludedCountries); - - const preparedPopularCountries = React.useMemo(() => { - return filteredCodes?.filter(country => { - return (popularCountries?.find(short => country?.code === short?.toUpperCase())); - }); - }, [popularCountries]); - - const codes = React.useMemo(() => { - let newCodes = filteredCodes; - - if (showOnly?.length) { - newCodes = filteredCodes?.filter(country => { - return (showOnly?.find(short => country?.code === short?.toUpperCase())); - }); - } - - return newCodes - }, [showOnly, excludedCountries]); - - const resultCountries = React.useMemo(() => { - const lowerSearchValue = searchValue.toLowerCase(); - - return codes.filter((country) => { - if (country?.dial_code.includes(searchValue) || - country?.name[lang || 'en'].toLowerCase().includes(lowerSearchValue.trim()) || - removeDiacritics(country?.name[lang || 'en'].toLowerCase()).includes(lowerSearchValue.trim()) - ) { - return country; - } - }); - }, [searchValue]); - - const renderItem = ({ item, index }: { item: CountryItem, index: number }) => { - let itemName = item?.name[lang]; - let checkName = itemName.length ? itemName : item?.name['en']; - - return ( - { - Keyboard.dismiss(); - typeof pickerButtonOnPress === 'function' && pickerButtonOnPress(item); - }} - /> + // ToDo refactor exclude and showOnly props to objects + let filteredCodes = countriesRemover(excludedCountries); + + const preparedPopularCountries = React.useMemo(() => { + return filteredCodes?.filter((country) => { + return popularCountries?.find( + (short) => country?.code === short?.toUpperCase() + ); + }); + }, [popularCountries]); + + const codes = React.useMemo(() => { + let newCodes = filteredCodes; + + if (showOnly?.length) { + newCodes = filteredCodes?.filter((country) => { + return showOnly?.find( + (short) => country?.code === short?.toUpperCase() ); - }; + }); + } + + return newCodes; + }, [showOnly, excludedCountries]); + + const resultCountries = React.useMemo(() => { + const lowerSearchValue = searchValue.toLowerCase(); + + return codes.filter((country) => { + if ( + country?.dial_code.includes(searchValue) || + country?.name[lang || "en"] + .toLowerCase() + .includes(lowerSearchValue.trim()) || + removeDiacritics(country?.name[lang || "en"].toLowerCase()).includes( + lowerSearchValue.trim() + ) + ) { + return country; + } + }); + }, [searchValue]); + + const renderItem = ({ + item, + index, + }: { + item: CountryItem; + index: number; + }) => { + let itemName = item?.name[lang]; + let checkName = itemName.length ? itemName : item?.name["en"]; return ( - '' + item + index} - initialNumToRender={10} - maxToRenderPerBatch={10} - style={[style?.itemsList]} - keyboardShouldPersistTaps={'handled'} - renderItem={renderItem} - ListHeaderComponent={(popularCountries && ListHeaderComponent) && - { - Keyboard.dismiss(); - typeof pickerButtonOnPress === 'function' && pickerButtonOnPress(item); - }} - /> - } - - {...rest} - /> - ) + { + Keyboard.dismiss(); + typeof pickerButtonOnPress === "function" && + pickerButtonOnPress(item); + }} + /> + ); + }; + + return ( + "" + item + index} + initialNumToRender={10} + maxToRenderPerBatch={10} + style={[style?.itemsList]} + keyboardShouldPersistTaps={"handled"} + renderItem={renderItem} + ListHeaderComponent={ + popularCountries && + ListHeaderComponent && ( + { + Keyboard.dismiss(); + typeof pickerButtonOnPress === "function" && + pickerButtonOnPress(item); + }} + /> + ) + } + {...rest} + /> + ); }; - -type StyleKeys = 'container' | 'modal' | 'modalInner' | 'searchBar' | 'countryMessage' | 'line'; +type StyleKeys = + | "container" + | "modal" + | "modalInner" + | "searchBar" + | "countryMessage" + | "line"; const styles: { [key in StyleKeys]: ViewStyle } = { - container: { - flex: 1, - position: 'absolute', - left: 0, - right: 0, - top: 0, - bottom: 0, - justifyContent: 'flex-end', - }, - modal: { - backgroundColor: 'white', - width: '100%', - maxWidth: Platform.OS === "web" ? 600 : undefined, - borderTopRightRadius: 15, - borderTopLeftRadius: 15, - padding: 10, - - shadowColor: '#000', - shadowOffset: { - width: 0, - height: 6, - }, - bottom: 0, - zIndex: 10, - shadowOpacity: 0.37, - shadowRadius: 7.49, - - elevation: 10, - }, - modalInner: { - zIndex: 99, - backgroundColor: 'white', - width: '100%', - }, - searchBar: { - flex: 1, - backgroundColor: '#f5f5f5', - borderRadius: 10, - height: 40, - padding: 5, - }, - countryMessage: { - justifyContent: 'center', - alignItems: 'center', - height: 250, - }, - line: { - width: '100%', - height: 1.5, - borderRadius: 2, - backgroundColor: '#eceff1', - alignSelf: 'center', - marginVertical: 5, + container: { + flex: 1, + position: "absolute", + left: 0, + right: 0, + top: 0, + bottom: 0, + justifyContent: "flex-end", + }, + modal: { + backgroundColor: "white", + width: "100%", + maxWidth: Platform.OS === "web" ? 600 : undefined, + borderTopRightRadius: 15, + borderTopLeftRadius: 15, + padding: 10, + + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 6, }, + bottom: 0, + zIndex: 10, + shadowOpacity: 0.37, + shadowRadius: 7.49, + + elevation: 10, + }, + modalInner: { + zIndex: 99, + backgroundColor: "white", + width: "100%", + }, + searchBar: { + flex: 1, + backgroundColor: "#f5f5f5", + borderRadius: 10, + height: 40, + padding: 5, + }, + countryMessage: { + justifyContent: "center", + alignItems: "center", + height: 250, + }, + line: { + width: "100%", + height: 1.5, + borderRadius: 2, + backgroundColor: "#eceff1", + alignSelf: "center", + marginVertical: 5, + }, };