Skip to content

Commit c19f7ca

Browse files
committed
Move zoom state up to a context provider so multiple ZoomContainers can share state
1 parent e8f8d3d commit c19f7ca

File tree

3 files changed

+99
-44
lines changed

3 files changed

+99
-44
lines changed

src/components/ZoomContainer.stories.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
import { type Meta } from "@storybook/react";
12
import React from "react";
23

3-
import { ZoomContainer } from "./ZoomContainer";
4+
import { ZoomContainer, ZoomProvider } from "./ZoomContainer";
45

56
export default {
67
component: ZoomContainer,
7-
};
8+
decorators: (Story) => (
9+
<ZoomProvider>
10+
<Story />
11+
</ZoomProvider>
12+
),
13+
} satisfies Meta<typeof ZoomContainer>;
814

915
export const Default = {
1016
args: {
@@ -23,3 +29,24 @@ export const Tall = {
2329
children: <img src="/chromatic-site-mobile.png" alt="" />,
2430
},
2531
};
32+
33+
export const Small = {
34+
args: {
35+
children: <img src="/capture-16b798d6.png" alt="" />,
36+
},
37+
};
38+
39+
export const Mirror = {
40+
render() {
41+
return (
42+
<div style={{ display: "flex", height: "100%", gap: 10 }}>
43+
<ZoomContainer>
44+
<img src="/A.png" alt="" />
45+
</ZoomContainer>
46+
<ZoomContainer>
47+
<img src="/B.png" alt="" />
48+
</ZoomContainer>
49+
</div>
50+
);
51+
},
52+
};

src/components/ZoomContainer.tsx

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
import { styled } from "@storybook/theming";
2-
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2+
import React, {
3+
createContext,
4+
Dispatch,
5+
ReactNode,
6+
SetStateAction,
7+
useCallback,
8+
useContext,
9+
useEffect,
10+
useMemo,
11+
useRef,
12+
useState,
13+
} from "react";
314

415
const TRANSITION_DURATION_MS = 200;
516

@@ -18,23 +29,33 @@ const Content = styled.div({
1829
width: "max-content",
1930
});
2031

32+
const initialState = {
33+
containerHeight: 0,
34+
containerWidth: 0,
35+
contentHeight: 0,
36+
contentWidth: 0,
37+
contentScale: 1,
38+
translateX: 0,
39+
translateY: 0,
40+
multiplierX: 1,
41+
multiplierY: 1,
42+
zoomed: false,
43+
transition: "none",
44+
transitionTimeout: 0,
45+
};
46+
47+
type State = typeof initialState;
48+
49+
export const ZoomContext = createContext<[State, Dispatch<SetStateAction<State>>]>(null as any);
50+
51+
export const ZoomProvider = ({ children }: { children: ReactNode }) => {
52+
return <ZoomContext.Provider value={useState(initialState)}>{children}</ZoomContext.Provider>;
53+
};
54+
2155
const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value));
2256

2357
export const useZoom = () => {
24-
const [state, setState] = useState({
25-
containerHeight: 0,
26-
containerWidth: 0,
27-
contentHeight: 0,
28-
contentWidth: 0,
29-
contentScale: 1,
30-
translateX: 0,
31-
translateY: 0,
32-
multiplierX: 1,
33-
multiplierY: 1,
34-
zoomed: false,
35-
transition: "none",
36-
transitionTimeout: 0,
37-
});
58+
const [state, setState] = useContext(ZoomContext);
3859

3960
const containerRef = useRef<HTMLDivElement>(null);
4061
const contentRef = useRef<HTMLDivElement>(null);
@@ -73,21 +94,25 @@ export const useZoom = () => {
7394
translateX,
7495
translateY,
7596
}));
76-
}, [containerRef, contentRef]);
97+
}, [containerRef, contentRef, setState]);
7798

7899
const onMouseEvent = useCallback(
79100
(e: MouseEvent) => {
80101
if (!containerRef.current) return;
81-
const { x, y } = containerRef.current.getBoundingClientRect();
102+
const { x, y, width, height } = containerRef.current.getBoundingClientRect();
82103

83104
setState((currentState) => {
105+
const { clientX: cx, clientY: cy, type: eventType } = e;
106+
const hovered = cx >= x && cx <= x + width && cy >= y && cy <= y + height;
107+
if (!hovered) return currentState;
108+
84109
const { containerHeight, containerWidth, contentHeight, contentWidth } = currentState;
85110
const ratioX = contentWidth < containerWidth ? contentWidth / contentHeight + 1 : 1;
86111
const ratioY = contentHeight < containerHeight ? contentHeight / contentWidth + 1 : 1;
87-
const multiplierX = ((e.clientX - x) / containerWidth) * 2 * 1.2 * ratioX - 0.2 * ratioX;
88-
const multiplierY = ((e.clientY - y) / containerHeight) * 2 * 1.2 * ratioY - 0.2 * ratioY;
112+
const multiplierX = ((cx - x) / containerWidth) * 2 * 1.2 * ratioX - 0.2 * ratioX;
113+
const multiplierY = ((cy - y) / containerHeight) * 2 * 1.2 * ratioY - 0.2 * ratioY;
89114

90-
const clicked = e.type === "click" && (e as any).pointerType !== "touch";
115+
const clicked = eventType === "click" && (e as any).pointerType !== "touch";
91116
const zoomed = clicked ? !currentState.zoomed : currentState.zoomed;
92117
const update = {
93118
...currentState,
@@ -108,7 +133,7 @@ export const useZoom = () => {
108133
};
109134
});
110135
},
111-
[containerRef]
136+
[containerRef, setState]
112137
);
113138

114139
const onToggleZoom = useCallback(
@@ -151,6 +176,7 @@ export const useZoom = () => {
151176
const contentStyle = {
152177
transform: `translate(${translateX}px, ${translateY}px) scale(${scale})`,
153178
transition: state.transition,
179+
...(state.contentScale < 1 && { cursor: state.zoomed ? "zoom-out" : "zoom-in" }),
154180
};
155181

156182
return {
@@ -173,8 +199,8 @@ export const ZoomContainer = ({
173199
children,
174200
render,
175201
}: {
176-
children?: React.ReactNode;
177-
render?: (props: ReturnType<typeof useZoom>["renderProps"]) => React.ReactChild;
202+
children?: ReactNode;
203+
render?: (props: ReturnType<typeof useZoom>["renderProps"]) => ReactNode;
178204
}) => {
179205
const { containerProps, contentProps, renderProps } = useZoom();
180206

src/screens/VisualTests/SnapshotComparison.tsx

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import React, { useEffect } from "react";
55
import { Link } from "../../components/design-system";
66
import { SnapshotImage } from "../../components/SnapshotImage";
77
import { Text } from "../../components/Text";
8-
import { ZoomContainer } from "../../components/ZoomContainer";
8+
import { ZoomContainer, ZoomProvider } from "../../components/ZoomContainer";
99
import { ComparisonResult, TestResult, TestStatus } from "../../gql/graphql";
1010
import { summarizeTests } from "../../utils/summarizeTests";
1111
import { useSelectedBuildState, useSelectedStoryState } from "./BuildContext";
@@ -283,24 +283,26 @@ export const SnapshotComparison = ({
283283
</Warning>
284284
)}
285285
{!isInProgress && selectedComparison && (
286-
<ZoomContainer
287-
render={(zoomProps) => (
288-
<SnapshotImage
289-
key={selectedComparison.id}
290-
componentName={selectedTest?.story?.component?.name}
291-
storyName={selectedTest?.story?.name}
292-
comparisonResult={selectedComparison.result ?? undefined}
293-
latestImage={selectedComparison.headCapture?.captureImage ?? undefined}
294-
baselineImage={selectedComparison.baseCapture?.captureImage ?? undefined}
295-
baselineImageVisible={baselineImageVisible}
296-
diffImage={selectedComparison.captureDiff?.diffImage ?? undefined}
297-
focusImage={selectedComparison.captureDiff?.focusImage ?? undefined}
298-
diffVisible={diffVisible}
299-
focusVisible={focusVisible}
300-
{...zoomProps}
301-
/>
302-
)}
303-
/>
286+
<ZoomProvider>
287+
<ZoomContainer
288+
render={(zoomProps) => (
289+
<SnapshotImage
290+
key={selectedComparison.id}
291+
componentName={selectedTest?.story?.component?.name}
292+
storyName={selectedTest?.story?.name}
293+
comparisonResult={selectedComparison.result ?? undefined}
294+
latestImage={selectedComparison.headCapture?.captureImage ?? undefined}
295+
baselineImage={selectedComparison.baseCapture?.captureImage ?? undefined}
296+
baselineImageVisible={baselineImageVisible}
297+
diffImage={selectedComparison.captureDiff?.diffImage ?? undefined}
298+
focusImage={selectedComparison.captureDiff?.focusImage ?? undefined}
299+
diffVisible={diffVisible}
300+
focusVisible={focusVisible}
301+
{...zoomProps}
302+
/>
303+
)}
304+
/>
305+
</ZoomProvider>
304306
)}
305307

306308
{!isInProgress && captureErrorData && (

0 commit comments

Comments
 (0)