Files
goldenChart/frontend/src/components/verificationBoard/ReproductionPathInput.tsx
T
2026-05-27 15:41:19 +09:00

116 lines
3.4 KiB
TypeScript

/**
* 재현경로 입력 — 직접 입력 + 관련 메뉴 경로 자동완성
*/
import React, { useEffect, useId, useRef, useState } from 'react';
import { searchAppNavigationPaths } from '../../utils/appNavigationPaths';
interface Props {
value: string;
onChange: (value: string) => void;
disabled?: boolean;
placeholder?: string;
}
const ReproductionPathInput: React.FC<Props> = ({
value,
onChange,
disabled = false,
placeholder = '예: 메뉴-실시간 차트-지표 추가 버튼',
}) => {
const listId = useId();
const wrapRef = useRef<HTMLDivElement>(null);
const [open, setOpen] = useState(false);
const [activeIdx, setActiveIdx] = useState(-1);
const suggestions = searchAppNavigationPaths(value);
useEffect(() => {
setActiveIdx(-1);
}, [value, suggestions.length]);
useEffect(() => {
const onDoc = (e: MouseEvent) => {
if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) {
setOpen(false);
}
};
document.addEventListener('mousedown', onDoc);
return () => document.removeEventListener('mousedown', onDoc);
}, []);
const pick = (path: string) => {
onChange(path);
setOpen(false);
setActiveIdx(-1);
};
const showList = open && !disabled && suggestions.length > 0;
return (
<div className="vbd-path-wrap" ref={wrapRef}>
<input
type="text"
className="vbd-path-input"
value={value}
disabled={disabled}
placeholder={placeholder}
autoComplete="off"
aria-autocomplete="list"
aria-controls={showList ? listId : undefined}
aria-expanded={showList}
onChange={e => {
onChange(e.target.value);
setOpen(true);
}}
onFocus={() => setOpen(true)}
onKeyDown={e => {
if (!showList) {
if (e.key === 'ArrowDown' && suggestions.length > 0) {
setOpen(true);
setActiveIdx(0);
e.preventDefault();
}
return;
}
if (e.key === 'ArrowDown') {
e.preventDefault();
setActiveIdx(i => Math.min(i + 1, suggestions.length - 1));
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setActiveIdx(i => Math.max(i - 1, 0));
} else if (e.key === 'Enter' && activeIdx >= 0) {
e.preventDefault();
pick(suggestions[activeIdx]!.path);
} else if (e.key === 'Escape') {
setOpen(false);
setActiveIdx(-1);
}
}}
/>
{showList && (
<ul id={listId} className="vbd-path-suggest" role="listbox">
{suggestions.map((s, i) => (
<li key={s.path} role="presentation">
<button
type="button"
role="option"
aria-selected={i === activeIdx}
className={`vbd-path-suggest-item${i === activeIdx ? ' vbd-path-suggest-item--active' : ''}`}
onMouseDown={e => e.preventDefault()}
onClick={() => pick(s.path)}
>
{s.path}
</button>
</li>
))}
</ul>
)}
{open && value.trim() && suggestions.length === 0 && (
<p className="vbd-path-hint">일치하는 메뉴 경로가 없습니다. 직접 입력한 경로가 저장됩니다.</p>
)}
</div>
);
};
export default ReproductionPathInput;