Skip to content

Commit 9e926a9

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

File tree

2 files changed

+78
-25
lines changed

2 files changed

+78
-25
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

0 commit comments

Comments
 (0)