diff --git a/README.md b/README.md index f7c22b5..79f0f39 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,32 @@ ## Multi-Select Component Setup in Next.js +- This is a fork of the original [Shadcn Multi-Select Component which changes](https://shadcn-multi-select-component.vercel.app/): + - React19 Support + - Sub-options support + - **Note:** This makes it such that ONLY the sub-options can be values, not the options itself + - See the `page.tsx` example in **Step 3.** below + - Fixed a bug with deleting options not working + - Removes the "Magic Wand" bouncing animation + ### Prerequisites -Ensure you have a Next.js project set up. If not, create one: +Ensure you have a Next.js/React project set up. If not, you can create create one: + +Next.JS: ```bash npx create-next-app my-app --typescript cd my-app ``` +React: + +```bash +npm create vite@latest my-app -- --template react-ts +cd my-app +``` + ### Step 1: Install shadcn Components Install required shadcn components: @@ -27,17 +44,9 @@ npx shadcn@latest add command popover button separator badge Create `multi-select.tsx` in your `components` directory: ```tsx -// src/components/multi-select.tsx - import * as React from "react"; import { cva, type VariantProps } from "class-variance-authority"; -import { - CheckIcon, - XCircle, - ChevronDown, - XIcon, - WandSparkles, -} from "lucide-react"; +import { CheckIcon, XCircle, ChevronDown, XIcon } from "lucide-react"; import { cn } from "@/lib/utils"; import { Separator } from "@/components/ui/separator"; @@ -63,7 +72,7 @@ import { * Uses class-variance-authority (cva) to define different styles based on "variant" prop. */ const multiSelectVariants = cva( - "m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300", + "m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-105! duration-300", { variants: { variant: { @@ -79,9 +88,25 @@ const multiSelectVariants = cva( defaultVariants: { variant: "default", }, - } + }, ); +export interface Option { + /** The text to display for the option. */ + label: string; + /** The unique value associated with the option. */ + value: string; + /** Optional icon component to display alongside the option. */ + icon?: React.ComponentType<{ className?: string }>; + iconClass?: string; + subCategories?: { + label: string; + value: string; + icon?: React.ComponentType<{ className?: string }>; + iconClass?: string; + }[]; +} + /** * Props for MultiSelect component */ @@ -92,23 +117,15 @@ interface MultiSelectProps * An array of option objects to be displayed in the multi-select component. * Each option object has a label, value, and an optional icon. */ - options: { - /** The text to display for the option. */ - label: string; - /** The unique value associated with the option. */ - value: string; - /** Optional icon component to display alongside the option. */ - icon?: React.ComponentType<{ className?: string }>; - }[]; + options: Option[]; + + selectedValues: string[]; /** * Callback function triggered when the selected values change. * Receives an array of the new selected values. */ - onValueChange: (value: string[]) => void; - - /** The default selected values when the component mounts. */ - defaultValue?: string[]; + setSelectedValues: (value: string[]) => void; /** * Placeholder text to be displayed when no values are selected. @@ -155,9 +172,9 @@ export const MultiSelect = React.forwardRef< ( { options, - onValueChange, + selectedValues, + setSelectedValues, variant, - defaultValue = [], placeholder = "Select options", animation = 0, maxCount = 3, @@ -166,15 +183,24 @@ export const MultiSelect = React.forwardRef< className, ...props }, - ref + ref, ) => { - const [selectedValues, setSelectedValues] = - React.useState(defaultValue); const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); - const [isAnimating, setIsAnimating] = React.useState(false); + const selectedValuesSet = React.useMemo(() => { + return new Set(selectedValues); + }, [selectedValues]); + const fullListOptions = React.useMemo(() => { + const allOptions = []; + for (const option of options) { + if (option.subCategories) { + allOptions.push(...option.subCategories); + } else allOptions.push(option); + } + return allOptions; + }, [options]); const handleInputKeyDown = ( - event: React.KeyboardEvent + event: React.KeyboardEvent, ) => { if (event.key === "Enter") { setIsPopoverOpen(true); @@ -182,21 +208,35 @@ export const MultiSelect = React.forwardRef< const newSelectedValues = [...selectedValues]; newSelectedValues.pop(); setSelectedValues(newSelectedValues); - onValueChange(newSelectedValues); } }; - const toggleOption = (option: string) => { - const newSelectedValues = selectedValues.includes(option) - ? selectedValues.filter((value) => value !== option) - : [...selectedValues, option]; - setSelectedValues(newSelectedValues); - onValueChange(newSelectedValues); + const toggleOption = (option: Option) => { + if (option.subCategories) { + const subCatValues = option.subCategories.map((subCat) => subCat.value); + + const isSubCatSelected = option.subCategories.every((subCat) => + selectedValuesSet.has(subCat.value), + ); + if (isSubCatSelected) { + for (const subCatValue of subCatValues) { + selectedValuesSet.delete(subCatValue); + } + } else { + for (const subCatValue of subCatValues) { + selectedValuesSet.add(subCatValue); + } + } + } else { + if (selectedValuesSet.has(option.value)) + selectedValuesSet.delete(option.value); + else selectedValuesSet.add(option.value); + } + setSelectedValues(Array.from(selectedValuesSet)); }; const handleClear = () => { setSelectedValues([]); - onValueChange([]); }; const handleTogglePopover = () => { @@ -206,17 +246,12 @@ export const MultiSelect = React.forwardRef< const clearExtraOptions = () => { const newSelectedValues = selectedValues.slice(0, maxCount); setSelectedValues(newSelectedValues); - onValueChange(newSelectedValues); }; const toggleAll = () => { - if (selectedValues.length === options.length) { + if (selectedValues.length === fullListOptions.length) { handleClear(); - } else { - const allValues = options.map((option) => option.value); - setSelectedValues(allValues); - onValueChange(allValues); - } + } else setSelectedValues(fullListOptions.map((option) => option.value)); }; return ( @@ -225,42 +260,53 @@ export const MultiSelect = React.forwardRef< onOpenChange={setIsPopoverOpen} modal={modalPopover} > - +