Skip to content

Commit 7e3c806

Browse files
feat(zoom): add minZoom override (fix #376)
1 parent 81c82f8 commit 7e3c806

File tree

7 files changed

+76
-31
lines changed

7 files changed

+76
-31
lines changed

docs/plugins/zoom.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ Zoom plugin adds the following `Lightbox` properties.
107107
<td>
108108
&#123;<br />
109109
&nbsp;&nbsp;ref?: React.ForwardedRef&#8203;&lt;ZoomRef&gt;;<br />
110+
&nbsp;&nbsp;minZoom?: number;<br />
110111
&nbsp;&nbsp;maxZoomPixelRatio?: number;<br />
111112
&nbsp;&nbsp;zoomInMultiplier?: number;<br />
112113
&nbsp;&nbsp;doubleTapDelay?: number;<br />
@@ -122,6 +123,7 @@ Zoom plugin adds the following `Lightbox` properties.
122123
<p>Zoom plugin settings:</p>
123124
<ul>
124125
<li>`ref` - Zoom plugin ref. See [Zoom Ref](#ZoomRef) for details.</li>
126+
<li>`minZoom` - override minimum zoom level (default: 1.0)</li>
125127
<li>`maxZoomPixelRatio` - ratio of image pixels to physical pixels at maximum zoom level</li>
126128
<li>`zoomInMultiplier` - zoom-in multiplier</li>
127129
<li>`doubleTapDelay` - double-tap maximum time delay (deprecated)</li>
@@ -133,9 +135,9 @@ Zoom plugin adds the following `Lightbox` properties.
133135
<li>`scrollToZoom` - if `true`, enables image zoom via scroll gestures for mouse and trackpad users</li>
134136
</ul>
135137
<p>
136-
Default: <span class="font-mono">&#123; maxZoomPixelRatio: 1, zoomInMultiplier: 2, doubleTapDelay: 300, doubleClickDelay: 500,
137-
doubleClickMaxStops: 2, keyboardMoveDistance: 50, wheelZoomDistanceFactor: 100, pinchZoomDistanceFactor:
138-
100, scrollToZoom: false &#125;</span>
138+
Default: <span class="font-mono">&#123; minZoom: 1, maxZoomPixelRatio: 1, zoomInMultiplier: 2,
139+
doubleTapDelay: 300, doubleClickDelay: 500, doubleClickMaxStops: 2, keyboardMoveDistance: 50,
140+
wheelZoomDistanceFactor: 100, pinchZoomDistanceFactor: 100, scrollToZoom: false &#125;</span>
139141
</p>
140142
</td>
141143
</tr>

src/plugins/zoom/ZoomButton.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,17 @@ export const ZoomButton = React.forwardRef<HTMLButtonElement, ZoomButtonProps>(f
3030
const wasEnabled = React.useRef(false);
3131
const wasFocused = React.useRef(false);
3232

33-
const { zoom, maxZoom, zoomIn: zoomInCallback, zoomOut: zoomOutCallback, disabled: zoomDisabled } = useZoom();
33+
const {
34+
zoom,
35+
minZoom,
36+
maxZoom,
37+
zoomIn: zoomInCallback,
38+
zoomOut: zoomOutCallback,
39+
disabled: zoomDisabled,
40+
} = useZoom();
3441
const { render } = useLightboxProps();
3542

36-
const disabled = zoomDisabled || (zoomIn ? zoom >= maxZoom : zoom <= 1);
43+
const disabled = zoomDisabled || (zoomIn ? zoom >= maxZoom : zoom <= minZoom);
3744

3845
React.useEffect(() => {
3946
if (disabled && wasEnabled.current && wasFocused.current) {

src/plugins/zoom/ZoomController.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export function ZoomContextProvider({ children }: ComponentProps) {
2020
const [zoomWrapper, setZoomWrapper] = React.useState<ActiveZoomWrapper>();
2121

2222
const { slideRect } = useController();
23+
const { ref, minZoom } = useZoomProps();
2324
const { imageRect, maxZoom } = useZoomImageRect(slideRect, zoomWrapper?.imageDimensions);
2425

2526
const { zoom, offsetX, offsetY, disabled, changeZoom, changeOffsets, zoomIn, zoomOut } = useZoomState(
@@ -30,14 +31,24 @@ export function ZoomContextProvider({ children }: ComponentProps) {
3031

3132
useZoomCallback(zoom, disabled);
3233

33-
useZoomSensors(zoom, maxZoom, disabled, changeZoom, changeOffsets, zoomWrapper?.zoomWrapperRef);
34+
useZoomSensors(
35+
zoom,
36+
minZoom,
37+
maxZoom,
38+
disabled,
39+
zoomIn,
40+
zoomOut,
41+
changeZoom,
42+
changeOffsets,
43+
zoomWrapper?.zoomWrapperRef,
44+
);
3445

3546
const zoomRef = React.useMemo(
36-
() => ({ zoom, maxZoom, offsetX, offsetY, disabled, zoomIn, zoomOut, changeZoom }),
37-
[zoom, maxZoom, offsetX, offsetY, disabled, zoomIn, zoomOut, changeZoom],
47+
() => ({ zoom, minZoom, maxZoom, offsetX, offsetY, disabled, zoomIn, zoomOut, changeZoom }),
48+
[zoom, minZoom, maxZoom, offsetX, offsetY, disabled, zoomIn, zoomOut, changeZoom],
3849
);
3950

40-
React.useImperativeHandle(useZoomProps().ref, () => zoomRef, [zoomRef]);
51+
React.useImperativeHandle(ref, () => zoomRef, [zoomRef]);
4152

4253
const context = React.useMemo(() => ({ ...zoomRef, setZoomWrapper }), [zoomRef, setZoomWrapper]);
4354

src/plugins/zoom/hooks/useZoomSensors.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@ function scaleZoom(value: number, delta: number, factor = 100, clamp = 2) {
2323

2424
export function useZoomSensors(
2525
zoom: number,
26+
minZoom: number,
2627
maxZoom: number,
2728
disabled: boolean,
29+
zoomIn: ReturnType<typeof useZoomState>["zoomIn"],
30+
zoomOut: ReturnType<typeof useZoomState>["zoomOut"],
2831
changeZoom: ReturnType<typeof useZoomState>["changeZoom"],
2932
changeOffsets: ReturnType<typeof useZoomState>["changeOffsets"],
3033
zoomWrapperRef?: React.RefObject<HTMLDivElement | null>,
@@ -86,17 +89,15 @@ export function useZoomSensors(
8689
}
8790
}
8891

89-
const handleChangeZoom = (zoomValue: number) => {
90-
preventDefault();
91-
changeZoom(zoomValue);
92-
};
93-
9492
if (key === "+" || (meta && key === "=")) {
95-
handleChangeZoom(zoom * zoomInMultiplier);
93+
preventDefault();
94+
zoomIn();
9695
} else if (key === "-" || (meta && key === "_")) {
97-
handleChangeZoom(zoom / zoomInMultiplier);
96+
preventDefault();
97+
zoomOut();
9898
} else if (meta && key === "0") {
99-
handleChangeZoom(1);
99+
preventDefault();
100+
changeZoom(1);
100101
}
101102
});
102103

@@ -156,11 +157,17 @@ export function useZoomSensors(
156157
timeStamp - lastPointerDown.current < (event.pointerType === "touch" ? doubleTapDelay : doubleClickDelay)
157158
) {
158159
lastPointerDown.current = 0;
159-
changeZoom(
160-
zoom !== maxZoom ? zoom * Math.max(maxZoom ** (1 / doubleClickMaxStops), zoomInMultiplier) : 1,
161-
false,
162-
...translateCoordinates(event),
163-
);
160+
161+
const targetZoom =
162+
zoom >= 1
163+
? zoom !== maxZoom
164+
? zoom * Math.max(maxZoom ** (1 / doubleClickMaxStops), zoomInMultiplier)
165+
: 1
166+
: zoom !== minZoom
167+
? zoom / Math.max(minZoom ** (-1 / doubleClickMaxStops), zoomInMultiplier)
168+
: 1;
169+
170+
changeZoom(targetZoom, false, ...translateCoordinates(event));
164171
} else {
165172
lastPointerDown.current = timeStamp;
166173
}

src/plugins/zoom/hooks/useZoomState.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export function useZoomState(
2525

2626
const { currentSlide, globalIndex } = useLightboxState();
2727
const { containerRect, slideRect } = useController();
28-
const { zoomInMultiplier } = useZoomProps();
28+
const { minZoom, zoomInMultiplier } = useZoomProps();
2929

3030
const currentSource = currentSlide && isImageSlide(currentSlide) ? currentSlide.src : undefined;
3131
const disabled = !currentSource || !zoomWrapperRef?.current;
@@ -54,7 +54,10 @@ export function useZoomState(
5454

5555
const changeZoom = React.useCallback(
5656
(targetZoom: number, rapid?: boolean, dx?: number, dy?: number) => {
57-
const newZoom = round(Math.min(Math.max(targetZoom + 0.001 < maxZoom ? targetZoom : maxZoom, 1), maxZoom), 5);
57+
const newZoom = round(
58+
Math.min(Math.max(targetZoom + 0.001 < maxZoom ? targetZoom : maxZoom, minZoom), maxZoom),
59+
5,
60+
);
5861

5962
if (newZoom === zoom) return;
6063

@@ -66,7 +69,7 @@ export function useZoomState(
6669

6770
setZoom(newZoom);
6871
},
69-
[zoom, maxZoom, changeOffsets, animate],
72+
[zoom, minZoom, maxZoom, changeOffsets, animate],
7073
);
7174

7275
const handleControllerRectChange = useEventCallback(() => {
@@ -81,9 +84,15 @@ export function useZoomState(
8184

8285
useLayoutEffect(handleControllerRectChange, [containerRect.width, containerRect.height, handleControllerRectChange]);
8386

84-
const zoomIn = React.useCallback(() => changeZoom(zoom * zoomInMultiplier), [zoom, zoomInMultiplier, changeZoom]);
87+
const zoomIn = React.useCallback(() => {
88+
const targetZoom = zoom * zoomInMultiplier;
89+
changeZoom(zoom < 1 && targetZoom > 1 ? 1 : targetZoom);
90+
}, [zoom, zoomInMultiplier, changeZoom]);
8591

86-
const zoomOut = React.useCallback(() => changeZoom(zoom / zoomInMultiplier), [zoom, zoomInMultiplier, changeZoom]);
92+
const zoomOut = React.useCallback(() => {
93+
const targetZoom = zoom / zoomInMultiplier;
94+
changeZoom(zoom > 1 && targetZoom < 1 ? 1 : targetZoom);
95+
}, [zoom, zoomInMultiplier, changeZoom]);
8796

8897
return { zoom, offsetX, offsetY, disabled, changeOffsets, changeZoom, zoomIn, zoomOut };
8998
}

src/plugins/zoom/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ declare module "../../types.js" {
99
zoom?: {
1010
/** Zoom plugin ref */
1111
ref?: React.ForwardedRef<ZoomRef>;
12+
/** override minimum zoom level (default: 1.0) */
13+
minZoom?: number;
1214
/** ratio of image pixels to physical pixels at maximum zoom level */
1315
maxZoomPixelRatio?: number;
1416
/** zoom-in multiplier */
@@ -81,6 +83,8 @@ declare module "../../types.js" {
8183
interface ZoomRef {
8284
/** current zoom level */
8385
zoom: number;
86+
/** minimum zoom level */
87+
minZoom: number;
8488
/** maximum zoom level */
8589
maxZoom: number;
8690
/** horizontal offset */

src/plugins/zoom/props.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { LightboxProps } from "../../index.js";
22

33
export const defaultZoomProps = {
4+
minZoom: 1,
45
maxZoomPixelRatio: 1,
56
zoomInMultiplier: 2,
67
doubleTapDelay: 300,
@@ -12,7 +13,11 @@ export const defaultZoomProps = {
1213
scrollToZoom: false,
1314
};
1415

15-
export const resolveZoomProps = (zoom: LightboxProps["zoom"]) => ({
16-
...defaultZoomProps,
17-
...zoom,
18-
});
16+
function validateMinZoom(minZoom: number) {
17+
return Math.min(Math.max(minZoom, Number.EPSILON), 1);
18+
}
19+
20+
export function resolveZoomProps(zoom: LightboxProps["zoom"]) {
21+
const { minZoom, ...rest } = { ...defaultZoomProps, ...zoom };
22+
return { minZoom: validateMinZoom(minZoom), ...rest };
23+
}

0 commit comments

Comments
 (0)