diff --git a/src/app/components/card-styled.tsx b/src/app/components/card-styled.tsx new file mode 100644 index 0000000..9625199 --- /dev/null +++ b/src/app/components/card-styled.tsx @@ -0,0 +1,6 @@ +import * as React from "react"; +import { Card } from "@/components/ui/card"; + +export const CardStyled = ({ children }: React.PropsWithChildren) => { + return {children}; +}; diff --git a/src/app/page.tsx b/src/app/page.tsx index 7381fad..27bd4a6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -8,7 +8,8 @@ import Link from "next/link"; import { cn } from "@/lib/utils"; import { Button, buttonVariants } from "@/components/ui/button"; -import { Card } from "@/components/ui/card"; +import { CardStyled } from "./components/card-styled"; +import { CardTitle } from "@/components/ui/card"; import { Icons } from "@/components/icons"; import { Form, @@ -25,9 +26,9 @@ import { PageHeaderDescription, PageHeaderHeading, } from "@/components/page-header"; -import { MultiSelect } from "@/components/multi-select"; +import { MultiSelect, type MultiSelectProps } from "@/components/multi-select"; -const frameworksList = [ +const frameworksList: MultiSelectProps["options"] = [ { value: "next.js", label: "Next.js", @@ -54,6 +55,34 @@ const frameworksList = [ icon: Icons.fish, }, ]; +const frameworksListIncludingDisabled: MultiSelectProps["options"] = [ + { + value: "next.js", + label: "Next.js", + icon: Icons.dog, + }, + { + value: "sveltekit", + label: "SvelteKit", + icon: Icons.cat, + disabled: true, + }, + { + value: "nuxt.js", + label: "Nuxt.js", + icon: Icons.turtle, + }, + { + value: "remix", + label: "Remix", + icon: Icons.rabbit, + }, + { + value: "astro", + label: "Astro", + icon: Icons.fish, + }, +]; const FormSchema = z.object({ frameworks: z @@ -63,12 +92,18 @@ const FormSchema = z.object({ }); export default function Home() { - const form = useForm>({ + const basicExampleForm = useForm>({ resolver: zodResolver(FormSchema), defaultValues: { frameworks: ["next.js", "nuxt.js"], }, }); + const disabledExampleForm = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { + frameworks: ["sveltekit", "astro"], + }, + }); function onSubmit(data: z.infer) { toast( @@ -77,7 +112,7 @@ export default function Home() { } return ( - + Multi select component assembled with shadcn/ui @@ -93,11 +128,16 @@ export default function Home() { - - - + + + Basic Example + + ( @@ -125,7 +165,46 @@ export default function Home() { - + + + + Disabled Usecase Example + + + ( + + Frameworks + + + + + Choose the frameworks you are interested in. + There is a disabled values in the list. + + + + )} + /> + + Submit + + + + ); } diff --git a/src/components/multi-select.tsx b/src/components/multi-select.tsx index 0f33d2b..108bfbd 100644 --- a/src/components/multi-select.tsx +++ b/src/components/multi-select.tsx @@ -54,7 +54,7 @@ const multiSelectVariants = cva( /** * Props for MultiSelect component */ -interface MultiSelectProps +export interface MultiSelectProps extends React.ButtonHTMLAttributes, VariantProps { /** @@ -68,6 +68,8 @@ interface MultiSelectProps value: string; /** Optional icon component to display alongside the option. */ icon?: React.ComponentType<{ className?: string }>; + /** Decides whether to disable the option or not. If true, user cannot interact with the option. */ + disabled?: boolean; }[]; /** @@ -141,6 +143,17 @@ export const MultiSelect = React.forwardRef< React.useState(defaultValue); const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); const [isAnimating, setIsAnimating] = React.useState(false); + const disabledOptions = options.filter((option) => option.disabled); + const disabledAndUncheckedOptions = disabledOptions.filter( + ({ value }) => !selectedValues.includes(value) + ); + const disabledValues = disabledOptions.map(({ value }) => value); + const defaultAndDisabledValues = defaultValue.filter((value) => + disabledValues.includes(value) + ); + const isSelectedAll = + selectedValues.length === + options.length - disabledAndUncheckedOptions.length; React.useEffect(() => { if (JSON.stringify(selectedValues) !== JSON.stringify(defaultValue)) { @@ -170,8 +183,8 @@ export const MultiSelect = React.forwardRef< }; const handleClear = () => { - setSelectedValues([]); - onValueChange([]); + setSelectedValues(defaultAndDisabledValues); + onValueChange(defaultAndDisabledValues); }; const handleTogglePopover = () => { @@ -185,12 +198,18 @@ export const MultiSelect = React.forwardRef< }; const toggleAll = () => { - if (selectedValues.length === options.length) { + if (isSelectedAll) { handleClear(); } else { - const allValues = options.map((option) => option.value); - setSelectedValues(allValues); - onValueChange(allValues); + const enabledValues = options + .filter(({ disabled }) => !disabled) + .map(({ value }) => value); + const newSelectedValues = [ + ...defaultAndDisabledValues, + ...enabledValues, + ]; + setSelectedValues(newSelectedValues); + onValueChange(newSelectedValues); } }; @@ -221,7 +240,9 @@ export const MultiSelect = React.forwardRef< key={value} className={cn( isAnimating ? "animate-bounce" : "", - multiSelectVariants({ variant }) + option?.disabled + ? multiSelectVariants({ variant: "secondary" }) + : multiSelectVariants({ variant }) )} style={{ animationDuration: `${animation}s` }} > @@ -229,13 +250,15 @@ export const MultiSelect = React.forwardRef< )} {option?.label} - { - event.stopPropagation(); - toggleOption(value); - }} - /> + {!option?.disabled && ( + { + event.stopPropagation(); + toggleOption(value); + }} + /> + )} ); })} @@ -305,7 +328,7 @@ export const MultiSelect = React.forwardRef< toggleOption(option.value)} className="cursor-pointer" >