Stori.
Ví dụ về Bottom Sheet
5 min read

Ví dụ về Bottom Sheet

React Native

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:
Read Next Article