yet-another-react-lightbox-lite 1.10.0 → 1.11.1
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 +107 -21
- package/dist/index.d.ts +6 -6
- package/dist/index.js +245 -228
- package/dist/styles.css +1 -1
- package/package.json +4 -8
- package/dist/styles.css.d.ts +0 -2
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
|
|
5
|
+
that provides essential lightbox features and slick UX with around 5KB bundle
|
|
6
6
|
size.
|
|
7
7
|
|
|
8
8
|
## Overview
|
|
@@ -12,7 +12,7 @@ size.
|
|
|
12
12
|
[](https://github.com/igordanchenko/yet-another-react-lightbox-lite/blob/main/LICENSE)
|
|
13
13
|
|
|
14
14
|
- **Built for React:** works with React 18+
|
|
15
|
-
- **UX:** supports keyboard, mouse, touchpad and touchscreen navigation
|
|
15
|
+
- **UX:** supports keyboard, mouse, touchpad, and touchscreen navigation
|
|
16
16
|
- **Zoom:** zoom is supported out of the box
|
|
17
17
|
- **Performance:** preloads a fixed number of images without compromising
|
|
18
18
|
performance or UX
|
|
@@ -20,7 +20,7 @@ size.
|
|
|
20
20
|
supported out of the box
|
|
21
21
|
- **Customization:** customize any UI element or add your own custom slides
|
|
22
22
|
- **No bloat:** supports only essential lightbox features
|
|
23
|
-
- **TypeScript:** type definitions come built-in
|
|
23
|
+
- **TypeScript:** type definitions come built-in with the package
|
|
24
24
|
|
|
25
25
|

|
|
26
26
|
|
|
@@ -91,6 +91,8 @@ To utilize responsive images with automatic resolution switching, provide
|
|
|
91
91
|
slides={[
|
|
92
92
|
{
|
|
93
93
|
src: "/image1x3840.jpg",
|
|
94
|
+
width: 3840,
|
|
95
|
+
height: 2560,
|
|
94
96
|
srcSet: [
|
|
95
97
|
{ src: "/image1x320.jpg", width: 320, height: 213 },
|
|
96
98
|
{ src: "/image1x640.jpg", width: 640, height: 427 },
|
|
@@ -112,7 +114,15 @@ advantage of the
|
|
|
112
114
|
[next/image](https://nextjs.org/docs/pages/api-reference/components/image)
|
|
113
115
|
component. The `next/image` component provides a more efficient way to handle
|
|
114
116
|
images in your Next.js project. You can replace the standard `<img>` element
|
|
115
|
-
with `next/image`
|
|
117
|
+
with `next/image` using the following `render.slide` render function.
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
declare module "yet-another-react-lightbox-lite" {
|
|
121
|
+
interface SlideImage {
|
|
122
|
+
blurDataURL?: string;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
116
126
|
|
|
117
127
|
```tsx
|
|
118
128
|
<Lightbox
|
|
@@ -140,7 +150,7 @@ with `next/image` with the following `render.slide` render function.
|
|
|
140
150
|
height={height}
|
|
141
151
|
loading="eager"
|
|
142
152
|
draggable={false}
|
|
143
|
-
blurDataURL={
|
|
153
|
+
blurDataURL={slide.blurDataURL}
|
|
144
154
|
style={{
|
|
145
155
|
minWidth: 0,
|
|
146
156
|
minHeight: 0,
|
|
@@ -158,7 +168,7 @@ with `next/image` with the following `render.slide` render function.
|
|
|
158
168
|
|
|
159
169
|
## API
|
|
160
170
|
|
|
161
|
-
Yet Another React Lightbox Lite comes with CSS stylesheet that needs to be
|
|
171
|
+
Yet Another React Lightbox Lite comes with a CSS stylesheet that needs to be
|
|
162
172
|
imported in your app.
|
|
163
173
|
|
|
164
174
|
```tsx
|
|
@@ -179,6 +189,10 @@ Image slide props:
|
|
|
179
189
|
|
|
180
190
|
- `src` - image source (required)
|
|
181
191
|
- `alt` - image `alt` attribute
|
|
192
|
+
- `width` - image width in pixels
|
|
193
|
+
- `height` - image height in pixels
|
|
194
|
+
- `srcSet` - alternative images for responsive resolution switching (see
|
|
195
|
+
[Responsive Images](#responsive-images))
|
|
182
196
|
|
|
183
197
|
### index
|
|
184
198
|
|
|
@@ -194,7 +208,7 @@ A callback to update current slide index state. This prop is required.
|
|
|
194
208
|
|
|
195
209
|
### labels
|
|
196
210
|
|
|
197
|
-
Type: `
|
|
211
|
+
Type: `object`
|
|
198
212
|
|
|
199
213
|
Custom UI labels / translations.
|
|
200
214
|
|
|
@@ -215,7 +229,8 @@ Type: `object`
|
|
|
215
229
|
|
|
216
230
|
Toolbar settings.
|
|
217
231
|
|
|
218
|
-
- `buttons` - custom toolbar buttons (type: `ReactNode[]`)
|
|
232
|
+
- `buttons` - custom toolbar buttons (type: `ReactNode[]`). Each button should
|
|
233
|
+
have a unique `key` attribute.
|
|
219
234
|
- `fixed` - if `true`, the toolbar is positioned statically above the carousel
|
|
220
235
|
|
|
221
236
|
Usage example:
|
|
@@ -226,13 +241,14 @@ Usage example:
|
|
|
226
241
|
fixed: true,
|
|
227
242
|
buttons: [
|
|
228
243
|
<button
|
|
244
|
+
key="custom-button"
|
|
229
245
|
type="button"
|
|
230
246
|
className="yarll__button"
|
|
231
247
|
onClick={() => {
|
|
232
248
|
// ...
|
|
233
249
|
}}
|
|
234
250
|
>
|
|
235
|
-
|
|
251
|
+
Download
|
|
236
252
|
</button>,
|
|
237
253
|
],
|
|
238
254
|
}}
|
|
@@ -247,7 +263,8 @@ Type: `object`
|
|
|
247
263
|
Carousel settings.
|
|
248
264
|
|
|
249
265
|
- `preload` - the lightbox preloads `(2 * preload + 1)` slides (default: `2`)
|
|
250
|
-
- `imageProps` - custom image slide attributes
|
|
266
|
+
- `imageProps` - custom image slide attributes (an object or a function
|
|
267
|
+
receiving the current slide and returning an object)
|
|
251
268
|
|
|
252
269
|
Usage example:
|
|
253
270
|
|
|
@@ -261,6 +278,17 @@ Usage example:
|
|
|
261
278
|
/>
|
|
262
279
|
```
|
|
263
280
|
|
|
281
|
+
You can also use a function to provide per-slide attributes:
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
<Lightbox
|
|
285
|
+
carousel={{
|
|
286
|
+
imageProps: (slide) => ({ "data-alt": slide.alt }),
|
|
287
|
+
}}
|
|
288
|
+
// ...
|
|
289
|
+
/>
|
|
290
|
+
```
|
|
291
|
+
|
|
264
292
|
### controller
|
|
265
293
|
|
|
266
294
|
Type: `object`
|
|
@@ -337,7 +365,7 @@ rendered right under the slide. Alternatively, you can use
|
|
|
337
365
|
`position: "absolute"` to position the extra elements relative to the slide.
|
|
338
366
|
|
|
339
367
|
For example, you can use the `slideFooter` render function to add slides
|
|
340
|
-
descriptions.
|
|
368
|
+
descriptions (see [Custom Slide Attributes](#custom-slide-attributes)).
|
|
341
369
|
|
|
342
370
|
```tsx
|
|
343
371
|
<Lightbox
|
|
@@ -355,11 +383,24 @@ descriptions.
|
|
|
355
383
|
Render custom controls or additional elements in the lightbox (use absolute
|
|
356
384
|
positioning).
|
|
357
385
|
|
|
358
|
-
For example, you can use the `render.controls` render function to implement
|
|
386
|
+
For example, you can use the `render.controls` render function to implement a
|
|
359
387
|
slides counter.
|
|
360
388
|
|
|
361
389
|
```tsx
|
|
390
|
+
const slides = [
|
|
391
|
+
{ src: "/image1.jpg" },
|
|
392
|
+
{ src: "/image2.jpg" },
|
|
393
|
+
{ src: "/image3.jpg" },
|
|
394
|
+
];
|
|
395
|
+
|
|
396
|
+
const [index, setIndex] = useState<number>();
|
|
397
|
+
|
|
398
|
+
// ...
|
|
399
|
+
|
|
362
400
|
<Lightbox
|
|
401
|
+
slides={slides}
|
|
402
|
+
index={index}
|
|
403
|
+
setIndex={setIndex}
|
|
363
404
|
render={{
|
|
364
405
|
controls: () =>
|
|
365
406
|
index !== undefined && (
|
|
@@ -368,8 +409,7 @@ slides counter.
|
|
|
368
409
|
</div>
|
|
369
410
|
),
|
|
370
411
|
}}
|
|
371
|
-
|
|
372
|
-
/>
|
|
412
|
+
/>;
|
|
373
413
|
```
|
|
374
414
|
|
|
375
415
|
#### iconPrev: () => ReactNode
|
|
@@ -386,7 +426,7 @@ Render custom `Close` icon.
|
|
|
386
426
|
|
|
387
427
|
### styles
|
|
388
428
|
|
|
389
|
-
Type: `
|
|
429
|
+
Type: `object`
|
|
390
430
|
|
|
391
431
|
Customization slots styles allow you to specify custom CSS styles or override
|
|
392
432
|
`--yarll__*` CSS variables by passing your custom styles through to the
|
|
@@ -441,6 +481,8 @@ Usage example:
|
|
|
441
481
|
## Custom Slide Attributes
|
|
442
482
|
|
|
443
483
|
You can add custom slide attributes with the following module augmentation.
|
|
484
|
+
Augmenting `GenericSlide` extends all slide types, including custom ones. To
|
|
485
|
+
extend only image slides, augment `SlideImage` instead.
|
|
444
486
|
|
|
445
487
|
```tsx
|
|
446
488
|
declare module "yet-another-react-lightbox-lite" {
|
|
@@ -450,12 +492,20 @@ declare module "yet-another-react-lightbox-lite" {
|
|
|
450
492
|
}
|
|
451
493
|
```
|
|
452
494
|
|
|
495
|
+
```tsx
|
|
496
|
+
declare module "yet-another-react-lightbox-lite" {
|
|
497
|
+
interface SlideImage {
|
|
498
|
+
blurDataURL?: string;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
453
503
|
## Custom Slides
|
|
454
504
|
|
|
455
505
|
You can add custom slide types through module augmentation and render them with
|
|
456
|
-
the `render.slide`
|
|
506
|
+
the `render.slide` function.
|
|
457
507
|
|
|
458
|
-
Here is an example demonstrating video
|
|
508
|
+
Here is an example demonstrating video slide support.
|
|
459
509
|
|
|
460
510
|
```tsx
|
|
461
511
|
declare module "yet-another-react-lightbox-lite" {
|
|
@@ -565,16 +615,16 @@ layout shift of some fixed-positioned page elements when the lightbox opens. To
|
|
|
565
615
|
address this, you can assign the `yarll__fixed` CSS class to your
|
|
566
616
|
fixed-positioned elements to keep them in place. Please note that the
|
|
567
617
|
fixed-positioned element container should not have its own border or padding
|
|
568
|
-
styles. If
|
|
569
|
-
|
|
618
|
+
styles. If it does, you can always add an extra wrapper that just defines the
|
|
619
|
+
fixed position without visual styles.
|
|
570
620
|
|
|
571
621
|
## Text Selection
|
|
572
622
|
|
|
573
623
|
The lightbox is rendered with the `user-select: none` CSS style. If you'd like
|
|
574
624
|
to make some of your custom elements user-selectable, use the
|
|
575
625
|
`yarll__selectable` CSS class. This class sets the `user-select: text` style and
|
|
576
|
-
turns off click-and-drag slide navigation, likely
|
|
577
|
-
selection UX.
|
|
626
|
+
turns off click-and-drag slide navigation, which would likely interfere with
|
|
627
|
+
text selection UX.
|
|
578
628
|
|
|
579
629
|
## Hooks
|
|
580
630
|
|
|
@@ -600,6 +650,42 @@ The hook provides an object with the following props:
|
|
|
600
650
|
- `changeZoom` - change zoom level
|
|
601
651
|
- `changeOffsets` - change position offsets
|
|
602
652
|
|
|
653
|
+
Usage example:
|
|
654
|
+
|
|
655
|
+
```tsx
|
|
656
|
+
function ZoomControls() {
|
|
657
|
+
const { zoom, maxZoom, changeZoom } = useZoom();
|
|
658
|
+
|
|
659
|
+
return (
|
|
660
|
+
<div style={{ position: "absolute", bottom: 16, left: 16 }}>
|
|
661
|
+
<button
|
|
662
|
+
type="button"
|
|
663
|
+
disabled={zoom >= maxZoom}
|
|
664
|
+
onClick={() => changeZoom(zoom * 2)}
|
|
665
|
+
>
|
|
666
|
+
Zoom In
|
|
667
|
+
</button>
|
|
668
|
+
<button
|
|
669
|
+
type="button"
|
|
670
|
+
disabled={zoom <= 1}
|
|
671
|
+
onClick={() => changeZoom(zoom / 2)}
|
|
672
|
+
>
|
|
673
|
+
Zoom Out
|
|
674
|
+
</button>
|
|
675
|
+
</div>
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
```tsx
|
|
681
|
+
<Lightbox
|
|
682
|
+
render={{
|
|
683
|
+
controls: () => <ZoomControls />,
|
|
684
|
+
}}
|
|
685
|
+
// ...
|
|
686
|
+
/>
|
|
687
|
+
```
|
|
688
|
+
|
|
603
689
|
## License
|
|
604
690
|
|
|
605
691
|
MIT © 2024 [Igor Danchenko](https://github.com/igordanchenko)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Key, Dispatch, SetStateAction, ReactNode,
|
|
1
|
+
import { ComponentProps, Key, Dispatch, SetStateAction, ReactNode, CSSProperties, MouseEvent } from 'react';
|
|
2
2
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
3
|
|
|
4
4
|
/** Lightbox props */
|
|
5
5
|
interface LightboxProps {
|
|
6
6
|
/** slides to display in the lightbox */
|
|
7
|
-
slides: Slide[];
|
|
7
|
+
slides: readonly Slide[];
|
|
8
8
|
/** slide index */
|
|
9
9
|
index: number | undefined;
|
|
10
10
|
/** slide index change callback */
|
|
@@ -55,7 +55,7 @@ interface SlideImage extends GenericSlide {
|
|
|
55
55
|
/** image 'alt' attribute */
|
|
56
56
|
alt?: string;
|
|
57
57
|
/** alternative images to be passed to the 'srcSet' */
|
|
58
|
-
srcSet?: ImageSource[];
|
|
58
|
+
srcSet?: readonly ImageSource[];
|
|
59
59
|
}
|
|
60
60
|
/** Image source */
|
|
61
61
|
interface ImageSource {
|
|
@@ -126,7 +126,7 @@ interface RenderSlideProps {
|
|
|
126
126
|
/** Toolbar settings */
|
|
127
127
|
interface ToolbarSettings {
|
|
128
128
|
/** custom toolbar buttons */
|
|
129
|
-
buttons?: ReactNode[];
|
|
129
|
+
buttons?: readonly ReactNode[];
|
|
130
130
|
/** if `true`, the toolbar is positioned statically above the carousel */
|
|
131
131
|
fixed?: boolean;
|
|
132
132
|
}
|
|
@@ -135,7 +135,7 @@ interface CarouselSettings {
|
|
|
135
135
|
/** the lightbox preloads (2 * preload + 1) slides */
|
|
136
136
|
preload?: number;
|
|
137
137
|
/** custom image slide attributes */
|
|
138
|
-
imageProps?: ComponentProps<"img"
|
|
138
|
+
imageProps?: ComponentProps<"img"> | ((slide: SlideImage) => ComponentProps<"img">);
|
|
139
139
|
}
|
|
140
140
|
/** Controller settings */
|
|
141
141
|
interface ControllerSettings {
|
|
@@ -151,7 +151,7 @@ interface ZoomSettings {
|
|
|
151
151
|
/** disable zoom on image slides */
|
|
152
152
|
disabled?: boolean;
|
|
153
153
|
/** zoom-enabled custom slide types */
|
|
154
|
-
supports?: SlideTypeKey[];
|
|
154
|
+
supports?: readonly SlideTypeKey[];
|
|
155
155
|
}
|
|
156
156
|
/** Customization slots */
|
|
157
157
|
interface SlotType {
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
|
-
import { useContext, createContext, useState, useEffect, useRef, useLayoutEffect, useCallback, useMemo
|
|
2
|
+
import { useContext, createContext, useState, useEffect, useRef, useLayoutEffect, useCallback, useMemo } from 'react';
|
|
3
3
|
import { flushSync, createPortal } from 'react-dom';
|
|
4
4
|
|
|
5
5
|
const cssPrefix = "yarll__";
|
|
@@ -10,7 +10,7 @@ function cssVar(name) {
|
|
|
10
10
|
return `--${cssPrefix}${name}`;
|
|
11
11
|
}
|
|
12
12
|
function clsx(...classes) {
|
|
13
|
-
return
|
|
13
|
+
return classes.filter(Boolean).join(" ");
|
|
14
14
|
}
|
|
15
15
|
function transition(callback) {
|
|
16
16
|
if (document.startViewTransition) {
|
|
@@ -49,7 +49,7 @@ function isImageSlide(slide) {
|
|
|
49
49
|
return (slide.type === undefined || slide.type === "image") && typeof slide.src === "string";
|
|
50
50
|
}
|
|
51
51
|
function getChildren(element) {
|
|
52
|
-
return element?.children || [];
|
|
52
|
+
return Array.from(element?.children || []);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
const LightboxContext = createContext(null);
|
|
@@ -71,7 +71,8 @@ function getImageDimensions(slide, rect) {
|
|
|
71
71
|
}
|
|
72
72
|
function ImageSlide({ slide, rect, zoom }) {
|
|
73
73
|
const [scale, setScale] = useState(1);
|
|
74
|
-
const { carousel: { imageProps } = {}, styles } = useLightboxContext();
|
|
74
|
+
const { carousel: { imageProps: imagePropsParam } = {}, styles } = useLightboxContext();
|
|
75
|
+
const imageProps = typeof imagePropsParam === "function" ? imagePropsParam(slide) : imagePropsParam;
|
|
75
76
|
useEffect(() => {
|
|
76
77
|
if (zoom <= scale)
|
|
77
78
|
return;
|
|
@@ -83,7 +84,8 @@ function ImageSlide({ slide, rect, zoom }) {
|
|
|
83
84
|
};
|
|
84
85
|
}, [zoom, scale]);
|
|
85
86
|
const srcSet = slide.srcSet
|
|
86
|
-
?.
|
|
87
|
+
?.slice()
|
|
88
|
+
.sort((a, b) => a.width - b.width)
|
|
87
89
|
.map((image) => `${image.src} ${image.width}w`)
|
|
88
90
|
.join(", ");
|
|
89
91
|
const [width, height] = getImageDimensions(slide, rect);
|
|
@@ -91,6 +93,14 @@ function ImageSlide({ slide, rect, zoom }) {
|
|
|
91
93
|
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 ?? "", ...imageProps }));
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
function useEventCallback(fn) {
|
|
97
|
+
const ref = useRef(fn);
|
|
98
|
+
useLayoutEffect(() => {
|
|
99
|
+
ref.current = fn;
|
|
100
|
+
});
|
|
101
|
+
return useCallback((...args) => ref.current(...args), []);
|
|
102
|
+
}
|
|
103
|
+
|
|
94
104
|
const ZoomContext = createContext(null);
|
|
95
105
|
const useZoom = makeUseContext(ZoomContext);
|
|
96
106
|
const ZoomInternalContext = createContext(null);
|
|
@@ -102,6 +112,8 @@ function Zoom({ children }) {
|
|
|
102
112
|
const [rect, setRect] = useState();
|
|
103
113
|
const observer = useRef(undefined);
|
|
104
114
|
const carouselRef = useRef(null);
|
|
115
|
+
const carouselRectRef = useRef(undefined);
|
|
116
|
+
const slideDimensionsRef = useRef([0, 0]);
|
|
105
117
|
const { index, slides, zoom: { supports, disabled } = {} } = useLightboxContext();
|
|
106
118
|
const [prevIndex, setPrevIndex] = useState(index);
|
|
107
119
|
if (index !== prevIndex) {
|
|
@@ -111,53 +123,61 @@ function Zoom({ children }) {
|
|
|
111
123
|
setPrevIndex(index);
|
|
112
124
|
}
|
|
113
125
|
const slide = slides[index];
|
|
114
|
-
const maxZoom = (isImageSlide(slide) && !disabled) || (
|
|
126
|
+
const maxZoom = slide && ((isImageSlide(slide) && !disabled) || (slide.type !== undefined && supports?.includes(slide.type)))
|
|
115
127
|
? 8
|
|
116
128
|
: 1;
|
|
129
|
+
const carouselHalfWidth = (rect?.width || 0) / 2;
|
|
130
|
+
const carouselHalfHeight = (rect?.height || 0) / 2;
|
|
131
|
+
const clampOffsets = useEventCallback((x = offsetX, y = offsetY, currentZoom = zoom) => {
|
|
132
|
+
const [slideHalfWidth, slideHalfHeight] = slideDimensionsRef.current;
|
|
133
|
+
const maxOffsetX = Math.max(slideHalfWidth * currentZoom - carouselHalfWidth, 0);
|
|
134
|
+
const maxOffsetY = Math.max(slideHalfHeight * currentZoom - carouselHalfHeight, 0);
|
|
135
|
+
setOffsetX(Math.min(maxOffsetX, Math.max(-maxOffsetX, x)));
|
|
136
|
+
setOffsetY(Math.min(maxOffsetY, Math.max(-maxOffsetY, y)));
|
|
137
|
+
});
|
|
117
138
|
useLayoutEffect(() => {
|
|
118
|
-
|
|
119
|
-
const carouselHalfHeight = (rect?.height || 0) / 2;
|
|
120
|
-
const [slideHalfWidth, slideHalfHeight] = Array.from(getChildren(Array.from(getChildren(carouselRef.current)).find((node) => node instanceof HTMLElement && !node.hidden)))
|
|
139
|
+
slideDimensionsRef.current = getChildren(getChildren(carouselRef.current).find((node) => node instanceof HTMLElement && !node.hidden))
|
|
121
140
|
.filter((node) => node instanceof HTMLElement)
|
|
122
141
|
.map((node) => [
|
|
123
142
|
Math.max(carouselHalfWidth - node.offsetLeft, node.offsetLeft + node.offsetWidth - carouselHalfWidth),
|
|
124
143
|
Math.max(carouselHalfHeight - node.offsetTop, node.offsetTop + node.offsetHeight - carouselHalfHeight),
|
|
125
144
|
])
|
|
126
145
|
.reduce(([maxWidth, maxHeight], [width, height]) => [Math.max(width, maxWidth), Math.max(height, maxHeight)], [0, 0]);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
setOffsetX(Math.min(maxOffsetX, Math.max(-maxOffsetX, offsetX)));
|
|
130
|
-
setOffsetY(Math.min(maxOffsetY, Math.max(-maxOffsetY, offsetY)));
|
|
131
|
-
}, [zoom, rect, offsetX, offsetY]);
|
|
146
|
+
clampOffsets();
|
|
147
|
+
}, [carouselHalfWidth, carouselHalfHeight, index, clampOffsets]);
|
|
132
148
|
const setCarouselRef = useCallback((node) => {
|
|
133
149
|
carouselRef.current = node;
|
|
134
150
|
observer.current?.disconnect();
|
|
135
151
|
observer.current = undefined;
|
|
136
|
-
const updateRect = () =>
|
|
152
|
+
const updateRect = () => {
|
|
153
|
+
carouselRectRef.current = node?.getBoundingClientRect();
|
|
154
|
+
setRect(node ? { width: node.clientWidth, height: node.clientHeight } : undefined);
|
|
155
|
+
};
|
|
156
|
+
updateRect();
|
|
137
157
|
if (node && typeof ResizeObserver !== "undefined") {
|
|
138
158
|
observer.current = new ResizeObserver(updateRect);
|
|
139
159
|
observer.current.observe(node);
|
|
140
160
|
}
|
|
141
|
-
else {
|
|
142
|
-
updateRect();
|
|
143
|
-
}
|
|
144
161
|
}, []);
|
|
145
|
-
const changeOffsets =
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const changeZoom = useCallback((targetZoom, event) => {
|
|
162
|
+
const changeOffsets = useEventCallback((dx, dy) => {
|
|
163
|
+
clampOffsets(offsetX + dx, offsetY + dy, zoom);
|
|
164
|
+
});
|
|
165
|
+
const changeZoom = useEventCallback((targetZoom, event) => {
|
|
150
166
|
const newZoom = Math.min(Math.max(targetZoom, 1), maxZoom);
|
|
151
167
|
setZoom(newZoom);
|
|
152
|
-
|
|
168
|
+
let newOffsetX = offsetX;
|
|
169
|
+
let newOffsetY = offsetY;
|
|
170
|
+
if (event && carouselRectRef.current) {
|
|
153
171
|
const { clientX, clientY } = event;
|
|
154
|
-
const { left, top, width, height } =
|
|
172
|
+
const { left, top, width, height } = carouselRectRef.current;
|
|
155
173
|
const zoomDelta = newZoom / zoom - 1;
|
|
156
|
-
|
|
174
|
+
newOffsetX += (left + width / 2 + offsetX - clientX) * zoomDelta;
|
|
175
|
+
newOffsetY += (top + height / 2 + offsetY - clientY) * zoomDelta;
|
|
157
176
|
}
|
|
158
|
-
|
|
177
|
+
clampOffsets(newOffsetX, newOffsetY, newZoom);
|
|
178
|
+
});
|
|
159
179
|
const context = useMemo(() => ({ rect, zoom, maxZoom, offsetX, offsetY, changeZoom, changeOffsets }), [rect, zoom, maxZoom, offsetX, offsetY, changeZoom, changeOffsets]);
|
|
160
|
-
const internalContext = useMemo(() => ({
|
|
180
|
+
const internalContext = useMemo(() => ({ setCarouselRef }), [setCarouselRef]);
|
|
161
181
|
return (jsx(ZoomContext.Provider, { value: context, children: jsx(ZoomInternalContext.Provider, { value: internalContext, children: children }) }));
|
|
162
182
|
}
|
|
163
183
|
|
|
@@ -186,10 +206,10 @@ function Carousel() {
|
|
|
186
206
|
return (jsx("div", { ref: setCarouselRef, style: styles?.carousel, className: cssClass("carousel"), role: "region", "aria-live": "polite", "aria-label": translateLabel(labels, "Photo gallery"), "aria-roledescription": translateLabel(labels, "Carousel"), children: rect &&
|
|
187
207
|
Array.from({ length: 2 * preload + 1 }).map((_, i) => {
|
|
188
208
|
const slideIndex = index - preload + i;
|
|
189
|
-
if (slideIndex < 0 || slideIndex >= slides.length)
|
|
190
|
-
return null;
|
|
191
209
|
const slide = slides[slideIndex];
|
|
192
|
-
|
|
210
|
+
if (!slide)
|
|
211
|
+
return null;
|
|
212
|
+
return (jsx(CarouselSlide, { rect: rect, slide: slide, slideIndex: slideIndex, current: slideIndex === index }, slide.key ?? (isImageSlide(slide) ? `${slideIndex}|${slide.src}` : `${slideIndex}`)));
|
|
193
213
|
}) }));
|
|
194
214
|
}
|
|
195
215
|
|
|
@@ -198,31 +218,38 @@ const useController = makeUseContext(ControllerContext);
|
|
|
198
218
|
function Controller({ setIndex, children }) {
|
|
199
219
|
const { slides, index } = useLightboxContext();
|
|
200
220
|
const exitHooks = useRef([]);
|
|
221
|
+
const closing = useRef(false);
|
|
222
|
+
const addExitHook = useCallback((hook) => {
|
|
223
|
+
exitHooks.current.push(hook);
|
|
224
|
+
return () => {
|
|
225
|
+
exitHooks.current = exitHooks.current.filter((h) => h !== hook);
|
|
226
|
+
};
|
|
227
|
+
}, []);
|
|
201
228
|
const context = useMemo(() => {
|
|
202
229
|
const prev = () => {
|
|
203
|
-
if (index > 0)
|
|
230
|
+
if (index > 0) {
|
|
204
231
|
transition(() => setIndex(index - 1));
|
|
232
|
+
}
|
|
205
233
|
};
|
|
206
234
|
const next = () => {
|
|
207
|
-
if (index < slides.length - 1)
|
|
235
|
+
if (index < slides.length - 1) {
|
|
208
236
|
transition(() => setIndex(index + 1));
|
|
237
|
+
}
|
|
209
238
|
};
|
|
210
239
|
const close = () => {
|
|
240
|
+
if (closing.current)
|
|
241
|
+
return;
|
|
242
|
+
closing.current = true;
|
|
211
243
|
Promise.all(exitHooks.current.map((hook) => hook()))
|
|
212
244
|
.catch(() => { })
|
|
213
245
|
.then(() => {
|
|
214
246
|
exitHooks.current = [];
|
|
215
|
-
|
|
247
|
+
closing.current = false;
|
|
248
|
+
setIndex(undefined);
|
|
216
249
|
});
|
|
217
250
|
};
|
|
218
|
-
const addExitHook = (hook) => {
|
|
219
|
-
exitHooks.current.push(hook);
|
|
220
|
-
return () => {
|
|
221
|
-
exitHooks.current.splice(exitHooks.current.indexOf(hook), 1);
|
|
222
|
-
};
|
|
223
|
-
};
|
|
224
251
|
return { prev, next, close, addExitHook };
|
|
225
|
-
}, [slides.length, index, setIndex]);
|
|
252
|
+
}, [slides.length, index, setIndex, addExitHook]);
|
|
226
253
|
return jsx(ControllerContext.Provider, { value: context, children: children });
|
|
227
254
|
}
|
|
228
255
|
|
|
@@ -232,14 +259,11 @@ function Button({ icon: Icon, renderIcon, label, onClick, disabled, className })
|
|
|
232
259
|
return (jsx("button", { type: "button", title: buttonLabel, "aria-label": buttonLabel, onClick: onClick, disabled: disabled, style: styles?.button, className: clsx(cssClass("button"), className), children: renderIcon?.() ?? jsx(Icon, { style: styles?.icon, className: cssClass("icon") }) }));
|
|
233
260
|
}
|
|
234
261
|
|
|
235
|
-
function
|
|
236
|
-
const icon = (props) => (jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24",
|
|
262
|
+
function createIcon(name, glyph) {
|
|
263
|
+
const icon = (props) => (jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", focusable: "false", "aria-hidden": true, ...props, children: glyph }));
|
|
237
264
|
icon.displayName = name;
|
|
238
265
|
return icon;
|
|
239
266
|
}
|
|
240
|
-
function createIcon(name, glyph) {
|
|
241
|
-
return svgIcon(name, jsxs("g", { fill: "currentColor", children: [jsx("path", { d: "M0 0h24v24H0z", fill: "none" }), glyph] }));
|
|
242
|
-
}
|
|
243
267
|
|
|
244
268
|
const Close = createIcon("Close", jsx("path", { d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" }));
|
|
245
269
|
|
|
@@ -256,13 +280,17 @@ function Navigation() {
|
|
|
256
280
|
const WHEEL_ZOOM_FACTOR = 100;
|
|
257
281
|
const WHEEL_SWIPE_DISTANCE = 100;
|
|
258
282
|
const WHEEL_SWIPE_COOLDOWN_TIME = 1000;
|
|
283
|
+
const WHEEL_EVENT_HISTORY_WINDOW = 3000;
|
|
259
284
|
const POINTER_SWIPE_DISTANCE = 100;
|
|
260
285
|
const KEYBOARD_ZOOM_FACTOR = 8 ** (1 / 4);
|
|
261
286
|
const KEYBOARD_MOVE_DISTANCE = 50;
|
|
262
287
|
const PINCH_ZOOM_DISTANCE_FACTOR = 100;
|
|
263
288
|
const PREVAILING_DIRECTION_FACTOR = 1.2;
|
|
289
|
+
function hasTwoPointers(pointers) {
|
|
290
|
+
return pointers.length === 2;
|
|
291
|
+
}
|
|
264
292
|
function distance(pointerA, pointerB) {
|
|
265
|
-
return (
|
|
293
|
+
return Math.hypot(pointerA.clientX - pointerB.clientX, pointerA.clientY - pointerB.clientY);
|
|
266
294
|
}
|
|
267
295
|
function useSensors() {
|
|
268
296
|
const wheelEvents = useRef([]);
|
|
@@ -271,7 +299,6 @@ function useSensors() {
|
|
|
271
299
|
const activePointers = useRef([]);
|
|
272
300
|
const pinchZoomDistance = useRef(undefined);
|
|
273
301
|
const { zoom, maxZoom, changeZoom, changeOffsets } = useZoom();
|
|
274
|
-
const { carouselRef } = useZoomInternal();
|
|
275
302
|
const { prev, next, close } = useController();
|
|
276
303
|
const { closeOnPullUp, closeOnPullDown, closeOnBackdropClick } = {
|
|
277
304
|
closeOnPullUp: true,
|
|
@@ -279,188 +306,171 @@ function useSensors() {
|
|
|
279
306
|
closeOnBackdropClick: true,
|
|
280
307
|
...useLightboxContext().controller,
|
|
281
308
|
};
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
309
|
+
const addPointer = (event) => {
|
|
310
|
+
removePointer(event);
|
|
311
|
+
activePointers.current.push(event);
|
|
312
|
+
};
|
|
313
|
+
const removePointer = (event) => {
|
|
314
|
+
activePointers.current = activePointers.current.filter((pointer) => pointer.pointerId !== event.pointerId);
|
|
315
|
+
};
|
|
316
|
+
const shouldIgnoreEvent = (event) => ("pointerType" in event && event.pointerType === "mouse" && event.buttons > 1) ||
|
|
317
|
+
(event.target instanceof Element &&
|
|
318
|
+
event.target.closest(`.${cssClass("button")}, .${cssClass("icon")}, .${cssClass("toolbar")}, .${cssClass("selectable")}`) !== null);
|
|
319
|
+
const onKeyDown = useEventCallback((event) => {
|
|
320
|
+
const { key, metaKey, ctrlKey } = event;
|
|
321
|
+
const meta = metaKey || ctrlKey;
|
|
322
|
+
const preventDefault = () => event.preventDefault();
|
|
323
|
+
const handleChangeZoom = (newZoom) => {
|
|
324
|
+
preventDefault();
|
|
325
|
+
changeZoom(newZoom);
|
|
326
|
+
};
|
|
327
|
+
if (key === "+" || (meta && key === "="))
|
|
328
|
+
handleChangeZoom(zoom * KEYBOARD_ZOOM_FACTOR);
|
|
329
|
+
if (key === "-" || (meta && key === "_"))
|
|
330
|
+
handleChangeZoom(zoom / KEYBOARD_ZOOM_FACTOR);
|
|
331
|
+
if (meta && key === "0")
|
|
332
|
+
handleChangeZoom(1);
|
|
333
|
+
if (key === "Escape")
|
|
334
|
+
close();
|
|
335
|
+
if (zoom > 1) {
|
|
336
|
+
const move = (deltaX, deltaY) => {
|
|
288
337
|
preventDefault();
|
|
289
|
-
|
|
338
|
+
changeOffsets(deltaX, deltaY);
|
|
290
339
|
};
|
|
291
|
-
if (key === "
|
|
292
|
-
|
|
293
|
-
if (key === "
|
|
294
|
-
|
|
295
|
-
if (meta && key === "0")
|
|
296
|
-
handleChangeZoom(1);
|
|
297
|
-
if (key === "Escape")
|
|
298
|
-
close();
|
|
299
|
-
if (zoom > 1) {
|
|
300
|
-
const move = (deltaX, deltaY) => {
|
|
301
|
-
preventDefault();
|
|
302
|
-
changeOffsets(deltaX, deltaY);
|
|
303
|
-
};
|
|
304
|
-
if (key === "ArrowUp")
|
|
305
|
-
move(0, KEYBOARD_MOVE_DISTANCE);
|
|
306
|
-
if (key === "ArrowDown")
|
|
307
|
-
move(0, -KEYBOARD_MOVE_DISTANCE);
|
|
308
|
-
if (key === "ArrowLeft")
|
|
309
|
-
move(KEYBOARD_MOVE_DISTANCE, 0);
|
|
310
|
-
if (key === "ArrowRight")
|
|
311
|
-
move(-KEYBOARD_MOVE_DISTANCE, 0);
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
340
|
+
if (key === "ArrowUp")
|
|
341
|
+
move(0, KEYBOARD_MOVE_DISTANCE);
|
|
342
|
+
if (key === "ArrowDown")
|
|
343
|
+
move(0, -KEYBOARD_MOVE_DISTANCE);
|
|
314
344
|
if (key === "ArrowLeft")
|
|
315
|
-
|
|
345
|
+
move(KEYBOARD_MOVE_DISTANCE, 0);
|
|
316
346
|
if (key === "ArrowRight")
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
347
|
+
move(-KEYBOARD_MOVE_DISTANCE, 0);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (key === "ArrowLeft") {
|
|
351
|
+
preventDefault();
|
|
352
|
+
prev();
|
|
353
|
+
}
|
|
354
|
+
if (key === "ArrowRight") {
|
|
355
|
+
preventDefault();
|
|
356
|
+
next();
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
const onPointerDown = useEventCallback((event) => {
|
|
360
|
+
if (shouldIgnoreEvent(event))
|
|
361
|
+
return;
|
|
362
|
+
addPointer(event);
|
|
363
|
+
if (hasTwoPointers(activePointers.current)) {
|
|
364
|
+
pinchZoomDistance.current = distance(activePointers.current[0], activePointers.current[1]);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
const onPointerMove = useEventCallback((event) => {
|
|
368
|
+
const activePointer = activePointers.current.find((pointer) => pointer.pointerId === event.pointerId);
|
|
369
|
+
if (!activePointer)
|
|
370
|
+
return;
|
|
371
|
+
if (hasTwoPointers(activePointers.current) && pinchZoomDistance.current) {
|
|
336
372
|
addPointer(event);
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
373
|
+
const currentDistance = distance(activePointers.current[0], activePointers.current[1]);
|
|
374
|
+
const delta = currentDistance - pinchZoomDistance.current;
|
|
375
|
+
if (Math.abs(delta) > 0) {
|
|
376
|
+
changeZoom(scaleZoom(zoom, delta, PINCH_ZOOM_DISTANCE_FACTOR), {
|
|
377
|
+
clientX: (activePointers.current[0].clientX + activePointers.current[1].clientX) / 2,
|
|
378
|
+
clientY: (activePointers.current[0].clientY + activePointers.current[1].clientY) / 2,
|
|
379
|
+
});
|
|
380
|
+
pinchZoomDistance.current = currentDistance;
|
|
340
381
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
return;
|
|
347
|
-
if (pointers.length === 2 && pinchZoomDistance.current) {
|
|
348
|
-
addPointer(event);
|
|
349
|
-
const currentDistance = distance(pointers[0], pointers[1]);
|
|
350
|
-
const delta = currentDistance - pinchZoomDistance.current;
|
|
351
|
-
if (Math.abs(delta) > 0) {
|
|
352
|
-
changeZoom(scaleZoom(zoom, delta, PINCH_ZOOM_DISTANCE_FACTOR), {
|
|
353
|
-
clientX: (pointers[0].clientX + pointers[1].clientX) / 2,
|
|
354
|
-
clientY: (pointers[0].clientY + pointers[1].clientY) / 2,
|
|
355
|
-
});
|
|
356
|
-
pinchZoomDistance.current = currentDistance;
|
|
357
|
-
}
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
if (zoom > 1) {
|
|
361
|
-
if (pointers.length === 1) {
|
|
362
|
-
changeOffsets(event.clientX - activePointer.clientX, event.clientY - activePointer.clientY);
|
|
363
|
-
}
|
|
364
|
-
addPointer(event);
|
|
365
|
-
}
|
|
366
|
-
};
|
|
367
|
-
const onPointerUp = (event) => {
|
|
368
|
-
const pointers = activePointers.current;
|
|
369
|
-
const activePointer = pointers.find((pointer) => pointer.pointerId === event.pointerId);
|
|
370
|
-
if (!activePointer)
|
|
371
|
-
return;
|
|
372
|
-
if (pointers.length === 1 && zoom === 1) {
|
|
373
|
-
const dx = event.clientX - activePointer.clientX;
|
|
374
|
-
const dy = event.clientY - activePointer.clientY;
|
|
375
|
-
const deltaX = Math.abs(dx);
|
|
376
|
-
const deltaY = Math.abs(dy);
|
|
377
|
-
if (deltaX > POINTER_SWIPE_DISTANCE && deltaX > PREVAILING_DIRECTION_FACTOR * deltaY) {
|
|
378
|
-
if (dx > 0) {
|
|
379
|
-
prev();
|
|
380
|
-
}
|
|
381
|
-
else {
|
|
382
|
-
next();
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
else if ((deltaY > POINTER_SWIPE_DISTANCE &&
|
|
386
|
-
deltaY > PREVAILING_DIRECTION_FACTOR * deltaX &&
|
|
387
|
-
((closeOnPullUp && dy < 0) || (closeOnPullDown && dy > 0))) ||
|
|
388
|
-
(closeOnBackdropClick &&
|
|
389
|
-
activePointer.target instanceof Element &&
|
|
390
|
-
Array.from(activePointer.target.classList).some((className) => [cssClass("slide"), cssClass("portal")].includes(className)))) {
|
|
391
|
-
close();
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
removePointer(event);
|
|
395
|
-
};
|
|
396
|
-
const onWheel = (event) => {
|
|
397
|
-
if (event.ctrlKey) {
|
|
398
|
-
if (Math.abs(event.deltaY) > Math.abs(event.deltaX)) {
|
|
399
|
-
changeZoom(scaleZoom(zoom, -event.deltaY, WHEEL_ZOOM_FACTOR), event);
|
|
400
|
-
}
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
if (zoom > 1) {
|
|
404
|
-
changeOffsets(-event.deltaX, -event.deltaY);
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
if (wheelCooldown.current && wheelCooldownMomentum.current) {
|
|
408
|
-
if (event.deltaX * wheelCooldownMomentum.current > 0 &&
|
|
409
|
-
(event.timeStamp <= wheelCooldown.current + WHEEL_SWIPE_COOLDOWN_TIME / 2 ||
|
|
410
|
-
(event.timeStamp <= wheelCooldown.current + WHEEL_SWIPE_COOLDOWN_TIME &&
|
|
411
|
-
Math.abs(event.deltaX) < PREVAILING_DIRECTION_FACTOR * Math.abs(wheelCooldownMomentum.current)))) {
|
|
412
|
-
wheelCooldownMomentum.current = event.deltaX;
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
wheelCooldown.current = null;
|
|
416
|
-
wheelCooldownMomentum.current = null;
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (zoom > 1) {
|
|
385
|
+
if (activePointers.current.length === 1) {
|
|
386
|
+
changeOffsets(event.clientX - activePointer.clientX, event.clientY - activePointer.clientY);
|
|
417
387
|
}
|
|
418
|
-
event
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
388
|
+
addPointer(event);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
const onPointerUp = useEventCallback((event) => {
|
|
392
|
+
const activePointer = activePointers.current.find((pointer) => pointer.pointerId === event.pointerId);
|
|
393
|
+
if (!activePointer)
|
|
394
|
+
return;
|
|
395
|
+
if (activePointers.current.length === 1 && zoom === 1) {
|
|
396
|
+
const dx = event.clientX - activePointer.clientX;
|
|
397
|
+
const dy = event.clientY - activePointer.clientY;
|
|
422
398
|
const deltaX = Math.abs(dx);
|
|
423
|
-
const deltaY = Math.abs(
|
|
424
|
-
if (deltaX >
|
|
425
|
-
if (dx
|
|
399
|
+
const deltaY = Math.abs(dy);
|
|
400
|
+
if (deltaX > POINTER_SWIPE_DISTANCE && deltaX > PREVAILING_DIRECTION_FACTOR * deltaY) {
|
|
401
|
+
if (dx > 0) {
|
|
426
402
|
prev();
|
|
427
403
|
}
|
|
428
404
|
else {
|
|
429
405
|
next();
|
|
430
406
|
}
|
|
431
|
-
wheelEvents.current = [];
|
|
432
|
-
wheelCooldown.current = event.timeStamp;
|
|
433
|
-
wheelCooldownMomentum.current = event.deltaX;
|
|
434
407
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
408
|
+
else if ((deltaY > POINTER_SWIPE_DISTANCE &&
|
|
409
|
+
deltaY > PREVAILING_DIRECTION_FACTOR * deltaX &&
|
|
410
|
+
((closeOnPullUp && dy < 0) || (closeOnPullDown && dy > 0))) ||
|
|
411
|
+
(closeOnBackdropClick &&
|
|
412
|
+
activePointer.target instanceof Element &&
|
|
413
|
+
(activePointer.target.classList.contains(cssClass("slide")) ||
|
|
414
|
+
activePointer.target.classList.contains(cssClass("portal"))))) {
|
|
415
|
+
close();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
removePointer(event);
|
|
419
|
+
});
|
|
420
|
+
const onWheel = useEventCallback((event) => {
|
|
421
|
+
if (event.ctrlKey) {
|
|
422
|
+
if (Math.abs(event.deltaY) > Math.abs(event.deltaX)) {
|
|
423
|
+
changeZoom(scaleZoom(zoom, -event.deltaY, WHEEL_ZOOM_FACTOR), event);
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (zoom > 1) {
|
|
428
|
+
changeOffsets(-event.deltaX, -event.deltaY);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (wheelCooldown.current && wheelCooldownMomentum.current) {
|
|
432
|
+
if (event.deltaX * wheelCooldownMomentum.current > 0 &&
|
|
433
|
+
(event.timeStamp <= wheelCooldown.current + WHEEL_SWIPE_COOLDOWN_TIME / 2 ||
|
|
434
|
+
(event.timeStamp <= wheelCooldown.current + WHEEL_SWIPE_COOLDOWN_TIME &&
|
|
435
|
+
Math.abs(event.deltaX) < PREVAILING_DIRECTION_FACTOR * Math.abs(wheelCooldownMomentum.current)))) {
|
|
436
|
+
wheelCooldownMomentum.current = event.deltaX;
|
|
438
437
|
return;
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
438
|
+
}
|
|
439
|
+
wheelCooldown.current = null;
|
|
440
|
+
wheelCooldownMomentum.current = null;
|
|
441
|
+
}
|
|
442
|
+
wheelEvents.current = wheelEvents.current.filter((e) => e.timeStamp > event.timeStamp - WHEEL_EVENT_HISTORY_WINDOW);
|
|
443
|
+
wheelEvents.current.push(event);
|
|
444
|
+
const dx = wheelEvents.current.map((e) => e.deltaX).reduce((a, b) => a + b, 0);
|
|
445
|
+
const deltaX = Math.abs(dx);
|
|
446
|
+
const deltaY = Math.abs(wheelEvents.current.map((e) => e.deltaY).reduce((a, b) => a + b, 0));
|
|
447
|
+
if (deltaX > WHEEL_SWIPE_DISTANCE && deltaX > PREVAILING_DIRECTION_FACTOR * deltaY) {
|
|
448
|
+
if (dx < 0) {
|
|
449
|
+
prev();
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
next();
|
|
453
|
+
}
|
|
454
|
+
wheelEvents.current = [];
|
|
455
|
+
wheelCooldown.current = event.timeStamp;
|
|
456
|
+
wheelCooldownMomentum.current = event.deltaX;
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
const onDoubleClick = useEventCallback((event) => {
|
|
460
|
+
if (shouldIgnoreEvent(event))
|
|
461
|
+
return;
|
|
462
|
+
changeZoom(zoom < maxZoom ? scaleZoom(zoom, 2, 1) : 1, event);
|
|
463
|
+
});
|
|
464
|
+
return useMemo(() => ({
|
|
465
|
+
onKeyDown,
|
|
466
|
+
onPointerDown,
|
|
467
|
+
onPointerMove,
|
|
468
|
+
onPointerUp,
|
|
469
|
+
onPointerLeave: onPointerUp,
|
|
470
|
+
onPointerCancel: onPointerUp,
|
|
471
|
+
onDoubleClick,
|
|
472
|
+
onWheel,
|
|
473
|
+
}), [onKeyDown, onPointerDown, onPointerMove, onPointerUp, onDoubleClick, onWheel]);
|
|
464
474
|
}
|
|
465
475
|
|
|
466
476
|
function setAttribute(element, attribute, value) {
|
|
@@ -480,6 +490,7 @@ function Portal({ children }) {
|
|
|
480
490
|
const cleanup = useRef([]);
|
|
481
491
|
const [mounted, setMounted] = useState(false);
|
|
482
492
|
const [visible, setVisible] = useState(false);
|
|
493
|
+
const portalRef = useRef(null);
|
|
483
494
|
const onTransitionEnd = useRef(undefined);
|
|
484
495
|
const restoreFocus = useRef(null);
|
|
485
496
|
const sensors = useSensors();
|
|
@@ -489,28 +500,36 @@ function Portal({ children }) {
|
|
|
489
500
|
cleanup.current = [];
|
|
490
501
|
}, []);
|
|
491
502
|
useEffect(() => addExitHook(() => new Promise((resolve) => {
|
|
492
|
-
|
|
503
|
+
const transitionDuration = (portalRef.current &&
|
|
504
|
+
Math.max(...getComputedStyle(portalRef.current).transitionDuration.split(",").map(parseFloat)) *
|
|
505
|
+
1000) ||
|
|
506
|
+
0;
|
|
507
|
+
const done = () => {
|
|
493
508
|
onTransitionEnd.current = undefined;
|
|
494
509
|
resolve();
|
|
495
510
|
};
|
|
511
|
+
const timeout = setTimeout(done, transitionDuration + 100);
|
|
512
|
+
onTransitionEnd.current = () => {
|
|
513
|
+
clearTimeout(timeout);
|
|
514
|
+
done();
|
|
515
|
+
};
|
|
496
516
|
handleCleanup();
|
|
497
517
|
setVisible(false);
|
|
498
518
|
})), [addExitHook, handleCleanup]);
|
|
499
519
|
useEffect(() => {
|
|
500
520
|
const property = cssVar("scrollbar-width");
|
|
501
521
|
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
502
|
-
if (scrollbarWidth
|
|
503
|
-
|
|
504
|
-
|
|
522
|
+
if (scrollbarWidth > 0) {
|
|
523
|
+
document.documentElement.style.setProperty(property, `${scrollbarWidth}px`);
|
|
524
|
+
}
|
|
525
|
+
setMounted(true);
|
|
505
526
|
return () => {
|
|
506
527
|
document.documentElement.style.removeProperty(property);
|
|
528
|
+
setMounted(false);
|
|
507
529
|
};
|
|
508
530
|
}, []);
|
|
509
|
-
useEffect(() => {
|
|
510
|
-
setMounted(true);
|
|
511
|
-
return () => setMounted(false);
|
|
512
|
-
}, []);
|
|
513
531
|
const handleRef = useCallback((node) => {
|
|
532
|
+
portalRef.current = node;
|
|
514
533
|
if (node) {
|
|
515
534
|
node.focus();
|
|
516
535
|
const preventWheelDefaults = (event) => event.preventDefault();
|
|
@@ -518,9 +537,7 @@ function Portal({ children }) {
|
|
|
518
537
|
cleanup.current.push(() => {
|
|
519
538
|
node.removeEventListener("wheel", preventWheelDefaults);
|
|
520
539
|
});
|
|
521
|
-
const
|
|
522
|
-
for (let i = 0; i < elements.length; i += 1) {
|
|
523
|
-
const element = elements[i];
|
|
540
|
+
for (const element of getChildren(node.parentElement)) {
|
|
524
541
|
if (!["TEMPLATE", "SCRIPT", "STYLE"].includes(element.tagName) && element !== node) {
|
|
525
542
|
cleanup.current.push(setAttribute(element, "inert", ""));
|
|
526
543
|
cleanup.current.push(setAttribute(element, "aria-hidden", "true"));
|
|
@@ -537,7 +554,7 @@ function Portal({ children }) {
|
|
|
537
554
|
}
|
|
538
555
|
}, [handleCleanup]);
|
|
539
556
|
return mounted
|
|
540
|
-
? createPortal(jsx("div", { "aria-modal": true, role: "dialog", "aria-label": translateLabel(labels, "Lightbox"), tabIndex: -1, ref: handleRef, style: styles?.portal, className: clsx(cssClass("portal"), !visible && cssClass("portal_closed"), className), onTransitionEnd: () => onTransitionEnd.current?.(), onFocus: (event) => {
|
|
557
|
+
? createPortal(jsx("div", { "aria-modal": true, role: "dialog", "aria-label": translateLabel(labels, "Lightbox"), tabIndex: -1, ref: handleRef, style: styles?.portal, className: clsx(cssClass("portal"), !visible && cssClass("portal_closed"), className), onTransitionEnd: (event) => event.target === portalRef.current && onTransitionEnd.current?.(), onFocus: (event) => {
|
|
541
558
|
if (!restoreFocus.current) {
|
|
542
559
|
restoreFocus.current = event.relatedTarget;
|
|
543
560
|
}
|
|
@@ -548,7 +565,7 @@ function Portal({ children }) {
|
|
|
548
565
|
function Toolbar() {
|
|
549
566
|
const { render: { iconClose } = {}, toolbar: { buttons, fixed } = {}, styles } = useLightboxContext();
|
|
550
567
|
const { close } = useController();
|
|
551
|
-
return (jsxs("div", { style: styles?.toolbar, className: clsx(cssClass("toolbar"), fixed && cssClass("toolbar_fixed")), children: [buttons
|
|
568
|
+
return (jsxs("div", { style: styles?.toolbar, className: clsx(cssClass("toolbar"), fixed && cssClass("toolbar_fixed")), children: [buttons, jsx(Button, { label: "Close", icon: Close, renderIcon: iconClose, onClick: close })] }));
|
|
552
569
|
}
|
|
553
570
|
|
|
554
571
|
function Lightbox({ slides, index, setIndex, ...rest }) {
|
package/dist/styles.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
html:has(body>.yarll__portal){overscroll-behavior:none}@media (prefers-reduced-motion:reduce){html:has(body>.yarll__portal)::view-transition-new(*),html:has(body>.yarll__portal)::view-transition-old(*){animation-duration:0s}}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;-webkit-user-select:none;-moz-user-select:none;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)}@media (prefers-reduced-motion:reduce){.yarll__portal{transition-duration:0s}}.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}.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,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,hsla(0,0%,100%,.4));cursor:default}.yarll__icon{display:block;fill:currentColor;height:var(--yarll__icon_size,32px);pointer-events:none;width:var(--yarll__icon_size,32px)}.yarll__selectable{-webkit-user-select:text;-moz-user-select:text;user-select:text}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yet-another-react-lightbox-lite",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.1",
|
|
4
4
|
"description": "Lightweight React lightbox component",
|
|
5
5
|
"author": "Igor Danchenko",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,10 +12,7 @@
|
|
|
12
12
|
"types": "./dist/index.d.ts",
|
|
13
13
|
"default": "./dist/index.js"
|
|
14
14
|
},
|
|
15
|
-
"./styles.css":
|
|
16
|
-
"types": "./dist/styles.css.d.ts",
|
|
17
|
-
"default": "./dist/styles.css"
|
|
18
|
-
}
|
|
15
|
+
"./styles.css": "./dist/styles.css"
|
|
19
16
|
},
|
|
20
17
|
"files": [
|
|
21
18
|
"dist"
|
|
@@ -24,13 +21,12 @@
|
|
|
24
21
|
"*.css"
|
|
25
22
|
],
|
|
26
23
|
"homepage": "https://github.com/igordanchenko/yet-another-react-lightbox-lite",
|
|
24
|
+
"funding": "https://github.com/sponsors/igordanchenko",
|
|
25
|
+
"bugs": "https://github.com/igordanchenko/yet-another-react-lightbox-lite/issues",
|
|
27
26
|
"repository": {
|
|
28
27
|
"type": "git",
|
|
29
28
|
"url": "git+https://github.com/igordanchenko/yet-another-react-lightbox-lite.git"
|
|
30
29
|
},
|
|
31
|
-
"bugs": {
|
|
32
|
-
"url": "https://github.com/igordanchenko/yet-another-react-lightbox-lite/issues"
|
|
33
|
-
},
|
|
34
30
|
"engines": {
|
|
35
31
|
"node": ">=18"
|
|
36
32
|
},
|
package/dist/styles.css.d.ts
DELETED