Fix overlay positioning for fixed elements and improve test fixture

Overlays for elements inside position:fixed contexts now use
position:fixed with viewport-relative coords, so they stay pinned
on scroll. Extracted shared positionOverlay() helper for consistent
coordinate handling across highlight, reposition, and IO callbacks.

Updated fixture with a real fixed footer scenario.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-03-27 23:15:43 -07:00
co-authored by Claude Opus 4.6
parent 9ffa802c89
commit 05c9776fd8
3 changed files with 73 additions and 30 deletions
@@ -1244,14 +1244,36 @@ if (IS_BROWSER) {
TYPE_LABELS[ap.id] = ap.name.toLowerCase().substring(0, 26);
}
function isInFixedContext(el) {
let p = el;
while (p && p !== document.body) {
if (getComputedStyle(p).position === 'fixed') return true;
p = p.parentElement;
}
return false;
}
function positionOverlay(overlay) {
const el = overlay._targetEl;
if (!el) return;
const rect = el.getBoundingClientRect();
if (overlay._isFixed) {
// Viewport-relative coords for fixed targets
overlay.style.top = `${rect.top - 2}px`;
overlay.style.left = `${rect.left - 2}px`;
} else {
// Document-relative coords for normal targets
overlay.style.top = `${rect.top + scrollY - 2}px`;
overlay.style.left = `${rect.left + scrollX - 2}px`;
}
overlay.style.width = `${rect.width + 4}px`;
overlay.style.height = `${rect.height + 4}px`;
}
function repositionOverlays() {
for (const o of overlays) {
if (!o._targetEl || o.classList.contains('impeccable-banner')) continue;
const rect = o._targetEl.getBoundingClientRect();
o.style.top = `${rect.top + scrollY - 2}px`;
o.style.left = `${rect.left + scrollX - 2}px`;
o.style.width = `${rect.width + 4}px`;
o.style.height = `${rect.height + 4}px`;
positionOverlay(o);
}
}
@@ -1272,11 +1294,7 @@ if (IS_BROWSER) {
if (!overlay) continue;
if (entry.isIntersecting) {
overlay.style.display = '';
const rect = entry.target.getBoundingClientRect();
overlay.style.top = `${rect.top + scrollY - 2}px`;
overlay.style.left = `${rect.left + scrollX - 2}px`;
overlay.style.width = `${rect.width + 4}px`;
overlay.style.height = `${rect.height + 4}px`;
positionOverlay(overlay);
} else {
overlay.style.display = 'none';
}
@@ -1284,13 +1302,16 @@ if (IS_BROWSER) {
}, { rootMargin: '99999px' });
const highlight = function(el, findings) {
const fixed = isInFixedContext(el);
const rect = el.getBoundingClientRect();
const outline = document.createElement('div');
outline.className = 'impeccable-overlay';
outline._targetEl = el;
outline._isFixed = fixed;
Object.assign(outline.style, {
position: 'absolute',
top: `${rect.top + scrollY - 2}px`, left: `${rect.left + scrollX - 2}px`,
position: fixed ? 'fixed' : 'absolute',
top: fixed ? `${rect.top - 2}px` : `${rect.top + scrollY - 2}px`,
left: fixed ? `${rect.left - 2}px` : `${rect.left + scrollX - 2}px`,
width: `${rect.width + 4}px`, height: `${rect.height + 4}px`,
zIndex: '99999', boxSizing: 'border-box',
});
@@ -1242,14 +1242,36 @@ if (IS_BROWSER) {
TYPE_LABELS[ap.id] = ap.name.toLowerCase().substring(0, 26);
}
function isInFixedContext(el) {
let p = el;
while (p && p !== document.body) {
if (getComputedStyle(p).position === 'fixed') return true;
p = p.parentElement;
}
return false;
}
function positionOverlay(overlay) {
const el = overlay._targetEl;
if (!el) return;
const rect = el.getBoundingClientRect();
if (overlay._isFixed) {
// Viewport-relative coords for fixed targets
overlay.style.top = `${rect.top - 2}px`;
overlay.style.left = `${rect.left - 2}px`;
} else {
// Document-relative coords for normal targets
overlay.style.top = `${rect.top + scrollY - 2}px`;
overlay.style.left = `${rect.left + scrollX - 2}px`;
}
overlay.style.width = `${rect.width + 4}px`;
overlay.style.height = `${rect.height + 4}px`;
}
function repositionOverlays() {
for (const o of overlays) {
if (!o._targetEl || o.classList.contains('impeccable-banner')) continue;
const rect = o._targetEl.getBoundingClientRect();
o.style.top = `${rect.top + scrollY - 2}px`;
o.style.left = `${rect.left + scrollX - 2}px`;
o.style.width = `${rect.width + 4}px`;
o.style.height = `${rect.height + 4}px`;
positionOverlay(o);
}
}
@@ -1270,11 +1292,7 @@ if (IS_BROWSER) {
if (!overlay) continue;
if (entry.isIntersecting) {
overlay.style.display = '';
const rect = entry.target.getBoundingClientRect();
overlay.style.top = `${rect.top + scrollY - 2}px`;
overlay.style.left = `${rect.left + scrollX - 2}px`;
overlay.style.width = `${rect.width + 4}px`;
overlay.style.height = `${rect.height + 4}px`;
positionOverlay(overlay);
} else {
overlay.style.display = 'none';
}
@@ -1282,13 +1300,16 @@ if (IS_BROWSER) {
}, { rootMargin: '99999px' });
const highlight = function(el, findings) {
const fixed = isInFixedContext(el);
const rect = el.getBoundingClientRect();
const outline = document.createElement('div');
outline.className = 'impeccable-overlay';
outline._targetEl = el;
outline._isFixed = fixed;
Object.assign(outline.style, {
position: 'absolute',
top: `${rect.top + scrollY - 2}px`, left: `${rect.left + scrollX - 2}px`,
position: fixed ? 'fixed' : 'absolute',
top: fixed ? `${rect.top - 2}px` : `${rect.top + scrollY - 2}px`,
left: fixed ? `${rect.left - 2}px` : `${rect.left + scrollX - 2}px`,
width: `${rect.width + 4}px`, height: `${rect.height + 4}px`,
zIndex: '99999', boxSizing: 'border-box',
});
+7 -6
View File
@@ -315,12 +315,13 @@
</div>
<div class="scenario">
<div class="scenario-label">3b. Fixed-like element (within containing block)</div>
<div style="position: relative; height: 80px; background: #f3f4f6; border-radius: 8px;">
<div style="position: fixed; /* won't actually be fixed due to test context */ bottom: 0; left: 0; right: 0; background: #1f2937; color: white; padding: 0.5rem;">
<span class="tiny">Tiny text in a would-be fixed footer.</span>
</div>
</div>
<div class="scenario-label">3b. Fixed footer (stays at bottom of viewport)</div>
<p>The fixed footer at the bottom of the page has tiny text. Its overlay should stay fixed too.</p>
</div>
<!-- Actual fixed footer -- outside scenario container so it's truly fixed -->
<div style="position: fixed; bottom: 0; left: 0; right: 0; background: #1f2937; color: white; padding: 0.5rem 1rem; z-index: 50;">
<span class="tiny">Tiny text in a fixed footer -- overlay should track this on scroll.</span>
</div>
<!-- ═══════════════════════════════════════════