Files
tm_panel/src/components/FormElements/select.tsx
T
AmirReza Jamali eab516c7f3 feat(tests, components): enhance Cypress commands and UI elements for improved testing and accessibility
- 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.
2026-04-25 13:25:23 +03:30

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>
);
}