ERP-node/frontend/components/ui/input.tsx

90 lines
3.6 KiB
TypeScript
Raw Permalink Normal View History

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