Skip to content
Merged
29 changes: 29 additions & 0 deletions apps/web/src/components/files/FileBrowserPanel.browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { page, userEvent } from "vite-plus/test/browser";

import { COMPOSER_MENTION_DRAG_TYPE } from "~/components/chat/composerMentionDrag";
import { dismissContextMenu } from "~/contextMenuFallback";
import { isMacPlatform } from "~/lib/utils";
import { render } from "../../../test/render";
import FileBrowserPanel from "./FileBrowserPanel";
import { dispatchPreviewAction } from "../preview/previewActionBus";

const entries: readonly ProjectEntry[] = [
{ path: "src", kind: "directory" },
Expand Down Expand Up @@ -115,4 +117,31 @@ describe("FileBrowserPanel", () => {
row.click();
await vi.waitFor(() => expect(onOpenFile).toHaveBeenCalledTimes(1));
});

it("mod+f from a tree row, or the panel find command, focuses the file search", async () => {
await using _screen = await render(
<FileBrowserPanel
environmentId={EnvironmentId.make("env-files")}
cwd="/repo"
projectName="repo"
selectedPath={null}
selectedPathRevealId={0}
onOpenFile={() => {}}
workspaceMutationId={null}
answersPanelFind
/>,
);
await vi.waitFor(() => expect(findTreeRow("README.md")).not.toBeNull());
findTreeRow("README.md")!.focus();

const modifier = isMacPlatform(navigator.platform) ? "Meta" : "Control";
await userEvent.keyboard(`{${modifier}>}f{/${modifier}}`);

const search = page.getByRole("searchbox", { name: "Search repo files" });
await expect.element(search).toHaveFocus();

(search.element() as HTMLElement).blur();
dispatchPreviewAction("find");
await expect.element(search).toHaveFocus();
});
});
35 changes: 33 additions & 2 deletions apps/web/src/components/files/FileBrowserPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { serializeComposerFileLink } from "@t3tools/shared/composerTrigger";
import * as AsyncResult from "effect/reactivity/AsyncResult";
import * as Cause from "effect/Cause";
import { ChevronsDownUpIcon, ChevronsUpDownIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { type Ref, useEffect, useEffectEvent, useRef, useState } from "react";

import { Button } from "~/components/ui/button";
import { InputGroup, InputGroupInput } from "~/components/ui/input-group";
Expand All @@ -33,6 +33,8 @@ import { buildFileTreePathUpdates } from "./fileTreePathReconciliation";
import { loadFileAncestors } from "./loadFileAncestors";
import { useDirectoryEntries } from "./useDirectoryEntries";
import { useProjectPathSearch } from "~/state/queries";
import { isMacPlatform } from "~/lib/utils";
import { type PreviewAction, subscribePreviewAction } from "../preview/previewActionBus";

interface FileBrowserPanelProps {
environmentId: EnvironmentId;
Expand All @@ -46,6 +48,7 @@ interface FileBrowserPanelProps {
hostFolder?: boolean;
onRefreshSelectedFile?: () => void;
workspaceMutationId: string | null;
answersPanelFind?: boolean;
}

function treePath(entry: ProjectEntry): string {
Expand Down Expand Up @@ -73,16 +76,21 @@ function RefreshFilesButton(props: { isPending: boolean; onRefresh: () => void }
);
}

function FileSearchField(props: {
function FileSearchField({
ref,
...props
}: {
ariaLabel: string;
name: string;
onClose: () => void;
onValueChange: (value: string) => void;
ref: Ref<HTMLInputElement>;
value: string;
}) {
return (
<InputGroup variant="ghost" className="h-7 min-w-0 flex-1">
<InputGroupInput
ref={ref}
type="search"
name={props.name}
size="sm"
Expand Down Expand Up @@ -142,6 +150,7 @@ export default function FileBrowserPanel({
onRefreshSelectedFile,
workspaceMutationId,
hostFolder = false,
answersPanelFind = false,
}: FileBrowserPanelProps) {
const panelPath = (relativePath: string) =>
hostFolder ? joinHostPath(cwd, relativePath) : relativePath;
Expand Down Expand Up @@ -320,6 +329,20 @@ export default function FileBrowserPanel({
setQuery("");
search.close();
};
const searchInputRef = useRef<HTMLInputElement>(null);
const focusSearch = () => {
searchInputRef.current?.focus();
searchInputRef.current?.select();
};
const onPanelAction = useEffectEvent((action: PreviewAction) => {
// Hidden panel tabs stay mounted; only the visible Files tab answers.
if (action === "find" && searchInputRef.current?.getClientRects().length) focusSearch();
});
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- subscribes to the panel find command, which fires while focus is anywhere in the right panel
useEffect(() => {
if (!answersPanelFind) return;
return subscribePreviewAction(onPanelAction);
}, [answersPanelFind]);
const expandedPathsRef = useRef(new Set<string>());
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- subscribes to the Pierre tree model to load directories as they are expanded
useEffect(() => {
Expand Down Expand Up @@ -482,13 +505,21 @@ export default function FileBrowserPanel({
// Capture dragstart before the tree selects the row so selection sees the drag flag.
onDragStartCapture={(event) => dragMention.handleDragStart(event.nativeEvent)}
onDragEnd={() => dragMention.handleDragEnd((path) => model.getItem(path)?.deselect())}
onKeyDown={(event) => {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const isModPressed = isMacPlatform(navigator.platform) ? event.metaKey : event.ctrlKey;
if (!isModPressed || event.altKey || event.shiftKey || event.key.toLowerCase() !== "f")
return;
event.preventDefault();
focusSearch();
}}
>
<div
className="flex h-10 min-h-10 shrink-0 items-center gap-1 border-b border-border/60 bg-background px-2 in-data-[preview-panel-mode=inline]:mb-1 in-data-[preview-panel-mode=inline]:h-9 in-data-[preview-panel-mode=inline]:min-h-9 in-data-[preview-panel-mode=inline]:border-b-transparent"
data-surface-subheader
>
<RefreshFilesButton isPending={isPending} onRefresh={handleRefresh} />
<FileSearchField
ref={searchInputRef}
name="project-files-search"
ariaLabel={`Search ${projectName} files`}
value={search.value}
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/files/FilePreviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1638,6 +1638,7 @@ export default function FilePreviewPanel({
}
selectedPathRevealId={revealRequestId}
onOpenFile={onOpenFile}
answersPanelFind={!previewPath}
workspaceMutationId={workspaceMutationId}
{...(previewPath && !isMedia && !isPdf
? {
Expand Down
Loading