aa90db
fix: autofill selector reopen + submit-button icon (v1.5.2)
- time
- 2026-04-23 21:59 UTC · 6 months ago
- machine
- koh2
- parent
- 43d2fd
↓ peach-browser-aa90db.tar.gzatlas at this save
files changed6 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/content/detect.ts+11 −0src/content/index.ts+13 −1src/content/inject.ts+16 −8manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "1.5.1",4+ "version": "1.5.2",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.5.1",4+ "version": "1.5.2",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.5.1",3+ "version": "1.5.2",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/content/detect.ts+11 −0
@@ -17,6 +17,15 @@1717 "datetime-local", "month", "week",1818 ]);1919 20+// Some sites render submit buttons as <input> with no type attribute (defaults to "text")21+// and a value like "Login" or "Submit". Catch those before injecting icons.22+function looksLikeSubmitButton(input: HTMLInputElement): boolean {23+ if (input.type === "password") return false;24+ const val = (input.value || input.getAttribute("value") || "").toLowerCase().trim();25+ if (!val) return false;26+ return ["submit", "login", "sign in", "log in", "continue", "next", "go"].some(w => val === w || val.startsWith(w + " "));27+}28+2029 function findLabelText(field: HTMLInputElement): string {2130 const ariaLabel = field.getAttribute("aria-label") ?? "";2231 if (ariaLabel) return ariaLabel.toLowerCase();@@ -95,6 +104,8 @@95104 const type = el.type.toLowerCase();96105 // Never treat buttons, checkboxes, hidden fields, etc. as autofill targets97106 if (SKIP_INPUT_TYPES.has(type)) continue;107+ // Catch <input> with no type attribute whose value text identifies it as a submit button108+ if (looksLikeSubmitButton(el)) continue;98109 const ac = el.autocomplete;99110 const name = (el.name + el.id + el.placeholder).toLowerCase();100111 src/content/index.ts+13 −1
@@ -75,6 +75,9 @@7575 let currentAnchorField: HTMLInputElement | null = null;7676 // Prevents two concurrent handleFieldInteraction calls for the same field7777 const pendingInteractions = new WeakSet<HTMLInputElement>();78+// Tracks the last field the user explicitly dismissed (Escape / click-outside / Tab).79+// Suppresses spurious reopens until focus moves to a different field.80+let lastDismissedField: HTMLInputElement | null = null;7881 7982 function debounce<T extends (...args: unknown[]) => void>(8083 fn: T,@@ -273,6 +276,7 @@273276 {274277 domain,275278 searchableEntries,279+ onDismiss: () => { lastDismissedField = anchorField; },276280 }277281 );278282 }@@ -298,6 +302,7 @@298302 if (username) setNativeValue(usernameEl, username);299303 if (password && passwordEl) setNativeValue(passwordEl, password);300304 },305+ onDismiss: () => { lastDismissedField = usernameEl; },301306 }302307 );303308 }@@ -311,6 +316,12 @@311316 // If a selector is already open for this exact field, don't flicker it.312317 if (currentDismiss && currentAnchorField === field) return;313318 319+ // If focus moved to a different field, the previous dismiss lock no longer applies.320+ if (field !== lastDismissedField) lastDismissedField = null;321+ // The user explicitly dismissed this field's selector — don't reopen it until322+ // focus has genuinely moved away and come back.323+ if (field === lastDismissedField) return;324+314325 // Prevent two concurrent calls for the same field (e.g. focusin + icon click315326 // firing simultaneously — the second call would dismiss the first's selector).316327 if (pendingInteractions.has(field)) return;@@ -360,7 +371,7 @@360371 field,361372 [],362373 () => {},363- { domain, locked: true }374+ { domain, locked: true, onDismiss: () => { lastDismissedField = field; } }364375 );365376 return;366377 }@@ -406,6 +417,7 @@406417 const cleanup = injectFieldIcon(el, () => {407418 currentDismiss?.();408419 currentDismiss = null;420+ lastDismissedField = null; // icon click is always an explicit user open — clear the dismiss lock409421 el.focus({ preventScroll: true });410422 void handleFieldInteraction(el);411423 });src/content/inject.ts+16 −8
@@ -36,6 +36,12 @@3636 * entries and re-open the selector with real data.3737 */3838 onUnlocked?: () => void;39+ /**40+ * Called when the user explicitly dismisses the selector (Escape, Tab, or41+ * click outside) — NOT when an entry is selected or on auto-close.42+ * Used by the caller to suppress spurious reopens on the same field.43+ */44+ onDismiss?: () => void;3945 }4046 4147 function getDomainFromUrl(url: string): string {@@ -861,17 +867,17 @@861867 862868 function handleKeyDown(e: KeyboardEvent) {863869 if (options.locked) {864- if (e.key === "Escape") { e.preventDefault(); dismiss(); }870+ if (e.key === "Escape") { e.preventDefault(); dismiss(true); }865871 return;866872 }867873 if (currentMode === "create" || currentMode === "success") {868- if (e.key === "Escape") { e.preventDefault(); dismiss(); }874+ if (e.key === "Escape") { e.preventDefault(); dismiss(true); }869875 return;870876 }871877 const allEntryEls = container.querySelectorAll(".entry:not(.empty-state)");872878 const count = allEntryEls.length;873879 if (document.activeElement === searchInput) {874- if (e.key === "Escape" || e.key === "Tab") { e.preventDefault(); dismiss(); return; }880+ if (e.key === "Escape" || e.key === "Tab") { e.preventDefault(); dismiss(true); return; }875881 if (count === 0) return;876882 if (!["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) return;877883 }@@ -891,7 +897,7 @@891897 }892898 break;893899 }894- case "Escape": case "Tab": e.preventDefault(); dismiss(); break;900+ case "Escape": case "Tab": e.preventDefault(); dismiss(true); break;895901 }896902 }897903 @@ -912,11 +918,12 @@912918 }913919 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);914920 915- function dismiss() {921+ function dismiss(userAction = false) {916922 stopObserving();917923 host.remove();918- document.removeEventListener("click", outsideClick);924+ document.removeEventListener("click", outsideClick, { capture: true });919925 document.removeEventListener("keydown", handleKeyDown);926+ if (userAction) options.onDismiss?.();920927 }921928 922929 function outsideClick(ev: MouseEvent) {@@ -924,9 +931,10 @@924931 // Never dismiss when clicking the anchor field itself — that click is925932 // what triggered the selector to open, and the user may click the field926933 // again to re-focus it without intending to close the selector.927- if (!path.includes(host) && !path.includes(anchorEl)) dismiss();934+ if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);928935 }929- setTimeout(() => document.addEventListener("click", outsideClick), 0);936+ // capture: true so pages that call stopPropagation() on click don't block our dismiss handler937+ setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);930938 931939 return dismiss;932940 }