Assurez-vous d'avoir configuré un projet Next.js. Sinon, créez-en un :
npx create-next-app mon-app --typescriptcd mon-app
Installez les composants shadcn requis :
npx shadcn@dernier init npx shadcn@latest ajouter un badge de séparateur de bouton popover de commande
Créez multi-select.tsx
dans votre répertoire components
:
// src/components/multi-select.tsximport * as React depuis "react"; import { cva, tapez VariantProps } depuis "class-variance-authority"; import { Icône de vérification, XCercle, ChevronDown, XIcône, WandSparkles,} depuis "lucide-react"; importer { cn } depuis "@/lib/utils"; importer { Separator } depuis "@/components/ui/separator"; importer { Button } depuis "@/components/ui/ bouton";importer { Badge } depuis "@/components/ui/badge";importer { Popover, Contenu popover, PopoverTrigger,} depuis "@/components/ui/popover" ; importer { Commande, CommandeVide, Groupe de commandes, Entrée de commande, Élément de commande, Liste de commandes, CommandSeparator,} from "@/components/ui/command";/** * Variantes pour le composant à sélection multiple permettant de gérer différents styles. * Utilise class-variance-authority (cva) pour définir différents styles basés sur la prop "variante". */const multiSelectVariants = cva( "transition m-1 facilité d'entrée-sortie délai-150 survol :-translate-y-1 survol :échelle-110 durée-300", {variantes : { variante : {par défaut : "border-foreground/10 texte-avant-plan bg-card hover:bg-card/80",secondaire : "border-foreground/10 bg-secondary texte-secondaire-avant-plan survol :bg- secondaire/80",destructeur : "bordure transparente bg-destructive texte-destructeur-avant-plan survol :bg-destructive/80",inversé : "inversé", },},defaultVariants : {variant : "default",}, });/** * Accessoires pour le composant MultiSelect */interface MultiSelectProps extends React.ButtonHTMLAttributes<HTMLButtonElement>,VariantProps<typeof multiSelectVariants> { /** * Un tableau d'objets d'option à afficher dans le composant à sélection multiple. * Chaque objet option a une étiquette, une valeur et une icône facultative. */ options :{/** le texte à afficher pour l'option. */label: string;/** La valeur unique associée à l'option. */value: string;/** Composant icône facultatif à afficher à côté de l'option. */icon?: React.ComponentType<{ className?: string }>; }[]; /** * Fonction de rappel déclenchée lorsque les valeurs sélectionnées changent. * Reçoit un tableau des nouvelles valeurs sélectionnées. */ onValueChange : (valeur : string[]) => void ; /** Les valeurs sélectionnées par défaut lors du montage du composant. */ Valeur par défaut ? : string[] ; /** * Texte d'espace réservé à afficher lorsqu'aucune valeur n'est sélectionnée. * Facultatif, la valeur par défaut est « Sélectionner les options ». */ espace réservé ? : chaîne ; /** * Durée de l'animation en secondes pour les effets visuels (par exemple, badges rebondissants). * Facultatif, la valeur par défaut est 0 (pas d'animation). */ animation ? : numéro ; /** * Nombre maximum d'éléments à afficher. Les éléments supplémentaires sélectionnés seront résumés. * Facultatif, la valeur par défaut est 3. */ maxCount? : nombre ; /** * La modalité du popover. Lorsqu'elle est définie sur true, l'interaction avec les éléments extérieurs * sera désactivée et seul le contenu contextuel sera visible pour les lecteurs d'écran. * Facultatif, la valeur par défaut est false. */ modalPopover ? : booléen ; /** * Si vrai, restitue le composant à sélection multiple en tant qu'enfant d'un autre composant. * Facultatif, la valeur par défaut est false. */ asChild? : booléen ; /** * Noms de classe supplémentaires pour appliquer des styles personnalisés au composant à sélection multiple. * Facultatif, peut être utilisé pour ajouter des styles personnalisés. */ className?: string;}export const MultiSelect = React.forwardRef< HTMLButtonElement, MultiSelectProps>( ({ options, onValueChange, variant, defaultValue = [], placeholder = "Sélectionner les options", animation = 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 = ( événement : React.KeyboardEvent<HTMLInputElement>) => { if (event.key === "Enter") {setIsPopoverOpen(true); } sinon if (event.key === "Backspace" && !event.currentTarget.value) {const newSelectedValues = [...selectedValues];newSelectedValues.pop();setSelectedValues(newSelectedValues);onValueChange(newSelectedValues); }};const toggleOption = (option : chaîne) => { const newSelectedValues = selectedValues.includes(option) ? selectedValues.filter((value) => valeur !== option): [...selectedValues, option]; 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-1 bordure arrondie-md min-h-10 h-auto items-center justifier-entre bg-inherit hover:bg-inherit", className)} >{selectedValues.length > 0 ? ( <div className="flex justifier-entre les éléments-centre 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({ variant }))}style={{ animationDuration: ` ${animation}s` }} >{IconComponent && ( <IconComponent className="h-4 w-4 mr-2" />)}{option?.label}<XCircle className="ml-2 h-4 w-4 curseur-pointeur" onClick={(event) => {event.stopPropagation();toggleOption(value }}); /> </Badge>); })} {selectedValues.length > maxCount && (<Badge className={cn("bg-transparent text-foreground border-foreground/1 survol :bg-transparent",isAnimating ? "animate-bounce" : "", multiSelectVariants({ variant }) )} style={{ animationDuration: `${animation}s` }}> {`+ ${selectedValues.length - maxCount} plus`} <XCircleclassName= "ml-2 h-4 w-4 curseur-pointeur"onClick={(event) => { event.stopPropagation(); clearExtraOptions();}} /></Badge> )}</div><div className="flex items-center justification-between"> <XIconclassName="h-4 mx-2 curseur-pointeur texte-muted-foreground "onClick={(event) => { event.stopPropagation(); handleClear();}} /> <Separatororientation="vertical"className="flex min-h-6 h-full" /> <ChevronDown className="h-4 mx-2 curseur-pointeur texte-muted-foreground" / ></div> </div>) : ( <div className="flex items-center justification-entre w-full mx-auto"><span className="text-sm text-muted-foreground mx-3"> {placeholder}</span><ChevronDown className="h-4 curseur-pointeur texte-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>Aucun résultat trouvé.</CommandEmpty> <CommandGroup><CommandItem key="all" onSelect={toggleAll} className="cursor-pointer "> <divclassName={cn( "mr-2 flex h-4 w-4 éléments-centre justifier-centre arrondi-sm bordure frontière-primaire", selectedValues.length === options.length? "bg-primary text-primary-foreground": "opacity-50 [&_svg]:invisible")} ><CheckIcon className="h-4 w-4" /> </ div> <span>(Sélectionner tout)</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 justifier-centre arrondi-sm frontière frontière-primaire", 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 texte -muted-foreground" /> )} <span>{option.label}</span></CommandItem> );})} </CommandGroup> <CommandSeparator /> <CommandGroup><div className="flex items-center justifier-entre"> {selectedValues.length > 0 && (<> <CommandItemonSelect={handleClear}className="flex-1 justifier-centre curseur-pointeur" >Effacer </CommandItem> <Separatororientation= "vertical"className="flex min-h-6 h-full" /></> )} <CommandItemonSelect={() => setIsPopoverOpen(false)}className="flex-1 justifier-centre curseur-pointeur max-w-full" >Fermer </CommandItem></div> </CommandGroup></CommandList> </Command></PopoverContent>{ animation > 0 && selectedValues.length > 0 && ( <WandSparklesclassName={cn( "cursor-pointer my-2 text-foreground bg-background w-3 h-3", estAnimating ? "" : "text-muted-foreground")}onClick={() => setIsAnimating(!isAnimating)} />)} </Popover>); });MultiSelect.displayName =</sp