Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/png-export-options.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,58 @@ describe('ChartCardMenuPro', () => {
vi.useRealTimers();
});

it('passes the option options to onClick', async () => {
vi.useFakeTimers();
const onClickMock = vi.fn();

render(
<ChartCardMenuPro
menuOptions={[
{
value: 'png',
labelKey: 'export.png',
onClick: onClickMock,
options: { backgroundColor: '#fff' },
},
]}
/>,
);
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(
<ChartCardMenuPro
onCustomDownload={onCustomDownload}
menuOptions={[
{
value: 'png',
labelKey: 'export.png',
onClick: onClickMock,
options: { backgroundColor: '#fff' },
},
]}
/>,
);
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,19 @@ export const ChartCardMenuPro: React.FC<ChartCardMenuProProps> = (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 (
Expand Down
4 changes: 4 additions & 0 deletions src/theme/defaults/defaults.ChartCardMenu.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export type ChartCardMenuOptionOnClickProps = {
dimensionsAndMeasures?: (Dimension | Measure)[];
containerRef?: React.RefObject<HTMLDivElement | null>;
theme: Theme;
/** Per-option settings supplied by the theme (see ChartCardMenuOption.options). */
options?: Record<string, unknown>;
onCustomDownload?: (props: (props: ChartCardMenuOptionOnClickProps) => void) => void;
};

Expand All @@ -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<string, unknown>;
onClick: (props: ChartCardMenuOptionOnClickProps) => void;
};

Expand Down
26 changes: 26 additions & 0 deletions src/theme/utils/export.utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
5 changes: 5 additions & 0 deletions src/theme/utils/export.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,11 @@ export function exportXLSX({
export async function exportPNG({
title,
containerRef,
options,
}: ChartCardMenuOptionOnClickProps): Promise<void> {
// 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');
Expand All @@ -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
Expand Down
Loading