fix(syncguard): prevent mobile input behind workspace overlay

This commit is contained in:
Artem Ovchinnikov committed 2026-10-21 21:39:53 +03:00
1 parent d527fa9764
commit ee0272a65f
2 files changed
+40

No files matched your search

@@ -57,6 +57,10 @@ export class SyncGuardOverlay {
const modalFocused = focused instanceof Element && focused.closest(".modal-container") !== null; const modalFocused = focused instanceof Element && focused.closest(".modal-container") !== null;
this.previousFocus = !modalFocused && focused instanceof HTMLElement ? focused : undefined; this.previousFocus = !modalFocused && focused instanceof HTMLElement ? focused : undefined;
doc.addEventListener("keydown", this.blockKeyboard, true); doc.addEventListener("keydown", this.blockKeyboard, true);
// Mobile input methods, dictation, and paste may mutate an editor without a keydown.
doc.addEventListener("beforeinput", this.blockEditorInput, true);
doc.addEventListener("paste", this.blockEditorInput, true);
doc.addEventListener("drop", this.blockEditorInput, true);
// Do not steal focus from an Obsidian conflict or setup dialogue which was already open. // Do not steal focus from an Obsidian conflict or setup dialogue which was already open.
if (!modalFocused) bypass.focus(); if (!modalFocused) bypass.focus();
} }
@@ -83,12 +87,24 @@ export class SyncGuardOverlay {
if (event.key === "Tab") this.bypassButton?.focus(); if (event.key === "Tab") this.bypassButton?.focus();
}; };
private readonly blockEditorInput = (event: Event): void => {
if (!this.root) return;
const target = event.target;
if (target instanceof Element && target.closest(".modal-container")) return;
if (target instanceof Node && this.root.contains(target)) return;
event.preventDefault();
event.stopImmediatePropagation();
};
close(): void { close(): void {
if (!this.root) return; if (!this.root) return;
const doc = this.workspace.ownerDocument; const doc = this.workspace.ownerDocument;
const focused = doc.activeElement; const focused = doc.activeElement;
const ownsFocus = focused instanceof Node && this.root.contains(focused); const ownsFocus = focused instanceof Node && this.root.contains(focused);
doc.removeEventListener("keydown", this.blockKeyboard, true); doc.removeEventListener("keydown", this.blockKeyboard, true);
doc.removeEventListener("beforeinput", this.blockEditorInput, true);
doc.removeEventListener("paste", this.blockEditorInput, true);
doc.removeEventListener("drop", this.blockEditorInput, true);
this.root.remove(); this.root.remove();
this.root = undefined; this.root = undefined;
this.stage = undefined; this.stage = undefined;
@@ -188,4 +188,28 @@ describe("SyncGuardOverlay", () => {
expect(doc.activeElement).toBe(button); expect(doc.activeElement).toBe(button);
overlay.dispose(); overlay.dispose();
}); });
it("blocks mobile beforeinput and paste into hidden editors but permits input in Obsidian dialogues", () => {
const { overlay, doc, editor } = setup();
overlay.update({ phase: "transferring", receivedFiles: 30 });
const modal = new FakeElement(doc, "div");
modal.className = "modal-container";
const input = modal.createEl("input");
for (const eventType of ["beforeinput", "paste", "drop"]) {
const preventDefault = vi.fn();
const stopImmediatePropagation = vi.fn();
doc.listeners.get(eventType)?.({
target: editor, preventDefault, stopImmediatePropagation,
} as unknown as KeyboardEvent);
expect(preventDefault).toHaveBeenCalledOnce();
expect(stopImmediatePropagation).toHaveBeenCalledOnce();
doc.listeners.get(eventType)?.({
target: input, preventDefault, stopImmediatePropagation,
} as unknown as KeyboardEvent);
expect(preventDefault).toHaveBeenCalledOnce();
}
overlay.dispose();
expect(doc.listeners.size).toBe(0);
});
}); });