fix: force export background on the live react-flow element

The html-to-image style-option override did not reliably beat the
react-flow root's colorMode background, so white exports stayed black.
Set the background colour directly on the live element for the duration
of the capture (then restore it), which html-to-image reads when it
clones computed styles.

Refs #165
This commit is contained in:
Pouzor
2026-06-29 01:13:55 +02:00
parent 46185d187f
commit f0af367c34
2 changed files with 47 additions and 25 deletions
+20 -10
View File
@@ -68,12 +68,18 @@ describe('exportToPng', () => {
expect(mockToPng).toHaveBeenCalledWith(el, expect.objectContaining({ backgroundColor: '#ffffff' }))
})
it('overrides the react-flow root background via inline style so white is visible', async () => {
it('forces the element background to white during capture, then restores it', async () => {
el.style.backgroundColor = 'rgb(13, 17, 23)'
let bgDuringCapture = ''
mockToPng.mockImplementation(() => {
bgDuringCapture = el.style.backgroundColor
return Promise.resolve('data:image/png;base64,abc')
})
await exportToPng(el, 'standard', 'white')
expect(mockToPng).toHaveBeenCalledWith(
el,
expect.objectContaining({ style: expect.objectContaining({ backgroundColor: '#ffffff' }) }),
)
// the live element is painted white only for the duration of the capture
expect(bgDuringCapture).toBe('rgb(255, 255, 255)')
// and is restored afterwards so the on-screen canvas is untouched
expect(el.style.backgroundColor).toBe('rgb(13, 17, 23)')
})
it('attaches the download anchor to the DOM so Firefox triggers the download', async () => {
@@ -117,12 +123,16 @@ describe('exportToSvg', () => {
expect(mockToSvg).toHaveBeenCalledWith(el, expect.objectContaining({ backgroundColor: '#ffffff' }))
})
it('overrides the react-flow root background via inline style so white is visible', async () => {
it('forces the element background to white during capture, then restores it', async () => {
el.style.backgroundColor = 'rgb(13, 17, 23)'
let bgDuringCapture = ''
mockToSvg.mockImplementation(() => {
bgDuringCapture = el.style.backgroundColor
return Promise.resolve('data:image/svg+xml;base64,abc')
})
await exportToSvg(el, 'white')
expect(mockToSvg).toHaveBeenCalledWith(
el,
expect.objectContaining({ style: expect.objectContaining({ backgroundColor: '#ffffff' }) }),
)
expect(bgDuringCapture).toBe('rgb(255, 255, 255)')
expect(el.style.backgroundColor).toBe('rgb(13, 17, 23)')
})
it('triggers a download', async () => {
+27 -15
View File
@@ -19,6 +19,24 @@ function backgroundColor(background: ExportBackground): string {
return (EXPORT_BACKGROUND_OPTIONS.find((o) => o.value === background) ?? EXPORT_BACKGROUND_OPTIONS[0]).color
}
// The `.react-flow` element paints its own opaque background (colorMode dark),
// and html-to-image's `backgroundColor` option only shows through transparent
// areas. Force the chosen colour directly on the live element for the duration
// of the capture, then restore whatever was there before.
async function withBackground<T>(
element: HTMLElement,
color: string,
capture: () => Promise<T>,
): Promise<T> {
const previous = element.style.backgroundColor
element.style.backgroundColor = color
try {
return await capture()
} finally {
element.style.backgroundColor = previous
}
}
export async function exportToPng(
element: HTMLElement,
quality: ExportQuality = 'high',
@@ -26,16 +44,12 @@ export async function exportToPng(
): Promise<void> {
const option = EXPORT_QUALITY_OPTIONS.find((o) => o.value === quality) ?? EXPORT_QUALITY_OPTIONS[1]
const color = backgroundColor(background)
const dataUrl = await toPng(element, {
backgroundColor: color,
pixelRatio: option.pixelRatio,
style: {
'--xy-controls-display': 'none',
// The .react-flow root paints its own opaque background (colorMode),
// which would hide the canvas backgroundColor above — override it inline.
const dataUrl = await withBackground(element, color, () =>
toPng(element, {
backgroundColor: color,
} as Partial<CSSStyleDeclaration>,
})
pixelRatio: option.pixelRatio,
}),
)
triggerDownload(dataUrl, 'homelable-canvas.png')
}
@@ -45,13 +59,11 @@ export async function exportToSvg(
background: ExportBackground = 'dark',
): Promise<void> {
const color = backgroundColor(background)
const dataUrl = await toSvg(element, {
backgroundColor: color,
style: {
'--xy-controls-display': 'none',
const dataUrl = await withBackground(element, color, () =>
toSvg(element, {
backgroundColor: color,
} as Partial<CSSStyleDeclaration>,
})
}),
)
triggerDownload(dataUrl, 'homelable-canvas.svg')
}