- Added support for custom border, background, and text styles in V2Input and V2Select components, allowing for greater flexibility in styling based on user-defined configurations. - Updated the input and select components to conditionally apply styles based on the presence of custom properties, improving the overall user experience and visual consistency. - Refactored the FileUploadComponent to handle chunked file uploads, enhancing the file upload process by allowing multiple files to be uploaded in batches, improving performance and user feedback during uploads.
90 lines
3.6 KiB
TypeScript
90 lines
3.6 KiB
TypeScript
import * as React from "react";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export interface InputProps extends React.ComponentProps<"input"> {
|
|
label?: string; // 툴팁에 표시할 라벨
|
|
enableEnterNavigation?: boolean; // Enter 키로 다음 필드 이동 활성화
|
|
}
|
|
|
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
({ className, type, label, enableEnterNavigation = false, onKeyDown, ...props }, ref) => {
|
|
const [showTooltip, setShowTooltip] = React.useState(false);
|
|
const [tooltipPosition, setTooltipPosition] = React.useState({ x: 0, y: 0 });
|
|
|
|
const handleMouseMove = (e: React.MouseEvent<HTMLInputElement>) => {
|
|
if (label) {
|
|
setTooltipPosition({ x: e.clientX, y: e.clientY });
|
|
}
|
|
};
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
// Enter 키 네비게이션
|
|
if (enableEnterNavigation && e.key === "Enter" && !e.shiftKey) {
|
|
e.preventDefault();
|
|
|
|
// 현재 input의 form 내에서 다음 input 찾기
|
|
const form = e.currentTarget.form;
|
|
if (form) {
|
|
const inputs = Array.from(form.querySelectorAll('input:not([disabled]):not([readonly]), select:not([disabled]), textarea:not([disabled]):not([readonly])')) as HTMLElement[];
|
|
const currentIndex = inputs.indexOf(e.currentTarget);
|
|
|
|
if (currentIndex !== -1 && currentIndex < inputs.length - 1) {
|
|
// 다음 input으로 포커스 이동
|
|
inputs[currentIndex + 1].focus();
|
|
}
|
|
} else {
|
|
// form이 없는 경우, 문서 전체에서 다음 input 찾기
|
|
const allInputs = Array.from(document.querySelectorAll('input:not([disabled]):not([readonly]), select:not([disabled]), textarea:not([disabled]):not([readonly])')) as HTMLElement[];
|
|
const currentIndex = allInputs.indexOf(e.currentTarget);
|
|
|
|
if (currentIndex !== -1 && currentIndex < allInputs.length - 1) {
|
|
allInputs[currentIndex + 1].focus();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 기존 onKeyDown 핸들러 호출
|
|
onKeyDown?.(e);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<input
|
|
ref={ref}
|
|
type={type}
|
|
data-slot="input"
|
|
className={cn(
|
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-10 w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-sm transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
className,
|
|
)}
|
|
onMouseEnter={() => label && setShowTooltip(true)}
|
|
onMouseLeave={() => setShowTooltip(false)}
|
|
onMouseMove={handleMouseMove}
|
|
onKeyDown={handleKeyDown}
|
|
{...props}
|
|
/>
|
|
|
|
{/* 툴팁 */}
|
|
{showTooltip && label && (
|
|
<div
|
|
className="pointer-events-none fixed z-[9999] rounded-md bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-md border border-border"
|
|
style={{
|
|
left: `${tooltipPosition.x + 10}px`,
|
|
top: `${tooltipPosition.y - 30}px`,
|
|
}}
|
|
>
|
|
{label}
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
);
|
|
|
|
Input.displayName = "Input";
|
|
|
|
export { Input };
|