дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
fix(workbench): focus a tool window's content once it loads (#1280)
Activating a tool window focuses its `[data-autofocus]` element or first content field one frame later, else its body. A lazily loaded view such as the Terminal renders its field after that, so the first ⌥F12 left focus on the body. When no target exists yet, the tool window now watches its body and focuses the target when it appears, unless focus has moved on.
Цей коміт міститься в:
1 джерело
aa33d2eeab
коміт
b440370f2d
2 files changed
+65
-7
No files matched your search
@@ -31,6 +31,34 @@ test("⌥F12 opens the Terminal tool window and toggles it closed", async ({ wor
|
||||
await expect(terminal).toBeHidden();
|
||||
});
|
||||
|
||||
test("the first ⌥F12 focuses the terminal input once the lazily loaded view mounts", async ({ workbench: page }) => {
|
||||
await page.keyboard.press("Alt+F12");
|
||||
const terminal = page.getByRole("region", { name: "Terminal", exact: true });
|
||||
await expect(terminal.getByRole("textbox", { name: "Terminal input" })).toBeFocused();
|
||||
// Reopened with the view already mounted, it focuses the input as well.
|
||||
await page.keyboard.press("Alt+F12");
|
||||
await expect(terminal).toBeHidden();
|
||||
await page.keyboard.press("Alt+F12");
|
||||
await expect(terminal.getByRole("textbox", { name: "Terminal input" })).toBeFocused();
|
||||
});
|
||||
|
||||
test("a terminal view that loads late doesn't take focus moved elsewhere meanwhile", async ({ workbench: page }) => {
|
||||
let release!: () => void;
|
||||
const loaded = new Promise<void>((resolve) => (release = resolve));
|
||||
await page.route(/\/src\/features\/terminal\/runtime\.ts/, async (route) => {
|
||||
await loaded;
|
||||
await route.continue();
|
||||
});
|
||||
await page.keyboard.press("Alt+F12");
|
||||
const terminal = page.getByRole("region", { name: "Terminal", exact: true });
|
||||
await expect(terminal).toBeVisible();
|
||||
const tree = page.getByRole("tree", { name: "Project" });
|
||||
await tree.getByRole("treeitem", { name: "README.md" }).click();
|
||||
release();
|
||||
await expect(terminal.getByRole("textbox", { name: "Terminal input" })).toBeAttached();
|
||||
await expect(tree).toBeFocused();
|
||||
});
|
||||
|
||||
test("Split Right gives the Terminal a tab group with its own header; Unsplit merges it back", async ({
|
||||
workbench: page,
|
||||
}) => {
|
||||
|
||||
@@ -716,6 +716,9 @@ function HeaderRow({
|
||||
);
|
||||
}
|
||||
|
||||
/** How long an activated tool window waits for lazily loaded content to focus. */
|
||||
const LATE_FOCUS_MS = 5000;
|
||||
|
||||
function ToolWindowView({ tw, hidden }: { tw: ToolWindowContribution; hidden: boolean }) {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
const focusRequest = useToolWindowLayout((s) => s.focusRequest);
|
||||
@@ -726,20 +729,47 @@ function ToolWindowView({ tw, hidden }: { tw: ToolWindowContribution; hidden: bo
|
||||
|
||||
useEffect(() => {
|
||||
if (!focusRequest || focusRequest.id !== tw.id || hidden) return;
|
||||
let observer: MutationObserver | null = null;
|
||||
let timer = 0;
|
||||
const stop = () => {
|
||||
observer?.disconnect();
|
||||
observer = null;
|
||||
window.clearTimeout(timer);
|
||||
};
|
||||
const raf = requestAnimationFrame(() => {
|
||||
const root = ref.current;
|
||||
if (!root) return;
|
||||
const body = root.querySelector<HTMLElement>("[data-toolwindow-body]");
|
||||
// Like IDEA, activation focuses the content (a field, list or tree), never a button or
|
||||
// link: a scripted focus on one draws its focus ring (an empty view's "New…" link).
|
||||
const content = [...(body?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [])].find(
|
||||
(el) => !el.matches('button, a[href], [role="button"], [role="link"]'),
|
||||
);
|
||||
if (body && !content && !body.hasAttribute("tabindex")) body.tabIndex = -1;
|
||||
const target = body?.querySelector<HTMLElement>("[data-autofocus]") ?? content ?? body ?? root;
|
||||
target.focus({ preventScroll: true });
|
||||
const contentTarget = () =>
|
||||
body?.querySelector<HTMLElement>("[data-autofocus]") ??
|
||||
[...(body?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [])].find(
|
||||
(el) => !el.matches('button, a[href], [role="button"], [role="link"]'),
|
||||
);
|
||||
const target = contentTarget();
|
||||
if (target || !body) {
|
||||
(target ?? root).focus({ preventScroll: true });
|
||||
return;
|
||||
}
|
||||
if (!body.hasAttribute("tabindex")) body.tabIndex = -1;
|
||||
body.focus({ preventScroll: true });
|
||||
// Lazily loaded views (the Terminal) render their field after this: focus it when it
|
||||
// appears, unless focus has moved on meanwhile.
|
||||
observer = new MutationObserver(() => {
|
||||
if (document.activeElement !== body) return stop();
|
||||
const late = contentTarget();
|
||||
if (!late) return;
|
||||
stop();
|
||||
late.focus({ preventScroll: true });
|
||||
});
|
||||
observer.observe(body, { childList: true, subtree: true, attributes: true, attributeFilter: ["data-autofocus"] });
|
||||
timer = window.setTimeout(stop, LATE_FOCUS_MS);
|
||||
});
|
||||
return () => cancelAnimationFrame(raf);
|
||||
return () => {
|
||||
cancelAnimationFrame(raf);
|
||||
stop();
|
||||
};
|
||||
}, [focusRequest, tw.id, hidden]);
|
||||
|
||||
return (
|
||||
|
||||
Reference in new issue
Заблокувати користувача