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
↓ peach-browser-3a5e8545.tar.gzatlas at this save
files changed4 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/content/inject.ts+43 −19manifest.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": {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",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": {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() {