shadcn-ui 增加 multi-select
前言
multi-select 是一個很常見的組件,但 shadcn-ui 並沒有提供
原本想說自己來刻一個吧,再查一下資料時,發現已經有人做好了!
試了一下發現完全符合我期望的功能,就不在造輪子直接用了 XD
demo: Fancy Multi Select
完全使用 shadcn-ui 的組件組裝而成,唯一就是多了 cmdk 套件
安裝方式
npm i cmdk
- 將下列程式碼(僅留存備份)拷貝到
@/component/ui/multi-select.tsx
。或至原專案拷貝原始碼
"use client";
import * as React from "react";
import { X } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import {
Command,
CommandGroup,
CommandItem,
} from "@/components/ui/command";
import { Command as CommandPrimitive } from "cmdk";
type Framework = Record<"value" | "label", string>;
const FRAMEWORKS = [
{
value: "next.js",
label: "Next.js",
},
{
value: "sveltekit",
label: "SvelteKit",
},
{
value: "nuxt.js",
label: "Nuxt.js",
},
{
value: "remix",
label: "Remix",
},
{
value: "astro",
label: "Astro",
},
{
value: "wordpress",
label: "WordPress",
},
{
value: "express.js",
label: "Express.js",
},
{
value: "nest.js",
label: "Nest.js",
}
] satisfies Framework[];
export function FancyMultiSelect() {
const inputRef = React.useRef<HTMLInputElement>(null);
const [open, setOpen] = React.useState(false);
const [selected, setSelected] = React.useState<Framework[]>([FRAMEWORKS[4]]);
const [inputValue, setInputValue] = React.useState("");
const handleUnselect = React.useCallback((framework: Framework) => {
setSelected(prev => prev.filter(s => s.value !== framework.value));
}, []);
const handleKeyDown = React.useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
const input = inputRef.current
if (input) {
if (e.key === "Delete" || e.key === "Backspace") {
if (input.value === "") {
setSelected(prev => {
const newSelected = [...prev];
newSelected.pop();
return newSelected;
})
}
}
// This is not a default behaviour of the <input /> field
if (e.key === "Escape") {
input.blur();
}
}
}, []);
const selectables = FRAMEWORKS.filter(framework => !selected.includes(framework));
return (
<Command onKeyDown={handleKeyDown} className="overflow-visible bg-transparent">
<div
className="group border border-input px-3 py-2 text-sm ring-offset-background rounded-md focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2"
>
<div className="flex gap-1 flex-wrap">
{selected.map((framework) => {
return (
<Badge key={framework.value} variant="secondary">
{framework.label}
<button
className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
onKeyDown={(e) => {
if (e.key === "Enter") {
handleUnselect(framework);
}
}}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onClick={() => handleUnselect(framework)}
>
<X className="h-3 w-3 text-muted-foreground hover:text-foreground" />
</button>
</Badge>
)
})}
{/* Avoid having the "Search" Icon */}
<CommandPrimitive.Input
ref={inputRef}
value={inputValue}
onValueChange={setInputValue}
onBlur={() => setOpen(false)}
onFocus={() => setOpen(true)}
placeholder="Select frameworks..."
className="ml-2 bg-transparent outline-none placeholder:text-muted-foreground flex-1"
/>
</div>
</div>
<div className="relative mt-2">
{open && selectables.length > 0 ?
<div className="absolute w-full z-10 top-0 rounded-md border bg-popover text-popover-foreground shadow-md outline-none animate-in">
<CommandGroup className="h-full overflow-auto">
{selectables.map((framework) => {
return (
<CommandItem
key={framework.value}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onSelect={(value) => {
setInputValue("")
setSelected(prev => [...prev, framework])
}}
className={"cursor-pointer"}
>
{framework.label}
</CommandItem>
);
})}
</CommandGroup>
</div>
: null}
</div>
</Command >
)
}