fix(editor): keep table hover open while typing in its search box

保持表格悬浮搜索框在输入法组合期间可用。
This commit is contained in:
Loc Nguyen
2026-10-19 23:21:02 +08:00
committed by GitHub
parent a438fdf21f
commit 4586dcd44d
3 changed files with 79 additions and 0 deletions
@@ -127,6 +127,7 @@ export function createQueryEditorHoverContent(options: QueryEditorHoverContentOp
layoutController || handleCopy || searchController
? () => {
layoutController?.mount();
searchController?.pinTooltip(dom.closest<HTMLElement>(".cm-tooltip") ?? dom);
if (handleCopy) document.addEventListener("copy", handleCopy);
}
: undefined,
@@ -517,4 +517,58 @@ describe("createHoverSearch", () => {
input.dispatchEvent(new Event("input"));
expect(target.firstChild).toBe(initialFirstChild);
});
describe("pinTooltip", () => {
function mountPinnedSearch() {
const host = document.createElement("div");
const target = document.createElement("div");
target.textContent = DDL;
const controller = createHoverSearch({ target, originalHtml: target.innerHTML, placeholder: "Search columns…", noResultLabel: "No matching columns" });
host.append(controller.element, target);
document.body.appendChild(host);
controller.pinTooltip(host);
const leave = vi.fn();
host.addEventListener("mouseleave", leave);
const input = controller.element.querySelector<HTMLInputElement>('[data-sql-hover-search-input="true"]')!;
return { controller, host, input, leave };
}
it("keeps the tooltip from seeing mouseleave while the search input is focused", () => {
const { host, input, leave } = mountPinnedSearch();
input.focus();
input.dispatchEvent(new CompositionEvent("compositionstart"));
host.dispatchEvent(new MouseEvent("mouseleave"));
expect(leave).not.toHaveBeenCalled();
});
it("lets mouseleave through when the search input is not focused", () => {
const { host, leave } = mountPinnedSearch();
host.dispatchEvent(new MouseEvent("mouseleave"));
expect(leave).toHaveBeenCalledTimes(1);
});
it("replays the missed mouseleave when the search input loses focus off the tooltip", () => {
const { host, input, leave } = mountPinnedSearch();
input.focus();
host.dispatchEvent(new MouseEvent("mouseleave"));
input.blur();
expect(leave).toHaveBeenCalledTimes(1);
});
it("stops pinning after destroy", () => {
const { controller, host, input, leave } = mountPinnedSearch();
input.focus();
controller.destroy();
host.dispatchEvent(new MouseEvent("mouseleave"));
expect(leave).toHaveBeenCalledTimes(1);
});
});
});
@@ -118,6 +118,12 @@ export interface HoverSearchController {
status: HTMLElement;
/** Focus the search input. */
focus(): void;
/**
* Keep the hover tooltip open while the search input has focus. CodeMirror
* closes hover tooltips on `mouseleave`, which Windows IME candidate windows
* trigger when they pop up over the pointer mid-composition.
*/
pinTooltip(host: HTMLElement): void;
/** Remove listeners; safe to call once when the tooltip is destroyed. */
destroy(): void;
}
@@ -310,14 +316,32 @@ export function createHoverSearch(options: HoverSearchOptions): HoverSearchContr
input.addEventListener("pointerdown", stopPointer);
input.addEventListener("mousedown", stopPointer);
let pinnedHost: HTMLElement | null = null;
const keepPinnedTooltipOpen = (event: Event) => {
if (event.target === pinnedHost && document.activeElement === input) event.stopImmediatePropagation();
};
const releasePinnedTooltip = () => {
// Hand the leave CodeMirror missed back to it once typing is over.
if (pinnedHost && !pinnedHost.matches(":hover")) pinnedHost.dispatchEvent(new MouseEvent("mouseleave"));
};
return {
element,
status,
focus() {
input.focus();
},
pinTooltip(host) {
pinnedHost = host;
// Capture on the document runs before CodeMirror's listener on the host.
document.addEventListener("mouseleave", keepPinnedTooltipOpen, true);
input.addEventListener("blur", releasePinnedTooltip);
},
destroy() {
cancelPending();
document.removeEventListener("mouseleave", keepPinnedTooltipOpen, true);
input.removeEventListener("blur", releasePinnedTooltip);
pinnedHost = null;
input.removeEventListener("input", scheduleSearch);
input.removeEventListener("keydown", stopKeyboard);
input.removeEventListener("keyup", stopKeyboard);