Select
Categorystyled
ArchitectureReact
Client
Component
Dependencies
Usage
import { Select } from "@/components/react/styled/select";
import { DemoWrapper } from "@/demos/react/demoWrapper";
import {
MoonIcon,
SunIcon,
ComputerDesktopIcon,
} from "@heroicons/react/24/solid";
export const SelectDemo = ({ code }: { code: string }) => {
return (
<DemoWrapper code={code}>
<Select
label="Choose your color preference"
defaultValueKey="systemDefault"
values={[
{
label: (
<>
<MoonIcon className="w-3 h-3" />
<span>Dark Mode</span>
</>
),
key: "darkMode",
},
{
label: (
<>
<SunIcon className="w-3 h-3" />
<span>Light Mode</span>
</>
),
key: "lightMode",
},
{
label: (
<>
<ComputerDesktopIcon className="w-3 h-3" />
<span>System default</span>
</>
),
key: "systemDefault",
},
]}
/>
</DemoWrapper>
);
};
Install Instructions
Verifying dependencies
Make sure the following dependencies are satisfied:
- dialog
- usePopover
Copy and paste the code into that file
import {
type ReactNode,
lazy,
useCallback,
useEffect,
useRef,
useState,
Suspense,
} from "react";
// Dialog
const Dialog = lazy(() =>
import("@/components/react/primitive/dialog").then((module) => ({
default: module.Dialog,
}))
);
// Icons
import { ChevronDownIcon } from "@heroicons/react/24/solid";
// CVA
import { cva } from "class-variance-authority";
// Hooks
import { usePopover } from "@/hooks/react/usePopover";
const popoverClasses = cva([
"left-[var(--left,_auto)]",
"right-[var(--right,_auto)]",
"top-[var(--top,_auto)]",
"bottom-[var(--bottom,_auto)]",
"w-auto",
"backdrop:opacity-0!",
"border",
"rounded-xl",
"overflow-y-auto",
"overflow-x-hidden",
"border-neutral-200",
"bg-neutral-100",
"dark:border-neutral-800",
"dark:bg-neutral-900",
"w-fit",
"justify-start",
"flex-col",
"data-[display=false]:pointer-events-none",
"opacity-0",
"data-[display=false]:blur-sm",
"data-[display=true]:blur-0",
"data-[display=true]:backdrop:opacity-0",
"data-[display=true]:opacity-100",
"motion-safe:data-[display=false]:data-[append-to=top]:translate-y-2",
"motion-safe:data-[display=false]:data-[append-to=bottom]:-translate-y-2",
"motion-safe:data-[display=true]:data-[append-to=bottom]:translate-y-0",
"motion-safe:data-[display=true]:data-[append-to=top]:translate-y-0",
"fixed",
"my-2",
"mx-0",
"p-0",
"z-50",
"duration-150",
]);
const mainButtonClasses = cva([
"flex",
"justify-start",
"align-middle",
"items-center",
"gap-2",
"transition-colors",
"text-sm",
"gap-2",
"px-4",
"py-2",
"border",
"border-neutral-200",
"bg-neutral-100",
"dark:border-neutral-800",
"dark:bg-neutral-900",
"rounded-xl",
"enabled:hover:bg-neutral-200",
"focus-visible:bg-neutral-200",
"dark:enabled:hover:bg-neutral-800",
"dark:focus-visible:bg-neutral-800",
"disabled:cursor-not-allowed",
"disabled:opacity-75",
]);
const popoverButtonClasses = cva([
"flex",
"w-[100%]",
"justify-start",
"align-middle",
"items-center",
"gap-2",
"px-4",
"py-2",
"text-sm",
"dark:data-[selected=true]:bg-primary-300/50",
"data-[selected=true]:bg-primary-300",
"transition-colors",
"enabled:hover:bg-neutral-300",
"dark:enabled:hover:bg-neutral-800",
"focus-visible:bg-neutral-300",
"dark:focus-visible:bg-neutral-800",
"outline-none",
]);
interface Value {
label: ReactNode;
key: string;
}
export interface SelectProps {
label: string;
placeholder?: string;
defaultValueKey?: string;
values: Value[];
onValue?: (v: string) => void;
value?: string;
className?: string;
disabled?: boolean;
}
const findDefaultValue = (values: Value[], key?: string) => {
let defaultValue: Value | null = null;
values.forEach((value) => {
if (value.key === key) defaultValue = value;
});
if (typeof key !== undefined && defaultValue === null)
console.error(`Select Component: Could found ${key} between the values`);
return defaultValue;
};
export const Select = ({
label,
values,
value,
placeholder = "Select",
defaultValueKey,
onValue,
className = "",
disabled,
}: SelectProps) => {
const dialogRef = useRef<HTMLDialogElement | null>(null);
const buttonRef = useRef<HTMLButtonElement | null>(null);
const [selectedValue, setSelectedValue] = useState<Value | null>(
findDefaultValue(values, value ? value : defaultValueKey)
);
const [dialogS, setDialogS] = useState(false);
const { close, open } = usePopover({
dialogRef,
invokerRef: buttonRef,
setState: setDialogS,
});
useEffect(() => {
if (typeof value === "undefined") return;
setSelectedValue(findDefaultValue(values, value));
}, [value, values]);
const onSelect = useCallback(
(v: Value) => {
setSelectedValue(v);
setDialogS(false);
onValue && onValue(v.key);
},
[onValue]
);
return (
<div className={className}>
<label className="text-md block pb-2 capitalize">{label}</label>
<button
disabled={disabled}
onClick={!disabled ? open : undefined}
className={mainButtonClasses()}
ref={buttonRef}
>
{selectedValue ? selectedValue.label : placeholder}
<ChevronDownIcon className="w-4 h-4" />
</button>
<Suspense fallback={null}>
<Dialog
state={dialogS}
setState={setDialogS}
className={popoverClasses()}
onExit={close}
ref={dialogRef}
>
{values.map((value) => {
return (
<button
key={value.key}
className={popoverButtonClasses()}
data-selected={selectedValue?.key === value.key}
onClick={onSelect.bind(null, value)}
>
{value.label}
</button>
);
})}
</Dialog>
</Suspense>
</div>
);
};