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
↓ peach-browser-e84a9fee.tar.gzatlas at this save
files changed4 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/content/inject.ts+33 −17manifest.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": {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",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": {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