2025-08-21 09:41:46 +09:00
|
|
|
import * as React from "react";
|
|
|
|
|
|
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
|
2025-11-05 16:36:32 +09:00
|
|
|
export interface InputProps extends React.ComponentProps<"input"> {
|
|
|
|
|
label?: string; // 툴팁에 표시할 라벨
|
|
|
|
|
enableEnterNavigation?: boolean; // Enter 키로 다음 필드 이동 활성화
|
2025-08-21 09:41:46 +09:00
|
|
|
}
|
|
|
|
|
|
2025-11-05 16:36:32 +09:00
|
|
|
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 shadow-xs 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";
|
|
|
|
|
|
2025-08-21 09:41:46 +09:00
|
|
|
export { Input };
|