Skip to content

feature: add 'disable' functionality #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/app/components/card-styled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as React from "react";
import { Card } from "@/components/ui/card";

export const CardStyled = ({ children }: React.PropsWithChildren) => {
return <Card className="w-full max-w-xl p-5">{children}</Card>;
};
99 changes: 89 additions & 10 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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",
Expand All @@ -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
Expand All @@ -63,12 +92,18 @@ const FormSchema = z.object({
});

export default function Home() {
const form = useForm<z.infer<typeof FormSchema>>({
const basicExampleForm = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
frameworks: ["next.js", "nuxt.js"],
},
});
const disabledExampleForm = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
frameworks: ["sveltekit", "astro"],
},
});

function onSubmit(data: z.infer<typeof FormSchema>) {
toast(
Expand All @@ -77,7 +112,7 @@ export default function Home() {
}

return (
<main className="flex min-h-screen:calc(100vh - 3rem) flex-col items-center justify-start space-y-3 p-3">
<main className="flex min-h-screen:calc(100vh - 3rem) flex-col items-center justify-start space-y-3 p-3 pb-20">
<PageHeader>
<PageHeaderHeading>Multi select component</PageHeaderHeading>
<PageHeaderDescription>assembled with shadcn/ui</PageHeaderDescription>
Expand All @@ -93,11 +128,16 @@ export default function Home() {
</Link>
</PageActions>
</PageHeader>
<Card className="w-full max-w-xl p-5">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">

<CardStyled>
<CardTitle className="mb-6">Basic Example</CardTitle>
<Form {...basicExampleForm}>
<form
onSubmit={basicExampleForm.handleSubmit(onSubmit)}
className="space-y-8"
>
<FormField
control={form.control}
control={basicExampleForm.control}
name="frameworks"
render={({ field }) => (
<FormItem>
Expand Down Expand Up @@ -125,7 +165,46 @@ export default function Home() {
</Button>
</form>
</Form>
</Card>
</CardStyled>

<CardStyled>
<CardTitle className="mb-6">Disabled Usecase Example</CardTitle>
<Form {...disabledExampleForm}>
<form
onSubmit={disabledExampleForm.handleSubmit(onSubmit)}
className="space-y-8"
>
<FormField
control={disabledExampleForm.control}
name="frameworks"
render={({ field }) => (
<FormItem>
<FormLabel>Frameworks</FormLabel>
<FormControl>
<MultiSelect
options={frameworksListIncludingDisabled}
onValueChange={field.onChange}
defaultValue={field.value}
placeholder="Select options"
variant="inverted"
animation={2}
maxCount={3}
/>
</FormControl>
<FormDescription>
Choose the frameworks you are interested in. <br />
There is a <strong>disabled</strong> values in the list.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button variant="default" type="submit" className="w-full">
Submit
</Button>
</form>
</Form>
</CardStyled>
</main>
);
}
56 changes: 40 additions & 16 deletions src/components/multi-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const multiSelectVariants = cva(
/**
* Props for MultiSelect component
*/
interface MultiSelectProps
export interface MultiSelectProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof multiSelectVariants> {
/**
Expand All @@ -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;
}[];

/**
Expand Down Expand Up @@ -141,6 +143,17 @@ export const MultiSelect = React.forwardRef<
React.useState<string[]>(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)) {
Expand Down Expand Up @@ -170,8 +183,8 @@ export const MultiSelect = React.forwardRef<
};

const handleClear = () => {
setSelectedValues([]);
onValueChange([]);
setSelectedValues(defaultAndDisabledValues);
onValueChange(defaultAndDisabledValues);
};

const handleTogglePopover = () => {
Expand All @@ -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);
}
};

Expand Down Expand Up @@ -221,21 +240,25 @@ 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` }}
>
{IconComponent && (
<IconComponent className="h-4 w-4 mr-2" />
)}
{option?.label}
<XCircle
className="ml-2 h-4 w-4 cursor-pointer"
onClick={(event) => {
event.stopPropagation();
toggleOption(value);
}}
/>
{!option?.disabled && (
<XCircle
className="ml-2 h-4 w-4 cursor-pointer"
onClick={(event) => {
event.stopPropagation();
toggleOption(value);
}}
/>
)}
</Badge>
);
})}
Expand Down Expand Up @@ -305,7 +328,7 @@ export const MultiSelect = React.forwardRef<
<div
className={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
selectedValues.length === options.length
isSelectedAll
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible"
)}
Expand All @@ -319,6 +342,7 @@ export const MultiSelect = React.forwardRef<
return (
<CommandItem
key={option.value}
disabled={option?.disabled}
onSelect={() => toggleOption(option.value)}
className="cursor-pointer"
>
Expand Down