模型选择下拉支持点击外部与 Esc 自动收起

为 ConversationModelSelect 增加点击外部关闭和 Esc 关闭,首页与项目右侧对话共用生效

补充点击外部与 Esc 收起的回归测试
This commit is contained in:
2026-09-07 14:21:38 +08:00
parent 8cce26feea
commit dab6a5c59d
2 changed files with 49 additions and 0 deletions
@@ -25,6 +25,7 @@ export function ConversationModelSelect({
const [error, setError] = useState('');
const [open, setOpen] = useState(false);
const initializedRef = useRef(false);
const containerRef = useRef<HTMLDivElement | null>(null);
const refresh = useCallback(async () => {
setBusy(true);
setError('');
@@ -63,6 +64,27 @@ export function ConversationModelSelect({
void refresh();
}, [refresh, lazy]);
useEffect(() => {
if (!open) return;
function handleOutsidePointerDown(event: MouseEvent) {
const target = event.target as Node | null;
if (containerRef.current && !containerRef.current.contains(target)) {
setOpen(false);
}
}
function handleEscape(event: KeyboardEvent) {
if (event.key === 'Escape') {
setOpen(false);
}
}
document.addEventListener('mousedown', handleOutsidePointerDown);
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('mousedown', handleOutsidePointerDown);
document.removeEventListener('keydown', handleEscape);
};
}, [open]);
async function select(id: string) {
onReady?.(false);
setBusy(true);
@@ -87,6 +109,7 @@ export function ConversationModelSelect({
return (
<div
ref={containerRef}
className={
className
? `conversation-model-select ${className}`
@@ -88,3 +88,29 @@ test('a failed save keeps submission unavailable', async () => {
await screen.findByText('模型选择保存失败');
expect(onReady).toHaveBeenLastCalledWith(false);
});
test('closes the menu when clicking outside', async () => {
const onReady = vi.fn();
render(<ConversationModelSelect disabled={false} onReady={onReady} />);
await screen.findByRole('button', { name: '对话模型' });
fireEvent.click(screen.getByRole('button', { name: '对话模型' }));
expect(screen.getByRole('option', { name: '快速' })).not.toBeNull();
fireEvent.mouseDown(document.body);
await waitFor(() =>
expect(screen.queryByRole('option', { name: '快速' })).toBeNull(),
);
});
test('closes the menu on Escape', async () => {
const onReady = vi.fn();
render(<ConversationModelSelect disabled={false} onReady={onReady} />);
await screen.findByRole('button', { name: '对话模型' });
fireEvent.click(screen.getByRole('button', { name: '对话模型' }));
expect(screen.getByRole('option', { name: '快速' })).not.toBeNull();
fireEvent.keyDown(document, { key: 'Escape' });
await waitFor(() =>
expect(screen.queryByRole('option', { name: '快速' })).toBeNull(),
);
});