diff --git a/.changeset/png-export-options.md b/.changeset/png-export-options.md
new file mode 100644
index 00000000..2e36052a
--- /dev/null
+++ b/.changeset/png-export-options.md
@@ -0,0 +1,5 @@
+---
+'@embeddable.com/remarkable-pro': minor
+---
+
+Chart menu options can now carry an `options` object, passed to `onClick` as `options`. PNG export supports `options.backgroundColor` to override the default transparent background.
diff --git a/src/components/charts/shared/ChartCard/ChartCardMenuPro/ChartCardMenuPro.test.tsx b/src/components/charts/shared/ChartCard/ChartCardMenuPro/ChartCardMenuPro.test.tsx
index 38cf2d3f..aff0abab 100644
--- a/src/components/charts/shared/ChartCard/ChartCardMenuPro/ChartCardMenuPro.test.tsx
+++ b/src/components/charts/shared/ChartCard/ChartCardMenuPro/ChartCardMenuPro.test.tsx
@@ -120,6 +120,58 @@ describe('ChartCardMenuPro', () => {
vi.useRealTimers();
});
+ it('passes the option options to onClick', async () => {
+ vi.useFakeTimers();
+ const onClickMock = vi.fn();
+
+ render(
+ ,
+ );
+ fireEvent.click(screen.getByText('export.png'));
+ await vi.runAllTimersAsync();
+
+ expect(onClickMock).toHaveBeenCalledWith(
+ expect.objectContaining({ options: { backgroundColor: '#fff' } }),
+ );
+ vi.useRealTimers();
+ });
+
+ it('passes the option options to onClick when using onCustomDownload', async () => {
+ vi.useFakeTimers();
+ const onClickMock = vi.fn();
+ const onCustomDownload = vi.fn((cb) => cb({ title: 'custom' }));
+
+ render(
+ ,
+ );
+ fireEvent.click(screen.getByText('export.png'));
+ await vi.runAllTimersAsync();
+
+ expect(onClickMock).toHaveBeenCalledWith(
+ expect.objectContaining({ title: 'custom', options: { backgroundColor: '#fff' } }),
+ );
+ vi.useRealTimers();
+ });
+
it('calls onCustomDownload instead of onClick directly when provided', async () => {
vi.useFakeTimers();
const onClickMock = vi.fn().mockResolvedValue(undefined);
diff --git a/src/components/charts/shared/ChartCard/ChartCardMenuPro/ChartCardMenuPro.tsx b/src/components/charts/shared/ChartCard/ChartCardMenuPro/ChartCardMenuPro.tsx
index 3dbedd27..ea414906 100644
--- a/src/components/charts/shared/ChartCard/ChartCardMenuPro/ChartCardMenuPro.tsx
+++ b/src/components/charts/shared/ChartCard/ChartCardMenuPro/ChartCardMenuPro.tsx
@@ -50,16 +50,19 @@ export const ChartCardMenuPro: React.FC = (props) => {
};
const handleOptionClick = (option: ChartCardMenuOption) => {
+ const run = (args: ChartCardMenuOptionOnClickProps) =>
+ option.onClick({ ...args, options: option.options });
+
if (option.isInstantAction) {
- option.onClick({ ...props, theme });
+ run({ ...props, theme });
return;
}
setIsLoading(true);
if (props.onCustomDownload) {
- props.onCustomDownload((args) => startAction(() => option.onClick(args)));
+ props.onCustomDownload((args) => startAction(() => run(args)));
return;
}
- startAction(() => option.onClick({ ...props, theme }));
+ startAction(() => run({ ...props, theme }));
};
return (
diff --git a/src/theme/defaults/defaults.ChartCardMenu.constants.ts b/src/theme/defaults/defaults.ChartCardMenu.constants.ts
index 96fc0344..c17eab9e 100644
--- a/src/theme/defaults/defaults.ChartCardMenu.constants.ts
+++ b/src/theme/defaults/defaults.ChartCardMenu.constants.ts
@@ -12,6 +12,8 @@ export type ChartCardMenuOptionOnClickProps = {
dimensionsAndMeasures?: (Dimension | Measure)[];
containerRef?: React.RefObject;
theme: Theme;
+ /** Per-option settings supplied by the theme (see ChartCardMenuOption.options). */
+ options?: Record;
onCustomDownload?: (props: (props: ChartCardMenuOptionOnClickProps) => void) => void;
};
@@ -21,6 +23,8 @@ export type ChartCardMenuOption = {
iconSrc?: string;
/** Instant actions run immediately: no loading state and no onCustomDownload interception. */
isInstantAction?: boolean;
+ /** Settings passed to onClick as `options`. PNG supports `backgroundColor`. */
+ options?: Record;
onClick: (props: ChartCardMenuOptionOnClickProps) => void;
};
diff --git a/src/theme/utils/export.utils.test.ts b/src/theme/utils/export.utils.test.ts
index 5f9b88d8..eeb5e15a 100644
--- a/src/theme/utils/export.utils.test.ts
+++ b/src/theme/utils/export.utils.test.ts
@@ -212,6 +212,32 @@ describe('exportPNG', () => {
expect(domtoimage.toPng).toHaveBeenCalledWith(el, expect.objectContaining({ cacheBust: true }));
});
+ it('does not set a background color by default', async () => {
+ const el = document.createElement('div');
+ (domtoimage.toPng as Mock).mockResolvedValue('data:image/png;base64,abc');
+
+ await exportPNG({ title: 'test', containerRef: { current: el }, theme: mockTheme });
+
+ expect((domtoimage.toPng as Mock).mock.calls[0]![1]).not.toHaveProperty('bgcolor');
+ });
+
+ it('passes options.backgroundColor to domtoimage as bgcolor', async () => {
+ const el = document.createElement('div');
+ (domtoimage.toPng as Mock).mockResolvedValue('data:image/png;base64,abc');
+
+ await exportPNG({
+ title: 'test',
+ containerRef: { current: el },
+ theme: mockTheme,
+ options: { backgroundColor: '#ffffff' },
+ });
+
+ expect(domtoimage.toPng).toHaveBeenCalledWith(
+ el,
+ expect.objectContaining({ bgcolor: '#ffffff' }),
+ );
+ });
+
it('sets the correct download filename', async () => {
const el = document.createElement('div');
(domtoimage.toPng as Mock).mockResolvedValue('data:image/png;base64,abc');
diff --git a/src/theme/utils/export.utils.ts b/src/theme/utils/export.utils.ts
index 68077cd8..45599c11 100644
--- a/src/theme/utils/export.utils.ts
+++ b/src/theme/utils/export.utils.ts
@@ -78,7 +78,11 @@ export function exportXLSX({
export async function exportPNG({
title,
containerRef,
+ options,
}: ChartCardMenuOptionOnClickProps): Promise {
+ // Any valid CSS color. When omitted, the PNG background is transparent.
+ const backgroundColor = options?.backgroundColor as string | undefined;
+
const element = containerRef?.current;
if (!element) {
throw new Error('exportPNG: element is undefined');
@@ -87,6 +91,7 @@ export async function exportPNG({
try {
const dataUrl = await domtoimage.toPng(element, {
cacheBust: true,
+ ...(backgroundColor && { bgcolor: backgroundColor }),
filter: (node: unknown) => {
if (node instanceof HTMLElement && node.hasAttribute('data-no-export')) {
return false; // exclude elements with data-no-export