feat: prevent label dropdown from closing after selection
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
import { Fragment, useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
|
import { HiEllipsisHorizontal, HiMiniPlus } from "react-icons/hi2";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
interface Item {
|
interface Item {
|
||||||
@@ -20,9 +21,12 @@ interface CheckboxDropdownProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
items?: Item[];
|
items?: Item[];
|
||||||
groups?: Group[];
|
groups?: Group[];
|
||||||
|
createNewItemLabel?: string;
|
||||||
menuSpacing?: "sm" | "md" | "lg";
|
menuSpacing?: "sm" | "md" | "lg";
|
||||||
position?: "left" | "right";
|
position?: "left" | "right";
|
||||||
handleSelect: (groupKey: string | null, item: { key: string }) => void;
|
handleSelect: (groupKey: string | null, item: { key: string }) => void;
|
||||||
|
handleEdit?: (key: string) => void;
|
||||||
|
handleCreate?: () => void;
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,9 +34,12 @@ export default function CheckboxDropdown({
|
|||||||
children,
|
children,
|
||||||
items,
|
items,
|
||||||
groups,
|
groups,
|
||||||
|
createNewItemLabel = "Create new",
|
||||||
menuSpacing = "sm",
|
menuSpacing = "sm",
|
||||||
position = "left",
|
position = "left",
|
||||||
handleSelect,
|
handleSelect,
|
||||||
|
handleEdit,
|
||||||
|
handleCreate,
|
||||||
asChild = true,
|
asChild = true,
|
||||||
}: CheckboxDropdownProps) {
|
}: CheckboxDropdownProps) {
|
||||||
const [selectedGroup, setSelectedGroup] = useState<string | null>(null);
|
const [selectedGroup, setSelectedGroup] = useState<string | null>(null);
|
||||||
@@ -43,6 +50,61 @@ export default function CheckboxDropdown({
|
|||||||
lg: "top-[38px]",
|
lg: "top-[38px]",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderMenuItems = (items: Item[], groupKey: string | null) => (
|
||||||
|
<>
|
||||||
|
{items.map((item) => (
|
||||||
|
<Menu.Item key={item.key}>
|
||||||
|
<div
|
||||||
|
className="group flex items-center rounded-[5px] p-2 hover:bg-light-200 dark:hover:bg-dark-300"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSelect(groupKey, { key: item.key });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id={item.key}
|
||||||
|
name={item.key}
|
||||||
|
type="checkbox"
|
||||||
|
className="h-[14px] w-[14px] rounded bg-transparent"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
onChange={() => handleSelect(groupKey, { key: item.key })}
|
||||||
|
checked={item.selected}
|
||||||
|
/>
|
||||||
|
{item.leftIcon && (
|
||||||
|
<span className="ml-3 flex items-center">{item.leftIcon}</span>
|
||||||
|
)}
|
||||||
|
<label
|
||||||
|
htmlFor={item.key}
|
||||||
|
className="ml-3 text-[12px] text-dark-900"
|
||||||
|
>
|
||||||
|
{item.value}
|
||||||
|
</label>
|
||||||
|
{handleEdit && (
|
||||||
|
<button
|
||||||
|
className="invisible ml-auto group-hover:visible"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
handleEdit(item.key);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HiEllipsisHorizontal size={20} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
{handleCreate && (
|
||||||
|
<button
|
||||||
|
className="flex w-full items-center rounded-[5px] p-2 px-2 text-[12px] hover:bg-light-200 dark:hover:bg-dark-300"
|
||||||
|
onClick={() => handleCreate()}
|
||||||
|
>
|
||||||
|
<HiMiniPlus size={20} className="pr-1.5" />
|
||||||
|
{createNewItemLabel}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
as="div"
|
as="div"
|
||||||
@@ -76,38 +138,8 @@ export default function CheckboxDropdown({
|
|||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
{!selectedGroup ? (
|
{!selectedGroup ? (
|
||||||
<>
|
<>
|
||||||
{items?.map((item) => (
|
{items && renderMenuItems(items, null)}
|
||||||
<Menu.Item key={item.key}>
|
|
||||||
<div
|
|
||||||
className="flex items-center rounded-[5px] p-2 hover:bg-light-200 dark:hover:bg-dark-300"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSelect(null, { key: item.key });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id={item.key}
|
|
||||||
name={item.key}
|
|
||||||
type="checkbox"
|
|
||||||
className="h-[14px] w-[14px] rounded bg-transparent"
|
|
||||||
onClick={(event) => event.stopPropagation()}
|
|
||||||
onChange={() => handleSelect(null, { key: item.key })}
|
|
||||||
checked={item.selected}
|
|
||||||
/>
|
|
||||||
{item.leftIcon && (
|
|
||||||
<span className="ml-3 flex items-center">
|
|
||||||
{item.leftIcon}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<label
|
|
||||||
htmlFor={item.key}
|
|
||||||
className="ml-3 text-[12px] text-dark-900"
|
|
||||||
>
|
|
||||||
{item.value}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</Menu.Item>
|
|
||||||
))}
|
|
||||||
{groups?.map((group) => (
|
{groups?.map((group) => (
|
||||||
<Menu.Item key={group.key}>
|
<Menu.Item key={group.key}>
|
||||||
<div
|
<div
|
||||||
@@ -127,42 +159,11 @@ export default function CheckboxDropdown({
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{groups
|
{groups?.find((g) => g.key === selectedGroup)?.items &&
|
||||||
?.find((g) => g.key === selectedGroup)
|
renderMenuItems(
|
||||||
?.items.map((item) => (
|
groups.find((g) => g.key === selectedGroup)?.items || [],
|
||||||
<Menu.Item key={item.key}>
|
selectedGroup,
|
||||||
<div
|
)}
|
||||||
className="flex items-center rounded-[5px] p-2 hover:bg-light-200 dark:hover:bg-dark-300"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSelect(selectedGroup, { key: item.key });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id={item.key}
|
|
||||||
name={item.key}
|
|
||||||
type="checkbox"
|
|
||||||
className="h-[14px] w-[14px] rounded bg-transparent"
|
|
||||||
onClick={(event) => event.stopPropagation()}
|
|
||||||
onChange={() =>
|
|
||||||
handleSelect(selectedGroup, { key: item.key })
|
|
||||||
}
|
|
||||||
checked={item.selected}
|
|
||||||
/>
|
|
||||||
{item.leftIcon && (
|
|
||||||
<span className="ml-3 flex items-center">
|
|
||||||
{item.leftIcon}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<label
|
|
||||||
htmlFor={item.key}
|
|
||||||
className="ml-3 text-[12px] text-dark-900"
|
|
||||||
>
|
|
||||||
{item.value}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</Menu.Item>
|
|
||||||
))}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu } from "@headlessui/react";
|
||||||
import { Fragment } from "react";
|
import { HiMiniPlus } from "react-icons/hi2";
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { HiEllipsisHorizontal, HiMiniPlus } from "react-icons/hi2";
|
|
||||||
|
|
||||||
import Badge from "~/components/Badge";
|
import Badge from "~/components/Badge";
|
||||||
import LabelIcon from "~/components/LabelIcon";
|
import CheckboxDropdown from "~/components/CheckboxDropdown";
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
interface LabelSelectorProps {
|
interface LabelSelectorProps {
|
||||||
cardPublicId: string;
|
cardPublicId: string;
|
||||||
labels: {
|
labels: {
|
||||||
publicId: string;
|
key: string;
|
||||||
name: string;
|
value: string;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
colourCode: string;
|
leftIcon: React.ReactNode;
|
||||||
}[];
|
}[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
@@ -35,16 +33,6 @@ export default function LabelSelector({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { register, handleSubmit, setValue, watch } = useForm({
|
|
||||||
values: Object.fromEntries(
|
|
||||||
labels.map((label) => [label.publicId, label.selected]) ?? [],
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = (values: Record<string, boolean>) => {
|
|
||||||
console.log({ values });
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectedLabels = labels.filter((label) => label.selected);
|
const selectedLabels = labels.filter((label) => label.selected);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -54,18 +42,21 @@ export default function LabelSelector({
|
|||||||
<div className="h-full w-[175px] animate-pulse rounded-[5px] bg-light-300 dark:bg-dark-300" />
|
<div className="h-full w-[175px] animate-pulse rounded-[5px] bg-light-300 dark:bg-dark-300" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Menu
|
<CheckboxDropdown
|
||||||
as="div"
|
items={labels}
|
||||||
className="relative flex w-full flex-wrap items-center text-left"
|
handleSelect={(_, label) =>
|
||||||
|
addOrRemoveLabel.mutate({ cardPublicId, labelPublicId: label.key })
|
||||||
|
}
|
||||||
|
handleEdit={(labelPublicId) => openModal("EDIT_LABEL", labelPublicId)}
|
||||||
|
handleCreate={() => openModal("NEW_LABEL")}
|
||||||
|
createNewItemLabel="Create new label"
|
||||||
|
asChild
|
||||||
>
|
>
|
||||||
{selectedLabels.length ? (
|
{selectedLabels.length ? (
|
||||||
<div className="flex flex-wrap gap-x-0.5">
|
<div className="flex flex-wrap gap-x-0.5">
|
||||||
{selectedLabels.map((label) => (
|
{selectedLabels.map((label) => (
|
||||||
<Menu.Button key={label.publicId}>
|
<Menu.Button key={label.key}>
|
||||||
<Badge
|
<Badge value={label.value} iconLeft={label.leftIcon} />
|
||||||
value={label.name}
|
|
||||||
iconLeft={<LabelIcon colourCode={label.colourCode} />}
|
|
||||||
/>
|
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
))}
|
))}
|
||||||
<Menu.Button>
|
<Menu.Button>
|
||||||
@@ -78,84 +69,7 @@ export default function LabelSelector({
|
|||||||
Add label
|
Add label
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
)}
|
)}
|
||||||
|
</CheckboxDropdown>
|
||||||
<Transition
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition ease-out duration-100"
|
|
||||||
enterFrom="transform opacity-0 scale-95"
|
|
||||||
enterTo="transform opacity-100 scale-100"
|
|
||||||
leave="transition ease-in duration-75"
|
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
|
||||||
leaveTo="transform opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<Menu.Items className="absolute right-[200px] top-[30px] z-10 mt-2 w-56 origin-top-right rounded-md border-[1px] border-light-600 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-500 dark:bg-dark-200">
|
|
||||||
<div className="p-2">
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
{labels.map((label) => (
|
|
||||||
<Menu.Item key={label.publicId}>
|
|
||||||
{() => (
|
|
||||||
<div
|
|
||||||
key={label.publicId}
|
|
||||||
className="group flex items-center rounded-[5px] p-2 hover:bg-light-200 dark:hover:bg-dark-300"
|
|
||||||
onClick={() => {
|
|
||||||
const newValue = !watch(label.publicId);
|
|
||||||
setValue(label.publicId, newValue);
|
|
||||||
|
|
||||||
addOrRemoveLabel.mutate({
|
|
||||||
cardPublicId,
|
|
||||||
labelPublicId: label.publicId,
|
|
||||||
});
|
|
||||||
|
|
||||||
handleSubmit(onSubmit);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id={label.publicId}
|
|
||||||
type="checkbox"
|
|
||||||
className="h-[14px] w-[14px] rounded bg-transparent"
|
|
||||||
onClick={(event) => event.stopPropagation()}
|
|
||||||
{...register(label.publicId)}
|
|
||||||
checked={watch(label.publicId)}
|
|
||||||
/>
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="ml-3 flex items-center">
|
|
||||||
<LabelIcon colourCode={label.colourCode} />
|
|
||||||
</span>
|
|
||||||
<label
|
|
||||||
htmlFor={label.publicId}
|
|
||||||
className="ml-3 text-sm"
|
|
||||||
>
|
|
||||||
{label.name}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="invisible group-hover:visible"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
openModal("EDIT_LABEL", label.publicId);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<HiEllipsisHorizontal size={20} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
))}
|
|
||||||
<button
|
|
||||||
onClick={() => openModal("NEW_LABEL")}
|
|
||||||
className="flex w-full items-center rounded-[5px] p-1.5 px-2 text-sm hover:bg-light-200 dark:hover:bg-dark-300"
|
|
||||||
>
|
|
||||||
<HiMiniPlus size={22} className="pr-2" />
|
|
||||||
Create new label
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</Menu.Items>
|
|
||||||
</Transition>
|
|
||||||
</Menu>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import ContentEditable from "react-contenteditable";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { IoChevronForwardSharp } from "react-icons/io5";
|
import { IoChevronForwardSharp } from "react-icons/io5";
|
||||||
|
|
||||||
|
import LabelIcon from "~/components/LabelIcon";
|
||||||
import Modal from "~/components/modal";
|
import Modal from "~/components/modal";
|
||||||
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
||||||
import { PageHead } from "~/components/PageHead";
|
import { PageHead } from "~/components/PageHead";
|
||||||
@@ -55,9 +56,10 @@ export default function CardPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...label,
|
key: label.publicId,
|
||||||
|
value: label.name,
|
||||||
selected: isSelected ?? false,
|
selected: isSelected ?? false,
|
||||||
colourCode: label.colourCode ?? "",
|
leftIcon: <LabelIcon colourCode={label.colourCode} />,
|
||||||
};
|
};
|
||||||
}) ?? [];
|
}) ?? [];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user