mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-10 06:14:06 +00:00
♻️ create custom autocomplete tag input
This commit is contained in:
@@ -5,8 +5,10 @@ import { useMemo, useState } from "react";
|
|||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { MultiSelectTags } from "./multi-select/multi-select-tags";
|
import {
|
||||||
import { TagInput, type TagInputProps } from "./tags/tag-input";
|
SuggestionsTagInput,
|
||||||
|
type SuggestionsTagInputProps
|
||||||
|
} from "./tags/suggestions-tag-input";
|
||||||
|
|
||||||
export type SelectedMachine = Pick<
|
export type SelectedMachine = Pick<
|
||||||
ListClientsResponse["clients"][number],
|
ListClientsResponse["clients"][number],
|
||||||
@@ -18,19 +20,14 @@ export type MachineSelectorProps = {
|
|||||||
selectedMachines?: SelectedMachine[];
|
selectedMachines?: SelectedMachine[];
|
||||||
onSelectMachines: (machine: SelectedMachine[]) => void;
|
onSelectMachines: (machine: SelectedMachine[]) => void;
|
||||||
} & Omit<
|
} & Omit<
|
||||||
TagInputProps,
|
SuggestionsTagInputProps,
|
||||||
| "activeTagIndex"
|
|
||||||
| "setActiveTagIndex"
|
|
||||||
| "placeholder"
|
|
||||||
| "size"
|
|
||||||
| "tags"
|
| "tags"
|
||||||
| "setTags"
|
| "setTags"
|
||||||
| "value"
|
| "suggestedOptions"
|
||||||
| "searchQuery"
|
| "searchQuery"
|
||||||
| "onSearchQueryChange"
|
| "onSearchQueryChange"
|
||||||
| "suggestedOptions"
|
| "activeTagIndex"
|
||||||
| "enableAutocomplete"
|
| "setActiveTagIndex"
|
||||||
| "autocompleteOptions"
|
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export function MachinesSelector({
|
export function MachinesSelector({
|
||||||
@@ -48,7 +45,7 @@ export function MachinesSelector({
|
|||||||
orgQueries.machineClients({ orgId, perPage: 3, query: debouncedValue })
|
orgQueries.machineClients({ orgId, perPage: 3, query: debouncedValue })
|
||||||
);
|
);
|
||||||
|
|
||||||
// always include the selected machines in the list of machines shown (if the user isn't searching)
|
// always include the selected machines in the list (if the user isn't searching)
|
||||||
const machinesShown = useMemo(() => {
|
const machinesShown = useMemo(() => {
|
||||||
const allMachines: Array<SelectedMachine> = [...machines];
|
const allMachines: Array<SelectedMachine> = [...machines];
|
||||||
if (debouncedValue.trim().length === 0) {
|
if (debouncedValue.trim().length === 0) {
|
||||||
@@ -60,117 +57,45 @@ export function MachinesSelector({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return allMachines;
|
return allMachines;
|
||||||
}, [machines, selectedMachines, debouncedValue]);
|
}, [machines, selectedMachines, debouncedValue]);
|
||||||
|
|
||||||
// const selectedMachinesIds = new Set(
|
|
||||||
// selectedMachines.map((m) => m.clientId)
|
|
||||||
// );
|
|
||||||
|
|
||||||
const [activeTagIndex, setActiveTagIndex] = useState<number | null>(null);
|
const [activeTagIndex, setActiveTagIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<SuggestionsTagInput
|
||||||
<TagInput
|
{...props}
|
||||||
{...props}
|
activeTagIndex={activeTagIndex}
|
||||||
activeTagIndex={activeTagIndex}
|
setActiveTagIndex={setActiveTagIndex}
|
||||||
setActiveTagIndex={setActiveTagIndex}
|
placeholder={t("accessClientSelect")}
|
||||||
placeholder={t("accessClientSelect")}
|
tags={selectedMachines.map((mc) => ({
|
||||||
size="sm"
|
id: mc.clientId.toString(),
|
||||||
tags={selectedMachines.map((mc) => ({
|
text: mc.name
|
||||||
id: mc.clientId.toString(),
|
}))}
|
||||||
text: mc.name
|
setTags={(newTags) => {
|
||||||
}))}
|
const tags =
|
||||||
setTags={(newTags) => {
|
typeof newTags === "function"
|
||||||
const tags =
|
? newTags(
|
||||||
typeof newTags === "function"
|
selectedMachines.map((mc) => ({
|
||||||
? newTags(
|
id: mc.clientId.toString(),
|
||||||
selectedMachines.map((mc) => ({
|
text: mc.name
|
||||||
id: mc.clientId.toString(),
|
}))
|
||||||
text: mc.name
|
)
|
||||||
}))
|
: newTags;
|
||||||
)
|
onSelectMachines(
|
||||||
: newTags;
|
tags.map((tag) => ({
|
||||||
onSelectMachines(
|
clientId: Number(tag.id),
|
||||||
tags.map((tag) => ({
|
name: tag.text
|
||||||
clientId: Number(tag.id),
|
}))
|
||||||
name: tag.text
|
);
|
||||||
}))
|
}}
|
||||||
);
|
searchQuery={machineSearchQuery}
|
||||||
}}
|
onSearchQueryChange={setMachineSearchQuery}
|
||||||
searchQuery={machineSearchQuery}
|
suggestedOptions={machinesShown.map((mc) => ({
|
||||||
onSearchQueryChange={setMachineSearchQuery}
|
id: mc.clientId.toString(),
|
||||||
suggestedOptions={machinesShown.map((mc) => ({
|
text: mc.name
|
||||||
id: mc.clientId.toString(),
|
}))}
|
||||||
text: mc.name
|
allowDuplicates={false}
|
||||||
}))}
|
/>
|
||||||
allowDuplicates={false}
|
|
||||||
restrictTagsToAutocompleteOptions
|
|
||||||
sortTags
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
// <MultiSelectTags
|
|
||||||
// emptyPlaceholder={t("machineNotFound")}
|
|
||||||
// searchPlaceholder={t("machineSearch")}
|
|
||||||
// value={selectedMachines.map((m) => ({
|
|
||||||
// ...m,
|
|
||||||
// text: m.name,
|
|
||||||
// id: m.clientId.toString()
|
|
||||||
// }))}
|
|
||||||
// onChange={(values) => {
|
|
||||||
// onSelectMachines(values);
|
|
||||||
// }}
|
|
||||||
// options={machinesShown.map((m) => ({
|
|
||||||
// ...m,
|
|
||||||
// id: m.clientId.toString(),
|
|
||||||
// text: m.name
|
|
||||||
// }))}
|
|
||||||
// onSearch={setMachineSearchQuery}
|
|
||||||
// searchQuery={machineSearchQuery}
|
|
||||||
// />
|
|
||||||
|
|
||||||
// <Command shouldFilter={false}>
|
|
||||||
// <CommandInput
|
|
||||||
// placeholder={t("machineSearch")}
|
|
||||||
// value={machineSearchQuery}
|
|
||||||
// onValueChange={setMachineSearchQuery}
|
|
||||||
// />
|
|
||||||
// <CommandList>
|
|
||||||
// <CommandEmpty>{t("machineNotFound")}</CommandEmpty>
|
|
||||||
// <CommandGroup>
|
|
||||||
// {machinesShown.map((m) => (
|
|
||||||
// <CommandItem
|
|
||||||
// value={`${m.name}:${m.clientId}`}
|
|
||||||
// key={m.clientId}
|
|
||||||
// onSelect={() => {
|
|
||||||
// let newMachineClients = [];
|
|
||||||
// if (selectedMachinesIds.has(m.clientId)) {
|
|
||||||
// newMachineClients = selectedMachines.filter(
|
|
||||||
// (mc) => mc.clientId !== m.clientId
|
|
||||||
// );
|
|
||||||
// } else {
|
|
||||||
// newMachineClients = [
|
|
||||||
// ...selectedMachines,
|
|
||||||
// m
|
|
||||||
// ];
|
|
||||||
// }
|
|
||||||
// onSelectMachines(newMachineClients);
|
|
||||||
// }}
|
|
||||||
// >
|
|
||||||
// <CheckIcon
|
|
||||||
// className={cn(
|
|
||||||
// "mr-2 h-4 w-4",
|
|
||||||
// selectedMachinesIds.has(m.clientId)
|
|
||||||
// ? "opacity-100"
|
|
||||||
// : "opacity-0"
|
|
||||||
// )}
|
|
||||||
// />
|
|
||||||
// {`${m.name}`}
|
|
||||||
// </CommandItem>
|
|
||||||
// ))}
|
|
||||||
// </CommandGroup>
|
|
||||||
// </CommandList>
|
|
||||||
// </Command>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,9 +41,6 @@ type AutocompleteProps = {
|
|||||||
usePortal?: boolean;
|
usePortal?: boolean;
|
||||||
/** Narrows the dropdown list from the main field (cmdk search filters further). */
|
/** Narrows the dropdown list from the main field (cmdk search filters further). */
|
||||||
filterQuery?: string;
|
filterQuery?: string;
|
||||||
/** When true, skip internal filtering and make the CommandInput controlled — parent owns filtering. */
|
|
||||||
disableSearch?: boolean;
|
|
||||||
onFilterQueryChange?: (value: string) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Autocomplete: React.FC<AutocompleteProps> = ({
|
export const Autocomplete: React.FC<AutocompleteProps> = ({
|
||||||
@@ -60,9 +57,7 @@ export const Autocomplete: React.FC<AutocompleteProps> = ({
|
|||||||
children,
|
children,
|
||||||
classStyleProps,
|
classStyleProps,
|
||||||
usePortal,
|
usePortal,
|
||||||
filterQuery = "",
|
filterQuery = ""
|
||||||
disableSearch = false,
|
|
||||||
onFilterQueryChange
|
|
||||||
}) => {
|
}) => {
|
||||||
const triggerContainerRef = useRef<HTMLDivElement | null>(null);
|
const triggerContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
@@ -75,13 +70,12 @@ export const Autocomplete: React.FC<AutocompleteProps> = ({
|
|||||||
const [commandResetKey, setCommandResetKey] = useState(0);
|
const [commandResetKey, setCommandResetKey] = useState(0);
|
||||||
|
|
||||||
const visibleOptions = useMemo(() => {
|
const visibleOptions = useMemo(() => {
|
||||||
if (disableSearch) return autocompleteOptions;
|
|
||||||
const q = filterQuery.trim().toLowerCase();
|
const q = filterQuery.trim().toLowerCase();
|
||||||
if (!q) return autocompleteOptions;
|
if (!q) return autocompleteOptions;
|
||||||
return autocompleteOptions.filter((option) =>
|
return autocompleteOptions.filter((option) =>
|
||||||
option.text.toLowerCase().includes(q)
|
option.text.toLowerCase().includes(q)
|
||||||
);
|
);
|
||||||
}, [autocompleteOptions, filterQuery, disableSearch]);
|
}, [autocompleteOptions, filterQuery]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPopoverOpen) {
|
if (isPopoverOpen) {
|
||||||
@@ -281,25 +275,15 @@ export const Autocomplete: React.FC<AutocompleteProps> = ({
|
|||||||
>
|
>
|
||||||
<Command
|
<Command
|
||||||
key={commandResetKey}
|
key={commandResetKey}
|
||||||
shouldFilter={!disableSearch}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-lg border-0 shadow-none",
|
"rounded-lg border-0 shadow-none",
|
||||||
classStyleProps?.command
|
classStyleProps?.command
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{disableSearch ? (
|
<CommandInput
|
||||||
<CommandInput
|
placeholder={t("searchPlaceholder")}
|
||||||
placeholder={t("searchPlaceholder")}
|
className="h-9"
|
||||||
className="h-9"
|
/>
|
||||||
value={filterQuery}
|
|
||||||
onValueChange={onFilterQueryChange}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<CommandInput
|
|
||||||
placeholder={t("searchPlaceholder")}
|
|
||||||
className="h-9"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<CommandList
|
<CommandList
|
||||||
className={cn(
|
className={cn(
|
||||||
"max-h-[300px]",
|
"max-h-[300px]",
|
||||||
|
|||||||
266
src/components/tags/suggestions-tag-input.tsx
Normal file
266
src/components/tags/suggestions-tag-input.tsx
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { type VariantProps } from "class-variance-authority";
|
||||||
|
import { Check } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList
|
||||||
|
} from "@app/components/ui/command";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverAnchor,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger
|
||||||
|
} from "@app/components/ui/popover";
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import { cn } from "@app/lib/cn";
|
||||||
|
import { tagVariants } from "./tag";
|
||||||
|
import { TagList } from "./tag-list";
|
||||||
|
import type { Tag, TagInputStyleClassesProps } from "./tag-input";
|
||||||
|
|
||||||
|
export type SuggestionsTagInputProps = {
|
||||||
|
tags: Tag[];
|
||||||
|
setTags: React.Dispatch<React.SetStateAction<Tag[]>>;
|
||||||
|
suggestedOptions: Tag[];
|
||||||
|
searchQuery: string;
|
||||||
|
onSearchQueryChange: (value: string) => void;
|
||||||
|
activeTagIndex: number | null;
|
||||||
|
setActiveTagIndex: React.Dispatch<React.SetStateAction<number | null>>;
|
||||||
|
placeholder?: string;
|
||||||
|
maxTags?: number;
|
||||||
|
onTagAdd?: (tag: string) => void;
|
||||||
|
onTagRemove?: (tag: string) => void;
|
||||||
|
allowDuplicates?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
usePortal?: boolean;
|
||||||
|
styleClasses?: TagInputStyleClassesProps;
|
||||||
|
} & VariantProps<typeof tagVariants>;
|
||||||
|
|
||||||
|
export function SuggestionsTagInput({
|
||||||
|
tags,
|
||||||
|
setTags,
|
||||||
|
suggestedOptions,
|
||||||
|
searchQuery,
|
||||||
|
onSearchQueryChange,
|
||||||
|
activeTagIndex,
|
||||||
|
setActiveTagIndex,
|
||||||
|
placeholder,
|
||||||
|
maxTags,
|
||||||
|
onTagAdd,
|
||||||
|
onTagRemove,
|
||||||
|
allowDuplicates = false,
|
||||||
|
disabled = false,
|
||||||
|
usePortal = false,
|
||||||
|
styleClasses = {},
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
shape,
|
||||||
|
borderStyle,
|
||||||
|
textCase,
|
||||||
|
interaction,
|
||||||
|
animation,
|
||||||
|
textStyle
|
||||||
|
}: SuggestionsTagInputProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
const triggerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const popoverContentRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [popoverWidth, setPopoverWidth] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleOutsideClick = (event: MouseEvent | TouchEvent) => {
|
||||||
|
if (
|
||||||
|
isOpen &&
|
||||||
|
triggerRef.current &&
|
||||||
|
popoverContentRef.current &&
|
||||||
|
!triggerRef.current.contains(event.target as Node) &&
|
||||||
|
!popoverContentRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handleOutsideClick);
|
||||||
|
return () =>
|
||||||
|
document.removeEventListener("mousedown", handleOutsideClick);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
if (open && triggerRef.current) {
|
||||||
|
setPopoverWidth(triggerRef.current.getBoundingClientRect().width);
|
||||||
|
}
|
||||||
|
if (open) setIsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleTag = (option: Tag) => {
|
||||||
|
const index = tags.findIndex((tag) => tag.text === option.text);
|
||||||
|
if (index >= 0) {
|
||||||
|
setTags(tags.filter((_, i) => i !== index));
|
||||||
|
onTagRemove?.(option.text);
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
!allowDuplicates &&
|
||||||
|
tags.some((tag) => tag.text === option.text)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
if (!maxTags || tags.length < maxTags) {
|
||||||
|
setTags([...tags, option]);
|
||||||
|
onTagAdd?.(option.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTag = (idToRemove: string) => {
|
||||||
|
const removed = tags.find((tag) => tag.id === idToRemove);
|
||||||
|
setTags(tags.filter((tag) => tag.id !== idToRemove));
|
||||||
|
if (removed) onTagRemove?.(removed.text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSortEnd = (oldIndex: number, newIndex: number) => {
|
||||||
|
setTags((current) => {
|
||||||
|
const next = [...current];
|
||||||
|
const [moved] = next.splice(oldIndex, 1);
|
||||||
|
next.splice(newIndex, 0, moved);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={isOpen} onOpenChange={handleOpenChange} modal={usePortal}>
|
||||||
|
<PopoverAnchor asChild>
|
||||||
|
<div
|
||||||
|
ref={triggerRef}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-row flex-wrap items-center gap-1.5 p-1.5 w-full rounded-md border border-input text-sm bg-transparent pr-1",
|
||||||
|
styleClasses?.inlineTagsContainer
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TagList
|
||||||
|
tags={tags}
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
shape={shape}
|
||||||
|
borderStyle={borderStyle}
|
||||||
|
textCase={textCase}
|
||||||
|
interaction={interaction}
|
||||||
|
animation={animation}
|
||||||
|
textStyle={textStyle}
|
||||||
|
onRemoveTag={removeTag}
|
||||||
|
onSortEnd={onSortEnd}
|
||||||
|
inlineTags
|
||||||
|
activeTagIndex={activeTagIndex}
|
||||||
|
setActiveTagIndex={setActiveTagIndex}
|
||||||
|
classStyleProps={{
|
||||||
|
tagListClasses: styleClasses?.tagList,
|
||||||
|
tagClasses: styleClasses?.tag
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
role="combobox"
|
||||||
|
type="button"
|
||||||
|
disabled={
|
||||||
|
disabled ||
|
||||||
|
(maxTags !== undefined &&
|
||||||
|
tags.length >= maxTags)
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"hover:bg-transparent ml-auto",
|
||||||
|
styleClasses?.autoComplete?.popoverTrigger
|
||||||
|
)}
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className={`lucide lucide-chevron-down h-4 w-4 shrink-0 opacity-50 transition-transform ${isOpen ? "rotate-180" : "rotate-0"}`}
|
||||||
|
>
|
||||||
|
<path d="m6 9 6 6 6-6" />
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
</div>
|
||||||
|
</PopoverAnchor>
|
||||||
|
<PopoverContent
|
||||||
|
ref={popoverContentRef}
|
||||||
|
side="bottom"
|
||||||
|
align="start"
|
||||||
|
forceMount
|
||||||
|
className={cn("p-0", styleClasses?.autoComplete?.popoverContent)}
|
||||||
|
style={{
|
||||||
|
width: `${popoverWidth}px`,
|
||||||
|
minWidth: `${popoverWidth}px`,
|
||||||
|
zIndex: 9999
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Command
|
||||||
|
shouldFilter={false}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border-0 shadow-none",
|
||||||
|
styleClasses?.autoComplete?.command
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CommandInput
|
||||||
|
placeholder={placeholder ?? t("searchPlaceholder")}
|
||||||
|
className="h-9"
|
||||||
|
value={searchQuery}
|
||||||
|
onValueChange={onSearchQueryChange}
|
||||||
|
/>
|
||||||
|
<CommandList
|
||||||
|
className={cn(
|
||||||
|
"max-h-[300px]",
|
||||||
|
styleClasses?.autoComplete?.commandList
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CommandEmpty>{t("noResults")}</CommandEmpty>
|
||||||
|
<CommandGroup
|
||||||
|
className={styleClasses?.autoComplete?.commandGroup}
|
||||||
|
>
|
||||||
|
{suggestedOptions.map((option) => {
|
||||||
|
const isChosen = tags.some(
|
||||||
|
(tag) => tag.text === option.text
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<CommandItem
|
||||||
|
key={option.id}
|
||||||
|
value={`${option.text} ${option.id}`}
|
||||||
|
onSelect={() => toggleTag(option)}
|
||||||
|
className={
|
||||||
|
styleClasses?.autoComplete
|
||||||
|
?.commandItem
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4 shrink-0",
|
||||||
|
isChosen
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{option.text}
|
||||||
|
</CommandItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -88,7 +88,6 @@ export interface TagInputProps
|
|||||||
searchQuery?: string;
|
searchQuery?: string;
|
||||||
onSearchQueryChange?: (value: string) => void;
|
onSearchQueryChange?: (value: string) => void;
|
||||||
autocompleteContent?: React.ReactNode;
|
autocompleteContent?: React.ReactNode;
|
||||||
suggestedOptions?: Tag[];
|
|
||||||
customTagRenderer?: (tag: Tag, isActiveTag: boolean) => React.ReactNode;
|
customTagRenderer?: (tag: Tag, isActiveTag: boolean) => React.ReactNode;
|
||||||
onFocus?: React.FocusEventHandler<HTMLInputElement>;
|
onFocus?: React.FocusEventHandler<HTMLInputElement>;
|
||||||
onBlur?: React.FocusEventHandler<HTMLInputElement>;
|
onBlur?: React.FocusEventHandler<HTMLInputElement>;
|
||||||
@@ -164,8 +163,7 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
generateTagId = uuid,
|
generateTagId = uuid,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
onSearchQueryChange,
|
onSearchQueryChange,
|
||||||
autocompleteContent,
|
autocompleteContent
|
||||||
suggestedOptions
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [inputValue, setInputValue] = React.useState("");
|
const [inputValue, setInputValue] = React.useState("");
|
||||||
@@ -196,7 +194,6 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (suggestedOptions !== undefined) return;
|
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
if (addOnPaste && newValue.includes(delimiter)) {
|
if (addOnPaste && newValue.includes(delimiter)) {
|
||||||
const splitValues = newValue
|
const splitValues = newValue
|
||||||
@@ -440,14 +437,6 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
onClearAll?.();
|
onClearAll?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const mainInputValue =
|
|
||||||
suggestedOptions !== undefined ? "" : effectiveQuery;
|
|
||||||
|
|
||||||
const useAutocompleteComponent =
|
|
||||||
enableAutocomplete || suggestedOptions !== undefined;
|
|
||||||
const resolvedAutocompleteOptions = suggestedOptions ?? autocompleteOptions;
|
|
||||||
const disableAutocompleteSearch = suggestedOptions !== undefined;
|
|
||||||
|
|
||||||
const displayedTags = sortTags ? [...tags].sort() : tags;
|
const displayedTags = sortTags ? [...tags].sort() : tags;
|
||||||
|
|
||||||
const truncatedTags = truncate
|
const truncatedTags = truncate
|
||||||
@@ -500,7 +489,7 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
!useAutocompleteComponent && !autocompleteContent && (
|
!enableAutocomplete && !autocompleteContent && (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -543,7 +532,7 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
? placeholderWhenFull
|
? placeholderWhenFull
|
||||||
: placeholder
|
: placeholder
|
||||||
}
|
}
|
||||||
value={mainInputValue}
|
value={effectiveQuery}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={handleInputFocus}
|
onFocus={handleInputFocus}
|
||||||
@@ -572,7 +561,7 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
))}
|
))}
|
||||||
{!useAutocompleteComponent && autocompleteContent && (
|
{!enableAutocomplete && autocompleteContent && (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -614,7 +603,7 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
? placeholderWhenFull
|
? placeholderWhenFull
|
||||||
: placeholder
|
: placeholder
|
||||||
}
|
}
|
||||||
value={mainInputValue}
|
value={effectiveQuery}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={handleInputFocus}
|
onFocus={handleInputFocus}
|
||||||
@@ -634,22 +623,16 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
{autocompleteContent}
|
{autocompleteContent}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{useAutocompleteComponent ? (
|
{enableAutocomplete ? (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
tags={tags}
|
tags={tags}
|
||||||
setTags={setTags}
|
setTags={setTags}
|
||||||
setInputValue={updateQuery}
|
setInputValue={updateQuery}
|
||||||
autocompleteOptions={
|
autocompleteOptions={
|
||||||
(resolvedAutocompleteOptions || []) as Tag[]
|
(autocompleteOptions || []) as Tag[]
|
||||||
}
|
}
|
||||||
filterQuery={effectiveQuery}
|
filterQuery={effectiveQuery}
|
||||||
disableSearch={disableAutocompleteSearch}
|
|
||||||
onFilterQueryChange={
|
|
||||||
disableAutocompleteSearch
|
|
||||||
? onSearchQueryChange
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
setTagCount={setTagCount}
|
setTagCount={setTagCount}
|
||||||
maxTags={maxTags}
|
maxTags={maxTags}
|
||||||
onTagAdd={onTagAdd}
|
onTagAdd={onTagAdd}
|
||||||
@@ -675,7 +658,7 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
// <CommandInput
|
// <CommandInput
|
||||||
// placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}
|
// placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}
|
||||||
// ref={inputRef}
|
// ref={inputRef}
|
||||||
// value={mainInputValue}
|
// value={effectiveQuery}
|
||||||
// disabled={disabled || (maxTags !== undefined && tags.length >= maxTags)}
|
// disabled={disabled || (maxTags !== undefined && tags.length >= maxTags)}
|
||||||
// onChangeCapture={handleInputChange}
|
// onChangeCapture={handleInputChange}
|
||||||
// onKeyDown={handleKeyDown}
|
// onKeyDown={handleKeyDown}
|
||||||
@@ -697,7 +680,7 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
? placeholderWhenFull
|
? placeholderWhenFull
|
||||||
: placeholder
|
: placeholder
|
||||||
}
|
}
|
||||||
value={mainInputValue}
|
value={effectiveQuery}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={handleInputFocus}
|
onFocus={handleInputFocus}
|
||||||
@@ -758,7 +741,7 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
{/* <CommandInput
|
{/* <CommandInput
|
||||||
placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}
|
placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
value={mainInputValue}
|
value={effectiveQuery}
|
||||||
disabled={disabled || (maxTags !== undefined && tags.length >= maxTags)}
|
disabled={disabled || (maxTags !== undefined && tags.length >= maxTags)}
|
||||||
onChangeCapture={handleInputChange}
|
onChangeCapture={handleInputChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
@@ -781,7 +764,7 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
? placeholderWhenFull
|
? placeholderWhenFull
|
||||||
: placeholder
|
: placeholder
|
||||||
}
|
}
|
||||||
value={mainInputValue}
|
value={effectiveQuery}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={handleInputFocus}
|
onFocus={handleInputFocus}
|
||||||
@@ -837,7 +820,7 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
{/* <CommandInput
|
{/* <CommandInput
|
||||||
placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}
|
placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
value={mainInputValue}
|
value={effectiveQuery}
|
||||||
disabled={disabled || (maxTags !== undefined && tags.length >= maxTags)}
|
disabled={disabled || (maxTags !== undefined && tags.length >= maxTags)}
|
||||||
onChangeCapture={handleInputChange}
|
onChangeCapture={handleInputChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
@@ -859,7 +842,7 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
? placeholderWhenFull
|
? placeholderWhenFull
|
||||||
: placeholder
|
: placeholder
|
||||||
}
|
}
|
||||||
value={mainInputValue}
|
value={effectiveQuery}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={handleInputFocus}
|
onFocus={handleInputFocus}
|
||||||
@@ -902,7 +885,7 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
? placeholderWhenFull
|
? placeholderWhenFull
|
||||||
: placeholder
|
: placeholder
|
||||||
}
|
}
|
||||||
value={mainInputValue}
|
value={effectiveQuery}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={handleInputFocus}
|
onFocus={handleInputFocus}
|
||||||
@@ -962,7 +945,7 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
? placeholderWhenFull
|
? placeholderWhenFull
|
||||||
: placeholder
|
: placeholder
|
||||||
}
|
}
|
||||||
value={mainInputValue}
|
value={effectiveQuery}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={handleInputFocus}
|
onFocus={handleInputFocus}
|
||||||
|
|||||||
Reference in New Issue
Block a user