react-native-reusables icon indicating copy to clipboard operation
react-native-reusables copied to clipboard

I'm trying to use the combobox component but it's not working properly

Open briskteq-faiz opened this issue 6 months ago • 1 comments

Combobox.tsx

import * as React from "react";
import {Text, View, type ListRenderItemInfo} from "react-native";
import {useSafeAreaInsets} from "react-native-safe-area-context";
import {cn} from "../../lib/utils";
import {Check, ChevronsUpDown, Search} from "../Icons";
import {
	BottomSheet,
	BottomSheetContent,
	BottomSheetFlatList,
	BottomSheetHeader,
	BottomSheetOpenTrigger,
	BottomSheetTextInput,
	useBottomSheet,
} from "./bottom-sheet";
import {Button, buttonTextVariants, buttonVariants} from "./button";

const HEADER_HEIGHT = 130;

interface ComboboxOption {
  label?: string;
  value?: string;
}

const Combobox = React.forwardRef<
  React.ElementRef<typeof Button>,
  Omit<React.ComponentPropsWithoutRef<typeof Button>, "children"> & {
    items: ComboboxOption[];
    placeholder?: string;
    inputProps?: React.ComponentPropsWithoutRef<typeof BottomSheetTextInput>;
    emptyText?: string;
    defaultSelectedItem?: ComboboxOption | null;
    selectedItem?: ComboboxOption | null;
    onSelectedItemChange?: (option: ComboboxOption | null) => void;
  }
>(
  (
    {
      className,
      textClass,
      variant = "outline",
      size = "sm",
      inputProps,
      placeholder,
      items,
      emptyText = "Nothing found...",
      defaultSelectedItem = null,
      selectedItem: selectedItemProp,
      onSelectedItemChange,
      ...props
    },
    ref,
  ) => {
    const insets = useSafeAreaInsets();
    const [search, setSearch] = React.useState("");
    const [selectedItem, setSelectedItem] =
      React.useState<ComboboxOption | null>(defaultSelectedItem);
    const bottomSheet = useBottomSheet();
    const inputRef =
      React.useRef<React.ComponentRef<typeof BottomSheetTextInput>>(null);

    const listItems = React.useMemo(() => {
      return search
        ? items.filter((item) => {
            return item.label
              ?.toLocaleLowerCase()
              .includes(search.toLocaleLowerCase());
          })
        : items;
    }, [items, search]);

    function onItemChange(listItem: ComboboxOption) {
      if (selectedItemProp?.value === listItem.value) {
        return null;
      }
      setSearch("");
      bottomSheet.close();
      return listItem;
    }

    const renderItem = React.useCallback(
      ({ item }: ListRenderItemInfo<unknown>) => {
        const listItem = item as ComboboxOption;
        const isSelected = onSelectedItemChange
          ? selectedItemProp?.value === listItem.value
          : selectedItem?.value === listItem.value;
        return (
          <Button
            variant="ghost"
            className="flex-1 flex-row items-center justify-between px-3 py-4"
            style={{ minHeight: 70 }}
            onPress={() => {
              if (onSelectedItemChange) {
                onSelectedItemChange(onItemChange(listItem));
                return;
              }
              setSelectedItem(onItemChange(listItem));
            }}
          >
            <View className="flex-1 flex-row">
              <Text className={"text-foreground text-xl"}>
                {listItem.label}
              </Text>
            </View>
            {isSelected && (
              <Check size={24} className={"mt-1.5 px-6 text-foreground"} />
            )}
          </Button>
        );
      },
      [selectedItem, selectedItemProp],
    );

    function onSubmitEditing() {
      const firstItem = listItems[0];
      if (!firstItem) return;
      if (onSelectedItemChange) {
        onSelectedItemChange(firstItem);
      } else {
        setSelectedItem(firstItem);
      }
      bottomSheet.close();
    }

    function onSearchIconPress() {
      if (!inputRef.current) return;
      const input = inputRef.current;
      if (input && "focus" in input && typeof input.focus === "function") {
        input.focus();
      }
    }

    const itemSelected = onSelectedItemChange ? selectedItemProp : selectedItem;

    return (
      <BottomSheet>
        <BottomSheetOpenTrigger
          ref={ref}
          className={buttonVariants({
            variant,
            size,
            className: cn("w-full flex-row", className),
          })}
          role="combobox"
          {...props}
        >
          <View className="flex-1 flex-row justify-between ">
            <Text
              className={buttonTextVariants({
                variant,
                size,
                className: cn(!itemSelected && "opacity-50", textClass),
              })}
              numberOfLines={1}
            >
              {itemSelected ? itemSelected.label : placeholder ?? ""}
            </Text>
            <ChevronsUpDown className="ml-2 text-foreground opacity-50" />
          </View>
        </BottomSheetOpenTrigger>
        <BottomSheetContent
          ref={bottomSheet.ref}
          onDismiss={() => {
            setSearch("");
          }}
        >
          <BottomSheetHeader className="border-b-0">
            <Text className="px-0.5 text-center font-bold text-foreground text-xl">
              {placeholder}
            </Text>
          </BottomSheetHeader>
          <View className="relative border-border border-b px-4 pb-4">
            <BottomSheetTextInput
              role="searchbox"
              ref={inputRef}
              className="pl-12"
              value={search}
              onChangeText={setSearch}
              onSubmitEditing={onSubmitEditing}
              returnKeyType="next"
              clearButtonMode="while-editing"
              placeholder="Search..."
              {...inputProps}
            />
            <Button
              variant={"ghost"}
              size="sm"
              className="absolute top-2.5 left-4"
              onPress={onSearchIconPress}
            >
              <Search size={18} className="text-foreground opacity-50" />
            </Button>
          </View>
          <BottomSheetFlatList
            data={listItems}
            contentContainerStyle={{
              paddingBottom: insets.bottom + HEADER_HEIGHT,
            }}
            renderItem={renderItem}
            keyExtractor={(item, index) =>
              (item as ComboboxOption)?.value ?? index.toString()
            }
            className={"px-4"}
            keyboardShouldPersistTaps="handled"
            ListEmptyComponent={() => {
              return (
                <View
                  className="flex-1 flex-row items-center justify-center px-3 py-5"
                  style={{ minHeight: 70 }}
                >
                  <Text className={"text-center text-muted-foreground text-xl"}>
                    {emptyText}
                  </Text>
                </View>
              );
            }}
          />
        </BottomSheetContent>
      </BottomSheet>
    );
  },
);

Combobox.displayName = "Combobox";

export {Combobox, type ComboboxOption};

When clicking on the input it opens up the bottom sheet, it consists of a input and a list of options. When I click on the input, the bottom sheet closes automatically.

BottomSheet.tsx:

import type { BottomSheetFooterProps as GBottomSheetFooterProps } from "@gorhom/bottom-sheet";
import {
	BottomSheetFlatList as GBottomSheetFlatList,
	BottomSheetFooter as GBottomSheetFooter,
	BottomSheetTextInput as GBottomSheetTextInput,
	BottomSheetView as GBottomSheetView,
	useBottomSheetModal,
	type BottomSheetBackdrop,
	type BottomSheetModal,
} from "@gorhom/bottom-sheet";
import React, { useCallback } from "react";
import {
	Keyboard,
	Pressable,
	View,
	type GestureResponderEvent,
	type ViewStyle,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { X } from "../../components/Icons";
import { cn } from "../../lib/utils";
import * as Slot from "../primitives/slot";
import { Button } from "./button";

// !IMPORTANT: This file is only for web.

type BottomSheetRef = React.ElementRef<typeof View>;
type BottomSheetProps = React.ComponentPropsWithoutRef<typeof View>;

interface BottomSheetContext {
	sheetRef: React.RefObject<BottomSheetModal>;
}

const BottomSheetContext = React.createContext({} as BottomSheetContext);

const BottomSheet = React.forwardRef<BottomSheetRef, BottomSheetProps>(
	({ ...props }, ref) => {
		return <View ref={ref} {...props} />;
	},
);

type BottomSheetContentRef = React.ElementRef<typeof BottomSheetModal>;

type BottomSheetContentProps = Omit<
	React.ComponentPropsWithoutRef<typeof BottomSheetModal>,
	"backdropComponent"
> & {
	backdropProps?: Partial<
		React.ComponentPropsWithoutRef<typeof BottomSheetBackdrop>
	>;
};

const BottomSheetContent = React.forwardRef<
	BottomSheetContentRef,
	BottomSheetContentProps
>(() => {
	return null;
});

const BottomSheetOpenTrigger = React.forwardRef<
	React.ElementRef<typeof Pressable>,
	React.ComponentPropsWithoutRef<typeof Pressable> & {
		asChild?: boolean;
	}
>(({ onPress, asChild = false, ...props }, ref) => {
	function handleOnPress() {
		window.alert(
			"Not implemented for web yet. Check `bottom-sheet.tsx` for more info.",
		);
	}
	const Trigger = asChild ? Slot.Pressable : Pressable;
	return <Trigger ref={ref} onPress={handleOnPress} {...props} />;
});

const BottomSheetCloseTrigger = React.forwardRef<
	React.ElementRef<typeof Pressable>,
	React.ComponentPropsWithoutRef<typeof Pressable> & {
		asChild?: boolean;
	}
>(({ onPress, asChild = false, ...props }, ref) => {
	const { dismiss } = useBottomSheetModal();
	function handleOnPress(ev: GestureResponderEvent) {
		dismiss();
		if (Keyboard.isVisible()) {
			Keyboard.dismiss();
		}
		onPress?.(ev);
	}
	const Trigger = asChild ? Slot.Pressable : Pressable;
	return <Trigger ref={ref} onPress={handleOnPress} {...props} />;
});

const BOTTOM_SHEET_HEADER_HEIGHT = 60; // BottomSheetHeader height

type BottomSheetViewProps = Omit<
	React.ComponentPropsWithoutRef<typeof GBottomSheetView>,
	"style"
> & {
	hadHeader?: boolean;
	style?: ViewStyle;
};

function BottomSheetView({
	className,
	children,
	hadHeader = true,
	style,
	...props
}: BottomSheetViewProps) {
	const insets = useSafeAreaInsets();
	return (
		<GBottomSheetView
			style={[
				{
					paddingBottom:
						insets.bottom + (hadHeader ? BOTTOM_SHEET_HEADER_HEIGHT : 0),
				},
				style,
			]}
			className={cn(`px-4`, className)}
			{...props}
		>
			{children}
		</GBottomSheetView>
	);
}

type BottomSheetTextInputRef = React.ElementRef<typeof GBottomSheetTextInput>;
type BottomSheetTextInputProps = React.ComponentPropsWithoutRef<
	typeof GBottomSheetTextInput
>;
const BottomSheetTextInput = React.forwardRef<
	BottomSheetTextInputRef,
	BottomSheetTextInputProps
>(({ className, placeholderClassName, ...props }, ref) => {
	return (
		<GBottomSheetTextInput
			ref={ref}
			className={cn(
				"h-14 items-center rounded-md border border-input bg-background px-3 text-foreground text-xl leading-[1.25] placeholder:text-muted-foreground disabled:opacity-50",
				className,
			)}
			placeholderClassName={cn("text-muted-foreground", placeholderClassName)}
			{...props}
		/>
	);
});

type BottomSheetFlatListRef = React.ElementRef<typeof GBottomSheetFlatList>;
type BottomSheetFlatListProps = React.ComponentPropsWithoutRef<
	typeof GBottomSheetFlatList
>;
const BottomSheetFlatList = React.forwardRef<
	BottomSheetFlatListRef,
	BottomSheetFlatListProps
>(({ className, ...props }, ref) => {
	const insets = useSafeAreaInsets();
	return (
		<GBottomSheetFlatList
			ref={ref}
			contentContainerStyle={[{ paddingBottom: insets.bottom }]}
			className={cn("py-4", className)}
			keyboardShouldPersistTaps="handled"
			{...props}
		/>
	);
});

type BottomSheetHeaderRef = React.ElementRef<typeof View>;
type BottomSheetHeaderProps = React.ComponentPropsWithoutRef<typeof View>;
const BottomSheetHeader = React.forwardRef<
	BottomSheetHeaderRef,
	BottomSheetHeaderProps
>(({ className, children, ...props }, ref) => {
	const { dismiss } = useBottomSheetModal();
	function close() {
		if (Keyboard.isVisible()) {
			Keyboard.dismiss();
		}
		dismiss();
	}
	return (
		<View
			ref={ref}
			className={cn(
				"flex-row items-center justify-between border-border border-b pl-4",
				className,
			)}
			{...props}
		>
			{children}
			<Button onPress={close} variant="ghost" className="pr-4">
				<X className="text-muted-foreground" size={24} />
			</Button>
		</View>
	);
});

type BottomSheetFooterRef = React.ElementRef<typeof View>;
type BottomSheetFooterProps = Omit<
	React.ComponentPropsWithoutRef<typeof View>,
	"style"
> & {
	bottomSheetFooterProps: GBottomSheetFooterProps;
	children?: React.ReactNode;
	style?: ViewStyle;
};

/**
 * To be used in a useCallback function as a props to BottomSheetContent
 */
const BottomSheetFooter = React.forwardRef<
	BottomSheetFooterRef,
	BottomSheetFooterProps
>(({ bottomSheetFooterProps, children, className, style, ...props }, ref) => {
	const insets = useSafeAreaInsets();
	return (
		<GBottomSheetFooter {...bottomSheetFooterProps}>
			<View
				ref={ref}
				style={[{ paddingBottom: insets.bottom + 6 }, style]}
				className={cn("px-4 pt-1.5", className)}
				{...props}
			>
				{children}
			</View>
		</GBottomSheetFooter>
	);
});

function useBottomSheet() {
	const ref = React.useRef<BottomSheetContentRef>(null);

	const open = useCallback(() => {
		ref.current?.present();
	}, []);

	const close = useCallback(() => {
		ref.current?.dismiss();
	}, []);

	return { ref, open, close };
}

export {
	BottomSheet,
	BottomSheetCloseTrigger,
	BottomSheetContent,
	BottomSheetFlatList,
	BottomSheetFooter,
	BottomSheetHeader,
	BottomSheetOpenTrigger,
	BottomSheetTextInput,
	BottomSheetView,
	useBottomSheet,
};

If someone could help me solve this I'm using this for address autocomplete

briskteq-faiz avatar Aug 12 '24 11:08 briskteq-faiz