Skip to content

Instantly share code, notes, and snippets.

@sajjadjaved01
Last active November 14, 2024 07:17
Show Gist options
  • Save sajjadjaved01/f4d8bd0678103fca48c7a2ab67cd94d9 to your computer and use it in GitHub Desktop.
Save sajjadjaved01/f4d8bd0678103fca48c7a2ab67cd94d9 to your computer and use it in GitHub Desktop.
Multiselect / Single-Select dropdown.
import { MaterialIcons } from "@expo/vector-icons";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import {
FlatList,
Image,
Pressable,
StyleSheet,
Text,
TouchableOpacity,
View,
ViewStyle,
TextStyle,
LayoutAnimation,
Platform,
UIManager,
ListRenderItem,
} from "react-native";
import DashedLine from "react-native-dashed-line";
import { RFValue } from "react-native-responsive-fontsize";
// Enable LayoutAnimation for Android
if (Platform.OS === 'android') {
UIManager.setLayoutAnimationEnabledExperimental?.(true);
}
export interface DropdownItem<T = any> {
id: string | number;
label: string;
value: T;
selected?: boolean;
disabled?: boolean;
icon?: string | { uri: string };
meta?: Record<string, any>;
}
export interface DropdownStyles {
container?: ViewStyle;
button?: ViewStyle;
buttonText?: TextStyle;
dropdown?: ViewStyle;
item?: ViewStyle;
itemText?: TextStyle;
selectedItem?: ViewStyle;
selectedItemText?: TextStyle;
disabledItem?: ViewStyle;
disabledItemText?: TextStyle;
separator?: ViewStyle;
icon?: ViewStyle;
}
export interface DropdownProps<T = any> {
items: DropdownItem<T>[];
value?: T | T[];
placeholder?: string;
multiSelect?: boolean;
searchable?: boolean;
maxHeight?: number;
minHeight?: number;
loading?: boolean;
disabled?: boolean;
showIcon?: boolean;
iconPosition?: 'left' | 'right';
customIcon?: React.ReactNode;
styles?: DropdownStyles;
onChange?: (value: T | T[], items: DropdownItem<T>[]) => void;
onOpen?: () => void;
onClose?: () => void;
renderItem?: ListRenderItem<DropdownItem<T>>;
keyExtractor?: (item: DropdownItem<T>) => string;
filterFunction?: (searchText: string, item: DropdownItem<T>) => boolean;
}
function Dropdown<T = any>({
items,
value,
placeholder = "Select",
multiSelect = false,
searchable = false,
maxHeight = 300,
minHeight = 0,
loading = false,
disabled = false,
showIcon = true,
iconPosition = 'right',
customIcon,
styles: customStyles,
onChange,
onOpen,
onClose,
renderItem,
keyExtractor,
filterFunction,
}: DropdownProps<T>) {
const [isOpen, setIsOpen] = useState(false);
const [searchText, setSearchText] = useState("");
const [selectedItems, setSelectedItems] = useState<DropdownItem<T>[]>([]);
// Memoized filtered items
const filteredItems = useMemo(() => {
if (!searchable || !searchText) return items;
return items.filter(item =>
filterFunction
? filterFunction(searchText, item)
: item.label.toLowerCase().includes(searchText.toLowerCase())
);
}, [items, searchText, filterFunction]);
// Initialize selected items based on value prop
useEffect(() => {
const initialSelected = items.filter(item =>
Array.isArray(value)
? value.includes(item.value)
: item.value === value
);
setSelectedItems(initialSelected);
}, [value, items]);
const toggleDropdown = useCallback(() => {
if (disabled) return;
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setIsOpen(prev => !prev);
if (!isOpen) {
onOpen?.();
} else {
onClose?.();
}
}, [disabled, isOpen, onOpen, onClose]);
const handleSelect = useCallback((item: DropdownItem<T>) => {
if (item.disabled) return;
let newSelected: DropdownItem<T>[];
if (multiSelect) {
newSelected = selectedItems.some(i => i.id === item.id)
? selectedItems.filter(i => i.id !== item.id)
: [...selectedItems, item];
} else {
newSelected = [item];
toggleDropdown();
}
setSelectedItems(newSelected);
onChange?.(
multiSelect
? newSelected.map(i => i.value)
: newSelected[0]?.value,
newSelected
);
}, [multiSelect, selectedItems, onChange, toggleDropdown]);
const renderDefaultItem: ListRenderItem<DropdownItem<T>> = ({ item }) => {
const isSelected = selectedItems.some(i => i.id === item.id);
const isDisabled = item.disabled;
return (
<TouchableOpacity
style={[
defaultStyles.item,
customStyles?.item,
isSelected && {...defaultStyles.selectedItem, ...customStyles?.selectedItem},
isDisabled && {...defaultStyles.disabledItem, ...customStyles?.disabledItem}
]}
onPress={() => handleSelect(item)}
disabled={isDisabled}
>
<View style={defaultStyles.itemContent}>
{item.icon && (
<Image
source={typeof item.icon === 'string' ? { uri: item.icon } : item.icon}
style={[defaultStyles.itemIcon, customStyles?.icon]}
/>
)}
<Text
style={[
defaultStyles.itemText,
customStyles?.itemText,
isSelected && {...defaultStyles.selectedItemText, ...customStyles?.selectedItemText},
isDisabled && {...defaultStyles.disabledItemText, ...customStyles?.disabledItemText}
]}
numberOfLines={1}
>
{item.label}
</Text>
{multiSelect && (
<MaterialIcons
name={isSelected ? "check-box" : "check-box-outline-blank"}
size={24}
color={isSelected ? "#2196F3" : "#757575"}
/>
)}
</View>
</TouchableOpacity>
);
};
const displayValue = useMemo(() => {
if (selectedItems.length === 0) return placeholder;
return selectedItems
.map(item => item.label)
.join(", ");
}, [selectedItems, placeholder]);
return (
<View style={[defaultStyles.container, customStyles?.container]}>
<Pressable
style={[
defaultStyles.button,
customStyles?.button,
disabled && defaultStyles.buttonDisabled,
isOpen && defaultStyles.buttonOpen
]}
onPress={toggleDropdown}
disabled={disabled}
>
{iconPosition === 'left' && showIcon && (
customIcon || (
<MaterialIcons
name={isOpen ? "keyboard-arrow-up" : "keyboard-arrow-down"}
size={24}
color="#757575"
/>
)
)}
<Text style={[defaultStyles.buttonText, customStyles?.buttonText]}>
{displayValue}
</Text>
{iconPosition === 'right' && showIcon && (
customIcon || (
<MaterialIcons
name={isOpen ? "keyboard-arrow-up" : "keyboard-arrow-down"}
size={24}
color="#757575"
/>
)
)}
</Pressable>
{isOpen && (
<View
style={[
defaultStyles.dropdown,
{ maxHeight },
customStyles?.dropdown
]}
>
{searchable && (
<TextInput
style={defaultStyles.searchInput}
placeholder="Search..."
value={searchText}
onChangeText={setSearchText}
autoCorrect={false}
/>
)}
<FlatList
data={filteredItems}
renderItem={renderItem || renderDefaultItem}
keyExtractor={keyExtractor || (item => item.id.toString())}
ItemSeparatorComponent={() => (
<DashedLine
style={[defaultStyles.separator, customStyles?.separator]}
dashColor="#E0E0E0"
/>
)}
style={{ flexGrow: 0 }}
keyboardShouldPersistTaps="handled"
ListEmptyComponent={() => (
<Text style={defaultStyles.emptyText}>
{loading ? "Loading..." : "No items found"}
</Text>
)}
/>
</View>
)}
</View>
);
}
const defaultStyles = StyleSheet.create({
container: {
position: 'relative',
zIndex: 1000,
},
button: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: '#fff',
borderWidth: 1,
borderColor: '#E0E0E0',
borderRadius: 8,
},
buttonOpen: {
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
borderBottomWidth: 0,
},
buttonDisabled: {
backgroundColor: '#F5F5F5',
opacity: 0.7,
},
buttonText: {
flex: 1,
fontSize: RFValue(16),
color: '#212121',
marginHorizontal: 8,
},
dropdown: {
position: 'absolute',
top: '100%',
left: 0,
right: 0,
backgroundColor: '#fff',
borderWidth: 1,
borderTopWidth: 0,
borderColor: '#E0E0E0',
borderBottomLeftRadius: 8,
borderBottomRightRadius: 8,
elevation: 5,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
},
searchInput: {
margin: 8,
paddingHorizontal: 12,
paddingVertical: 8,
borderWidth: 1,
borderColor: '#E0E0E0',
borderRadius: 4,
fontSize: RFValue(14),
},
item: {
padding: 12,
},
itemContent: {
flexDirection: 'row',
alignItems: 'center',
},
itemIcon: {
width: 24,
height: 24,
marginRight: 8,
},
itemText: {
flex: 1,
fontSize: RFValue(14),
color: '#212121',
},
selectedItem: {
backgroundColor: '#E3F2FD',
},
selectedItemText: {
color: '#2196F3',
fontWeight: '600',
},
disabledItem: {
opacity: 0.5,
},
disabledItemText: {
color: '#9E9E9E',
},
separator: {
marginHorizontal: 8,
},
emptyText: {
padding: 16,
textAlign: 'center',
color: '#757575',
},
});
export default Dropdown;
@sajjadjaved01
Copy link
Author

update to handle more types

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment