yet-another-react-lightbox-lite 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -10
- package/dist/index.d.ts +37 -3
- package/dist/index.js +238 -52
- package/dist/styles.css +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Lightweight React lightbox component. This is a trimmed-down version of the
|
|
4
4
|
[yet-another-react-lightbox](https://github.com/igordanchenko/yet-another-react-lightbox)
|
|
5
|
-
that provides essential lightbox features and slick UX with just 3KB bundle
|
|
5
|
+
that provides essential lightbox features and slick UX with just 4.3KB bundle
|
|
6
6
|
size.
|
|
7
7
|
|
|
8
8
|
## Overview
|
|
@@ -13,6 +13,7 @@ size.
|
|
|
13
13
|
|
|
14
14
|
- **Built for React:** works with React 18+
|
|
15
15
|
- **UX:** supports keyboard, mouse, touchpad and touchscreen navigation
|
|
16
|
+
- **Zoom:** zoom is supported out of the box
|
|
16
17
|
- **Performance:** preloads a fixed number of images without compromising
|
|
17
18
|
performance or UX
|
|
18
19
|
- **Responsive:** responsive images with automatic resolution switching are
|
|
@@ -269,14 +270,14 @@ An object providing custom render functions.
|
|
|
269
270
|
```tsx
|
|
270
271
|
<Lightbox
|
|
271
272
|
render={{
|
|
272
|
-
slide: ({ slide, rect, current }) => (
|
|
273
|
-
<CustomSlide {...{ slide, rect, current }} />
|
|
273
|
+
slide: ({ slide, rect, zoom, current }) => (
|
|
274
|
+
<CustomSlide {...{ slide, rect, zoom, current }} />
|
|
274
275
|
),
|
|
275
|
-
slideHeader: ({ slide, rect, current }) => (
|
|
276
|
-
<SlideHeader {...{ slide, rect, current }} />
|
|
276
|
+
slideHeader: ({ slide, rect, zoom, current }) => (
|
|
277
|
+
<SlideHeader {...{ slide, rect, zoom, current }} />
|
|
277
278
|
),
|
|
278
|
-
slideFooter: ({ slide, rect, current }) => (
|
|
279
|
-
<SlideFooter {...{ slide, rect, current }} />
|
|
279
|
+
slideFooter: ({ slide, rect, zoom, current }) => (
|
|
280
|
+
<SlideFooter {...{ slide, rect, zoom, current }} />
|
|
280
281
|
),
|
|
281
282
|
controls: () => <CustomControls />,
|
|
282
283
|
iconPrev: () => <IconPrev />,
|
|
@@ -287,15 +288,23 @@ An object providing custom render functions.
|
|
|
287
288
|
/>
|
|
288
289
|
```
|
|
289
290
|
|
|
290
|
-
#### slide: ({ slide, rect, current }) => ReactNode
|
|
291
|
+
#### slide: ({ slide, rect, zoom, current }) => ReactNode
|
|
291
292
|
|
|
292
293
|
Render custom slide type, or override the default image slide implementation.
|
|
293
294
|
|
|
294
|
-
|
|
295
|
+
Parameters:
|
|
296
|
+
|
|
297
|
+
- `slide` - slide object (type: `Slide`)
|
|
298
|
+
- `rect` - slide rect size (type: `Rect`)
|
|
299
|
+
- `zoom` - current zoom level (type: `number`)
|
|
300
|
+
- `current` - if `true`, the slide is the current slide in the viewport (type:
|
|
301
|
+
`boolean`)
|
|
302
|
+
|
|
303
|
+
#### slideHeader: ({ slide, rect, zoom, current }) => ReactNode
|
|
295
304
|
|
|
296
305
|
Render custom elements above each slide.
|
|
297
306
|
|
|
298
|
-
#### slideFooter: ({ slide, rect, current }) => ReactNode
|
|
307
|
+
#### slideFooter: ({ slide, rect, zoom, current }) => ReactNode
|
|
299
308
|
|
|
300
309
|
Render custom elements below or over each slide. By default, the content is
|
|
301
310
|
rendered right under the slide. Alternatively, you can use
|
|
@@ -515,6 +524,31 @@ fixed-positioned element container should not have its own border or padding
|
|
|
515
524
|
styles. If that's the case, you can always add an extra wrapper that just
|
|
516
525
|
defines the fixed position without visual styles.
|
|
517
526
|
|
|
527
|
+
## Hooks (experimental)
|
|
528
|
+
|
|
529
|
+
The library exports the following experimental hooks that you may find helpful
|
|
530
|
+
in customizing lightbox functionality. All experimental hooks are currently
|
|
531
|
+
exported with the `unstable_` prefix.
|
|
532
|
+
|
|
533
|
+
### useZoom
|
|
534
|
+
|
|
535
|
+
You can use the `useZoom` hook to build your custom zoom controls.
|
|
536
|
+
|
|
537
|
+
```tsx
|
|
538
|
+
import { unstable_useZoom as useZoom } from "yet-another-react-lightbox-lite";
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
The hook provides an object with the following props:
|
|
542
|
+
|
|
543
|
+
- `rect` - slide rect
|
|
544
|
+
- `zoom` - current zoom level (numeric value between `1` and `8`)
|
|
545
|
+
- `maxZoom` - maximum zoom level (`1` if zoom is not supported on the current
|
|
546
|
+
slide)
|
|
547
|
+
- `offsetX` - horizontal slide position offset
|
|
548
|
+
- `offsetY` - vertical slide position offset
|
|
549
|
+
- `changeZoom` - change zoom level
|
|
550
|
+
- `changeOffsets` - change position offsets
|
|
551
|
+
|
|
518
552
|
## License
|
|
519
553
|
|
|
520
554
|
MIT © 2024 [Igor Danchenko](https://github.com/igordanchenko)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { MouseEvent } from 'react';
|
|
2
2
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
3
|
|
|
4
4
|
/** Lightbox props */
|
|
@@ -17,6 +17,8 @@ interface LightboxProps {
|
|
|
17
17
|
toolbar?: ToolbarSettings;
|
|
18
18
|
/** controller settings */
|
|
19
19
|
controller?: ControllerSettings;
|
|
20
|
+
/** zoom settings */
|
|
21
|
+
zoom?: ZoomSettings;
|
|
20
22
|
/** customization slots styles */
|
|
21
23
|
styles?: SlotStyles;
|
|
22
24
|
/** CSS class of the lightbox root element */
|
|
@@ -91,8 +93,10 @@ interface Render {
|
|
|
91
93
|
interface RenderSlideProps {
|
|
92
94
|
/** slide */
|
|
93
95
|
slide: Slide;
|
|
94
|
-
/** slide */
|
|
96
|
+
/** slide rect size */
|
|
95
97
|
rect: Rect;
|
|
98
|
+
/** zoom level */
|
|
99
|
+
zoom: number;
|
|
96
100
|
/** if `true`, the slide is the current slide in the viewport */
|
|
97
101
|
current: boolean;
|
|
98
102
|
}
|
|
@@ -112,6 +116,11 @@ interface ControllerSettings {
|
|
|
112
116
|
/** if `true`, close the lightbox when the backdrop is clicked (default: `true`) */
|
|
113
117
|
closeOnBackdropClick?: boolean;
|
|
114
118
|
}
|
|
119
|
+
/** Zoom settings */
|
|
120
|
+
interface ZoomSettings {
|
|
121
|
+
/** zoom-enabled custom slide types */
|
|
122
|
+
supports?: SlideTypeKey[];
|
|
123
|
+
}
|
|
115
124
|
/** Customization slots */
|
|
116
125
|
interface SlotType {
|
|
117
126
|
/** lightbox portal (root) customization slot */
|
|
@@ -149,6 +158,31 @@ type Callback<T = void> = () => T;
|
|
|
149
158
|
/** Render function */
|
|
150
159
|
type RenderFunction<T = void> = [T] extends [void] ? () => React.ReactNode : (props: T) => React.ReactNode;
|
|
151
160
|
|
|
161
|
+
/** Lightbox component */
|
|
152
162
|
declare function Lightbox({ slides, index, setIndex, ...rest }: LightboxProps): react_jsx_runtime.JSX.Element | null;
|
|
153
163
|
|
|
154
|
-
|
|
164
|
+
/** Zoom context */
|
|
165
|
+
type ZoomContextType = {
|
|
166
|
+
/** slide rect */
|
|
167
|
+
rect?: Rect;
|
|
168
|
+
/** zoom level */
|
|
169
|
+
zoom: number;
|
|
170
|
+
/** maximum zoom level */
|
|
171
|
+
maxZoom: number;
|
|
172
|
+
/** horizontal slide position offset */
|
|
173
|
+
offsetX: number;
|
|
174
|
+
/** vertical slide position offset */
|
|
175
|
+
offsetY: number;
|
|
176
|
+
/** change zoom level */
|
|
177
|
+
changeZoom: (
|
|
178
|
+
/** new zoom value */
|
|
179
|
+
newZoom: number,
|
|
180
|
+
/** pointer/mouse/wheel event that determines zoom-in point */
|
|
181
|
+
event?: Pick<MouseEvent, "clientX" | "clientY">) => void;
|
|
182
|
+
/** change position offsets */
|
|
183
|
+
changeOffsets: (dx: number, dy: number) => void;
|
|
184
|
+
};
|
|
185
|
+
/** `useZoom` hook */
|
|
186
|
+
declare const useZoom: () => ZoomContextType;
|
|
187
|
+
|
|
188
|
+
export { type Callback, type ControllerSettings, type GenericSlide, type ImageSource, type Label, type Labels, type LightboxProps, type Rect, type Render, type RenderFunction, type RenderSlideProps, type Slide, type SlideImage, type SlideTypeKey, type SlideTypes, type Slot, type SlotStyles, type SlotType, type ToolbarSettings, type ZoomSettings, Lightbox as default, useZoom as unstable_useZoom };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
|
-
import { useContext, createContext, useState, useRef, useCallback, useMemo, useEffect, isValidElement, cloneElement } from 'react';
|
|
2
|
+
import { useContext, createContext, useState, useRef, useLayoutEffect, useCallback, useMemo, useEffect, isValidElement, cloneElement } from 'react';
|
|
3
3
|
import { flushSync, createPortal } from 'react-dom';
|
|
4
4
|
|
|
5
5
|
const cssPrefix = "yarll__";
|
|
@@ -33,6 +33,16 @@ function makeUseContext(context) {
|
|
|
33
33
|
return ctx;
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
+
function round(value, decimals = 0) {
|
|
37
|
+
const factor = 10 ** decimals;
|
|
38
|
+
return Math.round((value + Number.EPSILON) * factor) / factor;
|
|
39
|
+
}
|
|
40
|
+
function scaleZoom(value, delta, factor = 100, clamp = 2) {
|
|
41
|
+
return value * Math.min(1 + Math.abs(delta / factor), clamp) ** Math.sign(delta);
|
|
42
|
+
}
|
|
43
|
+
function isImageSlide(slide) {
|
|
44
|
+
return (slide.type === undefined || slide.type === "image") && typeof slide.src === "string";
|
|
45
|
+
}
|
|
36
46
|
|
|
37
47
|
const LightboxContext = createContext(null);
|
|
38
48
|
const useLightboxContext = makeUseContext(LightboxContext);
|
|
@@ -40,25 +50,43 @@ function LightboxContextProvider({ children, ...props }) {
|
|
|
40
50
|
return jsx(LightboxContext.Provider, { value: props, children: children });
|
|
41
51
|
}
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const sizes = imageAspectRatio
|
|
52
|
-
? `${imageAspectRatio < rect.width / rect.height ? Math.round(imageAspectRatio * rect.height) : rect.width}px`
|
|
53
|
-
: undefined;
|
|
54
|
-
return (jsx("img", { draggable: false, style: styles?.image, className: cssClass("slide_image"), srcSet: srcSet, sizes: sizes, src: slide.src, alt: slide.alt }));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function Carousel() {
|
|
58
|
-
const { slides, index, styles, render: { slide: renderSlide, slideHeader, slideFooter } = {} } = useLightboxContext();
|
|
53
|
+
const ZoomContext = createContext(null);
|
|
54
|
+
const useZoom = makeUseContext(ZoomContext);
|
|
55
|
+
const ZoomInternalContext = createContext(null);
|
|
56
|
+
const useZoomInternal = makeUseContext(ZoomInternalContext);
|
|
57
|
+
function Zoom({ children }) {
|
|
58
|
+
const [zoom, setZoom] = useState(1);
|
|
59
|
+
const [offsetX, setOffsetX] = useState(0);
|
|
60
|
+
const [offsetY, setOffsetY] = useState(0);
|
|
59
61
|
const [rect, setRect] = useState();
|
|
60
62
|
const observer = useRef();
|
|
61
|
-
const
|
|
63
|
+
const carouselRef = useRef(null);
|
|
64
|
+
const { index, slides, zoom: { supports } = {} } = useLightboxContext();
|
|
65
|
+
const slide = slides[index];
|
|
66
|
+
const maxZoom = isImageSlide(slide) || (supports || []).includes(slide.type) ? 8 : 1;
|
|
67
|
+
useLayoutEffect(() => {
|
|
68
|
+
setZoom(1);
|
|
69
|
+
setOffsetX(0);
|
|
70
|
+
setOffsetY(0);
|
|
71
|
+
}, [index]);
|
|
72
|
+
useLayoutEffect(() => {
|
|
73
|
+
const carouselHalfWidth = (rect?.width || 0) / 2;
|
|
74
|
+
const carouselHalfHeight = (rect?.height || 0) / 2;
|
|
75
|
+
const [slideHalfWidth, slideHalfHeight] = Array.from(Array.from(carouselRef.current?.children || []).find((node) => node instanceof HTMLElement && !node.hidden)
|
|
76
|
+
?.children || [])
|
|
77
|
+
.filter((node) => node instanceof HTMLElement)
|
|
78
|
+
.map((node) => [
|
|
79
|
+
Math.max(carouselHalfWidth - node.offsetLeft, node.offsetLeft + node.offsetWidth - carouselHalfWidth),
|
|
80
|
+
Math.max(carouselHalfHeight - node.offsetTop, node.offsetTop + node.offsetHeight - carouselHalfHeight),
|
|
81
|
+
])
|
|
82
|
+
.reduce(([maxWidth, maxHeight], [width, height]) => [Math.max(width, maxWidth), Math.max(height, maxHeight)], [0, 0]);
|
|
83
|
+
const maxOffsetX = Math.max(slideHalfWidth * zoom - carouselHalfWidth, 0);
|
|
84
|
+
const maxOffsetY = Math.max(slideHalfHeight * zoom - carouselHalfHeight, 0);
|
|
85
|
+
setOffsetX(Math.min(maxOffsetX, Math.max(-maxOffsetX, offsetX)));
|
|
86
|
+
setOffsetY(Math.min(maxOffsetY, Math.max(-maxOffsetY, offsetY)));
|
|
87
|
+
}, [zoom, rect, offsetX, offsetY]);
|
|
88
|
+
const setCarouselRef = useCallback((node) => {
|
|
89
|
+
carouselRef.current = node;
|
|
62
90
|
observer.current?.disconnect();
|
|
63
91
|
observer.current = undefined;
|
|
64
92
|
const updateRect = () => setRect(node ? { width: node.clientWidth, height: node.clientHeight } : undefined);
|
|
@@ -70,15 +98,75 @@ function Carousel() {
|
|
|
70
98
|
updateRect();
|
|
71
99
|
}
|
|
72
100
|
}, []);
|
|
73
|
-
|
|
101
|
+
const changeOffsets = useCallback((dx, dy) => {
|
|
102
|
+
setOffsetX(offsetX + dx);
|
|
103
|
+
setOffsetY(offsetY + dy);
|
|
104
|
+
}, [offsetX, offsetY]);
|
|
105
|
+
const changeZoom = useCallback((targetZoom, event) => {
|
|
106
|
+
const newZoom = Math.min(Math.max(targetZoom, 1), maxZoom);
|
|
107
|
+
setZoom(newZoom);
|
|
108
|
+
if (event && carouselRef.current) {
|
|
109
|
+
const { clientX, clientY } = event;
|
|
110
|
+
const { left, top, width, height } = carouselRef.current.getBoundingClientRect();
|
|
111
|
+
const zoomDelta = newZoom / zoom - 1;
|
|
112
|
+
changeOffsets((left + width / 2 + offsetX - clientX) * zoomDelta, (top + height / 2 + offsetY - clientY) * zoomDelta);
|
|
113
|
+
}
|
|
114
|
+
}, [zoom, maxZoom, offsetX, offsetY, changeOffsets]);
|
|
115
|
+
return (jsx(ZoomContext.Provider, { value: useMemo(() => ({ rect, zoom, maxZoom, offsetX, offsetY, changeZoom, changeOffsets }), [rect, zoom, maxZoom, offsetX, offsetY, changeZoom, changeOffsets]), children: jsx(ZoomInternalContext.Provider, { value: useMemo(() => ({ carouselRef, setCarouselRef }), [setCarouselRef]), children: children }) }));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getImageDimensions(slide, rect) {
|
|
119
|
+
const { width, height } = slide.srcSet?.[0] || slide;
|
|
120
|
+
const imageAspectRatio = width && height ? width / height : undefined;
|
|
121
|
+
const rectAspectRatio = rect.width / rect.height;
|
|
122
|
+
return imageAspectRatio
|
|
123
|
+
? [
|
|
124
|
+
round(imageAspectRatio < rectAspectRatio ? imageAspectRatio * rect.height : rect.width, 2),
|
|
125
|
+
round(imageAspectRatio > rectAspectRatio ? rect.width / imageAspectRatio : rect.height, 2),
|
|
126
|
+
]
|
|
127
|
+
: [];
|
|
128
|
+
}
|
|
129
|
+
function ImageSlide({ slide, rect, zoom }) {
|
|
130
|
+
const [scale, setScale] = useState(1);
|
|
131
|
+
const persistScaleTimeout = useRef();
|
|
132
|
+
const { styles } = useLightboxContext();
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (zoom && zoom > scale) {
|
|
135
|
+
clearTimeout(persistScaleTimeout.current);
|
|
136
|
+
persistScaleTimeout.current = setTimeout(() => {
|
|
137
|
+
persistScaleTimeout.current = undefined;
|
|
138
|
+
setScale(zoom);
|
|
139
|
+
}, 300);
|
|
140
|
+
}
|
|
141
|
+
}, [zoom, scale]);
|
|
142
|
+
const srcSet = slide.srcSet
|
|
143
|
+
?.sort((a, b) => a.width - b.width)
|
|
144
|
+
.map((image) => `${image.src} ${image.width}w`)
|
|
145
|
+
.join(", ");
|
|
146
|
+
const [width, height] = getImageDimensions(slide, rect);
|
|
147
|
+
const sizes = width ? `${round(width * scale, 2)}px` : undefined;
|
|
148
|
+
return (jsx("img", { draggable: false, style: styles?.image, className: cssClass("slide_image"), srcSet: srcSet, sizes: sizes, width: width, height: height, src: slide.src, alt: slide.alt }));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function Carousel() {
|
|
152
|
+
const { slides, index, styles, render: { slide: renderSlide, slideHeader, slideFooter } = {} } = useLightboxContext();
|
|
153
|
+
const { rect, zoom, offsetX, offsetY } = useZoom();
|
|
154
|
+
const { setCarouselRef } = useZoomInternal();
|
|
155
|
+
return (jsx("div", { ref: setCarouselRef, style: styles?.carousel, className: cssClass("carousel"), children: rect &&
|
|
74
156
|
Array.from({ length: 5 }).map((_, i) => {
|
|
75
157
|
const slideIndex = index - 2 + i;
|
|
76
158
|
if (slideIndex < 0 || slideIndex >= slides.length)
|
|
77
159
|
return null;
|
|
78
160
|
const slide = slides[slideIndex];
|
|
79
161
|
const current = slideIndex === index;
|
|
80
|
-
const context = { slide, rect, current };
|
|
81
|
-
return (jsxs("div", { role: "group", "aria-roledescription": "slide", className: cssClass("slide"), hidden: !current, style:
|
|
162
|
+
const context = { slide, rect, current, zoom: round(current ? zoom : 1, 3) };
|
|
163
|
+
return (jsxs("div", { role: "group", "aria-roledescription": "slide", className: cssClass("slide"), hidden: !current, style: {
|
|
164
|
+
transform: current && zoom > 1
|
|
165
|
+
? `translateX(${round(offsetX, 3)}px) translateY(${round(offsetY, 3)}px) scale(${round(zoom, 3)})`
|
|
166
|
+
: undefined,
|
|
167
|
+
...styles?.slide,
|
|
168
|
+
}, children: [slideHeader?.(context), renderSlide?.(context) ??
|
|
169
|
+
(isImageSlide(slide) && jsx(ImageSlide, { ...context })), slideFooter?.(context)] }, slide.key ?? `${slideIndex}-${isImageSlide(slide) ? slide.src : undefined}`));
|
|
82
170
|
}) }));
|
|
83
171
|
}
|
|
84
172
|
|
|
@@ -142,11 +230,25 @@ function Navigation() {
|
|
|
142
230
|
return (jsxs(Fragment, { children: [slides.length > 1 && (jsxs(Fragment, { children: [jsx(Button, { label: "Previous", icon: Previous, renderIcon: iconPrev, onClick: prev, className: cssClass("button_prev"), disabled: index <= 0 }), jsx(Button, { label: "Next", icon: Next, renderIcon: iconNext, onClick: next, className: cssClass("button_next"), disabled: index >= slides.length - 1 })] })), controls?.()] }));
|
|
143
231
|
}
|
|
144
232
|
|
|
233
|
+
const WHEEL_ZOOM_FACTOR = 100;
|
|
234
|
+
const WHEEL_SWIPE_DISTANCE = 100;
|
|
235
|
+
const WHEEL_SWIPE_COOLDOWN_TIME = 1000;
|
|
236
|
+
const POINTER_SWIPE_DISTANCE = 100;
|
|
237
|
+
const KEYBOARD_ZOOM_FACTOR = 8 ** (1 / 4);
|
|
238
|
+
const KEYBOARD_MOVE_DISTANCE = 50;
|
|
239
|
+
const PINCH_ZOOM_DISTANCE_FACTOR = 100;
|
|
240
|
+
const PREVAILING_DIRECTION_FACTOR = 1.2;
|
|
241
|
+
function distance(pointerA, pointerB) {
|
|
242
|
+
return ((pointerA.clientX - pointerB.clientX) ** 2 + (pointerA.clientY - pointerB.clientY) ** 2) ** 0.5;
|
|
243
|
+
}
|
|
145
244
|
function useSensors() {
|
|
146
245
|
const wheelEvents = useRef([]);
|
|
147
246
|
const wheelCooldown = useRef(null);
|
|
148
247
|
const wheelCooldownMomentum = useRef(null);
|
|
149
|
-
const
|
|
248
|
+
const activePointers = useRef([]);
|
|
249
|
+
const pinchZoomDistance = useRef();
|
|
250
|
+
const { zoom, changeZoom, changeOffsets } = useZoom();
|
|
251
|
+
const { carouselRef } = useZoomInternal();
|
|
150
252
|
const { prev, next, close } = useController();
|
|
151
253
|
const { closeOnPullUp, closeOnPullDown, closeOnBackdropClick } = {
|
|
152
254
|
closeOnPullUp: true,
|
|
@@ -156,34 +258,94 @@ function useSensors() {
|
|
|
156
258
|
};
|
|
157
259
|
return useMemo(() => {
|
|
158
260
|
const onKeyDown = (event) => {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
261
|
+
const meta = event.getModifierState("Meta");
|
|
262
|
+
const preventDefault = () => event.preventDefault();
|
|
263
|
+
const handleChangeZoom = (newZoom) => {
|
|
264
|
+
preventDefault();
|
|
265
|
+
changeZoom(newZoom);
|
|
266
|
+
};
|
|
267
|
+
if (event.key === "+" || (meta && event.key === "="))
|
|
268
|
+
handleChangeZoom(zoom * KEYBOARD_ZOOM_FACTOR);
|
|
269
|
+
if (event.key === "-" || (meta && event.key === "_"))
|
|
270
|
+
handleChangeZoom(zoom / KEYBOARD_ZOOM_FACTOR);
|
|
271
|
+
if (meta && event.key === "0")
|
|
272
|
+
handleChangeZoom(1);
|
|
273
|
+
if (event.key === "Escape")
|
|
274
|
+
close();
|
|
275
|
+
if (zoom > 1) {
|
|
276
|
+
const move = (deltaX, deltaY) => {
|
|
277
|
+
preventDefault();
|
|
278
|
+
changeOffsets(deltaX, deltaY);
|
|
279
|
+
};
|
|
280
|
+
if (event.key === "ArrowUp")
|
|
281
|
+
move(0, KEYBOARD_MOVE_DISTANCE);
|
|
282
|
+
if (event.key === "ArrowDown")
|
|
283
|
+
move(0, -KEYBOARD_MOVE_DISTANCE);
|
|
284
|
+
if (event.key === "ArrowLeft")
|
|
285
|
+
move(KEYBOARD_MOVE_DISTANCE, 0);
|
|
286
|
+
if (event.key === "ArrowRight")
|
|
287
|
+
move(-KEYBOARD_MOVE_DISTANCE, 0);
|
|
288
|
+
return;
|
|
169
289
|
}
|
|
290
|
+
if (event.key === "ArrowLeft")
|
|
291
|
+
prev();
|
|
292
|
+
if (event.key === "ArrowRight")
|
|
293
|
+
next();
|
|
294
|
+
};
|
|
295
|
+
const removePointer = (event) => {
|
|
296
|
+
const pointers = activePointers.current;
|
|
297
|
+
pointers.splice(0, pointers.length, ...pointers.filter((pointer) => pointer.pointerId !== event.pointerId));
|
|
298
|
+
};
|
|
299
|
+
const addPointer = (event) => {
|
|
300
|
+
event.persist();
|
|
301
|
+
removePointer(event);
|
|
302
|
+
activePointers.current.push(event);
|
|
170
303
|
};
|
|
171
304
|
const onPointerDown = (event) => {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
305
|
+
const pointers = activePointers.current;
|
|
306
|
+
if (event.target instanceof Element &&
|
|
307
|
+
(event.target.classList.contains(cssClass("button")) ||
|
|
308
|
+
event.target.classList.contains(cssClass("icon")) ||
|
|
309
|
+
carouselRef.current?.parentElement?.querySelector(`.${cssClass("toolbar")}`)?.contains(event.target))) {
|
|
310
|
+
return;
|
|
175
311
|
}
|
|
176
|
-
|
|
177
|
-
|
|
312
|
+
addPointer(event);
|
|
313
|
+
if (pointers.length === 2) {
|
|
314
|
+
pinchZoomDistance.current = distance(pointers[0], pointers[1]);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const onPointerMove = (event) => {
|
|
318
|
+
const pointers = activePointers.current;
|
|
319
|
+
const activePointer = pointers.find((pointer) => pointer.pointerId === event.pointerId);
|
|
320
|
+
if (pointers.length === 2 && pinchZoomDistance.current) {
|
|
321
|
+
addPointer(event);
|
|
322
|
+
const currentDistance = distance(pointers[0], pointers[1]);
|
|
323
|
+
const delta = currentDistance - pinchZoomDistance.current;
|
|
324
|
+
if (Math.abs(delta) > 0) {
|
|
325
|
+
changeZoom(scaleZoom(zoom, delta, PINCH_ZOOM_DISTANCE_FACTOR), {
|
|
326
|
+
clientX: (pointers[0].clientX + pointers[1].clientX) / 2,
|
|
327
|
+
clientY: (pointers[0].clientY + pointers[1].clientY) / 2,
|
|
328
|
+
});
|
|
329
|
+
pinchZoomDistance.current = currentDistance;
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (zoom > 1 && activePointer) {
|
|
334
|
+
if (pointers.length === 1) {
|
|
335
|
+
changeOffsets(event.clientX - activePointer.clientX, event.clientY - activePointer.clientY);
|
|
336
|
+
}
|
|
337
|
+
addPointer(event);
|
|
178
338
|
}
|
|
179
339
|
};
|
|
180
340
|
const onPointerUp = (event) => {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
341
|
+
const pointers = activePointers.current;
|
|
342
|
+
const activePointer = pointers.find((pointer) => pointer.pointerId === event.pointerId);
|
|
343
|
+
if (activePointer && pointers.length === 1 && zoom === 1) {
|
|
344
|
+
const dx = event.clientX - activePointer.clientX;
|
|
345
|
+
const dy = event.clientY - activePointer.clientY;
|
|
184
346
|
const deltaX = Math.abs(dx);
|
|
185
347
|
const deltaY = Math.abs(dy);
|
|
186
|
-
if (deltaX >
|
|
348
|
+
if (deltaX > POINTER_SWIPE_DISTANCE && deltaX > PREVAILING_DIRECTION_FACTOR * deltaY) {
|
|
187
349
|
if (dx > 0) {
|
|
188
350
|
prev();
|
|
189
351
|
}
|
|
@@ -191,21 +353,33 @@ function useSensors() {
|
|
|
191
353
|
next();
|
|
192
354
|
}
|
|
193
355
|
}
|
|
194
|
-
else if ((deltaY >
|
|
356
|
+
else if ((deltaY > POINTER_SWIPE_DISTANCE &&
|
|
357
|
+
deltaY > PREVAILING_DIRECTION_FACTOR * deltaX &&
|
|
358
|
+
((closeOnPullUp && dy < 0) || (closeOnPullDown && dy > 0))) ||
|
|
195
359
|
(closeOnBackdropClick &&
|
|
196
|
-
activePointer.
|
|
197
|
-
Array.from(activePointer.
|
|
360
|
+
activePointer.target instanceof Element &&
|
|
361
|
+
Array.from(activePointer.target.classList).some((className) => [cssClass("slide"), cssClass("portal")].includes(className)))) {
|
|
198
362
|
close();
|
|
199
363
|
}
|
|
200
|
-
activePointer.current = null;
|
|
201
364
|
}
|
|
365
|
+
removePointer(event);
|
|
202
366
|
};
|
|
203
367
|
const onWheel = (event) => {
|
|
368
|
+
if (event.ctrlKey) {
|
|
369
|
+
if (Math.abs(event.deltaY) > Math.abs(event.deltaX)) {
|
|
370
|
+
changeZoom(scaleZoom(zoom, -event.deltaY, WHEEL_ZOOM_FACTOR), event);
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (zoom > 1) {
|
|
375
|
+
changeOffsets(-event.deltaX, -event.deltaY);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
204
378
|
if (wheelCooldown.current && wheelCooldownMomentum.current) {
|
|
205
379
|
if (event.deltaX * wheelCooldownMomentum.current > 0 &&
|
|
206
|
-
(event.timeStamp <= wheelCooldown.current +
|
|
207
|
-
(event.timeStamp <= wheelCooldown.current +
|
|
208
|
-
Math.abs(event.deltaX) <
|
|
380
|
+
(event.timeStamp <= wheelCooldown.current + WHEEL_SWIPE_COOLDOWN_TIME / 2 ||
|
|
381
|
+
(event.timeStamp <= wheelCooldown.current + WHEEL_SWIPE_COOLDOWN_TIME &&
|
|
382
|
+
Math.abs(event.deltaX) < PREVAILING_DIRECTION_FACTOR * Math.abs(wheelCooldownMomentum.current)))) {
|
|
209
383
|
wheelCooldownMomentum.current = event.deltaX;
|
|
210
384
|
return;
|
|
211
385
|
}
|
|
@@ -218,7 +392,7 @@ function useSensors() {
|
|
|
218
392
|
const dx = wheelEvents.current.map((e) => e.deltaX).reduce((a, b) => a + b, 0);
|
|
219
393
|
const deltaX = Math.abs(dx);
|
|
220
394
|
const deltaY = Math.abs(wheelEvents.current.map((e) => e.deltaY).reduce((a, b) => a + b, 0));
|
|
221
|
-
if (deltaX >
|
|
395
|
+
if (deltaX > WHEEL_SWIPE_DISTANCE && deltaX > PREVAILING_DIRECTION_FACTOR * deltaY) {
|
|
222
396
|
if (dx < 0) {
|
|
223
397
|
prev();
|
|
224
398
|
}
|
|
@@ -233,12 +407,24 @@ function useSensors() {
|
|
|
233
407
|
return {
|
|
234
408
|
onKeyDown,
|
|
235
409
|
onPointerDown,
|
|
410
|
+
onPointerMove,
|
|
236
411
|
onPointerUp,
|
|
237
412
|
onPointerLeave: onPointerUp,
|
|
238
413
|
onPointerCancel: onPointerUp,
|
|
239
414
|
onWheel,
|
|
240
415
|
};
|
|
241
|
-
}, [
|
|
416
|
+
}, [
|
|
417
|
+
prev,
|
|
418
|
+
next,
|
|
419
|
+
close,
|
|
420
|
+
zoom,
|
|
421
|
+
changeZoom,
|
|
422
|
+
changeOffsets,
|
|
423
|
+
carouselRef,
|
|
424
|
+
closeOnPullUp,
|
|
425
|
+
closeOnPullDown,
|
|
426
|
+
closeOnBackdropClick,
|
|
427
|
+
]);
|
|
242
428
|
}
|
|
243
429
|
|
|
244
430
|
function setAttribute(element, attribute, value) {
|
|
@@ -334,7 +520,7 @@ function Toolbar() {
|
|
|
334
520
|
function Lightbox({ slides, index, setIndex, ...rest }) {
|
|
335
521
|
if (!Array.isArray(slides) || index === undefined || index < 0 || index >= slides.length)
|
|
336
522
|
return null;
|
|
337
|
-
return (jsx(LightboxContextProvider, { slides, index, ...rest, children: jsx(Controller, { setIndex, children: jsxs(Portal, { children: [jsx(Toolbar, {}), jsx(Carousel, {}), jsx(Navigation, {})] }) }) }));
|
|
523
|
+
return (jsx(LightboxContextProvider, { slides, index, ...rest, children: jsx(Controller, { setIndex, children: jsx(Zoom, { children: jsxs(Portal, { children: [jsx(Toolbar, {}), jsx(Carousel, {}), jsx(Navigation, {})] }) }) }) }));
|
|
338
524
|
}
|
|
339
525
|
|
|
340
|
-
export { Lightbox as default };
|
|
526
|
+
export { Lightbox as default, useZoom as unstable_useZoom };
|
package/dist/styles.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
body:has(>.yarll__portal){overscroll-behavior:none}body:has(>.yarll__portal:not(.yarll__no_scroll_lock)){height:100%;overflow:hidden;padding-right:var(--yarll__scrollbar-width,0)}body:has(>.yarll__portal:not(.yarll__no_scroll_lock)) .yarll_fixed{padding-right:var(--yarll__scrollbar-width,0)}.yarll__portal{align-items:center;display:flex;flex-direction:column;inset:0;justify-content:center;outline:none;overflow:hidden;overscroll-behavior:none;position:fixed;touch-action:none;-moz-user-select:none;user-select:none;-webkit-user-select:none;z-index:var(--yarll__portal_zindex,9999);-webkit-touch-callout:none;background-color:var(--yarll__backdrop_color,#000);color:var(--yarll__color,#fff);opacity:1;transition:var(--yarll__fade_transition,opacity .3s ease)}.yarll__portal_closed{opacity:0}.yarll__portal *{box-sizing:border-box}.yarll__carousel{align-self:stretch;flex:1;margin:var(--yarll__carousel_margin,16px);position:relative}.yarll__slide{align-items:center;display:flex;flex-direction:column;inset:0;justify-content:center;position:absolute}.yarll__slide[hidden]{display:none}.yarll__slide_image{display:block;
|
|
1
|
+
body:has(>.yarll__portal){overscroll-behavior:none}body:has(>.yarll__portal:not(.yarll__no_scroll_lock)){height:100%;overflow:hidden;padding-right:var(--yarll__scrollbar-width,0)}body:has(>.yarll__portal:not(.yarll__no_scroll_lock)) .yarll_fixed{padding-right:var(--yarll__scrollbar-width,0)}.yarll__portal{align-items:center;display:flex;flex-direction:column;inset:0;justify-content:center;outline:none;overflow:hidden;overscroll-behavior:none;position:fixed;touch-action:none;-moz-user-select:none;user-select:none;-webkit-user-select:none;z-index:var(--yarll__portal_zindex,9999);-webkit-touch-callout:none;background-color:var(--yarll__backdrop_color,#000);color:var(--yarll__color,#fff);opacity:1;transition:var(--yarll__fade_transition,opacity .3s ease)}.yarll__portal_closed{opacity:0}.yarll__portal *{box-sizing:border-box}.yarll__carousel{align-self:stretch;flex:1;margin:var(--yarll__carousel_margin,16px);position:relative}.yarll__slide{align-items:center;display:flex;flex-direction:column;inset:0;justify-content:center;position:absolute}.yarll__slide[hidden]{display:none}.yarll__slide_image{display:block;max-height:100%;max-width:100%;min-height:0;min-width:0;-o-object-fit:contain;object-fit:contain}.yarll__toolbar{align-items:center;display:flex;position:absolute;right:var(--yarll__toolbar_margin,8px);top:var(--yarll__toolbar_margin,8px);z-index:1}.yarll__toolbar_fixed{align-self:flex-end;margin-inline-end:var(--yarll__toolbar_margin,8px);position:static;z-index:unset}.yarll__toolbar_fixed,.yarll__toolbar_fixed+.yarll__carousel{margin-block-start:var(--yarll__toolbar_margin,8px)}.yarll__button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--yarll__button_background_color,transparent);border:0;color:var(--yarll__button_color,var(--yarll__button_color,hsla(0,0%,100%,.8)));cursor:pointer;filter:var(--yarll__button_filter,drop-shadow(2px 2px 2px rgba(0,0,0,.8)));margin:0;padding:var(--yarll__button_padding,8px)}.yarll__button:focus-visible{box-shadow:var(--yarll__button_focus_box_shadow,0 0 0 4px #fff);color:var(--yarll__button_color_active,#fff);outline:var(--yarll__button_focus_outline,6px double #000)}@supports not selector(:focus-visible){.yarll__button:focus{box-shadow:var(--yarll__button_focus_box_shadow,0 0 0 4px #fff);color:var(--yarll__button_color_active,#fff);outline:var(--yarll__button_focus_outline,6px double #000)}}@media (hover:hover){.yarll__button:focus-visible:hover,.yarll__button:focus:hover,.yarll__button:hover{color:var(--yarll__button_color_active,#fff)}}.yarll__button_next,.yarll__button_prev{padding:var(--yarll__navigation_button_padding,24px 8px);position:absolute}.yarll__button_prev{left:8px}.yarll__button_next{right:8px}.yarll__button:disabled{color:var(--yarll__button_color_disabled,var(--yarll__button_color_disabled,hsla(0,0%,100%,.4)));cursor:default}.yarll__icon{display:block;height:var(--yarll__icon_size,32px);pointer-events:none;width:var(--yarll__icon_size,32px)}
|