Dropdown + SearchBar + Clearable (React & Shadcn) را انتخاب کنید

Shadcn مجموعه ای خارق العاده از اجزای زیبا UI را درست در خارج از جعبه ارائه می دهد. یکی از متداول ترین اجزای مورد استفاده ، انتخاب کننده است. با این حال ، مؤلفه از ShadCN (که مبتنی بر Radix UI است) فاقد ویژگی های خاصی مانند عملکرد جستجو و امکان پاک کردن گزینه های انتخاب شده است.
در این راهنما ، من یک مؤلفه کشویی را انتخاب می کنم که از گزینه های جستجو و پاکسازی پشتیبانی می کند.
کشویی را انتخاب کنید
بیایید با لیستی از گزینه ها شروع کنیم:
const options = [
{ value: "apple": label: "Apple" },
{ value: "banana": label: "Banana" },
{ value: "avocado": label: "Avocado" },
// ...
];
ابتدا با استفاده از یک کشویی اساسی ایجاد می کنم
وت
که:
- لیستی از گزینه ها را نشان می دهد
- یک علامت چک برای گزینه انتخاب شده نشان می دهد
- شامل یک دکمه نزدیک است
import * as React from "react";
import { CheckIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import {
Command,
CommandGroup,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
export type SelectOption = {
value: string;
label: string;
};
export const InputSelect: React.FC<{
options: SelectOption[];
value?: string;
onValueChange?: (v: string) => void;
className?: string;
style?: React.CSSProperties;
children: React.ReactNode;
}> = ({
options,
value = "",
onValueChange,
className,
children,
}) => {
const [selectedValue, setSelectedValue] = React.useState<string>(value);
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
const onOptionSelect = (option: string) => {
setSelectedValue(option);
onValueChange?.(option);
setIsPopoverOpen(false);
};
return (
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild>
{children}
</PopoverTrigger>
<PopoverContent className={cn("w-auto p-0", className)} align="start">
<Command>
<CommandList className="max-h-[unset] overflow-y-hidden">
<CommandGroup className="max-h-[20rem] min-h-[10rem] overflow-y-auto">
{options.map((option) => {
const isSelected = selectedValue === option.value;
return (
<CommandItem
key={option.value}
onSelect={() => onOptionSelect(option.value)}
className="cursor-pointer"
>
<div
className={cn(
"mr-1 flex h-4 w-4 items-center justify-center",
isSelected ? "text-primary" : "invisible"
)}
>
<CheckIcon className="w-4 h-4" />
</div>
<span>{option.label}</span>
</CommandItem>
);
})}
</CommandGroup>
<CommandSeparator />
<CommandGroup>
<div className="flex items-center justify-between">
<CommandItem
onSelect={() => setIsPopoverOpen(false)}
className="justify-center flex-1 max-w-full cursor-pointer"
>
Close
</CommandItem>
</div>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
InputSelect.displayName = "InputSelect";
عملکرد جستجو را اضافه کنید
برای افزایش قابلیت استفاده ، من ادغام می کنم
برای قابلیت های جستجوی داخلی و
برای نمایش پیام در هنگام یافتن نتیجه.
export const InputSelect = () => {
// ...
return (
<Popover {...}>
<PopoverTrigger {...} />
<PopoverContent {...}>
<Command>
<CommandInput placeholder="Search..." />
<CommandList {...}>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup {...}>
// ...
</CommandGroup>
<CommandSeparator />
<CommandGroup>
// ...
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
اضافه کردن یک گزینه روشن
من همچنین می خواهم برای پاک کردن مقدار انتخاب شده هنگام انتخاب ، یک دکمه ارائه دهم.
import { Separator } from "@/components/ui/separator";
export const InputSelect: React.FC<{ ... }> = ({ ... }) => {
// ...
const onClearAllOptions = () => {
setSelectedValue("");
onValueChange?.("");
setIsPopoverOpen(false);
};
return (
<Popover {...}>
<PopoverTrigger {...} />
<PopoverContent {...}>
<Command>
<CommandInput {...} />
<CommandList {...}>
<CommandEmpty {...} />
<CommandGroup {...}>
// ...
</CommandGroup>
<CommandSeparator />
<CommandGroup>
<div className="flex items-center justify-between">
{selectedValue && (
<>
<CommandItem
onSelect={onClearAllOptions}
className="justify-center flex-1 cursor-pointer"
>
Clear
</CommandItem>
<Separator
orientation="vertical"
className="flex h-full mx-2 min-h-6"
/>
</>
)}
<CommandItem
onSelect={() => setIsPopoverOpen(false)}
className="justify-center flex-1 max-w-full cursor-pointer"
>
Close
</CommandItem>
</div>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
خیلی خوب ، اکنون من یک کشویی دارم که چنین است:
ماشه کشویی را اضافه کنید
اکنون ، برای آخرین مرحله ، می توانم یک ماشه را اضافه کنم تا در کشویی باز/بسته شود. من فقط می توانم از برای آن
import { Separator } from "@/components/ui/separator";
export const InputSelect: React.FC<{ ... }> = ({ ... }) => {
// ...
const onClearAllOptions = () => {
setSelectedValue("");
onValueChange?.("");
setIsPopoverOpen(false);
};
return (
<Popover {...}>
<PopoverTrigger asChild>
<Button
onClick={() => setIsPopoverOpen((prev) => !prev)}
variant="outline"
type="button"
className="flex h-11 w-full items-center justify-between p-1 [&_svg]:pointer-events-auto"
>
{selectedValue ? (
<div className="flex items-center justify-between w-full">
<div className="flex items-center justify-between w-full">
<div className="flex items-center px-3 text-foreground">
{options.find((v) => v.value === selectedValue)?.label}
</div>
<div className="flex items-center justify-between">
{selectedValue && (
<>
<X
className="mx-1 h-4 cursor-pointer text-muted-foreground"
onClick={(e) => {
e.stopPropagation();
onClearAllOptions();
}}
/>
<Separator orientation="vertical" className="flex h-full min-h-6" />
</>
)}
<ChevronDown className="h-4 mx-1 cursor-pointer text-muted-foreground" />
</div>
</div>
<div className="flex items-center justify-between">
{selectedValue && (
<>
<X
className={cn(
"mx-1 h-4 cursor-pointer text-muted-foreground",
)}
onClick={(e) => {
e.stopPropagation();
onClearAllOptions();
}}
/>
<Separator orientation="vertical" className="flex h-full min-h-6" />
</>
)}
<ChevronDown className="h-4 mx-1 cursor-pointer text-muted-foreground" />
</div>
</div>
) : (
<div className="flex items-center justify-between w-full mx-auto">
<span className="px-3 text-sm text-muted-foreground">{placeholder}</span>
<ChevronDown className="h-4 mx-1 cursor-pointer text-muted-foreground" />
</div>
)}
</Button>
</PopoverTrigger>
<PopoverContent {...}>
// ...
</PopoverContent>
</Popover>
);
};
با این پیشرفت ها ، من کاملاً کاربردی ساخته ام
مؤلفه ای که از جستجو و پاکسازی گزینه های انتخاب شده پشتیبانی می کند. اکنون می توانم فقط از این مؤلفه در هر جای برنامه خود استفاده کنم:
import * as React from "react";
import { InputSelect } from "path/to/input-select";
export const MyAwesomeComponent = () => {
const [value, setValue] = React.useState("apple");
const options = [...];
return (
<div>
<InputSelect
options={options}
value={value}
onValueChange{(v) => setValue(v)}
/>
</div>
);
}
گسترش قابلیت تنظیم (اختیاری)
در واکنش ، من از نظر فنی می توانم از هر چیزی عبور کنم children
تا زمانی که به عنوان یک مؤلفه کاربردی ارائه شود ، به یک مؤلفه بروید.
به عنوان مثال:
// By default React accepts children prop as a ReactNode type
export const CompA = ({ children: React.ReactNode }) => (
<div>{children}</div>
);
export const CompB = () => <CompA>hello</CompA>;
// We can also modify to accept a function!
export const CompA = ({ children: (v: { value: string; }) => React.ReactNode }) => (
<div>{children({ value: "foo" })}</div>
);
export const CompB = () => <CompA>{(prop) => <div>{prop.value}</div>}CompA>;
بنابراین ، برای
، من می توانم InputselectTrigger را به عنوان یک مؤلفه جداگانه استخراج کنم تا قابلیت تنظیم اضافی را ارائه دهم
export interface InputSelectProvided {
options: SelectOption[];
onValueChange?: (v: string) => void;
selectedValue: string;
setSelectedValue: React.Dispatch<React.SetStateAction<string>>;
isPopoverOpen: boolean;
setIsPopoverOpen: React.Dispatch<React.SetStateAction<boolean>>;
onOptionSelect: (v: string) => void;
onClearAllOptions: () => void;
}
export const InputSelect: React.FC<{
options: SelectOption[];
value?: string;
onValueChange?: (v: string) => void;
className?: string;
style?: React.CSSProperties;
children: (v: InputSelectProvided) => React.ReactNode;
}> = ({
options,
value = "",
onValueChange,
className,
children,
...restProps
}) => {
// ...
return (
<Popover {...}>
<PopoverTrigger asChild>
{children({
options,
onValueChange,
selectedValue,
setSelectedValue,
isPopoverOpen,
setIsPopoverOpen,
onOptionSelect,
onClearAllOptions,
})}
</PopoverTrigger>
// ...
</Popover>
);
};
InputSelect.displayName = "InputSelect";
export const InputSelectTrigger = React.forwardRef<
HTMLButtonElement,
InputSelectProvided & {
placeholder?: string;
className?: string;
children?: (v: SelectOption) => React.ReactNode;
style?: React.CSSProperties;
}
>(
(
{
options,
// onValueChange,
selectedValue,
// setSelectedValue,
// isPopoverOpen,
setIsPopoverOpen,
// onOptionSelect,
onClearAllOptions,
placeholder = "Select...",
className,
style,
...restProps,
},
ref,
) => {
return (
<Button
ref={ref}
onClick={() => setIsPopoverOpen((prev) => !prev)}
variant="outline"
type="button"
className={cn(
"flex h-11 w-full items-center justify-between p-1 [&_svg]:pointer-events-auto",
className,
)}
style={style}
{...restProps}
>
{selectedValue ? (
<div className="flex items-center justify-between w-full">
<div className="flex items-center px-3 text-foreground">
{option?.label}
</div>
<div className="flex items-center justify-between">
{selectedValue && clearable && (
<>
<X
className={cn(
"mx-1 h-4 cursor-pointer text-muted-foreground",
)}
onClick={(e) => {
e.stopPropagation();
onClearAllOptions();
}}
/>
<Separator orientation="vertical" className="flex h-full min-h-6" />
</>
)}
<ChevronDown className="h-4 mx-1 cursor-pointer text-muted-foreground" />
</div>
</div>
) : (
<div className="flex items-center justify-between w-full mx-auto">
<span className="px-3 text-sm text-muted-foreground">{placeholder}</span>
<ChevronDown className="h-4 mx-1 cursor-pointer text-muted-foreground" />
</div>
)}
</Button>
);
},
);
InputSelectTrigger.displayName = "InputSelectTrigger";
اکنون ، من همچنین می توانم پیشنهادات اضافی را به
وقتی به آن احتیاج دارم
import * as React from "react";
import { InputSelect, InputSelectTrigger } from "path/to/input-select";
export const MyAwesomeComponent = () => {
const [value, setValue] = React.useState("apple");
const options = [...];
return (
<div>
<InputSelect
options={options}
value={value}
onValueChange{(v) => setValue(v)}
>
{(provided) => <InputSelectTrigger {...provided} className="additional-styling" />}
</InputSelect>
</div>
);
}
لینک
نسخه ی نمایشی: https://shadcn-components-extend.vercel.app/؟component=input-select
کد: https://github.com/vic-ong/shadcn-components-extend/blob/main/src/compentents/extend/input-select.tsx