검증게시판 기능 추가
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 재현경로 입력 — 직접 입력 + 관련 메뉴 경로 자동완성
|
||||
*/
|
||||
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;
|
||||
Reference in New Issue
Block a user