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