autocomplete

mail@pastecode.io avatar
unknown
typescript
3 years ago
9.3 kB
4
Indexable
Never
import Colors from '@src/constants/Colors';
import Fonts from '@src/constants/Fonts';
import SharedStyles, { SCREEN_HEIGHT } from '@src/constants/SharedStyles';
import { delay, useKeyboard } from '@src/utils';
import { debounce } from 'lodash';
import React from 'react';
import {
  ActivityIndicator,
  Platform,
  StyleProp,
  StyleSheet,
  TextInput,
  TextInputProps,
  TextStyle,
  TouchableOpacity,
  View,
  ViewStyle,
} from 'react-native';
import { Box } from './Box';
import { Option } from './DropDownButton';
import { ErrorView } from './ErrorView';
import { List } from './FlatList';
import { MyIcon } from './MyIcon';
import { Text } from './Text';

const LIST_ITEM_HEIGHT = 50;

type ForwardedProps = {
  data: Option[];
  onOptionSelected?: (option?: Option) => void;
  selectedOption?: Option;
  handleSearch?: (text: string) => Promise<Option[]>;
  // allow user to tap outside to dismiss
  tapToDismiss?: boolean;
  optionContainerStyle?: ViewStyle;
  optionTextStyle?: TextStyle;
};

type DropDownListModalProps = {
  visible?: boolean;
  onRequestClose: () => void;
  width: number;
  height: number;
  pageX: number;
  pageY: number;
  maxHeight?: number;
  selectedOption?: Option;
} & ForwardedProps;

function DropDownListView(props: DropDownListModalProps) {
  const {
    visible,
    data,
    onRequestClose,
    onOptionSelected,
    tapToDismiss,
    height,
    width,
    pageX,
    pageY,
    selectedOption,
    maxHeight,
  } = props;
  // const [heightState, setHeightState] = React.useState(0);
  const [keyboardHeight] = useKeyboard();

  // const onLayout = React.useCallback((event: LayoutChangeEvent) => setHeightState(event.nativeEvent.layout.height), []);

  const keyExtractor = React.useCallback((option: Option) => option.value + option.name, []);

  const listHeight = maxHeight || 200;
  // calculate position of view
  let y = pageY;
  if (SCREEN_HEIGHT - keyboardHeight - (pageY + height) < data.length * LIST_ITEM_HEIGHT) {
    console.log('go top');
    y = -(LIST_ITEM_HEIGHT * data.length < listHeight ? LIST_ITEM_HEIGHT * data.length : listHeight);
  } else {
    console.log('go bottom');
    y = height;
  }

  // if (pageY + height + heightState > screenHeight - PADDING) {
  //   // go top
  //   y = -heightState - OFFSET;
  // } else {
  //   // go bottom
  //   y = OFFSET;
  // }

  // const viewStyle: ViewStyle = {
  //   position: 'absolute',
  //   left: x,
  //   ri
  //   top: 0,
  //   width,
  //   // opacity: heightState ? 1 : 0,
  // };

  const viewStyle: ViewStyle = {
    position: 'absolute',
    top: y,
  };

  if (!visible) {
    return null;
  }

  return (
    <View style={[optionStyles.list, { maxHeight: listHeight, position: 'absolute' }, viewStyle]}>
      <List
        keyboardShouldPersistTaps="handled"
        extraData={selectedOption}
        ItemSeparatorComponent={() => <Box style={optionStyles.separator} />}
        ListFooterComponent={null}
        keyExtractor={keyExtractor}
        data={data}>
        {(option) => {
          const selected = selectedOption && selectedOption.value === option.value;
          return (
            <TouchableOpacity
              onPress={() => onOptionSelected && onOptionSelected(option)}
              style={[optionStyles.container]}>
              <Text numberOfLines={1} primary={selected}>
                {option.name}
              </Text>
            </TouchableOpacity>
          );
        }}
      </List>
    </View>
  );
}

const optionStyles = StyleSheet.create({
  list: {
    ...SharedStyles.shadow1,
    backgroundColor: Colors.inputBackgroundColor,
    borderRadius: 4,
    width: '100%',
  },
  container: {
    height: 50,
    justifyContent: 'center',
    // paddingHorizontal: CONTAINER_PADDING,
    paddingHorizontal: 10,
    // backgroundColor: 'blue',
  },
  text: {
    color: Colors.textColor,
  },
  separator: {
    // ...sharedStyles.container,
    height: 0.5,
    backgroundColor: Colors.borderColor,
  },
});

type AutocompleteInputProps = {
  error?: any;
  label?: string;
  style?: StyleProp<ViewStyle>;
  inputProps?: TextInputProps;
} & ForwardedProps;

type DropDownButtonState = {
  width: number;
  height: number;
  pageX: number;
  pageY: number;
  visible: boolean;
};

export function AutoCompleteInput({
  onOptionSelected,
  selectedOption,
  style,
  error,
  label,
  inputProps,
  handleSearch,
  ...rest
}: AutocompleteInputProps) {
  const ref = React.useRef<TouchableOpacity>() as React.MutableRefObject<TouchableOpacity>;
  const inputRef = React.useRef<TextInput>() as React.MutableRefObject<TextInput>;
  const [isLoading, setIsLoading] = React.useState(false);
  const [data, setData] = React.useState<Option[]>([]);
  const [text, setText] = React.useState('');

  const [state, setState] = React.useState<DropDownButtonState>({
    width: 0,
    height: 0,
    pageX: 0,
    pageY: 0,
    visible: false,
  });

  const _onOptionSelected = React.useCallback(
    (option?: Option) => {
      setState({
        ...state,
        visible: false,
      });
      // console.log(onOptionSelected);
      onOptionSelected && onOptionSelected(option);
    },
    [state, onOptionSelected],
  );

  const handlerChange = React.useCallback(
    debounce(async (t: string) => {
      try {
        if (handleSearch && t) {
          setIsLoading(true);
          const locas = await handleSearch(t);
          setIsLoading(false);

          setData(locas);
          ref.current.measure((_, __, width, height, pageX, pageY) => {
            console.log({ width, height, pageX, pageY });

            setState({
              ...state,
              width,
              height,
              pageX,
              pageY,
              visible: !!locas.length,
            });
          });
        } else {
          setState({
            ...state,
            visible: false,
          });
        }
      } catch (e) {
        setIsLoading(false);
        console.log(e);
      }
    }, 1000),
    [],
  );

  const onPressClear = React.useCallback(async () => {
    setText('');
    onOptionSelected && onOptionSelected(undefined);
    await delay(300);
    inputRef.current?.focus();
  }, [inputRef, onOptionSelected]);

  const { width, height, pageX, pageY, visible } = state;

  const displayText = (selectedOption && selectedOption.name) || selectedOption || 'Select';
  const textStyle = (selectedOption && selectedOption.name) || selectedOption ? undefined : styles.placeholderText;

  const customStyle = Platform.OS === 'ios' ? { zIndex: 1 } : undefined;

  return (
    <Box style={[styles.container, customStyle]}>
      <View renderToHardwareTextureAndroid ref={ref}>
        {label && <Text style={styles.label}>{label}</Text>}
        <View
          style={[
            styles.inputAutocompleteContainer,
            { borderColor: error ? Colors.errorColor : Colors.borderColor },
            style,
          ]}>
          <Box full horizontal>
            {selectedOption ? (
              <Text full style={[textStyle, { alignSelf: 'center' }]}>
                {displayText}
              </Text>
            ) : (
              <TextInput
                value={text}
                ref={inputRef}
                style={styles.input}
                {...inputProps}
                onChangeText={(t: string) => {
                  setText(t);
                  handlerChange(t);
                }}
              />
            )}
            <TouchableOpacity onPress={onPressClear} disabled={!selectedOption} style={styles.iconContainer}>
              {isLoading ? (
                <ActivityIndicator animating />
              ) : (
                <MyIcon size={20} style={[styles.inputIconColor]} name={selectedOption ? 'times1' : 'chevron-down1'} />
              )}
            </TouchableOpacity>
          </Box>
        </View>
      </View>
      <ErrorView error={error} />

      <DropDownListView
        {...rest}
        visible={visible}
        width={width}
        onOptionSelected={_onOptionSelected}
        height={height}
        pageX={pageX}
        pageY={pageY}
        selectedOption={selectedOption}
        data={data}
        onRequestClose={() => setState({ ...state, visible: false })}
      />
    </Box>
  );
}

const styles = StyleSheet.create({
  container: {
    marginBottom: 10,
    // zIndex: 1,
    // overflow: 'visible',
    // elevation: 2,
  },
  inputAutocompleteContainer: {
    paddingLeft: 10,
    // backgroundColor: Colors.inputBackgroundColor,
    borderWidth: 1,
    borderColor: Colors.borderColor,
    height: 50,
    justifyContent: 'center',
    borderRadius: 5,
    // ...sharedStyles.shadow1,
  },
  iconContainer: {
    width: 50,
    alignItems: 'center',
    justifyContent: 'center',
  },
  inputIconColor: {
    color: Colors.inputIconColor,
    fontSize: 16,
  },
  primary: {
    color: Colors.primary,
  },
  label: {
    color: Colors.textColor,
    ...SharedStyles.inputLabelMarginBottom,
  },
  errorContainer: {
    ...SharedStyles.errorContainer,
  },
  placeholderText: {
    color: Colors.secondaryTextColor,
  },
  input: {
    flex: 1,
    color: Colors.textColor,
    // marginLeft: 10,
    fontSize: 16,
    fontFamily: Fonts.regular,
  },
  button: {
    justifyContent: 'center',
    alignItems: 'center',
  },
});