
Một ví dụ về Bottom Sheet
//ProvinceInput.tsx
import React, {
useRef,
useCallback,
useState,
useEffect,
useMemo,
forwardRef,
useImperativeHandle,
} from 'react';
import {
View,
TextInput as NativeTextInput,
FlatList,
TouchableOpacity,
// Keyboard,
Platform,
} from 'react-native';
import BottomSheet, {
BottomSheetBackdrop,
BottomSheetTextInput,
} from '@gorhom/bottom-sheet';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {Portal, TextInput} from 'react-native-paper';
import {AppDispatch, RootState} from '@/redux/Store';
import {useDispatch, useSelector} from 'react-redux';
import {SelectItem} from '@/models';
import {useTranslation} from 'react-i18next';
import Text from '@/components/core/Text';
import {COLORS} from '@/styles';
import DefaultInput, {DefaultInputProps, DefaultInputRef} from './DefaultInput';
import {getDistricts, selectProvince} from '@/redux/Reducer/location';
import {IconOutline} from '@ant-design/icons-react-native';
import SearchSvg from '@/assets/svg/Search';
import LottieView from 'lottie-react-native';
import {ANIMATION} from '@/constants/icons';
interface BottomSheetProps {
selectedProvince: SelectItem;
onSelectProvince: (province: SelectItem) => void;
onClose: () => void;
}
type BottomSheetRef = {
open: () => void;
close: () => void;
};
const BottomSheetProvince = forwardRef<BottomSheetRef, BottomSheetProps>(
(props, ref) => {
useImperativeHandle(ref, () => ({
open,
close,
}));
const {t} = useTranslation();
const {provinces} = useSelector((state: RootState) => state.location);
const {selectedProvince, onSelectProvince, onClose} = props;
const [searchText, setSearchText] = useState('');
const [filteredProvinces, setFilteredProvinces] =
useState<SelectItem[]>(provinces);
useEffect(() => {
const filtered = provinces.filter(province =>
province.textValue.toLowerCase().includes(searchText.toLowerCase()),
);
setFilteredProvinces(filtered);
}, [searchText, provinces]);
const handleProvinceSelection = useCallback(
(province: SelectItem) => {
clearInput();
onSelectProvince(province);
onClose();
},
[onClose, onSelectProvince],
);
const renderItem = useCallback(
({item}: {item: SelectItem}) => (
<TouchableOpacity
style={{
padding: 10,
backgroundColor:
item === selectedProvince ? COLORS.primary : 'white',
borderRadius: 5,
}}
onPress={() => handleProvinceSelection(item)}>
<Text
align="center"
color={item === selectedProvince ? COLORS.white : COLORS.black}
weight={item === selectedProvince ? 'bold' : 'regular'}>
{item.textValue}
</Text>
</TouchableOpacity>
),
[selectedProvince, handleProvinceSelection],
);
const flatListRef = useRef<FlatList>(null);
const inputRef = useRef<NativeTextInput>(null);
const sheetRef = useRef<BottomSheet>(null);
const snapPoints = useMemo(() => ['50%'], []);
const renderBackdrop = useCallback(
(propsBackdrop: any) => (
<BottomSheetBackdrop
{...propsBackdrop}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[],
);
const scrollToSelectedProvince = useCallback(() => {
const index = filteredProvinces.indexOf(selectedProvince);
if (index !== -1) {
flatListRef.current?.scrollToIndex({index, animated: true});
}
}, [filteredProvinces, selectedProvince]);
const clearInput = () => {
setSearchText('');
inputRef.current?.blur();
};
const open = () => {
scrollToSelectedProvince();
sheetRef.current?.collapse();
};
const close = () => {
sheetRef.current?.close();
};
const insets = useSafeAreaInsets();
return (
<Portal>
<BottomSheet
ref={sheetRef}
index={-1}
snapPoints={snapPoints}
enablePanDownToClose
topInset={insets.top}
// onClose={() => {
// Keyboard.dismiss();
// }}
backdropComponent={renderBackdrop}>
{provinces.length === 0 ? (
<View
style={{
flex: 1,
paddingBottom: insets.bottom,
marginHorizontal: 20,
gap: 10,
}}>
<Text
weight="bold"
align={'center'}
transform="uppercase"
size={14}>
{t('label:province')}
</Text>
<View
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}}>
<LottieView
source={ANIMATION.search}
loop
autoPlay
style={{width: 100, height: 100}}
/>
<Text weight="bold">{t('common:noData')}</Text>
<Text size={10}>{t('common:pleaseSelectYourDistrict')}</Text>
</View>
</View>
) : (
<View
style={{
flex: 1,
paddingBottom: insets.bottom,
marginHorizontal: 20,
gap: 10,
}}>
<Text
weight="bold"
align={'center'}
transform="uppercase"
size={14}>
{t('label:province')}
</Text>
<View
style={{
flexDirection: 'row',
backgroundColor: 'white',
paddingVertical: Platform.OS === 'android' ? 0 : 10,
paddingHorizontal: 10,
alignItems: 'center',
borderRadius: 8,
gap: Platform.OS === 'android' ? 5 : 10,
borderWidth: 1,
borderColor: '#00000040',
}}>
<SearchSvg size={15} />
<BottomSheetTextInput
style={{
fontSize: 12,
fontFamily: 'GoogleSans-Regular',
flex: 1,
}}
placeholder={t('label:search')}
value={searchText}
onChangeText={(text: string) => setSearchText(text)}
/>
</View>
{filteredProvinces.length === 0 ? (
<View
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}}>
<LottieView
source={ANIMATION.empty}
loop
autoPlay
style={{width: 100, height: 100}}
/>
<Text size={10}>{t('common:noResultFound')}</Text>
</View>
) : (
<FlatList
ref={flatListRef}
data={filteredProvinces}
renderItem={renderItem}
keyExtractor={item => item.id}
extraData={filteredProvinces}
// Handling can press on the list item while the keyboard is open
keyboardShouldPersistTaps="always"
/>
)}
</View>
)}
</BottomSheet>
</Portal>
);
},
);
const ProvinceInput = forwardRef<
DefaultInputRef,
DefaultInputProps & {onChangeSelect?: (item: SelectItem) => void}
>((props, ref) => {
const {onChangeSelect, ...others} = props;
const {t} = useTranslation();
const {selectedProvince} = useSelector((state: RootState) => state.location);
const dispatch: AppDispatch = useDispatch();
const bottomSheetRef = useRef<BottomSheetRef>(null);
const handleSelectProvince = (province: SelectItem) => {
dispatch(selectProvince(province));
onChangeSelect && onChangeSelect(province);
dispatch(getDistricts({provinceCode: province.id}));
};
const handleOpenBottomSheet = () => {
bottomSheetRef.current?.open();
};
const handleCloseBottomSheet = () => {
bottomSheetRef.current?.close();
};
useEffect(() => {
if (Object.keys(selectedProvince).length > 0) {
onChangeSelect && onChangeSelect(selectedProvince);
}
}, [selectedProvince, onChangeSelect]);
useImperativeHandle(ref, () => ({
focus,
blur,
clear,
isFocused,
setNativeProps,
}));
const textInputRef = useRef<DefaultInputRef>(null);
const focus = () => {
textInputRef.current?.focus();
};
const blur = () => {
textInputRef.current?.blur();
};
const clear = () => {
textInputRef.current?.clear();
};
const isFocused = () => {
return textInputRef.current?.isFocused() ?? false;
};
const setNativeProps = (nativeProps: Object) => {
textInputRef.current?.setNativeProps(nativeProps);
};
return (
<>
<TouchableOpacity onPress={() => handleOpenBottomSheet()}>
<View pointerEvents={'none'}>
<DefaultInput
ref={textInputRef}
label={t('label:province')}
value={selectedProvince.textValue}
left={
<TextInput.Icon
size={16}
icon={() => (
<IconOutline
name="environment"
size={18}
color={COLORS.primary}
style={{
marginBottom: 2,
}}
/>
)}
/>
}
right={
<TextInput.Icon
size={16}
icon={() => (
<IconOutline
name="caret-right"
size={12}
color={COLORS.primary}
style={{
marginBottom: 2,
}}
/>
)}
/>
}
{...others}
/>
</View>
</TouchableOpacity>
<BottomSheetProvince
ref={bottomSheetRef}
selectedProvince={selectedProvince}
onSelectProvince={handleSelectProvince}
onClose={handleCloseBottomSheet}
/>
</>
);
});
export default ProvinceInput;
Share this article
Share: