Skip to content

Commit 00ca9a9

Browse files
authored
Add: panZoom control option to support chart zoom functionality (#253)
1 parent a489949 commit 00ca9a9

File tree

7 files changed

+85
-10
lines changed

7 files changed

+85
-10
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"streamdown": patch
3+
---
4+
5+
Add PanZoom controls configurability for Mermaid diagrams.
6+
7+
- Support `controls.mermaid.panZoom` (boolean) to toggle zoom controls globally
8+
- Support `mermaid.config.panZoom` (boolean or `{ showControls?: boolean }`) per-instance
9+
- Keep defaults enabled; `false` explicitly hides the zoom controls
10+
11+
This is a non-breaking enhancement that aligns with existing control predicates.

apps/website/content/docs/configuration.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ The `controls` prop can be configured granularly:
143143
mermaid: {
144144
download: true, // Show mermaid download button
145145
copy: true, // Show mermaid copy button
146-
fullscreen: true // Show mermaid fullscreen button
146+
fullscreen: true, // Show mermaid fullscreen button
147+
panZoom: true // Show mermaid pan/zoom controls
147148
}
148149
}}
149150
>

packages/streamdown/__tests__/show-controls.test.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,50 @@ ${markdownWithCode}
192192
expect(codeButtons?.length).toBeGreaterThan(0);
193193
});
194194
});
195+
196+
it("should hide mermaid pan-zoom controls when panZoom is false", async () => {
197+
const markdownWithMermaid = `
198+
\`\`\`mermaid
199+
graph TD
200+
A-->B
201+
\`\`\`
202+
`;
203+
204+
const { container } = render(
205+
<Streamdown controls={{ mermaid: { panZoom: false } }}>
206+
{markdownWithMermaid}
207+
</Streamdown>
208+
);
209+
210+
await waitFor(() => {
211+
const zoomInButton = container.querySelector('button[title="Zoom in"]');
212+
expect(zoomInButton).toBeFalsy();
213+
});
214+
});
215+
216+
it("should show mermaid pan-zoom controls by default", async () => {
217+
const utils = await import("../lib/mermaid/utils");
218+
vi.spyOn(utils, "initializeMermaid").mockResolvedValue({
219+
render: vi.fn().mockResolvedValue({ svg: "<svg></svg>" }),
220+
} as any);
221+
const markdownWithMermaid = `
222+
\`\`\`mermaid
223+
graph TD
224+
A-->B
225+
\`\`\`
226+
`;
227+
228+
const { container } = render(
229+
<Streamdown controls={{ mermaid: {} }}>
230+
{markdownWithMermaid}
231+
</Streamdown>
232+
);
233+
234+
await waitFor(() => {
235+
const zoomInButton = container.querySelector('button[title="Zoom in"]');
236+
expect(zoomInButton).toBeTruthy();
237+
});
238+
});
195239
});
196240

197241
describe("with custom components", () => {

packages/streamdown/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export type ControlsConfig =
4141
download?: boolean;
4242
copy?: boolean;
4343
fullscreen?: boolean;
44+
panZoom?: boolean;
4445
};
4546
};
4647

packages/streamdown/lib/components.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,9 @@ const shouldShowMermaidControl = (
102102
code?: boolean;
103103
mermaid?:
104104
| boolean
105-
| { download?: boolean; copy?: boolean; fullscreen?: boolean };
105+
| { download?: boolean; copy?: boolean; fullscreen?: boolean; panZoom?: boolean };
106106
},
107-
controlType: "download" | "copy" | "fullscreen"
107+
controlType: "download" | "copy" | "fullscreen" | "panZoom"
108108
): boolean => {
109109
if (typeof config === "boolean") {
110110
return config;
@@ -640,10 +640,8 @@ const CodeComponent = ({
640640
const showMermaidControls = shouldShowControls(controlsConfig, "mermaid");
641641
const showDownload = shouldShowMermaidControl(controlsConfig, "download");
642642
const showCopy = shouldShowMermaidControl(controlsConfig, "copy");
643-
const showFullscreen = shouldShowMermaidControl(
644-
controlsConfig,
645-
"fullscreen"
646-
);
643+
const showFullscreen = shouldShowMermaidControl(controlsConfig, "fullscreen");
644+
const showPanZoomControls = shouldShowMermaidControl(controlsConfig, "panZoom");
647645

648646
return (
649647
<Suspense fallback={<CodeBlockSkeleton />}>
@@ -672,7 +670,11 @@ const CodeComponent = ({
672670
)}
673671
</div>
674672
)}
675-
<Mermaid chart={code} config={mermaidContext?.config} />
673+
<Mermaid
674+
chart={code}
675+
config={mermaidContext?.config}
676+
showControls={showPanZoomControls}
677+
/>
676678
</div>
677679
</Suspense>
678680
);

packages/streamdown/lib/mermaid/fullscreen-button.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,20 @@ export const MermaidFullscreenButton = ({
3838
...props
3939
}: MermaidFullscreenButtonProps) => {
4040
const [isFullscreen, setIsFullscreen] = useState(false);
41-
const { isAnimating } = useContext(StreamdownContext);
41+
const { isAnimating, controls: controlsConfig } = useContext(StreamdownContext);
42+
const showPanZoomControls = (() => {
43+
if (typeof controlsConfig === "boolean") {
44+
return controlsConfig;
45+
}
46+
const mermaidCtl = controlsConfig.mermaid;
47+
if (mermaidCtl === false) {
48+
return false;
49+
}
50+
if (mermaidCtl === true || mermaidCtl === undefined) {
51+
return true;
52+
}
53+
return (mermaidCtl as any).panZoom !== false;
54+
})();
4255

4356
const handleToggle = () => {
4457
setIsFullscreen(!isFullscreen);
@@ -121,6 +134,7 @@ export const MermaidFullscreenButton = ({
121134
className="h-full w-full [&>div]:h-full [&>div]:overflow-hidden [&_svg]:h-auto [&_svg]:w-auto"
122135
config={config}
123136
fullscreen={true}
137+
showControls={showPanZoomControls}
124138
/>
125139
</div>
126140
</div>

packages/streamdown/lib/mermaid/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ type MermaidProps = {
1010
className?: string;
1111
config?: MermaidConfig;
1212
fullscreen?: boolean;
13+
showControls?: boolean;
1314
};
1415

1516
export const Mermaid = ({
1617
chart,
1718
className,
1819
config,
1920
fullscreen = false,
21+
showControls = true,
2022
}: MermaidProps) => {
2123
const [error, setError] = useState<string | null>(null);
2224
const [isLoading, setIsLoading] = useState(true);
@@ -122,7 +124,7 @@ export const Mermaid = ({
122124
fullscreen={fullscreen}
123125
maxZoom={3}
124126
minZoom={0.5}
125-
showControls={true}
127+
showControls={showControls}
126128
zoomStep={0.1}
127129
>
128130
<div

0 commit comments

Comments
 (0)