UNPKG

react-native-autocomplete-dropdown

Version:

Dropdown Item picker with search and autocomplete (typeahead) functionality for react native

474 lines (470 loc) 15 kB
"use strict"; import React, { memo, useCallback, useEffect, useMemo, useRef, useState, useContext } from 'react'; import debounce from 'lodash.debounce'; import { Dimensions, Keyboard, Platform, Pressable, TextInput, TouchableOpacity, View, useColorScheme } from 'react-native'; import { moderateScale, ScaledSheet } from 'react-native-size-matters'; import { Dropdown } from './Dropdown'; import { NothingFound } from './NothingFound'; import { RightButton } from './RightButton'; import { ScrollViewListItem } from './ScrollViewListItem'; import { AutocompleteDropdownContext, AutocompleteDropdownContextProvider } from './AutocompleteDropdownContext'; import diacriticless from './diacriticless'; import { theme } from './theme'; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; export * from './types'; export { AutocompleteDropdownContextProvider }; export const AutocompleteDropdown = /*#__PURE__*/memo(props => { const { dataSet: dataSetProp, initialValue: initialValueProp, clearOnFocus = true, caseSensitive = false, ignoreAccents = true, trimSearchText = true, editable = true, enableLoadingIndicator = true, matchFrom, inputHeight = moderateScale(40, 0.2), suggestionsListMaxHeight = moderateScale(200, 0.2), // bottomOffset = 0, direction: directionProp, controller, onSelectItem: onSelectItemProp, onOpenSuggestionsList: onOpenSuggestionsListProp, useFilter, renderItem: customRenderItem, EmptyResultComponent, emptyResultText, onClear, onChangeText: onTextChange, debounce: debounceDelay = 0, onChevronPress: onChevronPressProp, onFocus: onFocusProp, onBlur: onBlurProp, onSubmit: onSubmitProp, closeOnSubmit, loading: loadingProp, LeftComponent, textInputProps, showChevron, showClear, rightButtonsContainerStyle, ChevronIconComponent, ClearIconComponent, RightIconComponent, onRightIconComponentPress, containerStyle, inputContainerStyle, suggestionsListTextStyle, ref } = props; const InputComponent = props.InputComponent || TextInput; const inputRef = useRef(null); const containerRef = useRef(null); const [searchText, setSearchText] = useState(''); const [inputValue, setInputValue] = useState(''); const [loading, setLoading] = useState(loadingProp); const [selectedItem, setSelectedItem] = useState(null); const [isOpened, setIsOpened] = useState(false); const initialDataSetRef = useRef(dataSetProp); const initialValueRef = useRef(initialValueProp); const [dataSet, setDataSet] = useState(dataSetProp); const matchFromStart = matchFrom === 'start' ? true : false; const { content, setContent, activeInputContainerRef, activeControllerRef, direction = directionProp, setDirection, controllerRefs } = useContext(AutocompleteDropdownContext); const systemTheme = useColorScheme(); const themeName = props.theme || systemTheme || 'light'; const styles = useMemo(() => getStyles(themeName), [themeName]); useEffect(() => { setLoading(loadingProp); }, [loadingProp]); const calculateDirection = useCallback(async ({ waitForKeyboard }) => { const [, positionY] = await new Promise(resolve => containerRef.current?.measureInWindow((...rect) => resolve(rect))); return new Promise(resolve => { setTimeout(() => { const kbHeight = Keyboard.metrics?.()?.height || 0; const screenHeight = Dimensions.get('window').height; setDirection((screenHeight - kbHeight) / 2 > positionY ? 'down' : 'up'); resolve(); }, waitForKeyboard ? Platform.select({ ios: 600, android: 250, default: 1 }) : 1 // wait for keyboard to show ); }); }, [setDirection]); const onClearPress = useCallback(() => { setSearchText(''); setInputValue(''); setSelectedItem(null); setIsOpened(false); inputRef.current?.blur(); if (typeof onClear === 'function') { onClear(); } }, [onClear]); /** methods */ const close = useCallback(() => { setIsOpened(false); setContent(undefined); }, [setContent]); const blur = useCallback(() => { inputRef.current?.blur(); }, []); const focus = useCallback(() => { inputRef.current?.focus(); }, []); const open = useCallback(async () => { if (directionProp) { setDirection(directionProp); } else { await calculateDirection({ waitForKeyboard: !!inputRef.current?.isFocused() }); } setTimeout(() => { setIsOpened(true); }, 0); }, [calculateDirection, directionProp, setDirection]); const toggle = useCallback(() => { isOpened ? close() : open(); }, [close, isOpened, open]); const clear = useCallback(() => { onClearPress(); }, [onClearPress]); useEffect(() => { if (ref) { if (typeof ref === 'function') { ref(inputRef.current); } else { ref.current = inputRef.current; } } }, [ref]); /** Set initial value */ useEffect(() => { const initialDataSet = initialDataSetRef.current; const initialValue = initialValueRef.current; let initialValueItem; if (typeof initialValue === 'string') { initialValueItem = initialDataSet?.find(el => el.id === initialValue); } else if (typeof initialValue === 'object' && initialValue.id) { initialValueItem = initialDataSet?.find(el => el.id === initialValue?.id); if (!initialValueItem) { // set the item as it is if it's not in the list initialValueItem = initialValue; } } if (initialValueItem) { setSelectedItem(initialValueItem); } }, []); useEffect(() => { return () => { setContent(undefined); setIsOpened(false); }; }, [setContent]); const setInputText = useCallback(text => { setSearchText(text); }, []); const setItem = useCallback(item => { setSelectedItem(item); }, []); useEffect(() => { if (activeControllerRef?.current) { controllerRefs?.current.push(activeControllerRef?.current); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const closeAll = useCallback(() => { controllerRefs?.current.forEach(c => { c?.blur?.(); c?.close?.(); }); }, [controllerRefs]); /** expose controller methods */ useEffect(() => { const methods = activeControllerRef ? { close, blur, focus, open, toggle, clear, setInputText, setItem } : null; if (activeControllerRef) { activeControllerRef.current = methods; } if (typeof controller === 'function') { controller(methods); } else if (controller) { controller.current = methods; } }, [blur, focus, clear, close, controller, activeControllerRef, open, setInputText, setItem, toggle]); useEffect(() => { if (selectedItem) { setInputValue(selectedItem.title ?? ''); } else { setInputValue(''); } }, [selectedItem]); useEffect(() => { setInputValue(searchText); }, [searchText]); useEffect(() => { if (typeof onSelectItemProp === 'function') { onSelectItemProp(selectedItem); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedItem]); useEffect(() => { if (typeof onOpenSuggestionsListProp === 'function') { onOpenSuggestionsListProp(isOpened); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOpened]); useEffect(() => { // renew state on close if (!isOpened && selectedItem && !loading && !inputRef.current?.isFocused()) { setInputValue(selectedItem.title || ''); } }, [isOpened, loading, searchText, selectedItem]); const _onSelectItem = useCallback(item => { setSelectedItem(item); inputRef.current?.blur(); setIsOpened(false); }, []); useEffect(() => { initialDataSetRef.current = dataSetProp; setDataSet(dataSetProp); }, [dataSetProp]); useEffect(() => { const initialDataSet = initialDataSetRef.current; if (!searchText?.length) { setDataSet(initialDataSet); return; } if (!Array.isArray(initialDataSet) || useFilter === false) { return; } let findWhat = caseSensitive ? searchText : searchText.toLowerCase(); if (ignoreAccents) { findWhat = diacriticless(findWhat); } if (trimSearchText) { findWhat = findWhat.trim(); } const newSet = initialDataSet.filter(item => { const titleStr = item.title || ''; const title = caseSensitive ? titleStr : titleStr.toLowerCase(); const findWhere = ignoreAccents ? diacriticless(title) : title; if (matchFromStart) { return typeof item.title === 'string' && findWhere.startsWith(findWhat); } else { return typeof item.title === 'string' && findWhere.indexOf(findWhat) !== -1; } }); setDataSet(newSet); }, [ignoreAccents, matchFromStart, caseSensitive, searchText, trimSearchText, useFilter]); const renderItem = useCallback(({ item }) => { if (typeof customRenderItem === 'function') { const EL = customRenderItem(item, searchText); return /*#__PURE__*/_jsx(TouchableOpacity, { onPress: () => _onSelectItem(item), children: EL }); } return /*#__PURE__*/_jsx(ScrollViewListItem, { title: item.title || '', highlight: searchText, style: suggestionsListTextStyle, theme: themeName, onPress: () => _onSelectItem(item), ignoreAccents: ignoreAccents }, item.id); }, [_onSelectItem, customRenderItem, ignoreAccents, searchText, suggestionsListTextStyle, themeName]); const ListEmptyComponent = useMemo(() => { return EmptyResultComponent ?? /*#__PURE__*/_jsx(NothingFound, { emptyResultText: emptyResultText }); }, [EmptyResultComponent, emptyResultText]); const debouncedEvent = useMemo(() => debounce(text => { if (typeof onTextChange === 'function') { onTextChange(text); } setLoading(false); }, debounceDelay), [debounceDelay, onTextChange]); const onChangeText = useCallback(text => { setSearchText(text); setInputValue(text); setLoading(true); debouncedEvent(text); }, [debouncedEvent]); const onChevronPress = useCallback(() => { toggle(); Keyboard.dismiss(); if (typeof onChevronPressProp === 'function') { onChevronPressProp(); } }, [onChevronPressProp, toggle]); const onFocus = useCallback(e => { if (clearOnFocus) { setSearchText(''); setInputValue(''); } if (typeof onFocusProp === 'function') { onFocusProp(e); } open(); }, [clearOnFocus, onFocusProp, open]); const onBlur = useCallback(e => { if (typeof onBlurProp === 'function') { onBlurProp(e); } }, [onBlurProp]); const onSubmit = useCallback(e => { inputRef.current?.blur(); if (closeOnSubmit) { close(); } if (typeof onSubmitProp === 'function') { onSubmitProp(e); } }, [close, closeOnSubmit, onSubmitProp]); const onPressOut = useCallback(e => { closeAll(); if (editable) { inputRef?.current?.focus(); } else { toggle(); } }, [closeAll, editable, toggle]); useEffect(() => { if (!content && !inputRef.current?.isFocused() || loading) { const db = debounce(() => { setIsOpened(false); }, 100); db(); return () => { db.cancel(); }; } }, [content, loading]); useEffect(() => { // searchTextRef if (searchText && inputRef.current?.isFocused() && !loading) { setIsOpened(true); } }, [loading, searchText]); useEffect(() => { if (isOpened && Array.isArray(dataSet)) { if (activeInputContainerRef) { activeInputContainerRef.current = containerRef.current; } setContent(/*#__PURE__*/_jsx(Dropdown, { dataSet: dataSet, direction: direction, flatListProps: props.flatListProps, inputHeight: inputHeight, suggestionsListContainerStyle: props.suggestionsListContainerStyle, suggestionsListMaxHeight: suggestionsListMaxHeight, renderItem: renderItem, ItemSeparatorComponent: props.ItemSeparatorComponent, ListEmptyComponent: ListEmptyComponent, theme: themeName })); } else { setContent(undefined); } }, [ListEmptyComponent, activeInputContainerRef, dataSet, direction, inputHeight, isOpened, props.suggestionsListContainerStyle, props.flatListProps, props.ItemSeparatorComponent, renderItem, setContent, suggestionsListMaxHeight, themeName]); return /*#__PURE__*/_jsx(View, { onStartShouldSetResponder: () => true, onTouchEnd: e => { e.stopPropagation(); }, style: [styles.container, containerStyle], children: /*#__PURE__*/_jsxs(View, { ref: containerRef, onLayout: _ => {} // it's necessary use onLayout here for Androd (bug?) , style: [styles.inputContainerStyle, inputContainerStyle], children: [LeftComponent, /*#__PURE__*/_jsx(Pressable, { style: styles.pressable, pointerEvents: Platform.select({ ios: 'box-only', default: 'auto' }), onPressOut: onPressOut, children: /*#__PURE__*/_jsx(InputComponent, { ref: inputRef, value: inputValue, onChangeText: onChangeText, autoCorrect: false, editable: editable, onBlur: onBlur, onFocus: onFocus, onSubmitEditing: onSubmit, placeholderTextColor: theme[themeName].inputPlaceholderColor, ...textInputProps, style: [styles.input, { height: inputHeight }, (textInputProps ?? {}).style] }) }), /*#__PURE__*/_jsx(RightButton, { isOpened: isOpened, inputHeight: inputHeight, onClearPress: onClearPress, onChevronPress: onChevronPress, showChevron: showChevron ?? true, showClear: showClear ?? (!!searchText || !!selectedItem), enableLoadingIndicator: enableLoadingIndicator, loading: loading, buttonsContainerStyle: rightButtonsContainerStyle, ChevronIconComponent: ChevronIconComponent, ClearIconComponent: ClearIconComponent, RightIconComponent: RightIconComponent, onRightIconComponentPress: onRightIconComponentPress })] }) }); }); const getStyles = (themeName = 'light') => ScaledSheet.create({ container: { marginVertical: 2 }, inputContainerStyle: { display: 'flex', flexDirection: 'row', backgroundColor: theme[themeName].inputBackgroundColor, borderRadius: 5, overflow: 'hidden' }, input: { flexGrow: 1, flexShrink: 1, overflow: 'hidden', paddingHorizontal: 13, fontSize: 16, color: theme[themeName].inputTextColor }, pressable: { flexGrow: 1, flexShrink: 1 } }); //# sourceMappingURL=index.js.map