mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
feat(editor): expand Replace from the Find bar with a chevron (#378)
The in-editor Find bar gains a chevron at its left that expands and collapses the Replace row (Replace, Replace All, Exclude), replacing the separate Replace icon button. Cmd/Ctrl+F opens the bar collapsed and Cmd/Ctrl+R expanded; the search text, options and focus are kept when toggling. The chevron reports aria-expanded. Fixes #362
This commit is contained in:
@@ -117,6 +117,16 @@
|
||||
background: var(--bg-editor-inline, var(--bg-elev));
|
||||
border-bottom: 1px solid var(--border-editor-tabs, var(--border));
|
||||
}
|
||||
.findExpandable {
|
||||
grid-template-columns: auto minmax(220px, 420px) auto 1fr;
|
||||
}
|
||||
.findExpand {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
.findExpandable .findField {
|
||||
grid-column: 2;
|
||||
}
|
||||
.findField {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -390,7 +390,25 @@ export function FindPanel({ target, version, replace, seq, onReplaceShown, onClo
|
||||
const canReplace = !!opts.search && !invalid;
|
||||
|
||||
return (
|
||||
<div className={styles.find} role="search" aria-label={replace ? "Replace in file" : "Find in file"}>
|
||||
<div
|
||||
className={clsx(styles.find, onReplaceShown && styles.findExpandable)}
|
||||
role="search"
|
||||
aria-label={replace ? "Replace in file" : "Find in file"}
|
||||
>
|
||||
{onReplaceShown && (
|
||||
<IconButton
|
||||
icon={replace ? "chevron-down" : "chevron-right"}
|
||||
title={replace ? "Hide Replace" : "Show Replace"}
|
||||
size="small"
|
||||
className={styles.findExpand}
|
||||
aria-expanded={replace}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => {
|
||||
if (replaceBoxRef.current?.contains(document.activeElement)) inputRef.current?.focus();
|
||||
onReplaceShown(!replace);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div ref={findBoxRef} className={styles.findField}>
|
||||
<SearchField
|
||||
ref={inputRef}
|
||||
@@ -450,15 +468,6 @@ export function FindPanel({ target, version, replace, seq, onReplaceShown, onClo
|
||||
pressed={scoped || opts.context !== "any"}
|
||||
onClick={() => setFilterMenu(true)}
|
||||
/>
|
||||
{onReplaceShown && (
|
||||
<IconButton
|
||||
icon="replace"
|
||||
title={replace ? "Hide Replace" : "Show Replace"}
|
||||
size="small"
|
||||
pressed={replace}
|
||||
onClick={() => onReplaceShown(!replace)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<IconButton icon="close" title="Close" shortcut="⎋" size="small" className={styles.findClose} onClick={close} />
|
||||
{replace && (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import type { EditorInput } from "@/workbench/contributions";
|
||||
import { useKeymap } from "@/platform/keymap";
|
||||
@@ -123,6 +124,76 @@ describe("Find in the diff viewer", () => {
|
||||
expect(screen.queryByRole("button", { name: "Show Replace" })).toBeNull();
|
||||
});
|
||||
|
||||
it.each(["Meta", "Ctrl"])("opens collapsed with %s+F and expanded with Replace", async (modifier) => {
|
||||
useKeymap.setState({
|
||||
overrides: {
|
||||
...KEYMAP,
|
||||
"find.inFile": [`${modifier}+F`],
|
||||
"find.replaceInFile": [`${modifier}+R`],
|
||||
},
|
||||
});
|
||||
render(<DiffEditor input={working} />);
|
||||
const v = await pane(1);
|
||||
const pressKey = (key: string) => {
|
||||
v.contentDOM.dispatchEvent(new FocusEvent("focus"));
|
||||
fireEvent.keyDown(v.contentDOM, {
|
||||
code: `Key${key}`,
|
||||
key: key.toLowerCase(),
|
||||
metaKey: modifier === "Meta",
|
||||
ctrlKey: modifier === "Ctrl",
|
||||
});
|
||||
};
|
||||
pressKey("F");
|
||||
expect(await screen.findByRole("button", { name: "Show Replace" })).toHaveAttribute("aria-expanded", "false");
|
||||
expect(screen.queryByRole("textbox", { name: "Replace" })).toBeNull();
|
||||
pressKey("R");
|
||||
expect(await screen.findByRole("button", { name: "Hide Replace" })).toHaveAttribute("aria-expanded", "true");
|
||||
expect(screen.getByRole("textbox", { name: "Replace" })).toBeInTheDocument();
|
||||
pressKey("F");
|
||||
expect(await screen.findByRole("button", { name: "Show Replace" })).toHaveAttribute("aria-expanded", "false");
|
||||
});
|
||||
|
||||
it("toggles with pointer and keyboard while preserving query, options, and focus", async () => {
|
||||
const user = userEvent.setup();
|
||||
useKeymap.setState({ overrides: KEYMAP });
|
||||
render(<DiffEditor input={working} />);
|
||||
const v = await press(1, "KeyF");
|
||||
const field = await screen.findByRole("textbox", { name: "Find" });
|
||||
fireEvent.change(field, { target: { value: "foo" } });
|
||||
const matchCase = screen.getByRole("button", { name: "Match Case" });
|
||||
await user.click(matchCase);
|
||||
const pressed = matchCase.getAttribute("aria-pressed");
|
||||
field.focus();
|
||||
(field as HTMLInputElement).setSelectionRange(1, 2);
|
||||
const disclosure = screen.getByRole("button", { name: "Show Replace" });
|
||||
expect(screen.getByRole("search").firstElementChild).toBe(disclosure);
|
||||
await user.click(disclosure);
|
||||
expect(field).toHaveFocus();
|
||||
expect((field as HTMLInputElement).selectionStart).toBe(1);
|
||||
expect((field as HTMLInputElement).selectionEnd).toBe(2);
|
||||
expect(field).toHaveValue("foo");
|
||||
expect(matchCase).toHaveAttribute("aria-pressed", pressed);
|
||||
const replacement = screen.getByRole("textbox", { name: "Replace" });
|
||||
await user.type(replacement, "baz");
|
||||
await user.click(screen.getByRole("button", { name: "Hide Replace" }));
|
||||
expect(field).toHaveFocus();
|
||||
expect(screen.queryByRole("textbox", { name: "Replace" })).toBeNull();
|
||||
const toggle = screen.getByRole("button", { name: "Show Replace" });
|
||||
toggle.focus();
|
||||
await user.keyboard("{Enter}");
|
||||
expect(toggle).toHaveAttribute("aria-expanded", "true");
|
||||
expect(toggle).toHaveFocus();
|
||||
expect(screen.getByRole("textbox", { name: "Replace" })).toHaveValue("baz");
|
||||
await user.keyboard(" ");
|
||||
expect(toggle).toHaveAttribute("aria-expanded", "false");
|
||||
await user.keyboard(" ");
|
||||
field.focus();
|
||||
await user.tab();
|
||||
expect(screen.getByRole("textbox", { name: "Replace" })).toHaveFocus();
|
||||
await user.click(screen.getByRole("button", { name: "Replace All" }));
|
||||
expect(v.state.doc.toString()).toBe("baz\nqux\nbaz\n");
|
||||
});
|
||||
|
||||
it("finds the next occurrence of the last search with ⌘G", async () => {
|
||||
useKeymap.setState({ overrides: KEYMAP });
|
||||
render(<DiffEditor input={revisions} />);
|
||||
|
||||
Reference in New Issue
Block a user