From ee0272a65f5c57b6250ca243645cfab807d45801 Mon Sep 17 00:00:00 2001 From: Artem Ovchinnikov <53350796+Akilaydin@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:39:53 +0300 Subject: [PATCH] fix(syncguard): prevent mobile input behind workspace overlay --- .../syncGuard/SyncGuardOverlay.ts | 16 +++++++++++++ .../syncGuard/SyncGuardOverlay.unit.spec.ts | 24 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/serviceFeatures/syncGuard/SyncGuardOverlay.ts b/src/serviceFeatures/syncGuard/SyncGuardOverlay.ts index d569588a..c7cd090d 100644 --- a/src/serviceFeatures/syncGuard/SyncGuardOverlay.ts +++ b/src/serviceFeatures/syncGuard/SyncGuardOverlay.ts @@ -57,6 +57,10 @@ export class SyncGuardOverlay { const modalFocused = focused instanceof Element && focused.closest(".modal-container") !== null; this.previousFocus = !modalFocused && focused instanceof HTMLElement ? focused : undefined; 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. if (!modalFocused) bypass.focus(); } @@ -83,12 +87,24 @@ export class SyncGuardOverlay { 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 { if (!this.root) return; const doc = this.workspace.ownerDocument; const focused = doc.activeElement; const ownsFocus = focused instanceof Node && this.root.contains(focused); 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 = undefined; this.stage = undefined; diff --git a/src/serviceFeatures/syncGuard/SyncGuardOverlay.unit.spec.ts b/src/serviceFeatures/syncGuard/SyncGuardOverlay.unit.spec.ts index 46de5ff1..e6743285 100644 --- a/src/serviceFeatures/syncGuard/SyncGuardOverlay.unit.spec.ts +++ b/src/serviceFeatures/syncGuard/SyncGuardOverlay.unit.spec.ts @@ -188,4 +188,28 @@ describe("SyncGuardOverlay", () => { expect(doc.activeElement).toBe(button); 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); + }); });