fix(ui): keep keyboard focus when collapsing standalone questions (#181296)

Preserve keyboard focus on Expand when a standalone question collapses, retaining its answer draft and Chat composer focus behavior.

Reviewed-by: @shakkernerd
This commit is contained in:
RoboClaw authored and GitHub committed 2026-10-23 07:26:16 +01:00
1 parent f5c8e0dbeb
commit d3359fe448
2 files changed
+37 -1

No files matched your search

@@ -1,4 +1,12 @@
import { action, createMemo, createOptimistic, createSignal, Show, untrack } from "solid-js";
import {
action,
createMemo,
createOptimistic,
createSignal,
onSettled,
Show,
untrack,
} from "solid-js";
import type { QuestionDraft } from "../../../app/question-prompt.ts";
import { Icon } from "../../../components/solid/icon.tsx";
import { registerChatSessionEnglish } from "../../../i18n/locales/en-chat-session.ts";
@@ -35,6 +43,7 @@ function QuestionPanelRequest(props: { panel: QuestionPanelProps }) {
const [pendingKind, setPendingKind] = createOptimistic<"submit" | "skip" | null>(null);
let resolving = false;
let panel: HTMLElement | undefined;
let expandButton: HTMLButtonElement | undefined;
let answerControl: HTMLInputElement | HTMLTextAreaElement | undefined;
let hostsInput: HTMLInputElement | undefined;
const options = new Set<HTMLButtonElement>();
@@ -73,6 +82,10 @@ function QuestionPanelRequest(props: { panel: QuestionPanelProps }) {
props.panel.onCollapsedChange(value);
} else {
setUncontrolledCollapsed(value);
// Controlled callers own their destination (for example, the chat composer).
if (value) {
onSettled(() => expandButton?.focus({ preventScroll: true }));
}
}
};
const updateDraft = (item: QuestionPanelQuestion, value: QuestionDraft) => {
@@ -316,6 +329,7 @@ function QuestionPanelRequest(props: { panel: QuestionPanelProps }) {
aria-label={model().title}
>
<button
ref={(element) => (expandButton = element)}
class="chat-question-panel__collapsed-button"
type="button"
onClick={() => setCollapsed(false)}
@@ -76,6 +76,28 @@ it.each(["question", "outside"] as const)("restores only question focus (%s)", a
expect(request).toHaveBeenCalledTimes(1);
});
it("keeps focus on the collapsed question so it can be reopened", async () => {
const { page } = await mount(vi.fn().mockResolvedValue({ question: question() }));
await questionPanelIn(page);
const input = page.querySelector("textarea")!;
input.value = "The design team";
input.dispatchEvent(new InputEvent("input", { bubbles: true }));
const collapse = page.querySelector<HTMLButtonElement>(".chat-question-panel__collapse")!;
collapse.focus();
collapse.click();
await waitForSolid(() => {
const expand = page.querySelector(".chat-question-panel__collapsed-button");
expect(expand).not.toBeNull();
expect(document.activeElement).toBe(expand);
});
page.querySelector<HTMLButtonElement>(".chat-question-panel__collapsed-button")!.click();
await waitForSolid(() => {
expect(document.activeElement).toBe(page.querySelector(".chat-question-panel"));
expect(page.querySelector("textarea")?.value).toBe("The design team");
});
});
it("recovers a failed reconnect lookup without losing the typed answer", async () => {
const request = vi
.fn()