Next.js プロジェクトが設定されていることを確認してください。そうでない場合は、作成します。
npx create-next-app my-app --typescriptcd my-app
必要な shadcn コンポーネントをインストールします。
npx shadcn@最新の初期化 npx shadcn@latest add コマンド ポップオーバー ボタン区切りバッジ
components
ディレクトリにmulti-select.tsx
作成します。
// src/components/multi-select.tsximport * as React from "react";import { cva, type VariantProps } from "class-variance-authority";import { チェックアイコン、 Xサークル、 シェブロンダウン、 Xアイコン、 WandSparkles,} from "lucide-react";import { cn } from "@/lib/utils";import { Separator } from "@/components/ui/separator";import { Button } from "@/components/ui/ button";import { バッジ } from "@/components/ui/badge";import { ポップオーバー、 ポップオーバーコンテンツ、 PopoverTrigger,} from "@/components/ui/popover";import { 指示、 コマンド空、 コマンドグループ、 コマンド入力、 コマンドアイテム、 コマンドリスト、 CommandSeparator,} from "@/components/ui/command";/** * さまざまなスタイルを処理するための複数選択コンポーネントのバリアント。 * class-variance-authority (cva) を使用して、「variant」プロパティに基づいてさまざまなスタイルを定義します。 */const multiSelectVariants = cva( "m-1 トランジション イーズインアウト 遅延-150 hover:-translate-y-1 hover:scale-110 継続時間-300", {バリアント: { バリアント: {デフォルト: "border-foreground/10 text-foreground bg-card hover:bg-card/80",secondary: "border-foreground/10 bg-secondary text-secondary-foreground hover:bg-セカンダリ/80",破壊的: "境界線-透明な背景-破壊的なテキスト-破壊的な-前景hover:bg-destructive/80",inverted: "反転", },},defaultVariants: { バリアント: "デフォルト",}, });/** * MultiSelect コンポーネントの Props */interface MultiSelectProps extends React.ButtonHTMLAttributes<HTMLButtonElement>,VariantProps<typeof multiSelectVariants> { /** * 複数選択コンポーネントに表示されるオプション オブジェクトの配列。 * 各オプション オブジェクトには、ラベル、値、およびオプションのアイコンがあります。 */ options: {/** オプションとして表示するテキスト。 */label: string;/** オプションに関連付けられた一意の値。 */value: string;/** オプションと一緒に表示するオプションのアイコン コンポーネント。 */icon?: React.ComponentType<{ className?: string }>; }[]; /** * 選択した値が変更されたときにトリガーされるコールバック関数。 * 新しく選択された値の配列を受け取ります。 */ onValueChange: (値: string[]) => void; /** コンポーネントのマウント時に選択されるデフォルト値。 */ デフォルト値?: 文字列[]; /** * 値が選択されていない場合に表示されるプレースホルダー テキスト。 * オプション。デフォルトは「オプションを選択」です。 */ プレースホルダー?: 文字列; /** * 視覚効果 (バッジの跳ね返りなど) のアニメーション期間 (秒単位)。 * オプション。デフォルトは 0 (アニメーションなし) です。 */ アニメーション?: 番号; /** * 表示する項目の最大数。追加で選ばれた項目がまとめられます。 * オプション、デフォルトは 3 です。 */ maxCount?: 数値; /** * ポップオーバーのモダリティ。 true に設定すると、外部要素 * との対話が無効になり、ポップオーバー コンテンツのみがスクリーン リーダーに表示されます。 * オプション。デフォルトは false です。 */ modalPopover?: ブール値; /** * true の場合、複数選択コンポーネントを別のコンポーネントの子としてレンダリングします。 * オプション。デフォルトは false です。 */ asChild?: ブール値; /** * 複数選択コンポーネントにカスタム スタイルを適用するための追加のクラス名。 * オプション。カスタム スタイルを追加するために使用できます。 */ className?: string;}export const MultiSelect = React.forwardRef< HTMLボタン要素、 MultiSelectProps>( ({ オプション、onValueChange、バリアント、defaultValue = []、プレースホルダー = "オプションの選択"、アニメーション = 0、maxCount = 3、modalPopover = false、asChild = false、className、...props},ref ) => {const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue);const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);const [isAnimating, setIsAnimating] = React.useState(false) ;const handleInputKeyDown = ( イベント: React.KeyboardEvent<HTMLInputElement>) => { if (event.key === "Enter") {setIsPopoverOpen(true); else if (event.key === "Backspace" && !event.currentTarget.value) {const newSelectedValues = [...selectedValues];newSelectedValues.pop();setSelectedValues(newSelectedValues);onValueChange(newSelectedValues); }};const toggleOption = (オプション: 文字列) => { const newSelectedValues = selectedValues.includes(オプション)? selectedValues.filter((value) => value !== オプション): [...selectedValues, オプション]; setSelectedValues(newSelectedValues); onValueChange(newSelectedValues);};const handleClear = () => { setSelectedValues([]); onValueChange([]);};const handleTogglePopover = () => { setIsPopoverOpen((prev) => !prev);};const clearExtraOptions = () => { const newSelectedValues = selectedValues.slice(0, maxCount); setSelectedValues(newSelectedValues); onValueChange(newSelectedValues);};const toggleAll = () => { if (selectedValues.length === options.length) {handleClear(); else {const allValues = options.map((option) => option.value);setSelectedValues(allValues);onValueChange(allValues); }};return ( <Popoveropen={isPopoverOpen}onOpenChange={setIsPopoverOpen}modal={modalPopover} ><PopoverTrigger asChild> <Buttonref={ref}{...props}onClick={handleTogglePopover}className={cn( "flex w-full p-1rounded-md border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit", className)} >{selectedValues.length > 0 ? ( <div className="flex justify-between items-center w-full"><div className="flex flex-wrap items- center"> {selectedValues.slice(0, maxCount).map((value) => {const option = options.find((o) => o.value === value);const IconComponent = option?.icon;return ( <Badgekey={value}className={cn( isAnimating ? "animate-bounce" : "", multiSelectVariants({ariant }))}style={{ animeDuration: ` ${animation}s` }} >{IconComponent && ( <IconComponent className="h-4 w-4 mr-2" />)}{option?.label}<XCircle className="ml-2 h-4 w-4 カーソルポインター" onClick={(event) => {event.stopPropagation();toggleOption(value); } /> </Badge>) })} {selectedValues.length > maxCount && (<Badge className={cn("bg-transparent text-foreground) border-foreground/1 hover:bg-transparent"、アニメーション化していますか? "animate-bounce" : "",multiSelectVariants({ariant }) )} style={{animationDuration: `${animation}s` }}> {`+ ${selectedValues.length - maxCount} more`} <XCircleclassName= "ml-2 h-4 w-4 カーソルポインタ"onClick={(event) => {event.stopPropagation(); clearExtraOptions();}} /></Badge> )}</div><div className="flex items-center justify-between"> <XIconclassName="h-4 mx-2cursor-pointer text-muted-foreground "onClick={(event) => {event.stopPropagation(); handleClear();}} /> <Separatororientation="vertical"className="flex min-h-6 h-full" /> <ChevronDown className="h-4 mx-2 カーソルポインター テキスト-ミュート-フォアグラウンド" / ></div> </div>) : ( <div className="flex items-center justify-between w-full mx-auto"><span className="text-sm text-muted-foreground mx-3"> {placeholder}</span><ChevronDown className="h-4 カーソルポインター text-muted-foreground mx-2" /> </div>)} </Button>< /PopoverTrigger><PopoverContent className="w-auto p-0" align="start" onEscapeKeyDown={() => setIsPopoverOpen(false)}> <Command><CommandInput placeholder="Search..." onKeyDown={handleInputKeyDown}/><CommandList> <CommandEmpty>結果が見つかりません。</CommandEmpty> <CommandGroup><CommandItem key="all" onSelect={toggleAll} className ="カーソルポインタ"> <divclassName={cn( "mr-2 flex h-4 w-4 items-center justify-centerrounded-sm border border-primary", selectedValues.length === options.length? "bg-primary text-primary-foreground": "opacity-50 [&_svg]:invisible")} >< CheckIcon className="h-4 w-4" /> </div> <span>(選択すべて)</span></CommandItem>{options.map((option) => { const isSelected = selectedValues.includes(option.value); return (<CommandItem key={option.value} onSelect={() => toggleOption(option.value)} className="cursor-pointer"> <divclassName={cn( "mr-2 flex h-4 w-4 items-center justify-centerrounded-sm border border-primary", isSelected? "bg-primary text-primary-foreground": "opacity-50 [&_svg]:invisible")} ><CheckIcon className="h-4 w-4" /> </div> {option.icon && (<option.icon className="mr-2 h-4 w-4 text -muted-foreground" /> )} <span>{option.label}</span></CommandItem> );})} </CommandGroup> <CommandSeparator /> <CommandGroup><div className="flex items-center justify-between"> {selectedValues.length > 0 && (<> <CommandItemonSelect={handleClear}className="flex-1 justify-center カーソルポインター" >クリア </CommandItem> <Separatororientation="vertical"className="flex min-h-6 h-full" /></> )} <CommandItemonSelect={() => setIsPopoverOpen(false)}className="flex-1 justify-center Cursor-pointer max-w-full" >Close </CommandItem></div> </CommandGroup></CommandList > </Command></PopoverContent>{animation > 0 && selectedValues.length > 0 && ( <WandSparklesclassName={cn( "カーソル-ポインター my-2 テキスト-前景 bg-背景 w-3 h-3", isAnimating ? "" : "text-muted-foreground")}onClick={() => setIsAnimating(!isAnimating)} />)} </Popover>); });MultiSelect.displayName =</sp