UNPKG

react-native-dropdown-picker-plus

Version:

A single / multiple, categorizable, customizable, localizable and searchable item picker (drop-down) component for react native which supports both Android & iOS.

1,957 lines (1,747 loc) 52.3 kB
import React, { Fragment, JSX, memo, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { ActivityIndicator, BackHandler, Dimensions, FlatList, Image, Modal, Platform, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native'; import { moderateScale } from 'react-native-size-matters'; import { ASCII_CODE, BADGE_COLORS, BADGE_DOT_COLORS, DROPDOWN_DIRECTION, GET_DROPDOWN_DIRECTION, GET_TRANSLATION, LANGUAGE, LIST_MODE, MODE, RTL_DIRECTION, RTL_STYLE, SCHEMA, TRANSLATIONS, } from '../constants'; import Colors from '../constants/colors'; import THEMES from '../themes'; import ListEmpty from './ListEmpty'; import RenderBadgeItem from './RenderBadgeItem'; import RenderListItem from './RenderListItem'; import PickerLabel from './PickerLabel'; const { distance, closest } = require('fastest-levenshtein'); const { height: WINDOW_HEIGHT } = Dimensions.get('window'); function Picker({ items = [], setItems = () => {}, open, setOpen = () => {}, value = null, setValue = callback => {}, activityIndicatorColor = Colors.GREY, ActivityIndicatorComponent = null, activityIndicatorSize = 30, allowFontScaling = false, addCustomItem = false, ArrowDownIconComponent = null, arrowIconContainerStyle = {}, arrowIconStyle = {}, ArrowUpIconComponent = null, autoScroll = false, badgeColors = BADGE_COLORS, badgeDotColors = BADGE_DOT_COLORS, badgeDotStyle = {}, badgeProps = {}, badgeSeparatorStyle = {}, badgeStyle = {}, badgeTextStyle = {}, bottomOffset = 0, categorySelectable = true, closeAfterSelecting = true, CloseIconComponent = null, closeIconContainerStyle = {}, closeIconStyle = {}, closeOnBackPressed = false, closeIconTestID, containerProps = {}, containerStyle = {}, customItemContainerStyle = {}, customItemLabelStyle = {}, disableBorderRadius = true, disabled = false, disabledItemContainerStyle = {}, disabledItemLabelStyle = {}, disabledStyle = {}, disableLocalSearch = false, dropDownContainerStyle = {}, dropDownDirection = DROPDOWN_DIRECTION.DEFAULT, dropDownLabelContainerStyle = {}, dropDownLabelTextStyle = {}, dropDownLabelY = 0, extendableBadgeContainer = false, flatListProps = {}, hidden = false, hideSelectedItemIcon = false, hideListItemsIcons = false, iconContainerStyle = {}, itemKey = null, itemLabelProps = {}, itemProps = {}, itemSeparator = false, itemSeparatorStyle = {}, label = '', labelProps = {}, language = LANGUAGE.DEFAULT, leftComponent = undefined, leftComponentIndentLabel = true, listChildContainerStyle = {}, listChildLabelStyle = {}, ListEmptyComponent = null, listItemContainerStyle = {}, listItemLabelStyle = {}, listMessageContainerStyle = {}, listMessageTextStyle = {}, listMode = LIST_MODE.DEFAULT, listParentContainerStyle = {}, listParentLabelStyle = {}, loading = false, max = null, maxHeight = 200, min = null, modalAnimationType = 'none', modalContentContainerStyle = {}, modalProps = {}, modalTitle, modalTitleStyle = {}, modalTitleContainerStyle = {}, mode = MODE.DEFAULT, multiple = false, multipleText = null, onChangeSearchText = text => {}, onChangeValue = value => {}, onClose = () => {}, onDirectionChanged = direction => {}, onLayout = e => {}, onOpen = () => {}, onPress = open => {}, onSelectItem = item => {}, placeholder = null, placeholderStyle = {}, props = {}, renderBadgeItem = null, renderListItem = null, rtl = false, schema = {}, scrollViewProps = {}, searchable = false, searchContainerStyle = {}, searchPlaceholder = null, searchPlaceholderTextColor = Colors.GREY, searchTextInputProps = {}, searchTextInputStyle = {}, searchWithRegionalAccents = false, selectedItemContainerStyle = {}, selectedItemLabelStyle = {}, showArrowIcon = true, showBadgeDot = true, showTickIcon = true, stickyHeader = false, style = {}, testID, textStyle = {}, theme = THEMES.DEFAULT, TickIconComponent = null, tickIconContainerStyle = {}, tickIconStyle = {}, translation = {}, zIndex = 5000, zIndexInverse = 6000, }) { const [necessaryItems, setNecessaryItems] = useState([]); const [searchText, setSearchText] = useState(''); const [pickerHeight, setPickerHeight] = useState(0); const [direction, setDirection] = useState(GET_DROPDOWN_DIRECTION(dropDownDirection)); const badgeFlatListRef = useRef(); const pickerRef = useRef(null); const initializationRef = useRef(false); const itemPositionsRef = useRef({}); const flatListRef = useRef(); const scrollViewRef = useRef(); const memoryRef = useRef({ items: [], value: null, }); const THEME = useMemo(() => THEMES[theme].default, [theme]); const ICON = useMemo(() => THEMES[theme].ICONS, [theme]); /** * The item schema. * @returns {object} */ const ITEM_SCHEMA = useMemo(() => ({ ...SCHEMA, ...schema }), [schema]); /** * componentDidMount. */ useEffect(() => { if (multiple) { memoryRef.current.value = Array.isArray(value) ? value : []; } else memoryRef.current.value = value; // Get initial selected items let initialSelectedItems = []; const valueNotNull = value !== null && Array.isArray(value) && value.length !== 0; if (valueNotNull) { if (multiple) { initialSelectedItems = items.filter(item => value.includes(item[ITEM_SCHEMA.value])); } else { initialSelectedItems = items.find(item => item[ITEM_SCHEMA.value] === value); } } setNecessaryItems(initialSelectedItems); }, []); useEffect(() => { if (closeOnBackPressed && open) { const backAction = () => { setOpen(false); return true; }; const backHandler = BackHandler.addEventListener('hardwareBackPress', backAction); return () => backHandler.remove(); } }, [open]); /** * Update necessary items. */ useEffect(() => { setNecessaryItems(state => [...state].map(item => { const _item = items.find(x => x[ITEM_SCHEMA.value] === item[ITEM_SCHEMA.value]); if (_item) { return { ...item, ..._item }; } return item; }), ); }, [items]); /** * Sync necessary items. */ useEffect(() => { if (multiple) { setNecessaryItems(state => { if (value === null || (Array.isArray(value) && value.length === 0)) return []; const newState = [...state].filter(item => value.includes(item[ITEM_SCHEMA.value])); const newItems = value.reduce((accumulator, currentValue) => { const itemIndex = newState.findIndex(item => item[ITEM_SCHEMA.value] === currentValue); if (itemIndex === -1) { const item = items.find(item => item[ITEM_SCHEMA.value] === currentValue); if (item) { return [...accumulator, item]; } return accumulator; } return accumulator; }, []); return [...newState, ...newItems]; }); } else { const state = []; if (value !== null) { const item = items.find(item => item[ITEM_SCHEMA.value] === value); if (item) { state.push(item); } } setNecessaryItems(state); } if (initializationRef.current) { onChangeValue(value); } else { initializationRef.current = true; } }, [value, items]); /** * Update value in the memory. */ useEffect(() => { memoryRef.current.value = value; }, [value]); /** * Update items in the memory. */ useEffect(() => { memoryRef.current.items = necessaryItems; }, [necessaryItems]); /** * Automatically scroll to the first selected item. */ useEffect(() => { if (open && autoScroll) { scroll(); } }, [open]); /** * dropDownDirection changed. */ useEffect(() => { setDirection(GET_DROPDOWN_DIRECTION(dropDownDirection)); }, [dropDownDirection]); /** * mode changed. */ useEffect(() => { if (mode === MODE.SIMPLE) badgeFlatListRef.current = null; }, [mode]); /** * onPressClose. */ const onPressClose = useCallback(() => { setOpen(false); setSearchText(''); onClose(); }, [setOpen, onClose]); /** * onPressClose. */ const onPressOpen = useCallback(() => { setOpen(true); onOpen(); }, [setOpen, onOpen]); /** * onPressToggle. */ const onPressToggle = useCallback(() => { const isOpen = !open; setOpen(isOpen); setSearchText(''); if (isOpen) onOpen(); else onClose(); return isOpen; }, [open, setOpen, onOpen, onClose]); /** * The sorted items. * @returns {object} */ const sortedItems = useMemo(() => { const sortedItems = items.filter( item => item[ITEM_SCHEMA.parent] === undefined || item[ITEM_SCHEMA.parent] === null, ); const children = items.filter( item => item[ITEM_SCHEMA.parent] !== undefined && item[ITEM_SCHEMA.parent] !== null, ); children.forEach(child => { const index = sortedItems.findIndex( item => item[ITEM_SCHEMA.parent] === child[ITEM_SCHEMA.parent] || item[ITEM_SCHEMA.value] === child[ITEM_SCHEMA.parent], ); if (index > -1) { sortedItems.splice(index + 1, 0, child); } }); return sortedItems; }, [items, ITEM_SCHEMA.parent, ITEM_SCHEMA.value]); /** * Scroll to the first selected item. */ const scroll = useCallback(() => { setTimeout(() => { if (scrollViewRef.current || flatListRef.current) { const isArray = Array.isArray(memoryRef.current.value); if (memoryRef.current.value === null || (isArray && memoryRef.current.value.length === 0)) return; const value = isArray ? memoryRef.current.value[0] : memoryRef.current.value; if (scrollViewRef.current && itemPositionsRef.current.hasOwnProperty(value)) { scrollViewRef.current?.scrollTo?.({ x: 0, y: itemPositionsRef.current[value], animated: true, }); } else { const index = sortedItems.findIndex(item => item[ITEM_SCHEMA.value] === value); if (index > -1) flatListRef.current?.scrollToIndex?.({ index, animated: true, }); } } }, 200); }, [sortedItems, ITEM_SCHEMA.value]); /** * onScrollToIndexFailed. */ const onScrollToIndexFailed = useCallback(({ averageItemLength, index }) => { flatListRef.current.scrollToOffset?.({ offset: averageItemLength * index, animated: true, }); }, []); /** * The indices of all parent items. * @returns {object} */ const stickyHeaderIndices = useMemo(() => { const stickyHeaderIndices = []; if (stickyHeader) { const parents = sortedItems.filter( item => item[ITEM_SCHEMA.parent] === undefined || item[ITEM_SCHEMA.parent] === null, ); parents.forEach(parent => { const index = sortedItems.findIndex( item => item[ITEM_SCHEMA.value] === parent[ITEM_SCHEMA.value], ); if (index > -1) stickyHeaderIndices.push(index); }); } return stickyHeaderIndices; }, [stickyHeader, sortedItems, ITEM_SCHEMA.parent, ITEM_SCHEMA.value]); /** * The items. * @returns {object} */ const _items = useMemo(() => { if (searchText.length === 0) { return sortedItems; } if (disableLocalSearch) return sortedItems; const normalizeText = text => text.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); let results = sortedItems .filter(item => { const label = String(item[ITEM_SCHEMA.label]).toLowerCase(); return normalizeText(label).includes(searchText.toLowerCase()); }) .sort((a, b) => { const labelA = normalizeText(String(a[ITEM_SCHEMA.label]).toLowerCase()); const labelB = normalizeText(String(b[ITEM_SCHEMA.label]).toLowerCase()); // Calculate distances from searchText const distA = distance(searchText.toLowerCase(), labelA); const distB = distance(searchText.toLowerCase(), labelB); // If distances are the same, compare by length if (distA === distB) { return labelA.length - labelB.length; } // Otherwise, sort by distance return distA - distB; }); const values = []; results.forEach((item, index) => { if ( item[ITEM_SCHEMA.parent] === undefined || item[ITEM_SCHEMA.parent] === null || values.includes(item[ITEM_SCHEMA.parent]) ) return; const parent = sortedItems.find(x => x[ITEM_SCHEMA.value] === item[ITEM_SCHEMA.parent]); values.push(item[ITEM_SCHEMA.parent]); results.splice(index, 0, parent); }); if ( (results.length === 0 || results.findIndex( item => String(item[ITEM_SCHEMA.label]).toLowerCase() === searchText.toLowerCase(), ) === -1) && addCustomItem ) { results.push({ [ITEM_SCHEMA.label]: searchText, [ITEM_SCHEMA.value]: searchText.replace(' ', '-'), custom: true, }); } return results; }, [ sortedItems, searchText, addCustomItem, disableLocalSearch, ITEM_SCHEMA.label, searchWithRegionalAccents, ITEM_SCHEMA.value, ITEM_SCHEMA.parent, ]); /** * The value. * @returns {string|object|null}} */ const _value = useMemo(() => { if (multiple) { return value === null ? [] : [...new Set(value)]; } return value; }, [value, multiple]); /** * Selected items only for multiple items. * @returns {object} */ const selectedItems = useMemo(() => { if (!multiple) return []; return necessaryItems.filter(item => _value.includes(item[ITEM_SCHEMA.value])); }, [necessaryItems, _value, multiple, ITEM_SCHEMA.value]); /** * The language. * @returns {string} */ const _language = useMemo(() => { if (TRANSLATIONS.hasOwnProperty(language)) return language; return LANGUAGE.FALLBACK; }, [language]); /** * Get translation. */ const _ = useCallback( key => GET_TRANSLATION(key, _language, translation), [_language, translation], ); /** * The placeholder. * @returns {string} */ const _placeholder = useMemo(() => placeholder ?? _('PLACEHOLDER'), [placeholder, _]); /** * The multiple text. * @returns {string} */ const _multipleText = useMemo( () => multipleText ?? _('SELECTED_ITEMS_COUNT_TEXT'), [multipleText, _], ); /** * The mode. * @returns {string} */ const _mode = useMemo(() => { try { return mode; } catch (e) { return MODE.SIMPLE; } }, [mode]); /** * Indicates whether the value is null. * @returns {boolean} */ const isNull = useMemo(() => { if (_value === null || (Array.isArray(_value) && _value.length === 0)) return true; return necessaryItems.length === 0; }, [_value, necessaryItems.length]); /** * Get the selected item. * @returns {object} */ const getSelectedItem = useCallback(() => { if (multiple) return _value; if (isNull) return null; try { return necessaryItems.find(item => item[ITEM_SCHEMA.value] === _value); } catch (e) { return null; } }, [_value, necessaryItems, isNull, multiple, ITEM_SCHEMA.value]); /** * Get the label of the selected item. * @param {string|null} fallback * @returns {string} */ const getLabel = useCallback( (fallback = null) => { const item = getSelectedItem(); if (multiple) if (item.length > 0) { let mtext = _multipleText; if (typeof mtext !== 'string') { mtext = mtext[item.length] ?? mtext.n; } return mtext.replace('{count}', item.length); } else return fallback; try { return item[ITEM_SCHEMA.label]; } catch (e) { return fallback; } }, [getSelectedItem, multiple, _multipleText, ITEM_SCHEMA.label], ); /** * The label of the selected item / placeholder. */ const _selectedItemLabel = useMemo(() => getLabel(_placeholder), [getLabel, _placeholder]); const [labelIndentWidth, setLabelIndentWidth] = useState(0); /** * The icon of the selected item. */ const _selectedItemIcon = useCallback(() => { if (multiple) return null; const item = getSelectedItem(); try { return item[ITEM_SCHEMA.icon] ?? null; } catch (e) { return null; } }, [getSelectedItem, multiple, ITEM_SCHEMA.icon]); /** * onPress. */ const __onPress = useCallback(async () => { const isOpen = !open; onPress(isOpen); if (isOpen && dropDownDirection === DROPDOWN_DIRECTION.AUTO) { const [, y] = await new Promise(resolve => pickerRef.current.measureInWindow((...args) => resolve(args)), ); const size = y + maxHeight + pickerHeight + bottomOffset; const direction = size < WINDOW_HEIGHT ? 'top' : 'bottom'; onDirectionChanged(direction); setDirection(direction); } onPressToggle(); }, [ open, onPress, onDirectionChanged, maxHeight, pickerHeight, bottomOffset, dropDownDirection, WINDOW_HEIGHT, ]); /** * onLayout. */ const __onLayout = useCallback( e => { if (Platform.OS !== 'web') e.persist(); onLayout(e); setPickerHeight(e.nativeEvent.layout.height); }, [onLayout], ); /** * Disable borderRadius for the picker. * @returns {object} */ const pickerNoBorderRadius = useMemo(() => { if (listMode === LIST_MODE.MODAL) return null; if (disableBorderRadius && open) { return direction === 'top' ? { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, } : { borderTopLeftRadius: 0, borderTopRightRadius: 0, }; } return {}; }, [disableBorderRadius, open, direction, listMode]); /** * Disable borderRadius for the drop down. * @returns {object} */ const dropDownNoBorderRadius = useMemo(() => { if (listMode === LIST_MODE.MODAL) return null; if (disableBorderRadius && open) { return direction === 'top' ? { borderTopLeftRadius: 0, borderTopRightRadius: 0, } : { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, }; } }, [disableBorderRadius, open, direction, listMode]); /** * The disabled style. * @returns {object} */ const _disabledStyle = useMemo(() => disabled && disabledStyle, [disabled]); /** * The zIndex. * @returns {number} */ const _zIndex = useMemo(() => { if (open) { return direction === 'top' ? zIndex : zIndexInverse; } return zIndex; }, [zIndex, direction, open]); /** * The style. * @returns {object} */ const _style = useMemo( () => [ RTL_DIRECTION(rtl, THEME.style), { zIndex: _zIndex, }, ...[style].flat(), ...[_disabledStyle].flat(), pickerNoBorderRadius, ], [rtl, style, _disabledStyle, pickerNoBorderRadius, _zIndex, THEME.style], ); /** * The placeholder style. * @returns {object} */ const _placeholderStyle = useMemo(() => isNull && placeholderStyle, [isNull, placeholderStyle]); /** * The style of the label. * @returns {object} */ const _displayValueStyle = useMemo( () => [THEME.label, ...[textStyle].flat(), ...[_placeholderStyle].flat()], [textStyle, _placeholderStyle, THEME.label], ); /** * The arrow icon style. * @returns {object} */ const _arrowIconStyle = useMemo( () => [THEME.arrowIcon, ...[arrowIconStyle].flat()], [arrowIconStyle, THEME.arrowIcon], ); /** * The dropdown container style. * @returns {object} */ const _dropDownContainerStyle = useMemo( () => [ THEME.dropDownContainer, { [direction]: pickerHeight - 1, maxHeight, zIndex: _zIndex, }, ...[dropDownContainerStyle].flat(), dropDownNoBorderRadius, ], [ direction, dropDownContainerStyle, dropDownNoBorderRadius, maxHeight, pickerHeight, _zIndex, THEME.dropDownContainer, ], ); /** * The modal content container style. * @returns {object} */ const _modalContentContainerStyle = useMemo( () => [THEME.modalContentContainer, ...[modalContentContainerStyle].flat()], [modalContentContainerStyle, THEME.modalContentContainer], ); /** * The zIndex of the container. * @returns {object} */ const zIndexContainer = useMemo( () => Platform.OS !== 'android' && { zIndex: _zIndex, }, [_zIndex], ); /** * The container style. * @returns {object} */ const _containerStyle = useMemo( () => [THEME.container, zIndexContainer, ...[containerStyle].flat()], [zIndexContainer, containerStyle, THEME.container], ); /** * The arrow icon container style. * @returns {object} */ const _arrowIconContainerStyle = useMemo( () => [RTL_STYLE(rtl, THEME.arrowIconContainer), ...[arrowIconContainerStyle].flat()], [rtl, arrowIconContainerStyle, THEME.arrowIconContainer], ); /** * The arrow component. * @returns {JSX.Element} */ const _ArrowComponent = useMemo(() => { if (!showArrowIcon) return null; let Component; if (open && ArrowUpIconComponent !== null) Component = <ArrowUpIconComponent style={_arrowIconStyle} />; else if (!open && ArrowDownIconComponent !== null) Component = <ArrowDownIconComponent style={_arrowIconStyle} />; else Component = <Image source={open ? ICON.ARROW_UP : ICON.ARROW_DOWN} style={_arrowIconStyle} />; return <View style={_arrowIconContainerStyle}>{Component}</View>; }, [ showArrowIcon, open, ArrowUpIconComponent, ArrowDownIconComponent, _arrowIconStyle, _arrowIconContainerStyle, ICON.ARROW_UP, ICON.ARROW_DOWN, ]); /** * The icon container style. * @returns {object} */ const _iconContainerStyle = useMemo( () => [RTL_STYLE(rtl, THEME.iconContainer), ...[iconContainerStyle].flat()], [rtl, iconContainerStyle, THEME.iconContainer], ); /** * The selected item icon component. * @returns {JSX.Element|null} */ const SelectedItemIconComponent = useMemo(() => { const Component = _selectedItemIcon(); if (hideSelectedItemIcon) return null; return ( Component !== null && ( <View style={_iconContainerStyle}> <Component /> </View> ) ); }, [_selectedItemIcon, hideSelectedItemIcon, _iconContainerStyle]); /** * The simple body component. * @returns {JSX.Element} */ const SimpleBodyComponent = useMemo( () => ( <> <View onLayout={event => { const { width } = event.nativeEvent.layout; if (leftComponentIndentLabel) setLabelIndentWidth(width); }} > {leftComponent || SelectedItemIconComponent} </View> <Text style={_displayValueStyle} allowFontScaling={allowFontScaling} {...labelProps}> {_selectedItemLabel} </Text> </> ), [leftComponent, SelectedItemIconComponent, _displayValueStyle, labelProps, _selectedItemLabel], ); /** * onPress badge. */ const onPressBadge = useCallback( badgeValue => { setValue(state => { const newState = [...state]; newState.filter(nsItem => nsItem !== badgeValue); return newState; }); }, [setValue], ); /** * The badge colors. * @returns {object} */ const _badgeColors = useMemo(() => { if (typeof badgeColors === 'string') return [badgeColors]; return badgeColors; }, [badgeColors]); /** * The badge dot colors. * @returns {object} */ const _badgeDotColors = useMemo(() => { if (typeof badgeDotColors === 'string') return [badgeDotColors]; return badgeDotColors; }, [badgeDotColors]); /** * Get badge color. * @param {string} str * @returns {string} */ const getBadgeColor = useCallback( str => { str = `${str}`; const index = Math.abs(ASCII_CODE(str)) % _badgeColors.length; return _badgeColors[index]; }, [_badgeColors], ); /** * Get badge dot color. * @param {string} str * @returns {string} */ const getBadgeDotColor = useCallback( str => { str = `${str}`; const index = Math.abs(ASCII_CODE(str)) % _badgeDotColors.length; return _badgeDotColors[index]; }, [_badgeDotColors], ); /** * The render badge component. * @returns {JSX.Element} */ const RenderBadgeComponent = useMemo( () => (renderBadgeItem !== null ? renderBadgeItem : RenderBadgeItem), [renderBadgeItem], ); /** * Render badge. * @returns {JSX.Element} */ const __renderBadge = useCallback( ({ item }) => ( <RenderBadgeComponent props={badgeProps} rtl={rtl} label={item[ITEM_SCHEMA.label]} value={item[ITEM_SCHEMA.value]} IconComponent={item[ITEM_SCHEMA.icon] ?? null} textStyle={textStyle} badgeStyle={badgeStyle} badgeTextStyle={badgeTextStyle} badgeDotStyle={badgeDotStyle} getBadgeColor={getBadgeColor} getBadgeDotColor={getBadgeDotColor} showBadgeDot={showBadgeDot} onPress={onPressBadge} theme={theme} THEME={THEME} /> ), [ badgeDotStyle, badgeStyle, badgeTextStyle, getBadgeColor, getBadgeDotColor, onPressBadge, rtl, showBadgeDot, textStyle, THEME, theme, badgeProps, ITEM_SCHEMA.label, ITEM_SCHEMA.value, ITEM_SCHEMA.icon, ], ); /** * The badge key. * @returns {string} */ const _itemKey = useMemo(() => { if (itemKey === null) return ITEM_SCHEMA.value; return itemKey; }, [itemKey, ITEM_SCHEMA.value]); /** * The key extractor. * @returns {string} */ const keyExtractor = useCallback(item => `${item[_itemKey]}`, [_itemKey]); /** * The badge separator style. * @returns {object} */ const _badgeSeparatorStyle = useMemo( () => [THEME.badgeSeparator, ...[badgeSeparatorStyle].flat()], [badgeSeparatorStyle, THEME.badgeSeparator], ); /** * The badge separator component. * @returns {JSX.Element} */ const BadgeSeparatorComponent = useCallback( () => <View style={_badgeSeparatorStyle} />, [_badgeSeparatorStyle], ); /** * The label container style. * @returns {object} */ const labelContainerStyle = useMemo( () => [ THEME.labelContainer, rtl && { transform: [{ scaleX: -1 }], }, ], [rtl, THEME.labelContainer], ); /** * Badge list empty component. * @returns {JSX.Element} */ const BadgeListEmptyComponent = useCallback( () => ( <View style={labelContainerStyle}> <Text style={_displayValueStyle} allowFontScaling={allowFontScaling} {...labelProps}> {_placeholder} </Text> </View> ), [_displayValueStyle, labelContainerStyle, labelProps, _placeholder], ); /** * Set ref. */ const setBadgeFlatListRef = useCallback(ref => { badgeFlatListRef.current = ref; }, []); /** * The extendable badge container style. * @returns {object} */ const extendableBadgeContainerStyle = useMemo( () => [RTL_DIRECTION(rtl, THEME.extendableBadgeContainer)], [rtl, THEME.extendableBadgeContainer], ); /** * The extendable badge item container style. * @returns {object} */ const extendableBadgeItemContainerStyle = useMemo( () => [ THEME.extendableBadgeItemContainer, rtl && { marginEnd: 0, marginStart: THEME.extendableBadgeItemContainer.marginEnd, }, ], [rtl, THEME.extendableBadgeItemContainer], ); /** * Extendable badge container. * @returns {JSX.Element} */ const ExtendableBadgeContainer = useCallback( ({ selectedItems }) => { if (selectedItems.length > 0) { return ( <View style={extendableBadgeContainerStyle}> {selectedItems.map((item, index) => ( <View key={index} style={extendableBadgeItemContainerStyle}> <__renderBadge item={item} /> </View> ))} </View> ); } return <BadgeListEmptyComponent />; }, [extendableBadgeContainerStyle, extendableBadgeItemContainerStyle], ); /** * The badge body component. * @returns {JSX.Element} */ const BadgeBodyComponent = useMemo(() => { if (extendableBadgeContainer) { return <ExtendableBadgeContainer selectedItems={selectedItems} />; } return ( <FlatList ref={setBadgeFlatListRef} data={selectedItems} renderItem={__renderBadge} horizontal showsHorizontalScrollIndicator={false} keyExtractor={keyExtractor} ItemSeparatorComponent={BadgeSeparatorComponent} ListEmptyComponent={BadgeListEmptyComponent} style={THEME.listBody} contentContainerStyle={THEME.listBodyContainer} inverted={rtl} /> ); }, [ rtl, extendableBadgeContainer, selectedItems, __renderBadge, keyExtractor, BadgeSeparatorComponent, BadgeListEmptyComponent, setBadgeFlatListRef, THEME.listBody, ]); const LoadingBodyComponent = ( <View style={{ flexDirection: 'row' }}> <View style={{ paddingLeft: moderateScale(10), paddingRight: moderateScale(17.5), }} > <ActivityIndicator size={moderateScale(10)} color="#0000ff" /> </View> <Text style={[_displayValueStyle, { flex: 0 }]} allowFontScaling={allowFontScaling} {...labelProps} > Loading </Text> </View> ); /** * The body component. */ const _BodyComponent = useMemo(() => { if (loading) { return LoadingBodyComponent; } switch (_mode) { case MODE.SIMPLE: return SimpleBodyComponent; case MODE.BADGE: return multiple ? BadgeBodyComponent : SimpleBodyComponent; default: // } }, [_mode, SimpleBodyComponent, BadgeBodyComponent, multiple]); /** * The list item container style. * @returns {object} */ const _listItemContainerStyle = useMemo( () => [ RTL_DIRECTION(rtl, THEME.listItemContainer), ...[listItemContainerStyle].flat(), stickyHeader && { backgroundColor: THEME.style.backgroundColor }, ], [ rtl, listItemContainerStyle, THEME.listItemContainer, stickyHeader, THEME.style.backgroundColor, ], ); /** * The tick icon container style. * @returns {object} */ const _tickIconContainerStyle = useMemo( () => [RTL_STYLE(rtl, THEME.tickIconContainer), ...[tickIconContainerStyle].flat()], [rtl, tickIconContainerStyle, THEME.tickIconContainer], ); /** * The list item label style. * @returns {object} */ const _listItemLabelStyle = useMemo( () => [THEME.listItemLabel, ...[textStyle].flat(), ...[listItemLabelStyle].flat()], [textStyle, listItemLabelStyle, THEME.listItemLabel], ); /** * The tick icon style. * @returns {object} */ const _tickIconStyle = useMemo( () => [THEME.tickIcon, ...[tickIconStyle].flat()], [tickIconStyle, THEME.tickIcon], ); /** * The search container style. * @returns {object} */ const _searchContainerStyle = useMemo( () => [ RTL_DIRECTION(rtl, THEME.searchContainer), ...[searchContainerStyle].flat(), !searchable && !modalTitle && listMode === LIST_MODE.MODAL && { flexDirection: 'row-reverse', }, ], [rtl, listMode, searchable, modalTitle, searchContainerStyle, THEME.searchContainer], ); /** * The search text input style. * @returns {object} */ const _searchTextInputStyle = useMemo( () => [textStyle, THEME.searchTextInput, ...[searchTextInputStyle].flat()], [textStyle, searchTextInputStyle, THEME.searchTextInput], ); /** * The close icon container style. * @returns {object} */ const _closeIconContainerStyle = useMemo( () => [RTL_STYLE(rtl, THEME.closeIconContainer), ...[closeIconContainerStyle].flat()], [rtl, closeIconContainerStyle, THEME.closeIconContainer], ); /** * The close icon style. * @returns {object} */ const _closeIconStyle = useMemo( () => [THEME.closeIcon, ...[closeIconStyle].flat()], [closeIconStyle, THEME.closeIcon], ); /** * The list message container style. * @returns {objects} */ const _listMessageContainerStyle = useMemo( () => [THEME.listMessageContainer, ...[listMessageContainerStyle].flat()], [listMessageContainerStyle, THEME.listMessageContainer], ); /** * The list message text style. * @returns {object} */ let _listMessageTextStyle; _listMessageTextStyle = useMemo( () => [THEME.listMessageText, ...[textStyle].flat(), ...[listMessageTextStyle].flat()], [listMessageTextStyle, THEME.listMessageText, textStyle], ); /** * onPress item. */ const onPressItem = useCallback( (item, customItem = false) => { // if pressed item was a custom item by the user, add it to the list of items (?) if (customItem !== false) { item.custom = false; setItems(state => [...state, item]); } // call onSelectItem() callback for item/s now selected after item press. // Not a reliable method for external value changes. if (multiple) { if (memoryRef.current.value?.includes(item[ITEM_SCHEMA.value])) { const index = memoryRef.current.items.findIndex( x => x[ITEM_SCHEMA.value] === item[ITEM_SCHEMA.value], ); if (index > -1) { memoryRef.current.items.splice(index, 1); onSelectItem(memoryRef.current.items.slice()); } } else { onSelectItem([...memoryRef.current.items, item]); } } else { onSelectItem(item); } setValue(state => { // call setValue() callback to change selected value/s after item press. if (multiple) { const newState = state === null || state === undefined ? [] : [...state]; if (newState.includes(item[ITEM_SCHEMA.value])) { // if value already included, remove it if doing so wouldn't go under min number if (!Number.isInteger(min) || min < newState.length) { newState.splice(newState.indexOf(item[ITEM_SCHEMA.value]), 1); } } else if (!Number.isInteger(max) || max > newState.length) { // if value not already included, add it if doing so wouldn't go above max number newState.push(item[ITEM_SCHEMA.value]); } return newState; } return item[ITEM_SCHEMA.value]; // single-value picker }); // adjust necessary items after item press. // if single-item picker, set necessary items with array whose only element is the item pressed. // if multi-item picker, if item in necessary items remove it or if not then add it, within min/max constraints setNecessaryItems(state => { if (multiple) { const newState = [...state]; const itemIndex = newState.findIndex( x => x[ITEM_SCHEMA.value] === item[ITEM_SCHEMA.value], ); if (itemIndex > -1) { // If pressed item already in necessary items, remove it if doing so doesn't go below min number of items if (!Number.isInteger(min) || min < newState.length) { newState.splice(itemIndex, 1); } } else if (!Number.isInteger(max) || max > newState.length) { // If pressed item not already in necessary items, add it if doing so doesn't go above max number of items newState.push(item); } return newState; } // if a single-item picker, set pressed item as array of only necessary item return [item]; }); // if picker is a single-item picker and to close after an item gets selected, close it since press selected item. if (closeAfterSelecting && !multiple) onPressClose(); }, [ closeAfterSelecting, max, min, multiple, onPressClose, onSelectItem, setItems, setValue, ITEM_SCHEMA.value, ], ); /** * The tick icon component. * @returns {JSX.Element} */ const _TickIconComponent = useCallback(() => { if (!showTickIcon) return null; let Component; if (TickIconComponent !== null) Component = <TickIconComponent style={_tickIconStyle} />; else Component = <Image source={ICON.TICK} style={_tickIconStyle} />; return <View style={_tickIconContainerStyle}>{Component}</View>; }, [TickIconComponent, _tickIconStyle, _tickIconContainerStyle, showTickIcon, ICON.TICK]); /** * The renderItem component. * @returns {JSX.Element} */ const RenderItemComponent = useMemo( () => (renderListItem !== null ? renderListItem : RenderListItem), [renderListItem], ); /** * The selected item container style. * @returns {object} */ const _selectedItemContainerStyle = useMemo( () => [THEME.selectedItemContainer, selectedItemContainerStyle], [selectedItemContainerStyle, THEME.selectedItemContainer], ); /** * The selected item label style. * @returns {object} */ const _selectedItemLabelStyle = useMemo( () => [THEME.selectedItemLabel, selectedItemLabelStyle], [selectedItemLabelStyle, THEME.selectedItemLabel], ); /** * The disabled item container style. * @returns {object} */ const _disabledItemContainerStyle = useMemo( () => [THEME.disabledItemContainer, disabledItemContainerStyle], [disabledItemContainerStyle, THEME.disabledItemContainer], ); /** * The disabled item label style. * @returns {object} */ const _disabledItemLabelStyle = useMemo( () => [THEME.disabledItemContainer, disabledItemLabelStyle], [disabledItemLabelStyle, THEME.disabledItemContainer], ); /** * Set item position. * @param {string|number|boolean} value * @param {number} y */ const setItemPosition = useCallback( (value, y) => { if (autoScroll && listMode === LIST_MODE.SCROLLVIEW) itemPositionsRef.current[value] = y; }, [autoScroll, listMode], ); /** * Render list item. * @returns {JSX.Element} */ const __renderListItem = useCallback( ({ item }) => { let IconComponent = hideListItemsIcons ? null : item[ITEM_SCHEMA.icon] ?? null; if (IconComponent) { IconComponent = ( <View style={_iconContainerStyle}> <IconComponent /> </View> ); } let isSelected; if (multiple) { isSelected = _value.includes(item[ITEM_SCHEMA.value]); } else { isSelected = _value === item[ITEM_SCHEMA.value]; } return ( <RenderItemComponent rtl={rtl} item={item} label={item[ITEM_SCHEMA.label]} value={item[ITEM_SCHEMA.value]} parent={item?.[ITEM_SCHEMA.parent] ?? null} selectable={item?.[ITEM_SCHEMA.selectable] ?? null} disabled={item?.[ITEM_SCHEMA.disabled] ?? false} custom={item.custom ?? false} props={itemProps} labelProps={itemLabelProps} isSelected={isSelected} IconComponent={IconComponent} TickIconComponent={_TickIconComponent} listItemContainerStyle={_listItemContainerStyle} listItemLabelStyle={_listItemLabelStyle} listChildContainerStyle={listChildContainerStyle} listChildLabelStyle={listChildLabelStyle} listParentContainerStyle={listParentContainerStyle} listParentLabelStyle={listParentLabelStyle} customItemContainerStyle={customItemContainerStyle} customItemLabelStyle={customItemLabelStyle} selectedItemContainerStyle={_selectedItemContainerStyle} selectedItemLabelStyle={_selectedItemLabelStyle} disabledItemContainerStyle={_disabledItemContainerStyle} disabledItemLabelStyle={_disabledItemLabelStyle} labelStyle={item?.[ITEM_SCHEMA.labelStyle] ?? {}} containerStyle={item?.[ITEM_SCHEMA.containerStyle] ?? {}} categorySelectable={categorySelectable} onPress={onPressItem} setPosition={setItemPosition} theme={theme} THEME={THEME} /> ); }, [ categorySelectable, customItemContainerStyle, customItemLabelStyle, itemLabelProps, itemProps, listChildContainerStyle, listChildLabelStyle, listParentContainerStyle, listParentLabelStyle, multiple, onPressItem, rtl, theme, THEME, _disabledItemContainerStyle, _disabledItemLabelStyle, _iconContainerStyle, _listItemContainerStyle, _listItemLabelStyle, _selectedItemContainerStyle, _selectedItemLabelStyle, _TickIconComponent, _value, ITEM_SCHEMA.icon, ITEM_SCHEMA.value, ITEM_SCHEMA.label, ITEM_SCHEMA.parent, ITEM_SCHEMA.selectable, ITEM_SCHEMA.disabled, ITEM_SCHEMA.containerStyle, setItemPosition, ], ); /** * The item separator. * @returns {JSX.Element|null} */ const ItemSeparatorComponent = useCallback(() => { if (!itemSeparator) return null; return <View style={[THEME.itemSeparator, ...[itemSeparatorStyle].flat()]} />; }, [itemSeparator, THEME.itemSeparator]); /** * The search placeholder. * @returns {string} */ const _searchPlaceholder = useMemo(() => { if (searchPlaceholder !== null) return searchPlaceholder; return _('SEARCH_PLACEHOLDER'); }, [searchPlaceholder, _]); /** * onChangeSearchText. * @param {string} text */ const _onChangeSearchText = useCallback( text => { setSearchText(text); onChangeSearchText(text); }, [onChangeSearchText], ); /** * The close icon component. * @returns {JSX.Element} */ const _CloseIconComponent = useMemo(() => { if (listMode !== LIST_MODE.MODAL) return null; let Component; if (CloseIconComponent !== null) Component = <CloseIconComponent style={_closeIconStyle} />; else Component = <Image source={ICON.CLOSE} style={_closeIconStyle} />; return ( <TouchableOpacity testID={closeIconTestID} style={_closeIconContainerStyle} onPress={onPressClose} > {Component} </TouchableOpacity> ); }, [ listMode, CloseIconComponent, _closeIconStyle, _closeIconContainerStyle, onPressClose, ICON.CLOSE, closeIconTestID, ]); /** * Indicates if the search component is visible. * @returns {boolean} */ const isSearchComponentVisible = useMemo(() => { if (listMode === LIST_MODE.MODAL) return true; return searchable; }, [listMode, searchable]); /** * modalTitleStyle. * @returns {object} */ const _modalTitleStyle = useMemo( () => [THEME.modalTitle, ...[modalTitleStyle].flat(), ...[textStyle].flat()], [textStyle, modalTitleStyle, THEME.modalTitle], ); /** * The search component. * @returns {JSX.Element} */ const SearchComponent = useMemo( () => isSearchComponentVisible && ( <View style={_searchContainerStyle}> <View style={{ flexDirection: 'row', justifyContent: 'space-between', flex: 1, }} > <View style={{ flexDirection: 'column', width: '80%' }}> {listMode === LIST_MODE.MODAL && ( <View style={{ paddingTop: moderateScale(5), paddingBottom: searchable ? moderateScale(15) : 0, ...modalTitleContainerStyle, }} > <Text style={_modalTitleStyle} allowFontScaling={allowFontScaling}> {modalTitle} </Text> </View> )} {searchable && ( <TextInput value={searchText} onChangeText={_onChangeSearchText} style={_searchTextInputStyle} placeholder={_searchPlaceholder} placeholderTextColor={searchPlaceholderTextColor} allowFontScaling={allowFontScaling} {...searchTextInputProps} /> )} </View> {_CloseIconComponent} </View> </View> ), [ isSearchComponentVisible, listMode, modalTitle, searchable, searchPlaceholderTextColor, searchText, _modalTitleStyle, _onChangeSearchText, _searchContainerStyle, _searchPlaceholder, _searchTextInputStyle, _CloseIconComponent, ], ); /** * The dropdown component wrapper. * @returns {JSX.Element} */ const DropDownComponentWrapper = useCallback( Component => ( <View style={_dropDownContainerStyle}> {SearchComponent} {Component} </View> ), [_dropDownContainerStyle, SearchComponent], ); /** * The ActivityIndicatorComponent. * @returns {JSX.Element} */ const _ActivityIndicatorComponent = useCallback(() => { let Component; if (ActivityIndicatorComponent !== null) Component = ActivityIndicatorComponent; else Component = ActivityIndicator; return <Component size={activityIndicatorSize} color={activityIndicatorColor} />; }, [ActivityIndicatorComponent, activityIndicatorSize, activityIndicatorColor]); /** * The ListEmptyComponent. * @returns {JSX.Element} */ const _ListEmptyComponent = useCallback(() => { let Component; const message = _('NOTHING_TO_SHOW'); if (ListEmptyComponent !== null) Component = ListEmptyComponent; else Component = ListEmpty; return ( <Component listMessageContainerStyle={_listMessageContainerStyle} listMessageTextStyle={_listMessageTextStyle} ActivityIndicatorComponent={_ActivityIndicatorComponent} loading={loading} message={message} allowFontScaling={allowFontScaling} /> ); }, [_, ListEmptyComponent, loading]); /** * onRequestCloseModal. */ const onRequestCloseModal = useCallback(() => { setOpen(false); }, [setOpen]); /** * The dropdown flatlist component. * @returns {JSX.Element} */ const DropDownFlatListComponent = useMemo( () => ( <FlatList ref={flatListRef} style={[ styles.flex, { backgroundColor: 'white', borderRadius: _dropDownContainerStyle.flat()[0].borderRadius, }, ]} contentContainerStyle={THEME.flatListContentContainer} ListEmptyComponent={_ListEmptyComponent} data={_items} renderItem={__renderListItem} keyExtractor={keyExtractor} keyboardShouldPersistTaps={'handled'} extraData={_value} ItemSeparatorComponent={ItemSeparatorComponent} stickyHeaderIndices={stickyHeaderIndices} onScrollToIndexFailed={onScrollToIndexFailed} {...flatListProps} /> ), [ flatListProps, ItemSeparatorComponent, keyExtractor, _items, _ListEmptyComponent, _value, __renderListItem, _dropDownContainerStyle, THEME.flatListContentContainer, stickyHeaderIndices, onScrollToIndexFailed, ], ); /** * The dropdown scrollview component. * @returns {JSX.Element} */ const DropDownScrollViewComponent = useMemo( () => ( <ScrollView ref={scrollViewRef} nestedScrollEnabled stickyHeaderIndices={stickyHeaderIndices} {...scrollViewProps} > {_items.map((item, index) => ( <Fragment key={item[_itemKey]}> {index > 0 && ItemSeparatorComponent()} {__renderListItem({ item })} </Fragment> ))} {_items.length === 0 && _ListEmptyComponent()} </ScrollView> ), [ __renderListItem, _itemKey, scrollViewProps, _ListEmptyComponent, stickyHeaderIndices, _items, ItemSeparatorComponent, ], ); /** * The dropdown modal component. * @returns {JSX.Element} */ const DropDownModalComponent = useMemo( () => ( <Modal animationType={modalAnimationType} visible={open} presentationStyle="fullScreen" onRequestClose={onRequestCloseModal} {...modalProps} > <SafeAreaView style={_modalContentContainerStyle}> {SearchComponent} {DropDownFlatListComponent} </SafeAreaView> </Modal> ), [ open, SearchComponent, _modalContentContainerStyle, modalProps, modalAnimationType, onRequestCloseModal, DropDownFlatListComponent, ], ); /** * The dropdown component. * @r