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
6 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 −8
manifest.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": {

view file

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",

view file

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": {

view file

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 

view file

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   });

view file

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 }

view file