Skip to content

Commit bc99c45

Browse files
authored
fix(files): zoom file viewer content, not the browser page (#4741)
* fix(files): zoom file viewer content, not the browser page * fix(files): use effect lifecycle for SVG blob URL to survive strict mode
1 parent 3b18d3b commit bc99c45

3 files changed

Lines changed: 72 additions & 36 deletions

File tree

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,15 +1071,21 @@ const HtmlPreview = memo(function HtmlPreview({ content }: { content: string })
10711071
})
10721072

10731073
function SvgPreview({ content }: { content: string }) {
1074-
const wrappedContent = `<!DOCTYPE html><html><head><style>body{margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:transparent;}svg{max-width:100%;max-height:100vh;}</style></head><body>${content}</body></html>`
1074+
const [blobUrl, setBlobUrl] = useState('')
1075+
1076+
useEffect(() => {
1077+
const url = URL.createObjectURL(new Blob([content], { type: 'image/svg+xml' }))
1078+
setBlobUrl(url)
1079+
return () => URL.revokeObjectURL(url)
1080+
}, [content])
10751081

10761082
return (
10771083
<ZoomablePreview className='h-full' contentClassName='h-full w-full'>
1078-
<iframe
1079-
srcDoc={wrappedContent}
1080-
sandbox=''
1081-
title='SVG Preview'
1082-
className='h-full w-full border-0'
1084+
<img
1085+
src={blobUrl}
1086+
alt='SVG preview'
1087+
className='max-h-full max-w-full select-none object-contain'
1088+
draggable={false}
10831089
/>
10841090
</ZoomablePreview>
10851091
)

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-wheel-zoom.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,39 @@
1+
interface BindPreviewWheelZoomOptions {
2+
/**
3+
* Called for non-modifier wheel events (two-finger scroll). When provided,
4+
* the container's native scrolling is suppressed and the consumer drives
5+
* pan via `deltaX` / `deltaY`. Use for transform-based viewers (e.g. image)
6+
* where the content is not a real scroll container.
7+
*/
8+
onPan?: (event: WheelEvent) => void
9+
}
10+
111
/**
2-
* Bind browser pinch/ctrl-wheel zoom and horizontal wheel gestures for preview scroll containers.
12+
* Bind browser pinch/ctrl-wheel zoom and horizontal wheel gestures for preview
13+
* scroll containers. Trackpad pinch fires `wheel` with `ctrlKey=true`; without
14+
* a non-passive native listener the browser falls back to page zoom. `metaKey`
15+
* is also accepted so Cmd+scroll zooms on macOS, matching Figma/tldraw/Excalidraw.
316
*/
417
export function bindPreviewWheelZoom(
518
container: HTMLElement,
6-
onZoom: (event: WheelEvent) => void
19+
onZoom: (event: WheelEvent) => void,
20+
options: BindPreviewWheelZoomOptions = {}
721
): () => void {
22+
const { onPan } = options
23+
824
const onWheel = (event: WheelEvent) => {
9-
if (event.ctrlKey) {
25+
if (event.ctrlKey || event.metaKey) {
1026
event.preventDefault()
1127
onZoom(event)
1228
return
1329
}
1430

31+
if (onPan) {
32+
event.preventDefault()
33+
onPan(event)
34+
return
35+
}
36+
1537
const horizontalDelta = event.deltaX !== 0 ? event.deltaX : event.shiftKey ? event.deltaY : 0
1638
if (horizontalDelta === 0 || container.scrollWidth <= container.clientWidth) return
1739

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/zoomable-preview.tsx

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
'use client'
22

3-
import type { MouseEvent, ReactNode, WheelEvent } from 'react'
4-
import { useCallback, useLayoutEffect, useRef, useState } from 'react'
3+
import type { MouseEvent, ReactNode } from 'react'
4+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
55
import { cn } from '@/lib/core/utils/cn'
66
import { PreviewToolbar } from './preview-toolbar'
7+
import { bindPreviewWheelZoom } from './preview-wheel-zoom'
78

89
const ZOOM_MIN = 0.25
910
const ZOOM_MAX = 4
@@ -133,31 +134,39 @@ export function ZoomablePreview({
133134
applyZoom(clampZoom(zoom / ZOOM_BUTTON_FACTOR))
134135
}
135136

136-
const handleWheel = (e: WheelEvent<HTMLDivElement>) => {
137-
e.preventDefault()
138-
if (e.ctrlKey || e.metaKey) {
139-
hasInteractedRef.current = true
140-
const rect = e.currentTarget.getBoundingClientRect()
141-
applyZoom(
142-
clampZoom(zoomRef.current * Math.exp(-e.deltaY * ZOOM_WHEEL_SENSITIVITY)),
143-
e.clientX - rect.left,
144-
e.clientY - rect.top
145-
)
146-
} else {
147-
hasInteractedRef.current = true
148-
setOffset((currentOffset) =>
149-
clampOffset(
150-
containerSizeRef.current,
151-
contentSizeRef.current,
152-
{
153-
x: currentOffset.x - e.deltaX,
154-
y: currentOffset.y - e.deltaY,
155-
},
156-
zoomRef.current
137+
useEffect(() => {
138+
const viewport = viewportRef.current
139+
if (!viewport) return
140+
141+
return bindPreviewWheelZoom(
142+
viewport,
143+
(event) => {
144+
hasInteractedRef.current = true
145+
const rect = viewport.getBoundingClientRect()
146+
applyZoom(
147+
clampZoom(zoomRef.current * Math.exp(-event.deltaY * ZOOM_WHEEL_SENSITIVITY)),
148+
event.clientX - rect.left,
149+
event.clientY - rect.top
157150
)
158-
)
159-
}
160-
}
151+
},
152+
{
153+
onPan: (event) => {
154+
hasInteractedRef.current = true
155+
setOffset((currentOffset) =>
156+
clampOffset(
157+
containerSizeRef.current,
158+
contentSizeRef.current,
159+
{
160+
x: currentOffset.x - event.deltaX,
161+
y: currentOffset.y - event.deltaY,
162+
},
163+
zoomRef.current
164+
)
165+
)
166+
},
167+
}
168+
)
169+
}, [applyZoom])
161170

162171
useLayoutEffect(() => {
163172
const updateSizes = () => {
@@ -257,7 +266,6 @@ export function ZoomablePreview({
257266
onMouseMove={handleMouseMove}
258267
onMouseUp={handleMouseUp}
259268
onMouseLeave={handleMouseUp}
260-
onWheel={handleWheel}
261269
>
262270
<div className='pointer-events-none absolute inset-0 flex items-center justify-center'>
263271
<div

0 commit comments

Comments
 (0)