e84a9fee3cfa

capture-phase keydown intercept instead of focusout loop — eliminates tab hang (v2.0.0)

time
2026-04-25 04:54 UTC · 6 months ago
machine
koh2
parent
460535802d51
4 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/content/inject.ts+33 −17
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "1.9.9",4+  "version": "2.0.0",55   "description": "Zero-knowledge password manager",66   "browser_specific_settings": {77     "gecko": {

view file

manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "1.9.9",4+  "version": "2.0.0",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "1.9.9",3+  "version": "2.0.0",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/content/inject.ts+33 −17
@@ -892,10 +892,6 @@892892       }893893     }894894 895-    // Stop all keyboard events from bubbling to the page. Without this,896-    // events propagate via the composed Shadow DOM path and page focus-trap897-    // handlers (e.g. Etsy's modal) can intercept keystrokes typed into the898-    // Peach unlock input and route them to the page's own password field.899895     pwInp.addEventListener("keydown", (e) => {900896       e.stopPropagation();901897       if (e.key === "Enter") void tryUnlock();@@ -907,22 +903,36 @@907903     wrap.append(head, pwInp, errEl, unlockBtn);908904     container.appendChild(wrap);909905 910-    // Focus synchronously first, then again after layout — page focus traps911-    // (e.g. Etsy's modal) often run in the next frame and steal focus back.912-    // Re-assert focus whenever the input loses it to something outside our913-    // shadow root, as long as the overlay is still in the DOM.914-    const refocus = (e?: FocusEvent) => {915-      if (!host.isConnected) return;916-      if (e) {917-        const related = e.relatedTarget;918-        // Focus moving within the shadow root (e.g. to the unlock button) is fine.919-        if (related instanceof Node && shadow.contains(related)) return;906+    // On pages with aggressive focus traps (Etsy modal, Google sign-in, etc.)907+    // the page reclaims focus from our Shadow DOM input before the user can908+    // type. Fighting over focus with a focusout→refocus loop hangs the tab.909+    // Instead: intercept keystrokes at the document capture phase — this runs910+    // before any page handler regardless of which element technically has911+    // OS focus — and route them into our input.912+    const captureKeydown = (e: KeyboardEvent) => {913+      if (!host.isConnected) {914+        document.removeEventListener("keydown", captureKeydown, true);915+        return;920916       }921-      pwInp.focus({ preventScroll: true });917+      // Let browser shortcuts (Ctrl/Cmd/Alt combos) and Tab pass through.918+      if (e.ctrlKey || e.metaKey || e.altKey || e.key === "Tab") return;919+      // Intercept before page handlers and give our input focus.920+      e.stopImmediatePropagation();921+      if (e.key === "Escape") { dismiss(true); return; }922+      if (e.key === "Enter") { void tryUnlock(); return; }923+      // For all other printable/editing keys, ensure our input has focus so924+      // the browser delivers the keystroke there naturally.925+      if (document.activeElement !== host) {926+        pwInp.focus({ preventScroll: true });927+      }922928     };929+    captureKeydownRef = captureKeydown;930+    document.addEventListener("keydown", captureKeydown, true);931+932+    // Best-effort focus on open — may be overridden by page focus trap,933+    // but the capture listener above handles typing regardless.923934     pwInp.focus({ preventScroll: true });924-    requestAnimationFrame(() => refocus());925-    pwInp.addEventListener("focusout", refocus);935+    requestAnimationFrame(() => { if (host.isConnected) pwInp.focus({ preventScroll: true }); });926936   }927937 928938   function handleKeyDown(e: KeyboardEvent) {@@ -978,11 +988,17 @@978988   }979989   const stopObserving = observeAnchorPosition(anchorEl, updatePosition);980990 991+  let captureKeydownRef: ((e: KeyboardEvent) => void) | null = null;992+981993   function dismiss(userAction = false) {982994     stopObserving();983995     host.remove();984996     document.removeEventListener("click", outsideClick, { capture: true });985997     document.removeEventListener("keydown", handleKeyDown);998+    if (captureKeydownRef) {999+      document.removeEventListener("keydown", captureKeydownRef, true);1000+      captureKeydownRef = null;1001+    }9861002     if (userAction) options.onDismiss?.();9871003   }9881004 

view file