c69a11d167
Clears only the search text and reopens the list, the selected project stays. Requested during manual testing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
import * as React from 'react';
|
||
import { Icon } from '@tnsu/ui-kit';
|
||
import { useProject } from '../../app/project/ProjectContext';
|
||
import { ProjectStatusBadge } from '../../app/project/ProjectStatusBadge';
|
||
import { IAccessibleProject } from '../../app/project/projectTypes';
|
||
import { useOutsideClose } from '../../shared/hooks/useOutsideClose';
|
||
import { ProjectList } from './ProjectList';
|
||
import { HUB_TEXTS } from './texts';
|
||
import styles from './ProjectPicker.module.scss';
|
||
|
||
const TEXTS = HUB_TEXTS.picker;
|
||
|
||
/** Поле выбора проекта с поиском — как в stroy-bot-main. */
|
||
export const ProjectPicker: React.FC = () => {
|
||
const { projects, loadState, currentProject, selectProject } = useProject();
|
||
const selectedTitle = currentProject?.title ?? '';
|
||
const [query, setQuery] = React.useState<string>(selectedTitle);
|
||
const [isOpen, setIsOpen] = React.useState<boolean>(false);
|
||
const wrapperRef = React.useRef<HTMLDivElement>(null);
|
||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||
|
||
// Выбор мог поменяться извне (адрес, восстановление из сессии) — пока список закрыт, поле показывает его.
|
||
React.useEffect(() => {
|
||
if (!isOpen) setQuery(selectedTitle);
|
||
}, [selectedTitle, isOpen]);
|
||
|
||
useOutsideClose(wrapperRef, isOpen, (reason) => {
|
||
setIsOpen(false);
|
||
setQuery(selectedTitle);
|
||
if (reason === 'escape') inputRef.current?.blur();
|
||
});
|
||
|
||
const onFocus = (event: React.FocusEvent<HTMLInputElement>): void => {
|
||
event.target.select();
|
||
setIsOpen(true);
|
||
};
|
||
|
||
const onChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||
setQuery(event.target.value);
|
||
setIsOpen(true);
|
||
};
|
||
|
||
// Очистка не сбрасывает выбранный проект — только текст поиска, чтобы сразу искать другой.
|
||
const onClear = (event: React.MouseEvent): void => {
|
||
event.preventDefault();
|
||
setQuery('');
|
||
setIsOpen(true);
|
||
inputRef.current?.focus();
|
||
};
|
||
|
||
// mousedown, а не click: иначе поле теряет фокус раньше, чем срабатывает выбор.
|
||
const onPick = (event: React.MouseEvent, project: IAccessibleProject): void => {
|
||
event.preventDefault();
|
||
selectProject(project);
|
||
setIsOpen(false);
|
||
};
|
||
|
||
return (
|
||
<div className={styles.picker}>
|
||
<label className={styles.label} htmlFor="stroybot-project-search">
|
||
{TEXTS.label}
|
||
</label>
|
||
<div className={styles.field} ref={wrapperRef}>
|
||
<span className={styles.searchIcon} aria-hidden="true">
|
||
<Icon name="search" size="sm" />
|
||
</span>
|
||
<input
|
||
id="stroybot-project-search"
|
||
ref={inputRef}
|
||
type="text"
|
||
className={styles.input}
|
||
placeholder={TEXTS.placeholder}
|
||
value={query}
|
||
autoComplete="off"
|
||
onFocus={onFocus}
|
||
onChange={onChange}
|
||
/>
|
||
{query && (
|
||
<button
|
||
type="button"
|
||
className={styles.clearButton}
|
||
title={TEXTS.clear}
|
||
aria-label={TEXTS.clear}
|
||
onMouseDown={onClear}
|
||
>
|
||
<Icon name="close" size="sm" />
|
||
</button>
|
||
)}
|
||
{currentProject?.status && (
|
||
<span className={styles.selectedBadge}>
|
||
<ProjectStatusBadge status={currentProject.status} />
|
||
</span>
|
||
)}
|
||
{isOpen && (
|
||
<div className={styles.dropdown}>
|
||
<ProjectList loadState={loadState} projects={projects} query={query} onPick={onPick} />
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|