mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
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:
co-authored by
Claude Opus 4.6
parent
9ffa802c89
commit
05c9776fd8
@@ -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
@@ -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>
|
||||
|
||||
<!-- ═══════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user