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:
@@ -68,12 +68,18 @@ describe('exportToPng', () => {
|
|||||||
expect(mockToPng).toHaveBeenCalledWith(el, expect.objectContaining({ backgroundColor: '#ffffff' }))
|
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')
|
await exportToPng(el, 'standard', 'white')
|
||||||
expect(mockToPng).toHaveBeenCalledWith(
|
// the live element is painted white only for the duration of the capture
|
||||||
el,
|
expect(bgDuringCapture).toBe('rgb(255, 255, 255)')
|
||||||
expect.objectContaining({ style: expect.objectContaining({ backgroundColor: '#ffffff' }) }),
|
// 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 () => {
|
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' }))
|
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')
|
await exportToSvg(el, 'white')
|
||||||
expect(mockToSvg).toHaveBeenCalledWith(
|
expect(bgDuringCapture).toBe('rgb(255, 255, 255)')
|
||||||
el,
|
expect(el.style.backgroundColor).toBe('rgb(13, 17, 23)')
|
||||||
expect.objectContaining({ style: expect.objectContaining({ backgroundColor: '#ffffff' }) }),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('triggers a download', async () => {
|
it('triggers a download', async () => {
|
||||||
|
|||||||
@@ -19,6 +19,24 @@ function backgroundColor(background: ExportBackground): string {
|
|||||||
return (EXPORT_BACKGROUND_OPTIONS.find((o) => o.value === background) ?? EXPORT_BACKGROUND_OPTIONS[0]).color
|
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(
|
export async function exportToPng(
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
quality: ExportQuality = 'high',
|
quality: ExportQuality = 'high',
|
||||||
@@ -26,16 +44,12 @@ export async function exportToPng(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const option = EXPORT_QUALITY_OPTIONS.find((o) => o.value === quality) ?? EXPORT_QUALITY_OPTIONS[1]
|
const option = EXPORT_QUALITY_OPTIONS.find((o) => o.value === quality) ?? EXPORT_QUALITY_OPTIONS[1]
|
||||||
const color = backgroundColor(background)
|
const color = backgroundColor(background)
|
||||||
const dataUrl = await toPng(element, {
|
const dataUrl = await withBackground(element, color, () =>
|
||||||
backgroundColor: color,
|
toPng(element, {
|
||||||
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.
|
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
} as Partial<CSSStyleDeclaration>,
|
pixelRatio: option.pixelRatio,
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
triggerDownload(dataUrl, 'homelable-canvas.png')
|
triggerDownload(dataUrl, 'homelable-canvas.png')
|
||||||
}
|
}
|
||||||
@@ -45,13 +59,11 @@ export async function exportToSvg(
|
|||||||
background: ExportBackground = 'dark',
|
background: ExportBackground = 'dark',
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const color = backgroundColor(background)
|
const color = backgroundColor(background)
|
||||||
const dataUrl = await toSvg(element, {
|
const dataUrl = await withBackground(element, color, () =>
|
||||||
backgroundColor: color,
|
toSvg(element, {
|
||||||
style: {
|
|
||||||
'--xy-controls-display': 'none',
|
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
} as Partial<CSSStyleDeclaration>,
|
}),
|
||||||
})
|
)
|
||||||
|
|
||||||
triggerDownload(dataUrl, 'homelable-canvas.svg')
|
triggerDownload(dataUrl, 'homelable-canvas.svg')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user