@duocvo/react-native-select-dropdown
Version:
This package was forked from https://github.com/AdelRedaa97/react-native-select-dropdown. Allow multiple select dropdown with search and custom styles.
225 lines (221 loc) • 8.18 kB
JavaScript
import React, {forwardRef, useImperativeHandle} from 'react';
import {View, TouchableOpacity, FlatList} from 'react-native';
import {isExist} from './helpers/isExist';
import Input from './components/Input';
import DropdownOverlay from './components/DropdownOverlay';
import DropdownModal from './components/DropdownModal';
import DropdownWindow from './components/DropdownWindow';
import {useSelectDropdown} from './hooks/useSelectDropdown';
import {useLayoutDropdown} from './hooks/useLayoutDropdown';
import {useRefs} from './hooks/useRefs';
import {findIndexInArr} from './helpers/findIndexInArr';
const SelectDropdown = (
{
data /* array */,
onSelect /* function */,
renderButton /* function returns React component for the dropdown button */,
renderButtonMultiple /* function returns React component for the dropdown button when multiple is true */,
renderItem /* function returns React component for each dropdown Item */,
defaultValue /* any */,
defaultValueByIndex /* integer */,
disabled /* boolean */,
disabledIndexes /* array of disabled items index */,
disableAutoScroll /* boolean */,
testID /* dropdown menu testID */,
onFocus /* function */,
onBlur /* function */,
onScrollEndReached /* function */,
/////////////////////////////
statusBarTranslucent /* boolean */,
dropdownStyle /* style object for search input */,
dropdownOverlayColor /* string */,
showsVerticalScrollIndicator /* boolean */,
/////////////////////////////
search /* boolean */,
searchInputStyle /* style object for search input */,
searchInputTxtColor /* text color for search input */,
searchInputTxtStyle /* text style for search input */,
searchPlaceHolder /* placeholder text for search input */,
searchPlaceHolderColor /* text color for search input placeholder */,
renderSearchInputLeftIcon /* function returns React component for search input icon */,
renderSearchInputRightIcon /* function returns React component for search input icon */,
onChangeSearchInputText /* function callback when the search input text changes, this will automatically disable the dropdown's interna search to be implemented manually outside the component */,
multiple = false, // for multiple select
autoFocusSearchInput = false, // for auto focus the search input
isRemoveDiacritics = false, // remove diacritics from search input text
isShowFullHeight = false, // show full dropdown height when keyboard is opened
},
ref,
) => {
const disabledInternalSearch = !!onChangeSearchInputText;
/* ******************* hooks ******************* */
const {dropdownButtonRef, dropDownFlatlistRef} = useRefs();
const {
dataArr, //
selectedItem,
selectedItems,
selectItem,
selectItems,
reset,
searchTxt,
setSearchTxt,
} = useSelectDropdown(data, defaultValueByIndex, defaultValue, disabledInternalSearch, multiple, isRemoveDiacritics);
const {isVisible, setIsVisible, buttonLayout, onDropdownButtonLayout, dropdownWindowStyle, onRequestClose} =
useLayoutDropdown(data, dropdownStyle, isShowFullHeight);
useImperativeHandle(ref, () => ({
reset: () => {
reset();
},
openDropdown: () => {
openDropdown();
},
closeDropdown: () => {
closeDropdown();
},
selectIndex: index => {
selectItem(index);
},
}));
/* ******************* Methods ******************* */
const openDropdown = () => {
dropdownButtonRef.current.measure(async (fx, fy, w, h, px, py) => {
onDropdownButtonLayout(w, h, px, py);
await new Promise(resolve => setTimeout(resolve, 200));
setIsVisible(true);
onFocus && onFocus();
if (!disableAutoScroll) {
scrollToSelectedItem();
}
});
};
const closeDropdown = () => {
setIsVisible(false);
setSearchTxt('');
onBlur && onBlur();
};
const scrollToSelectedItem = () => {
const indexInCurrArr = findIndexInArr(selectedItem, dataArr);
setTimeout(() => {
if (disableAutoScroll) {
return;
}
if (indexInCurrArr > 1) {
dropDownFlatlistRef?.current?.scrollToIndex({
index: search ? indexInCurrArr - 1 : indexInCurrArr,
animated: true,
});
}
}, 200);
};
const onSelectItem = (item, index) => {
const indexInOriginalArr = findIndexInArr(item, data);
onSelect && onSelect(item, indexInOriginalArr);
if (multiple) {
selectItems(indexInOriginalArr);
} else {
selectItem(indexInOriginalArr);
closeDropdown();
}
};
const onScrollToIndexFailed = error => {
dropDownFlatlistRef.current.scrollToOffset({
offset: error.averageItemLength * error.index,
animated: true,
});
setTimeout(() => {
if (dataArr.length !== 0 && dropDownFlatlistRef) {
dropDownFlatlistRef.current.scrollToIndex({index: error.index, animated: true});
}
}, 100);
};
/* ******************** Render Methods ******************** */
const renderSearchView = () => {
return (
search && (
<Input
searchViewWidth={buttonLayout.w}
value={searchTxt}
valueColor={searchInputTxtColor}
placeholder={searchPlaceHolder}
placeholderTextColor={searchPlaceHolderColor}
onChangeText={txt => {
setSearchTxt(txt);
disabledInternalSearch && onChangeSearchInputText(txt);
}}
inputStyle={searchInputStyle}
inputTextStyle={searchInputTxtStyle}
renderLeft={renderSearchInputLeftIcon}
renderRight={renderSearchInputRightIcon}
autoFocus={autoFocusSearchInput}
/>
)
);
};
const renderFlatlistItem = ({item, index}) => {
let isSelected = false;
if (multiple) {
isSelected = selectedItems.some(selectedItem => selectedItem?.index === index);
} else {
const indexInCurrArr = findIndexInArr(selectedItem, dataArr);
isSelected = index == indexInCurrArr;
}
let clonedElement = renderItem ? renderItem(item, index, isSelected) : <View />;
let props = {...clonedElement.props};
return (
isExist(item) && (
<TouchableOpacity
{...props}
disabled={disabledIndexes?.includes(index)}
activeOpacity={0.8}
onPress={() => onSelectItem(item, index)}>
{props?.children}
</TouchableOpacity>
)
);
};
const renderDropdown = () => {
return (
isVisible && (
<DropdownModal statusBarTranslucent={statusBarTranslucent} visible={isVisible} onRequestClose={onRequestClose}>
<DropdownOverlay onPress={closeDropdown} backgroundColor={dropdownOverlayColor} />
<DropdownWindow layoutStyle={dropdownWindowStyle}>
<FlatList
testID={testID}
data={dataArr}
keyExtractor={(item, index) => index.toString()}
ref={dropDownFlatlistRef}
renderItem={renderFlatlistItem}
ListHeaderComponent={renderSearchView()}
stickyHeaderIndices={search && [0]}
keyboardShouldPersistTaps="always"
onEndReached={() => onScrollEndReached && onScrollEndReached()}
onEndReachedThreshold={0.5}
showsVerticalScrollIndicator={showsVerticalScrollIndicator}
onScrollToIndexFailed={onScrollToIndexFailed}
/>
</DropdownWindow>
</DropdownModal>
)
);
};
///////////////////////////////////////////////////////
let clonedElement = multiple ? (
renderButtonMultiple ? (
renderButtonMultiple(selectedItems, isVisible)
) : (
<View />
)
) : renderButton ? (
renderButton(selectedItem, isVisible)
) : (
<View />
);
let props = {...clonedElement.props};
return (
<TouchableOpacity {...props} activeOpacity={0.8} ref={dropdownButtonRef} disabled={disabled} onPress={openDropdown}>
{renderDropdown()}
{props?.children}
</TouchableOpacity>
);
};
export default forwardRef((props, ref) => SelectDropdown(props, ref));