дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
fix(ui): no focus ring on controls that were only clicked (#1213)
The webview focuses a clicked button and the focus stays there: across project window-tab switches, and popups hand it back to their trigger. The next keystroke switches the engine to keyboard modality, so that button got a focus ring (window tabs, the title-bar project switcher). A pressed control is now marked and keeps no ring until Tab or an arrow key moves focus onto it. Focus itself is unchanged, since actions resolve their context from it. Closes #1211
Цей коміт міститься в:
1 джерело
b473876d59
коміт
71c0515af6
4 files changed
+87
No files matched your search
@@ -16,6 +16,7 @@ import { auxiliaryWindow } from "./platform/auxWindow";
|
||||
import { installRepeatedDoubleClick } from "./platform/doubleClick";
|
||||
import { installErrorLogging } from "./platform/errorLog";
|
||||
import { installKeyboardDispatcher } from "./platform/keymap";
|
||||
import { installPointerFocus } from "./platform/pointerFocus";
|
||||
import { isTauri } from "./platform/ipc";
|
||||
import { hideSplash } from "./platform/splash";
|
||||
import { prefetchLazyComponents } from "./ui";
|
||||
@@ -28,6 +29,7 @@ if (import.meta.env.DEV && isTauri()) {
|
||||
}
|
||||
|
||||
installRepeatedDoubleClick();
|
||||
installPointerFocus();
|
||||
installErrorLogging();
|
||||
|
||||
// Auxiliary windows (Settings `settings-<parent>`, comparisons `comparison-<parent>-<n>`, Sync with
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { installPointerFocus } from "./pointerFocus";
|
||||
|
||||
let uninstall: () => void;
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<button id="tab" role="tab"><span id="label">go-app</span></button>
|
||||
<button id="other">Other</button>
|
||||
<div id="row" role="treeitem" tabindex="0">row</div>`;
|
||||
uninstall = installPointerFocus();
|
||||
});
|
||||
afterEach(() => uninstall());
|
||||
|
||||
const el = (id: string) => document.getElementById(id)!;
|
||||
const press = (id: string) => el(id).dispatchEvent(new PointerEvent("pointerdown", { bubbles: true }));
|
||||
const key = (k: string) => document.dispatchEvent(new KeyboardEvent("keydown", { key: k, bubbles: true }));
|
||||
const marked = (id: string) => el(id).hasAttribute("data-pointer-focus");
|
||||
|
||||
describe("installPointerFocus", () => {
|
||||
it("marks the control under a press, not other focusable elements", () => {
|
||||
press("label");
|
||||
press("row");
|
||||
expect([marked("tab"), marked("row")]).toEqual([true, false]);
|
||||
});
|
||||
|
||||
it("keeps the mark when focus comes back without keyboard navigation (a popup closed with Esc)", () => {
|
||||
press("tab");
|
||||
el("tab").focus();
|
||||
el("other").focus();
|
||||
key("Escape");
|
||||
el("tab").focus();
|
||||
expect(marked("tab")).toBe(true);
|
||||
});
|
||||
|
||||
it("clears the mark when Tab or an arrow key moves focus onto the control", () => {
|
||||
press("tab");
|
||||
press("other");
|
||||
key("Tab");
|
||||
el("tab").focus();
|
||||
expect(marked("tab")).toBe(false);
|
||||
key("ArrowRight");
|
||||
el("other").focus();
|
||||
expect(marked("other")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* No focus ring on a control the user only clicked. The webview focuses a clicked button and
|
||||
* the focus outlives the click (across window-tab switches, and popups hand it back to their
|
||||
* trigger); the next keystroke then switches the engine to keyboard modality and rings a button
|
||||
* that was never reached by keyboard. Focus itself stays put (actions resolve their context from
|
||||
* it): a pressed control is marked, `global.css` drops the ring of marked controls, and moving
|
||||
* focus onto one with Tab or the arrow keys clears its mark.
|
||||
*/
|
||||
const CONTROL = "button, [role='button'], [role='tab'], a[href], summary";
|
||||
const MARK = "data-pointer-focus";
|
||||
const NAVIGATION = new Set(["Tab", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"]);
|
||||
|
||||
export function installPointerFocus(win: Window = window): () => void {
|
||||
const doc = win.document;
|
||||
let navigating = false;
|
||||
const onPointerDown = (e: PointerEvent) => {
|
||||
navigating = false;
|
||||
if (e.target instanceof Element) e.target.closest(CONTROL)?.setAttribute(MARK, "");
|
||||
};
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
navigating = NAVIGATION.has(e.key);
|
||||
};
|
||||
const onFocusIn = (e: FocusEvent) => {
|
||||
if (navigating && e.target instanceof Element) e.target.removeAttribute(MARK);
|
||||
};
|
||||
doc.addEventListener("pointerdown", onPointerDown, true);
|
||||
doc.addEventListener("keydown", onKeyDown, true);
|
||||
doc.addEventListener("focusin", onFocusIn, true);
|
||||
return () => {
|
||||
doc.removeEventListener("pointerdown", onPointerDown, true);
|
||||
doc.removeEventListener("keydown", onKeyDown, true);
|
||||
doc.removeEventListener("focusin", onFocusIn, true);
|
||||
};
|
||||
}
|
||||
@@ -152,6 +152,12 @@ a:hover {
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* A control the user only clicked keeps no ring once a keystroke turns on keyboard modality
|
||||
(`platform/pointerFocus.ts`); `!important` covers the components' own focus rings. */
|
||||
[data-pointer-focus]:focus-visible {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--bg-editor-selection);
|
||||
}
|
||||
|
||||
Reference in new issue
Заблокувати користувача