3a5e85455435

replace blur-based list dismiss with mousedown watcher — focus traps can't close the list (v3.6.135)

time
2026-05-30 00:56 UTC · 5 months ago
machine
koh2
parent
062f87c4a7e5
4 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/content/inject.ts+43 −19
manifest.firefox.json+1 −1
@@ -2,7 +2,7 @@22   "_comment_audio_capture": "Firefox does not honor the Chrome 'audioCapture' optional_permission — getUserMedia in WebExtensions is gated solely by the runtime permission prompt. Microphone access for chirp pairing works from the popup under the existing '<all_urls>' host permission; no manifest entry is required or accepted here.",33   "manifest_version": 3,44   "name": "Peach Password Manager",5-  "version": "3.6.134",5+  "version": "3.6.135",66   "description": "Zero-knowledge password manager",77   "browser_specific_settings": {88     "gecko": {

view file

manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "3.6.134",4+  "version": "3.6.135",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": "3.6.134",3+  "version": "3.6.135",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/content/inject.ts+43 −19
@@ -1784,15 +1784,30 @@17841784     const rows = rowsWrapper.appendChild(document.createElement("div"));17851785     rows.className = "identity-rows-inner";17861786 1787+    // Hides the list and removes the document-level click watcher.1788+    function hideList() {1789+      list.style.display = "none";1790+      document.removeEventListener("mousedown", onDocMousedown, { capture: true });1791+    }1792+1793+    // Dismiss list on any mousedown that originates outside the popup's shadow1794+    // DOM. This is the ONLY close trigger for the list — blur is intentionally1795+    // NOT used, because modal focus traps fire blur without a mousedown (they1796+    // call element.focus() programmatically in response to focusin/focusout),1797+    // which caused the list to vanish before the user could pick from it.1798+    function onDocMousedown(e: MouseEvent) {1799+      if (e.composedPath().includes(host)) return;1800+      hideList();1801+    }1802+17871803     function pick(value: string) {17881804       inp.value = value;1789-      list.style.display = "none";1805+      hideList();17901806       suppressNextFocus = true;17911807       inp.focus();1792-      // If a page focus trap intercepts the focus() call above and redirects1793-      // to anchorEl before inp ever fires a focus event, suppressNextFocus1794-      // would strand the user (the list won't show on their next click).1795-      // Clear it after a short window so subsequent clicks still work.1808+      // If a focus trap intercepts inp.focus() and redirects to anchorEl before1809+      // inp ever fires its focus event, suppressNextFocus would strand the user.1810+      // Clear after a short window so subsequent clicks still work.17961811       setTimeout(() => { suppressNextFocus = false; }, 120);17971812     }17981813 @@ -1800,7 +1815,7 @@18001815       const q = inp.value.toLowerCase();18011816       const filtered = typed.filter(s => !q || s.value.toLowerCase().includes(q)).slice(0, 8);18021817       rows.innerHTML = "";1803-      if (!filtered.length) { list.style.display = "none"; return; }1818+      if (!filtered.length) { hideList(); return; }18041819       for (const s of filtered) {18051820         const row = rows.appendChild(document.createElement("button"));18061821         row.type = "button"; row.className = "identity-suggest-row";@@ -1823,18 +1838,25 @@18231838             pick(s.value);18241839           } else if (e.key === "Escape") {18251840             e.preventDefault();1826-            list.style.display = "none";1841+            hideList();18271842             suppressNextFocus = true;18281843             inp.focus();18291844           }18301845         });1846+        // Row blur only fires during keyboard navigation (arrow keys / Tab).1847+        // Focus traps don't produce row blur events since rows aren't focused1848+        // when the trap fires; this handler is therefore trap-safe.18311849         row.addEventListener("blur", (e) => {18321850           const rt = (e as FocusEvent).relatedTarget as Node | null;18331851           if (rt && (rt === inp || list.contains(rt))) return;1834-          setTimeout(() => { list.style.display = "none"; }, 150);1852+          setTimeout(() => { hideList(); }, 150);18351853         });18361854       }18371855       list.style.display = "";1856+      // Register the close-on-outside-click watcher each time the list shows.1857+      // addEventListener deduplicates identical (fn, options) pairs, so1858+      // re-rendering on input events does not stack up listeners.1859+      document.addEventListener("mousedown", onDocMousedown, { capture: true });18381860     }18391861 18401862     inp.addEventListener("focus", () => {@@ -1849,19 +1871,21 @@18491871         (rows.firstElementChild as HTMLElement | null)?.focus();18501872       } else if (e.key === "Escape") {18511873         e.preventDefault();1852-        list.style.display = "none";1874+        hideList();1875+      } else if (e.key === "Tab") {1876+        // Tab moves focus away intentionally — close the list.1877+        hideList();18531878       }18541879     });1855-    inp.addEventListener("blur", (e) => {1856-      const rt = (e as FocusEvent).relatedTarget as Node | null;1857-      if (rt && list.contains(rt)) return;1858-      // A focus trap on the page (e.g. a modal's aria-modal enforcer) fires when1859-      // inp gains focus and immediately moves focus back to anchorEl. Don't1860-      // close the list for this case — the user never intentionally left the field.1861-      // The list will close naturally when the user picks a row or clicks outside.1862-      if (rt === anchorEl) return;1863-      setTimeout(() => { list.style.display = "none"; }, 150);1864-    });1880+    // No blur handler on inp. Blur fires for ANY focus movement including1881+    // programmatic focus() calls from modal focus traps, which caused the list1882+    // to close before the user could interact. The document mousedown listener1883+    // (registered in render()) handles intentional click-outside dismissal,1884+    // and the keydown handler above covers keyboard Tab/Escape.1885+1886+    // Ensure the watcher is removed if the main popup is dismissed while the1887+    // list is visible.1888+    cleanupHooks.push(hideList);18651889   }18661890 18671891   async function showCreateForm() {

view file