import _ from 'lodash'; import React, { JSXElementConstructor, ReactElement, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react'; import { Dimensions, FlatList, I18nManager, Image, Keyboard, KeyboardEvent, Modal, StyleSheet, Text, TouchableHighlight, TouchableWithoutFeedback, View, ViewStyle, } from 'react-native'; import { useDetectDevice } from '../../toolkits'; import { useDeviceOrientation } from '../../useDeviceOrientation'; import CInput from '../TextInput'; import { DropdownProps } from './model'; import { styles } from './styles'; const { isTablet } = useDetectDevice; const ic_down = require('../../assets/down.png'); const DropdownComponent: ( props: DropdownProps ) => ReactElement> | null = React.forwardRef((props, currentRef) => { const orientation = useDeviceOrientation(); const { testID, itemTestIDField, onChange, style = {}, containerStyle, placeholderStyle, selectedTextStyle, itemContainerStyle, itemTextStyle, inputSearchStyle, iconStyle, selectedTextProps = {}, data = [], labelField, valueField, value, activeColor = '#F6F7F8', fontFamily, iconColor = 'gray', searchPlaceholder, placeholder = 'Select item', search = false, maxHeight = 340, minHeight = 0, disable = false, keyboardAvoiding = true, inverted = true, renderLeftIcon, renderRightIcon, renderItem, renderInputSearch, onFocus, onBlur, autoScroll = true, showsVerticalScrollIndicator = true, dropdownPosition = 'auto', flatListProps, searchQuery, statusBarIsTranslucent, backgroundColor, onChangeText, confirmSelectItem, onConfirmSelectItem, accessibilityLabel, itemAccessibilityLabelField, } = props; const ref = useRef(null); const refList = useRef(null); const [visible, setVisible] = useState(false); const [currentValue, setCurrentValue] = useState(null); const [listData, setListData] = useState(data); const [position, setPosition] = useState(); const [keyboardHeight, setKeyboardHeight] = useState(0); const [searchText, setSearchText] = useState(''); const { width: W, height: H } = Dimensions.get('window'); const styleContainerVertical: ViewStyle = useMemo(() => { return { backgroundColor: 'rgba(0,0,0,0.1)', alignItems: 'center', }; }, []); const styleHorizontal: ViewStyle = useMemo(() => { return { marginBottom: 20, width: W / 2, alignSelf: 'center', }; }, [W]); useImperativeHandle(currentRef, () => { return { open: eventOpen, close: eventClose }; }); useEffect(() => { setListData([...data]); if (searchText) { onSearch(searchText); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [data, searchText]); const eventOpen = () => { if (!disable) { setVisible(true); if (onFocus) { onFocus(); } if (searchText.length > 0) { onSearch(searchText); } scrollIndex(); } }; const eventClose = useCallback(() => { if (!disable) { setVisible(false); if (onBlur) { onBlur(); } } }, [disable, onBlur]); const font = useCallback(() => { if (fontFamily) { return { fontFamily: fontFamily, }; } else { return {}; } }, [fontFamily]); const _measure = useCallback(() => { if (ref && ref?.current) { ref.current.measure((_width, _height, px, py, fx, fy) => { const isFull = orientation === 'LANDSCAPE' && !isTablet; const w = Math.floor(px); const top = isFull ? 20 : Math.floor(py) + Math.floor(fy) + 2; const bottom = H - top; const left = I18nManager.isRTL ? W - Math.floor(px) - Math.floor(fx) : Math.floor(fx); setPosition({ isFull, w, top, bottom: Math.floor(bottom), left, height: Math.floor(py), }); }); } }, [H, W, orientation]); const onKeyboardDidShow = useCallback( (e: KeyboardEvent) => { _measure(); setKeyboardHeight(e.endCoordinates.height); }, [_measure] ); const onKeyboardDidHide = useCallback(() => { setKeyboardHeight(0); _measure(); }, [_measure]); useEffect(() => { const susbcriptionKeyboardDidShow = Keyboard.addListener( 'keyboardDidShow', onKeyboardDidShow ); const susbcriptionKeyboardDidHide = Keyboard.addListener( 'keyboardDidHide', onKeyboardDidHide ); return () => { if (typeof susbcriptionKeyboardDidShow?.remove === 'function') { susbcriptionKeyboardDidShow.remove(); } else { Keyboard.removeListener('keyboardDidShow', onKeyboardDidShow); } if (typeof susbcriptionKeyboardDidHide?.remove === 'function') { susbcriptionKeyboardDidHide.remove(); } else { Keyboard.removeListener('keyboardDidHide', onKeyboardDidHide); } }; }, [onKeyboardDidHide, onKeyboardDidShow]); const getValue = useCallback(() => { const defaultValue = typeof value === 'object' ? _.get(value, valueField) : value; const getItem = data.filter((e) => _.isEqual(defaultValue, _.get(e, valueField)) ); if (getItem.length > 0) { setCurrentValue(getItem[0]); } else { setCurrentValue(null); } }, [data, value, valueField]); useEffect(() => { getValue(); }, [value, data, getValue]); const scrollIndex = useCallback(() => { if (autoScroll && data.length > 0 && listData.length === data.length) { setTimeout(() => { if (refList && refList?.current) { const defaultValue = typeof value === 'object' ? _.get(value, valueField) : value; const index = _.findIndex(listData, (e: any) => _.isEqual(defaultValue, _.get(e, valueField)) ); if (index > -1 && index <= listData.length - 1) { refList?.current?.scrollToIndex({ index: index, animated: false, }); } } }, 200); } }, [autoScroll, data.length, listData, value, valueField]); const showOrClose = useCallback(() => { if (!disable) { if (keyboardHeight > 0 && visible) { return Keyboard.dismiss(); } _measure(); setVisible(!visible); setListData(data); if (!visible) { if (onFocus) { onFocus(); } } else { if (onBlur) { onBlur(); } } if (searchText.length > 0) { onSearch(searchText); } scrollIndex(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ disable, keyboardHeight, visible, _measure, data, searchText, scrollIndex, onFocus, onBlur, ]); const onSearch = useCallback( (text: string) => { if (text.length > 0) { const defaultFilterFunction = (e: any) => { const item = _.get(e, labelField) ?.toLowerCase() .replace(' ', '') .normalize('NFD') .replace(/[\u0300-\u036f]/g, ''); const key = text .toLowerCase() .replace(' ', '') .normalize('NFD') .replace(/[\u0300-\u036f]/g, ''); return item.indexOf(key) >= 0; }; const propSearchFunction = (e: any) => { const labelText = _.get(e, labelField); return searchQuery?.(text, labelText); }; const dataSearch = data.filter( searchQuery ? propSearchFunction : defaultFilterFunction ); setListData(dataSearch); } else { setListData(data); } }, [data, labelField, searchQuery] ); const onSelect = useCallback( (item: any) => { if (confirmSelectItem && onConfirmSelectItem) { return onConfirmSelectItem(item); } if (onChangeText) { setSearchText(''); onChangeText(''); } onSearch(''); setCurrentValue(item); onChange(item); eventClose(); }, [ confirmSelectItem, eventClose, onChange, onChangeText, onConfirmSelectItem, onSearch, ] ); const _renderDropdown = () => { const isSelected = currentValue && _.get(currentValue, valueField); return ( {renderLeftIcon?.(visible)} {isSelected !== null ? _.get(currentValue, labelField) : placeholder} {renderRightIcon ? ( renderRightIcon(visible) ) : ( )} ); }; const _renderItem = useCallback( ({ item, index }: { item: any; index: number }) => { const isSelected = currentValue && _.get(currentValue, valueField); const selected = _.isEqual(_.get(item, valueField), isSelected); _.assign(item, { _index: index }); return ( onSelect(item)} > {renderItem ? ( renderItem(item, selected) ) : ( {_.get(item, labelField)} )} ); }, [ accessibilityLabel, activeColor, currentValue, font, itemAccessibilityLabelField, itemContainerStyle, itemTestIDField, itemTextStyle, labelField, onSelect, renderItem, valueField, ] ); const renderSearch = useCallback(() => { if (search) { if (renderInputSearch) { return renderInputSearch((text) => { if (onChangeText) { setSearchText(text); onChangeText(text); } onSearch(text); }); } else { return ( { if (onChangeText) { setSearchText(e); onChangeText(e); } onSearch(e); }} placeholderTextColor="gray" iconStyle={[{ tintColor: iconColor }, iconStyle]} /> ); } } return null; }, [ accessibilityLabel, font, iconColor, iconStyle, inputSearchStyle, onChangeText, onSearch, renderInputSearch, search, searchPlaceholder, testID, searchText, ]); const _renderList = useCallback( (isTopPosition: boolean) => { const isInverted = !inverted ? false : isTopPosition; const _renderListHelper = () => { return ( index.toString()} showsVerticalScrollIndicator={showsVerticalScrollIndicator} /> ); }; return ( {isInverted && _renderListHelper()} {renderSearch()} {!isInverted && _renderListHelper()} ); }, [ _renderItem, accessibilityLabel, flatListProps, listData, inverted, renderSearch, scrollIndex, showsVerticalScrollIndicator, testID, ] ); const _renderModal = useCallback(() => { if (visible && position) { const { isFull, w, top, bottom, left, height } = position; const onAutoPosition = () => { if (keyboardHeight > 0) { return bottom < keyboardHeight + height; } return bottom < (search ? 150 : 100); }; if (w && top && bottom) { const styleVertical: ViewStyle = { left: left, maxHeight: maxHeight, minHeight: minHeight, }; const isTopPosition = dropdownPosition === 'auto' ? onAutoPosition() : dropdownPosition === 'top'; let keyboardStyle: ViewStyle = {}; let extendHeight = !isTopPosition ? top : bottom + height; if (keyboardAvoiding && keyboardHeight > 0 && isTopPosition) { extendHeight = keyboardHeight; } return ( {_renderList(isTopPosition)} ); } return null; } return null; }, [ visible, search, position, keyboardHeight, maxHeight, minHeight, dropdownPosition, keyboardAvoiding, statusBarIsTranslucent, showOrClose, styleContainerVertical, backgroundColor, containerStyle, styleHorizontal, _renderList, ]); return ( {_renderDropdown()} {_renderModal()} ); }); export default DropdownComponent;