fix: add inline clear action to conversation search

Show a clear button when the conversation search contains text. Clearing restores the scoped conversation list and returns focus to the input, with regression coverage for visibility and filtering.

Release note: Conversation search now includes an X button to clear keywords and return to the current project's conversations.
This commit is contained in:
mesalogo
2026-09-12 22:10:25 +08:00
parent d36a05f7ad
commit 719c9fd6cd
3 changed files with 63 additions and 0 deletions
+31
View File
@@ -2183,6 +2183,10 @@ describe("App", () => {
]);
const { container } = render(<App />);
const search = await screen.findByLabelText("搜索对话");
const searchControls = within(search.parentElement!);
expect(
searchControls.queryByRole("button", { name: "清除搜索" }),
).not.toBeInTheDocument();
const conversationList =
container.querySelector<HTMLElement>(".conversation-list");
if (!conversationList) {
@@ -2219,6 +2223,33 @@ describe("App", () => {
).not.toBeInTheDocument();
});
const clearSearch = searchControls.getByRole("button", {
name: "清除搜索",
});
clearSearch.focus();
fireEvent.click(clearSearch);
expect(search).toHaveValue("");
expect(search).toHaveFocus();
expect(
searchControls.queryByRole("button", { name: "清除搜索" }),
).not.toBeInTheDocument();
await waitFor(() => {
expect(
within(conversationList).getByText("标题里的 Alpha"),
).toBeInTheDocument();
expect(
within(conversationList).getByText("正文命中的会话"),
).toBeInTheDocument();
expect(
within(conversationList).queryByText("其他项目里的 Alpha"),
).not.toBeInTheDocument();
});
fireEvent.change(search, { target: { value: " " } });
fireEvent.click(
searchControls.getByRole("button", { name: "清除搜索" }),
);
expect(search).toHaveValue("");
fireEvent.change(search, { target: { value: "missing topic" } });
expect(
await within(conversationList).findByText("没有匹配的对话"),
+16
View File
@@ -2448,6 +2448,7 @@ function App(): React.JSX.Element {
] = useState(true);
const [incompleteMagicTodoCount, setIncompleteMagicTodoCount] = useState(0);
const [searchQuery, setSearchQuery] = useState("");
const searchInputRef = useRef<HTMLInputElement>(null);
const deferredSearchQuery = useDeferredValue(searchQuery);
const [searchConversationSnapshot, setSearchConversationSnapshot] =
useState(conversations);
@@ -8181,10 +8182,25 @@ function App(): React.JSX.Element {
<Search size={15} />
<input
aria-label={t("sidebar.searchLabel")}
ref={searchInputRef}
onChange={(event) => setSearchQuery(event.target.value)}
placeholder={t("sidebar.searchPlaceholder")}
value={searchQuery}
/>
{searchQuery.length > 0 && (
<button
aria-label={t("conversation.clearSearch")}
className="icon-button sidebar-search__clear"
onClick={() => {
setSearchQuery("");
searchInputRef.current?.focus();
}}
title={t("conversation.clearSearch")}
type="button"
>
<X aria-hidden="true" size={14} />
</button>
)}
</div>
<nav className="primary-nav" aria-label={t("navigation.label")}>
+16
View File
@@ -3373,10 +3373,12 @@ textarea:focus-visible {
}
.sidebar-search {
position: relative;
display: flex;
min-width: 248px;
align-items: center;
padding: 9px 11px;
padding-right: 40px;
border: 1px solid var(--border-default);
border-radius: var(--radius-control);
margin-bottom: 15px;
@@ -3386,6 +3388,7 @@ textarea:focus-visible {
}
.sidebar-search input {
min-width: 0;
width: 100%;
border: 0;
outline: 0;
@@ -3398,6 +3401,19 @@ textarea:focus-visible {
color: var(--text-muted);
}
.sidebar-search > svg {
flex-shrink: 0;
}
.sidebar-search .sidebar-search__clear {
position: absolute;
top: 50%;
right: var(--space-2);
width: 24px;
height: 24px;
transform: translateY(-50%);
}
.primary-nav {
display: flex;
min-width: 248px;