a45e13531957

fix: inline popup mounts at document root for non-modal forms + compensates fixed containing-block ancestors (backdrop-filter/transform); renders on-field instead of clipped off-screen (v3.6.149)

time
2026-06-03 14:07 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
inline popup fixes
parent
5233be9a5934
4 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/content/inject.ts+53 −17
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.148",5+  "version": "3.6.149",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.148",4+  "version": "3.6.149",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.148",3+  "version": "3.6.149",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/content/inject.ts+53 −17
@@ -1310,19 +1310,19 @@13101310 ): () => void {13111311   const host = document.createElement("div");13121312   // When anchorEl is inside a focus-trapping modal the modal's JS focus trap fires1313-  // when Peach's internal inputs gain focus (they are outside the modal's DOM subtree),1314-  // immediately blurring them and closing the identity suggest list.1315-  // Fix: detect the containing form or modal and append the host INSIDE it so focus1316-  // traps see Peach as within the modal. Use position:fixed so the popup positions1317-  // correctly relative to the viewport regardless of the container's CSS context —1318-  // overflow:hidden and most transforms do not affect fixed children.1313+  // when Peach's internal inputs gain focus, immediately blurring them and closing1314+  // the suggest list. Mounting the host INSIDE the modal makes the trap treat Peach1315+  // as in-bounds (v3.6.131).13191316   //1320-  // Detection priority:1321-  //   1. <form> ancestor — always inside any focus-trap container wrapping the form1322-  //   2. ARIA modal (dialog / aria-modal / role=dialog)1323-  //   3. position:fixed ancestor — catches modals that omit ARIA attributes1317+  // We deliberately do NOT treat every <form> as a modal (the v3.6.132 broadening).1318+  // A plain login page whose card uses transform/filter/backdrop-filter establishes1319+  // a containing block for position:fixed, so a host mounted inside it lands offset1320+  // and is clipped by the card's overflow. juniesdream.com/admin/login is the1321+  // canonical case: section.admin-login has backdrop-filter:blur(14px), which pushed1322+  // the popup ~400px off the field and clipped it behind overflow:clip — the user1323+  // saw nothing. Real modals are detected by ARIA or a fixed-position overlay;1324+  // everything else mounts at the document root, where fixed == viewport.13241325   const modalAncestor = (1325-    anchorEl.closest<HTMLElement>('form') ??13261326     anchorEl.closest<HTMLElement>('dialog, [aria-modal="true"], [role="dialog"]') ??13271327     (() => {13281328       let cur: HTMLElement | null = anchorEl.parentElement;@@ -1333,7 +1333,7 @@13331333       return null;13341334     })()13351335   );1336-  host.style.cssText = `position: ${modalAncestor ? "fixed" : "absolute"}; z-index: 2147483647;`;1336+  host.style.cssText = `position: fixed; z-index: 2147483647;`;13371337   host.style.setProperty("--peach-accent", accentHex);13381338   host.style.setProperty("--peach-accent-dim", withAlpha(accentHex, "28"));13391339   host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));@@ -1344,9 +1344,10 @@13441344   style.textContent = SELECTOR_STYLES;13451345 13461346   const rect = anchorEl.getBoundingClientRect();1347-  // Fixed positioning uses viewport-relative coords; absolute uses document-relative.1348-  host.style.top = `${rect.bottom + (modalAncestor ? 0 : window.scrollY) + 5}px`;1349-  host.style.left = `${rect.left + (modalAncestor ? 0 : window.scrollX)}px`;1347+  // Viewport-relative placeholder; updatePosition() re-asserts this after mount,1348+  // applying containing-block compensation once host.parentElement is known.1349+  host.style.top = `${rect.bottom + 5}px`;1350+  host.style.left = `${rect.left}px`;13501351   // Spec §1.1: 320px width. The Math.max() floor preserves usability on13511352   // wide single-input fields (e.g. centered email input on signup pages).13521353   // Frozen at open time — updatePosition re-asserts this so page CSS cannot@@ -2876,13 +2877,48 @@28762877 28772878   (modalAncestor ?? document.body).appendChild(host);28782879 2880+  // position:fixed normally resolves to the viewport, but an ancestor with2881+  // transform / filter / backdrop-filter / perspective / contain / will-change2882+  // establishes a containing block, so "fixed" then resolves to that ancestor's2883+  // padding box instead. When the host is mounted inside such an ancestor (a2884+  // modal) we subtract its border-box origin so the popup still lands at the2885+  // field's true viewport position. At the document root there is no such2886+  // ancestor and the offset is zero.2887+  function establishesFixedContainingBlock(s: CSSStyleDeclaration): boolean {2888+    const has = (p: string) => { const v = s.getPropertyValue(p); return v !== "" && v !== "none"; };2889+    return (2890+      has("transform") || has("perspective") || has("translate") ||2891+      has("rotate") || has("scale") || has("filter") ||2892+      has("backdrop-filter") || has("-webkit-backdrop-filter") ||2893+      /\b(transform|perspective|filter)\b/.test(s.getPropertyValue("will-change")) ||2894+      /\b(paint|layout|strict|content)\b/.test(s.getPropertyValue("contain"))2895+    );2896+  }2897+  function containingBlockOffset(): { dx: number; dy: number } {2898+    let el: HTMLElement | null = host.parentElement;2899+    while (el && el !== document.documentElement) {2900+      const s = getComputedStyle(el);2901+      if (establishesFixedContainingBlock(s)) {2902+        const r = el.getBoundingClientRect();2903+        return {2904+          dx: r.left + (parseFloat(s.borderLeftWidth) || 0),2905+          dy: r.top + (parseFloat(s.borderTopWidth) || 0),2906+        };2907+      }2908+      el = el.parentElement;2909+    }2910+    return { dx: 0, dy: 0 };2911+  }2912+28792913   function updatePosition() {28802914     const r = anchorEl.getBoundingClientRect();2881-    host.style.top = `${r.bottom + (modalAncestor ? 0 : window.scrollY) + 5}px`;2882-    host.style.left = `${r.left + (modalAncestor ? 0 : window.scrollX)}px`;2915+    const { dx, dy } = containingBlockOffset();2916+    host.style.top = `${r.bottom - dy + 5}px`;2917+    host.style.left = `${r.left - dx}px`;28832918     host.style.width = `${popupWidth}px`;28842919   }28852920   const stopObserving = observeAnchorPosition(anchorEl, updatePosition);2921+  updatePosition();28862922 28872923   if (options.locked) {28882924     renderLocked();

view file