eab516c7f3
- Added new Cypress commands for mocking admin-related API interactions, including list, create, update, delete, and change status functionalities. - Introduced data-cy attributes across various components (e.g., Breadcrumbs, Forms, Tables) to improve testability and accessibility. - Updated existing components to support new data-cy attributes for better integration with Cypress tests.
171 lines
5.0 KiB
TypeScript
171 lines
5.0 KiB
TypeScript
"use client";
|
|
|
|
import { ChevronUpIcon } from "@/assets/icons";
|
|
import { cn } from "@/lib/utils";
|
|
import { ILabelValue } from "@/types/shared";
|
|
import { useEffect, useId, useState, type ChangeEvent } from "react";
|
|
import {
|
|
type FieldErrors,
|
|
type FieldValues,
|
|
type Path,
|
|
type UseFormRegister,
|
|
type UseFormRegisterReturn,
|
|
} from "react-hook-form";
|
|
|
|
type PropsType<T extends FieldValues> = {
|
|
name: Path<T>;
|
|
label: string;
|
|
items: ILabelValue[];
|
|
prefixIcon?: React.ReactNode;
|
|
onChange?: (e: ChangeEvent<HTMLSelectElement>) => void;
|
|
className?: string;
|
|
register?: UseFormRegister<T>;
|
|
errors?: FieldErrors<T>;
|
|
required?: boolean;
|
|
clearable?: boolean;
|
|
onClear?: () => void;
|
|
value?: string;
|
|
dataCy?: string;
|
|
} & (
|
|
| { placeholder?: string; defaultValue?: string }
|
|
| { placeholder: string; defaultValue?: string }
|
|
) &
|
|
Partial<UseFormRegisterReturn>;
|
|
|
|
export function Select<T extends FieldValues>({
|
|
items,
|
|
label,
|
|
defaultValue,
|
|
placeholder,
|
|
prefixIcon,
|
|
className,
|
|
name,
|
|
register,
|
|
errors,
|
|
required,
|
|
onChange,
|
|
onBlur,
|
|
ref,
|
|
clearable = false,
|
|
onClear,
|
|
value: controlledValue,
|
|
dataCy,
|
|
...props
|
|
}: PropsType<T>) {
|
|
const id = useId();
|
|
|
|
const [isOptionSelected, setIsOptionSelected] = useState(!!defaultValue);
|
|
const [value, setValue] = useState(defaultValue || "");
|
|
const error = errors && errors[name];
|
|
|
|
useEffect(() => {
|
|
if (controlledValue !== undefined) {
|
|
setValue(controlledValue);
|
|
setIsOptionSelected(!!controlledValue);
|
|
}
|
|
}, [controlledValue]);
|
|
|
|
const handleClear = () => {
|
|
if (controlledValue === undefined) {
|
|
setValue("");
|
|
}
|
|
setIsOptionSelected(false);
|
|
onClear?.();
|
|
if (onChange) {
|
|
const event = {
|
|
target: { value: "", name },
|
|
} as unknown as ChangeEvent<HTMLSelectElement>;
|
|
onChange(event);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={cn("space-y-3", className)}>
|
|
<label
|
|
htmlFor={id}
|
|
className="block text-body-sm font-semibold capitalize text-dark dark:text-white"
|
|
>
|
|
{label}
|
|
{required && <span className="ml-1 select-none text-error">*</span>}
|
|
</label>
|
|
|
|
<div className="relative">
|
|
{prefixIcon && (
|
|
<div className="absolute left-4 top-1/2 -translate-y-1/2">
|
|
{prefixIcon}
|
|
</div>
|
|
)}
|
|
|
|
<select
|
|
id={id}
|
|
name={name}
|
|
ref={ref}
|
|
value={value}
|
|
onChange={(e) => {
|
|
if (controlledValue === undefined) {
|
|
setValue(e.target.value);
|
|
}
|
|
setIsOptionSelected(!!e.target.value);
|
|
onChange?.(e);
|
|
}}
|
|
onBlur={onBlur}
|
|
data-cy={dataCy}
|
|
className={cn(
|
|
"w-full appearance-none rounded-xl border border-stroke/70 bg-white/90 px-5.5 py-3 shadow-[0_1px_2px_rgba(16,24,40,0.04),0_8px_20px_rgba(16,24,40,0.04)] outline-none backdrop-blur-[1px] transition-all duration-200 focus:border-primary/70 focus:bg-white focus:shadow-[0_0_0_2px_rgba(59,130,246,0.14),0_10px_24px_rgba(59,130,246,0.08)] active:border-primary/70 dark:border-dark-3/80 dark:bg-dark-2/85 dark:focus:border-primary/80 dark:focus:shadow-[0_0_0_2px_rgba(59,130,246,0.2),0_10px_24px_rgba(15,23,42,0.45)] [&>option]:text-dark-5 dark:[&>option]:text-dark-6",
|
|
isOptionSelected && "text-dark dark:text-white",
|
|
error &&
|
|
"border-red/80 focus:border-red focus:shadow-[0_0_0_2px_rgba(239,68,68,0.16),0_10px_24px_rgba(239,68,68,0.1)] dark:border-red/80 dark:focus:shadow-[0_0_0_2px_rgba(239,68,68,0.24),0_10px_24px_rgba(127,29,29,0.45)]",
|
|
prefixIcon && "pl-11.5",
|
|
clearable && value && "pr-20",
|
|
)}
|
|
>
|
|
{placeholder && (
|
|
<option value="" disabled>
|
|
{placeholder}
|
|
</option>
|
|
)}
|
|
|
|
{items.map((item) => (
|
|
<option key={item.value} value={item.value}>
|
|
{item.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
|
|
{clearable && value && (
|
|
<button
|
|
type="button"
|
|
onClick={handleClear}
|
|
data-cy={dataCy ? `${dataCy}-clear` : undefined}
|
|
className="absolute right-10 top-1/2 -translate-y-1/2 text-dark-5 transition hover:scale-110 hover:text-dark dark:text-dark-6 dark:hover:text-white"
|
|
>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 16 16"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
d="M12 4L4 12M4 4L12 12"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
)}
|
|
|
|
<ChevronUpIcon className="pointer-events-none absolute right-4 top-1/2 -translate-y-1/2 rotate-180" />
|
|
</div>
|
|
|
|
{error && (
|
|
<p className="text-red mt-1.5 text-sm font-medium">
|
|
{error.message as string}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|