refactor(ui): migrar componente Select a HeroUI
This commit is contained in:
1 parent
0beb12e6ad
commit
b56fe10cbe
2 files changed
+141
-107
No files matched your search
@@ -83,8 +83,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
hasError ? "text-rose-600 dark:text-rose-400" : "text-slate-700 dark:text-slate-300"
|
hasError ? "text-rose-600 dark:text-rose-400" : "text-slate-700 dark:text-slate-300"
|
||||||
}`}>
|
}`}>
|
||||||
{label}
|
{label}
|
||||||
|
{required}
|
||||||
{required && <span className="ml-1 text-rose-500 font-bold">*</span>}
|
|
||||||
</Label>
|
</Label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
+140
-105
@@ -1,6 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React from 'react';
|
|
||||||
import { FiChevronDown, FiAlertCircle } from 'react-icons/fi';
|
import React from "react";
|
||||||
|
import { Select as HeroSelect, Label, Description, FieldError, ListBox, ListBoxItem } from "@heroui/react";
|
||||||
|
import type { Key } from "@heroui/react";
|
||||||
|
import { FiChevronDown, FiAlertCircle } from "react-icons/fi";
|
||||||
|
|
||||||
export interface SelectOption {
|
export interface SelectOption {
|
||||||
value: string | number;
|
value: string | number;
|
||||||
@@ -8,158 +11,190 @@ export interface SelectOption {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
|
export interface SelectProps {
|
||||||
label?: string;
|
label?: string;
|
||||||
helperText?: string;
|
helperText?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
leftIcon?: React.ReactNode;
|
leftIcon?: React.ReactNode;
|
||||||
options?: SelectOption[];
|
options?: SelectOption[];
|
||||||
sizeVariant?: 'sm' | 'md' | 'lg';
|
sizeVariant?: "sm" | "md" | "lg";
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
value?: string | number;
|
||||||
|
defaultValue?: string | number;
|
||||||
|
onChange?: (value: string | null) => void;
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sizeStyles = {
|
const sizeStyles = {
|
||||||
sm: {
|
sm: {
|
||||||
select: 'py-2 text-xs rounded-lg',
|
select: "py-2 text-xs rounded-lg",
|
||||||
icon: 'text-sm',
|
icon: "text-sm",
|
||||||
leftPad: 'pl-8',
|
leftPad: "pl-8",
|
||||||
rightPad: 'pr-8',
|
rightPad: "pr-8",
|
||||||
},
|
},
|
||||||
md: {
|
md: {
|
||||||
select: 'py-2.5 text-sm rounded-xl',
|
select: "py-2.5 text-sm rounded-xl",
|
||||||
icon: 'text-base',
|
icon: "text-base",
|
||||||
leftPad: 'pl-10',
|
leftPad: "pl-10",
|
||||||
rightPad: 'pr-10',
|
rightPad: "pr-10",
|
||||||
},
|
},
|
||||||
lg: {
|
lg: {
|
||||||
select: 'py-3.5 text-base rounded-2xl',
|
select: "py-3.5 text-base rounded-2xl",
|
||||||
icon: 'text-lg',
|
icon: "text-lg",
|
||||||
leftPad: 'pl-12',
|
leftPad: "pl-12",
|
||||||
rightPad: 'pr-12',
|
rightPad: "pr-12",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
export const Select = React.forwardRef<HTMLDivElement, SelectProps>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
label,
|
label,
|
||||||
helperText,
|
helperText,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
leftIcon,
|
leftIcon,
|
||||||
options,
|
options = [],
|
||||||
sizeVariant = 'md',
|
sizeVariant = "md",
|
||||||
fullWidth = true,
|
fullWidth = true,
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
disabled,
|
disabled,
|
||||||
required,
|
required,
|
||||||
className = '',
|
className = "",
|
||||||
|
value,
|
||||||
|
defaultValue,
|
||||||
|
onChange,
|
||||||
children,
|
children,
|
||||||
...props
|
|
||||||
},
|
},
|
||||||
ref
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const selectId = id || name || (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined);
|
const selectId = id || name || (label ? label.toLowerCase().replace(/\s+/g, "-") : undefined);
|
||||||
|
|
||||||
const hasError = Boolean(errorMessage);
|
const hasError = Boolean(errorMessage);
|
||||||
const currentSize = sizeStyles[sizeVariant];
|
const currentSize = sizeStyles[sizeVariant];
|
||||||
|
|
||||||
|
const selectedKey = value !== undefined && value !== null && value !== "" ? String(value) : undefined;
|
||||||
|
|
||||||
|
const defaultSelectedKey =
|
||||||
|
defaultValue !== undefined && defaultValue !== null && defaultValue !== "" ? String(defaultValue) : undefined;
|
||||||
|
|
||||||
|
const handleSelectionChange = (key: Key | null) => {
|
||||||
|
if (!onChange) return;
|
||||||
|
onChange(key !== null ? String(key) : null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${fullWidth ? 'w-full' : 'inline-block'} space-y-1.5`}>
|
<div className={`${fullWidth ? "w-full" : "inline-block"} space-y-1.5`}>
|
||||||
{/* Label */}
|
<HeroSelect
|
||||||
{label && (
|
ref={ref}
|
||||||
<div className="flex items-center justify-between">
|
id={selectId}
|
||||||
<label
|
name={name}
|
||||||
htmlFor={selectId}
|
selectedKey={selectedKey}
|
||||||
|
defaultSelectedKey={defaultSelectedKey}
|
||||||
|
onSelectionChange={handleSelectionChange}
|
||||||
|
isDisabled={disabled}
|
||||||
|
isRequired={required}
|
||||||
|
isInvalid={hasError}
|
||||||
|
className="w-full">
|
||||||
|
{label && (
|
||||||
|
<Label
|
||||||
className={`block text-xs font-semibold select-none transition-colors ${
|
className={`block text-xs font-semibold select-none transition-colors ${
|
||||||
hasError
|
hasError ? "text-rose-600 dark:text-rose-400" : "text-slate-700 dark:text-slate-300"
|
||||||
? 'text-rose-600 dark:text-rose-400'
|
}`}>
|
||||||
: 'text-slate-700 dark:text-slate-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{label}
|
{label}
|
||||||
{required && <span className="ml-1 text-rose-500 font-bold">*</span>}
|
{required && <span className="ml-1 text-rose-500 font-bold">*</span>}
|
||||||
</label>
|
</Label>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Select Wrapper */}
|
|
||||||
<div className="relative flex items-center">
|
|
||||||
{/* Left Icon */}
|
|
||||||
{leftIcon && (
|
|
||||||
<div
|
|
||||||
className={`absolute left-3.5 pointer-events-none flex items-center justify-center transition-colors ${
|
|
||||||
hasError
|
|
||||||
? 'text-rose-500'
|
|
||||||
: 'text-slate-400 dark:text-slate-500'
|
|
||||||
} ${currentSize.icon}`}
|
|
||||||
>
|
|
||||||
{leftIcon}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Core Select Element */}
|
<HeroSelect.Trigger
|
||||||
<select
|
|
||||||
ref={ref}
|
|
||||||
id={selectId}
|
|
||||||
name={name}
|
|
||||||
disabled={disabled}
|
|
||||||
required={required}
|
|
||||||
className={`
|
className={`
|
||||||
w-full px-3.5 font-normal transition-all duration-200 outline-none appearance-none cursor-pointer
|
relative flex w-full items-center
|
||||||
|
font-normal transition-all duration-200
|
||||||
|
outline-none appearance-none cursor-pointer
|
||||||
bg-white text-slate-900
|
bg-white text-slate-900
|
||||||
border border-slate-300
|
border border-slate-300
|
||||||
hover:border-slate-400
|
hover:border-slate-400
|
||||||
focus:border-cyan-500 focus:ring-4 focus:ring-cyan-500/15
|
focus:border-cyan-500
|
||||||
dark:bg-slate-900/90 dark:text-slate-100
|
focus:ring-4
|
||||||
dark:border-slate-800 dark:hover:border-slate-700
|
focus:ring-cyan-500/15
|
||||||
dark:focus:border-cyan-500 dark:focus:ring-cyan-500/25
|
dark:bg-slate-900/90
|
||||||
disabled:opacity-60 disabled:bg-slate-100 disabled:dark:bg-slate-950 disabled:cursor-not-allowed
|
dark:text-slate-100
|
||||||
${leftIcon ? currentSize.leftPad : ''}
|
dark:border-slate-800
|
||||||
${currentSize.rightPad}
|
dark:hover:border-slate-700
|
||||||
${
|
dark:focus:border-cyan-500
|
||||||
hasError
|
dark:focus:ring-cyan-500/25
|
||||||
? '!border-rose-500 !ring-rose-500/20 text-rose-900 dark:text-rose-200 focus:!border-rose-500 focus:!ring-rose-500/20'
|
disabled:opacity-60
|
||||||
: ''
|
disabled:bg-slate-100
|
||||||
}
|
disabled:dark:bg-slate-950
|
||||||
|
disabled:cursor-not-allowed
|
||||||
${currentSize.select}
|
${currentSize.select}
|
||||||
|
${leftIcon ? currentSize.leftPad : ""}
|
||||||
|
${currentSize.rightPad}
|
||||||
|
${hasError ? "border-rose-500! ring-rose-500/20! text-rose-900 dark:text-rose-200" : ""}
|
||||||
${className}
|
${className}
|
||||||
`}
|
`}>
|
||||||
{...props}
|
{leftIcon && (
|
||||||
>
|
<span
|
||||||
{options
|
className={`
|
||||||
? options.map((opt) => (
|
absolute left-3.5
|
||||||
<option
|
pointer-events-none
|
||||||
key={opt.value}
|
flex items-center justify-center
|
||||||
value={opt.value}
|
transition-colors
|
||||||
disabled={opt.disabled}
|
${hasError ? "text-rose-500" : "text-slate-400 dark:text-slate-500"}
|
||||||
className="bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100"
|
${currentSize.icon}
|
||||||
>
|
`}>
|
||||||
{opt.label}
|
{leftIcon}
|
||||||
</option>
|
</span>
|
||||||
))
|
)}
|
||||||
: children}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
{/* Right Chevron Icon */}
|
<HeroSelect.Value className="w-full text-left" />
|
||||||
<div
|
|
||||||
className={`absolute right-3.5 pointer-events-none flex items-center justify-center text-slate-400 dark:text-slate-500 ${currentSize.icon}`}
|
|
||||||
>
|
|
||||||
<FiChevronDown />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Error or Helper Message */}
|
<HeroSelect.Indicator
|
||||||
{hasError ? (
|
className={`
|
||||||
<p className="flex items-center gap-1.5 text-xs text-rose-600 dark:text-rose-400 animate-fadeIn">
|
absolute right-3.5
|
||||||
<FiAlertCircle className="shrink-0 text-xs" />
|
pointer-events-none
|
||||||
<span>{errorMessage}</span>
|
flex items-center justify-center
|
||||||
</p>
|
text-slate-400 dark:text-slate-500
|
||||||
) : helperText ? (
|
${currentSize.icon}
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400">{helperText}</p>
|
`}>
|
||||||
) : null}
|
<FiChevronDown />
|
||||||
|
</HeroSelect.Indicator>
|
||||||
|
</HeroSelect.Trigger>
|
||||||
|
|
||||||
|
<HeroSelect.Popover className="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-xl overflow-hidden z-50 min-w-(--trigger-width)">
|
||||||
|
<ListBox className="p-1 outline-none">
|
||||||
|
{options.length > 0
|
||||||
|
? options.map((option) => (
|
||||||
|
<ListBoxItem
|
||||||
|
key={String(option.value)}
|
||||||
|
id={String(option.value)}
|
||||||
|
textValue={option.label}
|
||||||
|
isDisabled={option.disabled}
|
||||||
|
className="px-3 py-2 text-sm rounded-lg cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 data-[selected=true]:bg-cyan-500/10 data-[selected=true]:text-cyan-600 dark:data-[selected=true]:text-cyan-400 outline-none transition-colors">
|
||||||
|
{option.label}
|
||||||
|
</ListBoxItem>
|
||||||
|
))
|
||||||
|
: (children as any)}
|
||||||
|
</ListBox>
|
||||||
|
</HeroSelect.Popover>
|
||||||
|
|
||||||
|
{hasError ? (
|
||||||
|
<FieldError className="flex items-center gap-1.5 text-xs text-rose-600 dark:text-rose-400">
|
||||||
|
<FiAlertCircle className="shrink-0 text-xs" />
|
||||||
|
<span>{errorMessage}</span>
|
||||||
|
</FieldError>
|
||||||
|
) : helperText ? (
|
||||||
|
<Description className="text-xs text-slate-500 dark:text-slate-400">{helperText}</Description>
|
||||||
|
) : null}
|
||||||
|
</HeroSelect>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Select.displayName = 'Select';
|
Select.displayName = "Select";
|
||||||
Reference in new issue
Block a user