Skip to content

Instantly share code, notes, and snippets.

@gburtini
Created September 14, 2024 19:14
Show Gist options
  • Select an option

  • Save gburtini/7a4fcdb1a99e4a8dd4781cf24f871e31 to your computer and use it in GitHub Desktop.

Select an option

Save gburtini/7a4fcdb1a99e4a8dd4781cf24f871e31 to your computer and use it in GitHub Desktop.
MIT no attributon starter for building a multi-select, without filter or combobox functionality
/**
* MIT License, no attribution
*
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the "Software"), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* @see https://github.com/aws/mit-0
*/
import React, { useState, useCallback, useRef } from "react";
import { ScrollArea } from "~/components/ui/scroll-area";
import { Button } from "~/components/ui/button";
import { Check } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "~/components/ui/tooltip";
import { cn } from "~/lib/utils";
interface Option {
id: string;
label: string;
}
export default function InlineMultiSelectBox({
options,
selectedOptions,
onChange,
className,
}: {
options: Option[];
selectedOptions: string[];
onChange: React.Dispatch<React.SetStateAction<string[]>>;
className?: string;
}) {
const [isDragging, setIsDragging] = useState(false);
const lastSelectedRef = useRef<string | null>(null);
const handleOptionClick = useCallback(
(optionId: string) => {
onChange((prevSelected) => {
if (prevSelected.includes(optionId)) {
return prevSelected.filter((id) => id !== optionId);
} else {
return [...prevSelected, optionId];
}
});
lastSelectedRef.current = optionId;
},
[onChange],
);
const handleMouseDown = useCallback(
(optionId: string) => {
setIsDragging(true);
handleOptionClick(optionId);
},
[handleOptionClick],
);
const handleMouseEnter = useCallback(
(optionId: string) => {
if (isDragging) {
handleOptionClick(optionId);
}
},
[isDragging, handleOptionClick],
);
const handleMouseUp = useCallback(() => {
setIsDragging(false);
}, []);
return (
<ScrollArea
className={cn("h-60 rounded-md border", className)}
onMouseLeave={handleMouseUp}
>
<div
className="p-4"
id="multi-select"
role="listbox"
aria-multiselectable="true"
>
{options.map((option) => (
<TooltipProvider key={option.id}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
className={`w-full justify-start ${
selectedOptions.includes(option.id) ? "bg-accent" : ""
}`}
onMouseDown={() => handleMouseDown(option.id)}
onMouseEnter={() => handleMouseEnter(option.id)}
onMouseUp={handleMouseUp}
role="option"
aria-selected={selectedOptions.includes(option.id)}
>
<Check
className={`mr-2 h-4 w-4 flex-shrink-0 ${
selectedOptions.includes(option.id)
? "opacity-100"
: "opacity-0"
}`}
/>
<span className="truncate">{option.label}</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{option.label}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
))}
</div>
</ScrollArea>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment