Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions apps/web/src/components/ChatView.logic.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,33 @@ it("keeps keyboard focus on a sidebar row on window return, but not a clicked ro

await userEvent.click(clicked);
expect(document.activeElement).toBe(clicked);
expect(shouldRefocusComposerOnWindowFocus(document.activeElement)).toBe(true);
expect(shouldRefocusComposerOnWindowFocus(document.activeElement, document.getSelection())).toBe(
true,
);

await userEvent.tab();
expect(document.activeElement).toBe(tabbed);
expect(shouldRefocusComposerOnWindowFocus(document.activeElement)).toBe(false);
expect(shouldRefocusComposerOnWindowFocus(document.activeElement, document.getSelection())).toBe(
false,
);
});

it("leaves a text selection in the timeline alone on window return", async () => {
const timeline = document.createElement("div");
timeline.tabIndex = -1;
const reply = document.createElement("p");
reply.textContent = "Steel bridges are also easy to recycle at the end of their life.";
timeline.append(reply);
document.body.append(timeline);
using _timeline = { [Symbol.dispose]: () => timeline.remove() };

await userEvent.tripleClick(reply);
expect(document.activeElement).toBe(timeline);
expect(document.getSelection()?.toString()).toContain("Steel bridges");
expect(
shouldRefocusComposerOnWindowFocus(document.activeElement, document.getSelection()),
"refocusing the composer would clear the selected reply text",
).toBe(false);
});

it("refocuses the composer on window return from a clicked area after a bare key press", async () => {
Expand All @@ -48,7 +70,7 @@ it("refocuses the composer on window return from a clicked area after a bare key
// A window switch can deliver a lone modifier key without moving focus.
await userEvent.keyboard("{Shift}");
expect(
shouldRefocusComposerOnWindowFocus(document.activeElement),
shouldRefocusComposerOnWindowFocus(document.activeElement, document.getSelection()),
"a clicked area counts as reached from the keyboard after a bare key press",
).toBe(true);

Expand All @@ -57,5 +79,7 @@ it("refocuses the composer on window return from a clicked area after a bare key
await userEvent.keyboard("{F6}");
controller.move(1);
expect(document.activeElement).toBe(conversation);
expect(shouldRefocusComposerOnWindowFocus(document.activeElement)).toBe(false);
expect(shouldRefocusComposerOnWindowFocus(document.activeElement, document.getSelection())).toBe(
false,
);
});
10 changes: 6 additions & 4 deletions apps/web/src/components/ChatView.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1078,15 +1078,17 @@ export function hasServerAcknowledgedLocalDispatch(input: {
);
}

// Returning to the window should land the caret in the composer, so the reader can type right
// away. The exceptions are places where focus is deliberate: another text field, a terminal in
// the drawer or the right panel, an open dialog, menu, or popup, or a region or sidebar row
// reached from the keyboard. A plain focused button or a clicked row yields to the composer.
// Returning to the window should land the caret in the composer, except where focus or a
// selection is deliberate: another text field, a terminal, an open dialog, menu, or popup, a
// region or sidebar row reached from the keyboard, or selected text the composer would clear.
// A plain focused button or a clicked row yields to the composer.
export function shouldRefocusComposerOnWindowFocus(
activeElement:
| (Pick<Element, "tagName" | "closest" | "getAttribute"> & { isContentEditable?: boolean })
| null,
selection: Pick<Selection, "isCollapsed"> | null,
): boolean {
if (selection !== null && !selection.isCollapsed) return false;
if (activeElement === null || activeElement.tagName === "BODY") return true;
if (
activeElement.tagName === "INPUT" ||
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2137,7 +2137,7 @@ export default function ChatView(props: ChatViewProps) {
if (document.querySelector("[data-mcp-app-fullscreen]") !== null) return;
if (
!isInSideChatPane(document.activeElement) &&
shouldRefocusComposerOnWindowFocus(document.activeElement)
shouldRefocusComposerOnWindowFocus(document.activeElement, document.getSelection())
) {
focusComposerStable();
}
Expand Down
Loading