Skip to content

Commit ba2aa6d

Browse files
committed
fix(prepare): force content-visibility:visible before capture (#281) and fix fixed elements inside scroll wrappers (#364)
1 parent 946ec83 commit ba2aa6d

2 files changed

Lines changed: 63 additions & 1 deletion

File tree

src/core/prepare.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { inlineExternalDefsAndSymbols } from '../modules/svgDefs.js'
1010
import { cache } from '../core/cache.js'
1111
import { freezeSticky } from '../modules/changeCSS.js'
1212
import { resolveBlobUrlsInTree } from '../utils/clone.helpers.js'
13-
import { stabilizeLayout } from '../utils/prepare.helpers.js'
13+
import { stabilizeLayout, forceContentVisibility } from '../utils/prepare.helpers.js'
1414

1515
/**
1616
* Prepares a clone of an element for capture, inlining pseudo-elements and generating CSS classes.
@@ -37,6 +37,9 @@ export async function prepareClone(element, options = {}) {
3737

3838
stabilizeLayout(element)
3939

40+
// #281: Force content-visibility:visible so Safari/Chromium don't skip offscreen elements
41+
const undoContentVisibility = forceContentVisibility(element)
42+
4043
try {
4144
inlineExternalDefsAndSymbols(element)
4245
} catch (e) {
@@ -48,6 +51,8 @@ export async function prepareClone(element, options = {}) {
4851
} catch (e) {
4952
console.warn('deepClone failed:', e)
5053
throw e
54+
} finally {
55+
undoContentVisibility()
5156
}
5257
try {
5358
await inlinePseudoElements(element, clone, sessionCache, options)
@@ -108,6 +113,24 @@ export async function prepareClone(element, options = {}) {
108113
cloneNode.style.overflow = 'hidden'
109114
cloneNode.style.scrollbarWidth = 'none'
110115
cloneNode.style.msOverflowStyle = 'none'
116+
117+
// #364: Before wrapping with translate, adjust fixed/absolute descendants
118+
// so they don't shift when the translate wrapper creates a new containing block.
119+
try {
120+
const positioned = cloneNode.querySelectorAll('*')
121+
for (const child of positioned) {
122+
if (!(child instanceof HTMLElement)) continue
123+
const pos = child.style.position
124+
if (pos === 'fixed' || pos === 'absolute') {
125+
const curTop = parseFloat(child.style.top) || 0
126+
const curLeft = parseFloat(child.style.left) || 0
127+
child.style.top = `${curTop + scrollY}px`
128+
child.style.left = `${curLeft + scrollX}px`
129+
if (pos === 'fixed') child.style.position = 'absolute'
130+
}
131+
}
132+
} catch { /* non-blocking */ }
133+
111134
const inner = document.createElement('div')
112135
inner.style.transform = `translate(${-scrollX}px, ${-scrollY}px)`
113136
inner.style.willChange = 'transform'

src/utils/prepare.helpers.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,42 @@ export function stabilizeLayout(element) {
2121
element.style.border = `${outlineWidth} solid transparent`
2222
}
2323
}
24+
25+
/**
26+
* #281: Force content-visibility to 'visible' on all descendants that use 'auto'.
27+
* Safari (and some Chromium) skip rendering/style computation for content-visibility:auto
28+
* elements outside the viewport, causing blank captures.
29+
* Returns an undo function to restore original values.
30+
* @param {Element} root
31+
* @returns {() => void}
32+
*/
33+
export function forceContentVisibility(root) {
34+
const saved = []
35+
try {
36+
const all = root.querySelectorAll('*')
37+
for (const el of all) {
38+
if (!(el instanceof HTMLElement)) continue
39+
const cv = el.style.contentVisibility || ''
40+
const cs = getComputedStyle(el)
41+
const computed = cs.contentVisibility || cs.getPropertyValue('content-visibility') || ''
42+
if (computed === 'auto' || computed === 'hidden') {
43+
saved.push({ el, original: cv })
44+
el.style.contentVisibility = 'visible'
45+
}
46+
}
47+
// Check root itself
48+
if (root instanceof HTMLElement) {
49+
const cs = getComputedStyle(root)
50+
const computed = cs.contentVisibility || cs.getPropertyValue('content-visibility') || ''
51+
if (computed === 'auto' || computed === 'hidden') {
52+
saved.push({ el: root, original: root.style.contentVisibility || '' })
53+
root.style.contentVisibility = 'visible'
54+
}
55+
}
56+
} catch { /* non-blocking */ }
57+
return () => {
58+
for (const { el, original } of saved) {
59+
try { el.style.contentVisibility = original } catch {}
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)