yet-another-react-lightbox 1.8.0 → 1.9.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 CHANGED
@@ -14,10 +14,11 @@ Modern React lightbox component. Performant, easy to use, customizable and exten
14
14
  - **Performance:** preloads limited number of images without compromising performance or UX
15
15
  - **Responsive:** responsive images with automatic resolution switching are supported out of the box
16
16
  - **Video:** video slides are supported via an optional plugin
17
+ - **Zoom:** image zoom is supported via an optional plugin
17
18
  - **Customization:** customize any UI element or add your own custom slides
18
19
  - **No bloat:** never bundle rarely used features; add optional features via plugins
19
- - **RTL:** compatible with RTL layout
20
20
  - **TypeScript:** type definitions come built-in in the package
21
+ - **RTL:** compatible with RTL layout
21
22
 
22
23
  ## Documentation
23
24
 
@@ -99,13 +100,14 @@ const App = () => {
99
100
  {
100
101
  src: "/image1x3840.jpg",
101
102
  alt: "image 1",
102
- aspectRatio: 3 / 2,
103
+ width: 3840,
104
+ height: 2560,
103
105
  srcSet: [
104
- { src: "/image1x320.jpg", width: 320 },
105
- { src: "/image1x640.jpg", width: 640 },
106
- { src: "/image1x1200.jpg", width: 1200 },
107
- { src: "/image1x2048.jpg", width: 2048 },
108
- { src: "/image1x3840.jpg", width: 3840 },
106
+ { src: "/image1x320.jpg", width: 320, height: 213 },
107
+ { src: "/image1x640.jpg", width: 640, height: 427 },
108
+ { src: "/image1x1200.jpg", width: 1200, height: 800 },
109
+ { src: "/image1x2048.jpg", width: 2048, height: 1365 },
110
+ { src: "/image1x3840.jpg", width: 3840, height: 2560 },
109
111
  ]
110
112
  },
111
113
  // ...
@@ -134,6 +136,7 @@ The following plugins come bundled in the package:
134
136
  - [Slideshow](https://yet-another-react-lightbox.vercel.app/plugins/slideshow) - adds slideshow autoplay feature
135
137
  - [Thumbnails](https://yet-another-react-lightbox.vercel.app/plugins/thumbnails) - adds thumbnails track
136
138
  - [Video](https://yet-another-react-lightbox.vercel.app/plugins/video) - adds support for video slides
139
+ - [Zoom](https://yet-another-react-lightbox.vercel.app/plugins/zoom) - adds image zoom feature
137
140
 
138
141
  ## License
139
142
 
@@ -4,4 +4,4 @@ export declare type IconButtonProps = Omit<React.DetailedHTMLProps<React.ButtonH
4
4
  icon: React.ElementType;
5
5
  renderIcon?: () => React.ReactNode;
6
6
  };
7
- export declare const IconButton: React.FC<IconButtonProps>;
7
+ export declare const IconButton: React.ForwardRefExoticComponent<Pick<IconButtonProps, "value" | "children" | "onPointerDown" | "onPointerMove" | "onPointerUp" | "onPointerLeave" | "onPointerCancel" | "onTouchStart" | "onTouchMove" | "onTouchEnd" | "onTouchCancel" | "onKeyDown" | "onKeyUp" | "onWheel" | "key" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "className" | "contentEditable" | "contextMenu" | "dir" | "draggable" | "hidden" | "id" | "lang" | "placeholder" | "slot" | "spellCheck" | "style" | "tabIndex" | "title" | "translate" | "radioGroup" | "role" | "about" | "datatype" | "inlist" | "prefix" | "property" | "resource" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "color" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "inputMode" | "is" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancelCapture" | "onTouchEndCapture" | "onTouchMoveCapture" | "onTouchStartCapture" | "onPointerDownCapture" | "onPointerMoveCapture" | "onPointerUpCapture" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeaveCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "form" | "label" | "autoFocus" | "disabled" | "formAction" | "formEncType" | "formMethod" | "formNoValidate" | "formTarget" | "name" | "icon" | "renderIcon"> & React.RefAttributes<HTMLButtonElement>>;
@@ -1,3 +1,4 @@
1
1
  import * as React from "react";
2
2
  import { clsx, cssClass } from "../utils.js";
3
- export const IconButton = ({ label, className, icon: Icon, renderIcon, onClick, ...rest }) => (React.createElement("button", { type: "button", "aria-label": label, className: clsx(cssClass("button"), className), onClick: onClick, ...rest }, renderIcon ? renderIcon() : React.createElement(Icon, { className: cssClass("icon") })));
3
+ export const IconButton = React.forwardRef(({ label, className, icon: Icon, renderIcon, onClick, ...rest }, ref) => (React.createElement("button", { ref: ref, type: "button", "aria-label": label, className: clsx(cssClass("button"), className), onClick: onClick, ...rest }, renderIcon ? renderIcon() : React.createElement(Icon, { className: cssClass("icon") }))));
4
+ IconButton.displayName = "IconButton";
@@ -3,9 +3,9 @@ import { adjustDevicePixelRatio, clsx, cssClass, hasWindow } from "../utils.js";
3
3
  import { useLatest } from "../hooks/index.js";
4
4
  import { useEvents } from "../contexts/index.js";
5
5
  import { ErrorIcon, LoadingIcon } from "./Icons.js";
6
- import { activeSlideStatus, SLIDE_STATUS_COMPLETE, SLIDE_STATUS_ERROR, SLIDE_STATUS_LOADING } from "../consts.js";
6
+ import { activeSlideStatus, SLIDE_STATUS_COMPLETE, SLIDE_STATUS_ERROR, SLIDE_STATUS_LOADING, } from "../consts.js";
7
7
  export const ImageSlide = ({ slide: image, offset, render, rect, imageFit }) => {
8
- var _a;
8
+ var _a, _b, _c, _d, _e, _f, _g;
9
9
  const [status, setStatus] = React.useState(SLIDE_STATUS_LOADING);
10
10
  const latestStatus = useLatest(status);
11
11
  const { publish } = useEvents();
@@ -40,32 +40,28 @@ export const ImageSlide = ({ slide: image, offset, render, rect, imageFit }) =>
40
40
  const onError = React.useCallback(() => {
41
41
  setStatus(SLIDE_STATUS_ERROR);
42
42
  }, []);
43
+ const cover = image.imageFit === "cover" || (image.imageFit !== "contain" && imageFit === "cover");
44
+ const nonInfinite = (value, fallback) => (Number.isFinite(value) ? value : fallback);
45
+ const maxWidth = adjustDevicePixelRatio(nonInfinite(Math.max(...((_b = (_a = image.srcSet) === null || _a === void 0 ? void 0 : _a.map((x) => x.width)) !== null && _b !== void 0 ? _b : []).concat(image.width ? [image.width] : [])), ((_c = imageRef.current) === null || _c === void 0 ? void 0 : _c.naturalWidth) || 0));
46
+ const maxHeight = adjustDevicePixelRatio(nonInfinite(Math.max(...((_e = (_d = image.srcSet) === null || _d === void 0 ? void 0 : _d.map((x) => x.height).filter((x) => Boolean(x))) !== null && _e !== void 0 ? _e : []).concat(image.height ? [image.height] : [])), (image.aspectRatio && maxWidth ? maxWidth / image.aspectRatio : (_f = imageRef.current) === null || _f === void 0 ? void 0 : _f.naturalHeight) || 0));
47
+ const style = maxWidth && maxHeight ? { maxWidth, maxHeight } : undefined;
48
+ const srcSet = (_g = image.srcSet) === null || _g === void 0 ? void 0 : _g.sort((a, b) => a.width - b.width).map((item) => `${item.src} ${item.width}w`).join(", ");
49
+ const estimateActualWidth = () => {
50
+ if (rect && !cover) {
51
+ if (image.width && image.height) {
52
+ return (rect.height / image.height) * image.width;
53
+ }
54
+ if (image.aspectRatio) {
55
+ return rect.height * image.aspectRatio;
56
+ }
57
+ }
58
+ return Number.MAX_VALUE;
59
+ };
60
+ const sizes = srcSet && rect && hasWindow()
61
+ ? `${Math.ceil((Math.min(estimateActualWidth(), rect.width) / window.innerWidth) * 100)}vw`
62
+ : undefined;
43
63
  return (React.createElement(React.Fragment, null,
44
- React.createElement("img", { ref: setImageRef, onLoad: onLoad, onError: onError, className: clsx(cssClass("slide_image"), cssClass("fullsize"), (image.imageFit === "cover" || (image.imageFit !== "contain" && imageFit === "cover")) &&
45
- cssClass("slide_image_cover"), status !== SLIDE_STATUS_COMPLETE && cssClass("slide_image_loading")), draggable: false, alt: image.alt, ...(image.srcSet
46
- ? {
47
- ...(rect && hasWindow()
48
- ? {
49
- sizes: `${Math.ceil((Math.min(image.aspectRatio ? rect.height * image.aspectRatio : Number.MAX_VALUE, rect.width) /
50
- window.innerWidth) *
51
- 100)}vw`,
52
- }
53
- : null),
54
- srcSet: image.srcSet
55
- .sort((a, b) => a.width - b.width)
56
- .map((item) => `${item.src} ${item.width}w`)
57
- .join(", "),
58
- style: {
59
- maxWidth: `${adjustDevicePixelRatio(Math.max(...image.srcSet.map((x) => x.width)))}px`,
60
- },
61
- }
62
- : {
63
- style: imageRef.current && ((_a = imageRef.current) === null || _a === void 0 ? void 0 : _a.naturalWidth) > 0
64
- ? {
65
- maxWidth: `${adjustDevicePixelRatio(imageRef.current.naturalWidth)}px`,
66
- }
67
- : undefined,
68
- }), src: image.src }),
64
+ React.createElement("img", { ref: setImageRef, onLoad: onLoad, onError: onError, className: clsx(cssClass("slide_image"), cssClass("fullsize"), cover && cssClass("slide_image_cover"), status !== SLIDE_STATUS_COMPLETE && cssClass("slide_image_loading")), draggable: false, alt: image.alt, style: style, sizes: sizes, srcSet: srcSet, src: image.src }),
69
65
  status !== SLIDE_STATUS_COMPLETE && (React.createElement("div", { className: cssClass("slide_placeholder") },
70
66
  status === SLIDE_STATUS_LOADING &&
71
67
  ((render === null || render === void 0 ? void 0 : render.iconLoading) ? (render.iconLoading()) : (React.createElement(LoadingIcon, { className: clsx(cssClass("icon"), cssClass("slide_loading")) }))),
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import { makeUseContext } from "../utils.js";
2
+ import { isDefined, makeUseContext } from "../utils.js";
3
3
  const TimeoutsContext = React.createContext(null);
4
4
  export const useTimeouts = makeUseContext("useTimeouts", "TimeoutsContext", TimeoutsContext);
5
5
  export const TimeoutsProvider = ({ children }) => {
@@ -16,7 +16,7 @@ export const TimeoutsProvider = ({ children }) => {
16
16
  return id;
17
17
  };
18
18
  const clearTimeout = (id) => {
19
- if (typeof id !== "undefined") {
19
+ if (isDefined(id)) {
20
20
  removeTimeout(id);
21
21
  window.clearTimeout(id);
22
22
  }
@@ -1,4 +1,6 @@
1
1
  export * from "./useContainerRect.js";
2
2
  export * from "./useEnhancedEffect.js";
3
3
  export * from "./useLatest.js";
4
+ export * from "./useMotionPreference.js";
5
+ export * from "./useRTL.js";
4
6
  export * from "./useSensors.js";
@@ -1,4 +1,6 @@
1
1
  export * from "./useContainerRect.js";
2
2
  export * from "./useEnhancedEffect.js";
3
3
  export * from "./useLatest.js";
4
+ export * from "./useMotionPreference.js";
5
+ export * from "./useRTL.js";
4
6
  export * from "./useSensors.js";
@@ -0,0 +1 @@
1
+ export declare const useMotionPreference: () => boolean;
@@ -0,0 +1,12 @@
1
+ import * as React from "react";
2
+ import { useEnhancedEffect } from "./useEnhancedEffect.js";
3
+ export const useMotionPreference = () => {
4
+ const [reduceMotion, setReduceMotion] = React.useState(false);
5
+ useEnhancedEffect(() => {
6
+ var _a;
7
+ const mediaQuery = (_a = window.matchMedia) === null || _a === void 0 ? void 0 : _a.call(window, "(prefers-reduced-motion: reduce)");
8
+ mediaQuery === null || mediaQuery === void 0 ? void 0 : mediaQuery.addEventListener("change", () => setReduceMotion(mediaQuery.matches));
9
+ setReduceMotion(mediaQuery === null || mediaQuery === void 0 ? void 0 : mediaQuery.matches);
10
+ }, []);
11
+ return reduceMotion;
12
+ };
@@ -0,0 +1 @@
1
+ export declare const useRTL: () => boolean;
@@ -0,0 +1,9 @@
1
+ import * as React from "react";
2
+ import { useEnhancedEffect } from "./useEnhancedEffect.js";
3
+ export const useRTL = () => {
4
+ const [isRTL, setIsRTL] = React.useState(false);
5
+ useEnhancedEffect(() => {
6
+ setIsRTL(window.getComputedStyle(window.document.documentElement).direction === "rtl");
7
+ }, []);
8
+ return isRTL;
9
+ };
@@ -4,7 +4,10 @@ export const useSensors = () => {
4
4
  return React.useMemo(() => {
5
5
  const notifySubscribers = (type, event) => {
6
6
  var _a;
7
- (_a = subscribers[type]) === null || _a === void 0 ? void 0 : _a.forEach((listener) => listener(event));
7
+ (_a = subscribers[type]) === null || _a === void 0 ? void 0 : _a.forEach((listener) => {
8
+ if (!event.isPropagationStopped())
9
+ listener(event);
10
+ });
8
11
  };
9
12
  return {
10
13
  registerSensors: {
@@ -25,7 +28,7 @@ export const useSensors = () => {
25
28
  if (!subscribers[type]) {
26
29
  subscribers[type] = [];
27
30
  }
28
- subscribers[type].push(callback);
31
+ subscribers[type].unshift(callback);
29
32
  return () => {
30
33
  const listeners = subscribers[type];
31
34
  if (listeners) {
@@ -1,14 +1,16 @@
1
1
  import * as React from "react";
2
2
  import { Component, ComponentProps } from "../../types.js";
3
- import { SubscribeSensors } from "../hooks/index.js";
3
+ import { ContainerRect, SubscribeSensors } from "../hooks/index.js";
4
4
  declare type ControllerState = {
5
5
  currentIndex: number;
6
6
  globalIndex: number;
7
- isRTL: boolean;
8
7
  };
9
8
  export declare type ControllerContextType = ControllerState & {
10
9
  latestProps: React.MutableRefObject<ComponentProps>;
11
10
  subscribeSensors: SubscribeSensors<HTMLDivElement>;
11
+ transferFocus: () => void;
12
+ containerRect: ContainerRect;
13
+ containerRef: React.RefObject<HTMLDivElement>;
12
14
  };
13
15
  export declare const useController: () => ControllerContextType;
14
16
  export declare const Controller: Component;
@@ -1,21 +1,21 @@
1
1
  import * as React from "react";
2
2
  import { LightboxDefaultProps } from "../../types.js";
3
- import { cleanup, clsx, cssClass, cssVar, isRTL, makeUseContext } from "../utils.js";
3
+ import { cleanup, clsx, cssClass, cssVar, makeUseContext } from "../utils.js";
4
4
  import { createModule } from "../config.js";
5
- import { useContainerRect, useEnhancedEffect, useLatest, useSensors } from "../hooks/index.js";
5
+ import { useContainerRect, useEnhancedEffect, useLatest, useRTL, useSensors, } from "../hooks/index.js";
6
6
  import { useEvents, useTimeouts } from "../contexts/index.js";
7
7
  const SWIPE_OFFSET_THRESHOLD = 30;
8
8
  const ControllerContext = React.createContext(null);
9
9
  export const useController = makeUseContext("useController", "ControllerContext", ControllerContext);
10
10
  export const Controller = ({ children, ...props }) => {
11
- const { containerRef, setContainerRef } = useContainerRect();
11
+ const { containerRef, setContainerRef, containerRect } = useContainerRect();
12
12
  const { registerSensors, subscribeSensors } = useSensors();
13
13
  const { subscribe, publish } = useEvents();
14
14
  const { setTimeout, clearTimeout } = useTimeouts();
15
+ const isRTL = useLatest(useRTL());
15
16
  const [state, setState] = React.useState({
16
17
  currentIndex: props.index,
17
18
  globalIndex: props.index,
18
- isRTL: false,
19
19
  });
20
20
  const latestProps = useLatest(props);
21
21
  const refs = React.useRef({
@@ -31,7 +31,7 @@ export const Controller = ({ children, ...props }) => {
31
31
  refs.current.props = props;
32
32
  useEnhancedEffect(() => {
33
33
  const preventDefault = (event) => {
34
- if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) {
34
+ if (Math.abs(event.deltaX) > Math.abs(event.deltaY) || event.ctrlKey) {
35
35
  event.preventDefault();
36
36
  }
37
37
  };
@@ -45,12 +45,6 @@ export const Controller = ({ children, ...props }) => {
45
45
  }
46
46
  };
47
47
  }, [containerRef]);
48
- useEnhancedEffect(() => {
49
- const node = containerRef.current;
50
- if (node) {
51
- setState((prev) => ({ ...prev, isRTL: isRTL(node) }));
52
- }
53
- }, [containerRef]);
54
48
  React.useEffect(() => {
55
49
  var _a;
56
50
  if (refs.current.props.controller.focus) {
@@ -87,7 +81,7 @@ export const Controller = ({ children, ...props }) => {
87
81
  clearTimeout(current.swipeIntentCleanup);
88
82
  current.swipeIntentCleanup = undefined;
89
83
  }, [clearTimeout]);
90
- const rtl = React.useCallback((value) => (refs.current.state.isRTL ? -1 : 1) * (typeof value === "number" ? value : 1), [refs]);
84
+ const rtl = React.useCallback((value) => (isRTL.current ? -1 : 1) * (typeof value === "number" ? value : 1), [isRTL]);
91
85
  const isSwipeValid = React.useCallback((offset) => {
92
86
  const { state: { currentIndex }, props: { carousel, slides }, } = refs.current;
93
87
  return !(carousel.finite &&
@@ -275,13 +269,24 @@ export const Controller = ({ children, ...props }) => {
275
269
  }
276
270
  }, [updateSwipeOffset, setTimeout, clearTimeout, swipe, resetSwipe, rerender, isSwipeValid, containerRef]);
277
271
  React.useEffect(() => subscribeSensors("onWheel", onWheel), [subscribeSensors, onWheel]);
272
+ const transferFocus = React.useCallback(() => { var _a; return (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, [containerRef]);
278
273
  const context = React.useMemo(() => ({
279
274
  latestProps,
280
275
  currentIndex: state.currentIndex,
281
276
  globalIndex: state.globalIndex,
282
- isRTL: state.isRTL,
283
277
  subscribeSensors,
284
- }), [latestProps, state.currentIndex, state.globalIndex, state.isRTL, subscribeSensors]);
278
+ transferFocus,
279
+ containerRect,
280
+ containerRef,
281
+ }), [
282
+ latestProps,
283
+ state.currentIndex,
284
+ state.globalIndex,
285
+ subscribeSensors,
286
+ transferFocus,
287
+ containerRect,
288
+ containerRef,
289
+ ]);
285
290
  return (React.createElement("div", { ref: setContainerRef, className: clsx(cssClass("container"), cssClass("fullsize"), refs.current.swipeState === "swipe" && cssClass("container_swipe")), style: {
286
291
  ...(refs.current.swipeAnimationDuration !== LightboxDefaultProps.animation.swipe
287
292
  ? {
@@ -293,7 +298,6 @@ export const Controller = ({ children, ...props }) => {
293
298
  [cssVar("controller_touch_action")]: props.controller.touchAction,
294
299
  }
295
300
  : null),
296
- }, role: "presentation", "aria-live": "polite", tabIndex: -1, ...registerSensors },
297
- React.createElement(ControllerContext.Provider, { value: context }, children)));
301
+ }, role: "presentation", "aria-live": "polite", tabIndex: -1, ...registerSensors }, containerRect && (React.createElement(ControllerContext.Provider, { value: context }, children))));
298
302
  };
299
303
  export const ControllerModule = createModule("controller", Controller);
@@ -4,18 +4,20 @@ import { cssClass, label as translateLabel } from "../utils.js";
4
4
  import { IconButton, NextIcon, PreviousIcon } from "../components/index.js";
5
5
  import { useEvents } from "../contexts/index.js";
6
6
  import { useController } from "./Controller.js";
7
+ import { useLatest, useRTL } from "../hooks/index.js";
7
8
  export const NavigationButton = ({ publish, labels, label, icon, renderIcon, action, disabled, }) => (React.createElement(IconButton, { label: translateLabel(labels, label), icon: icon, renderIcon: renderIcon, className: cssClass(`navigation_${action}`), disabled: disabled, "aria-disabled": disabled, onClick: () => {
8
9
  publish(action);
9
10
  } }));
10
11
  export const Navigation = ({ slides, carousel: { finite }, labels, render: { buttonPrev, buttonNext, iconPrev, iconNext }, }) => {
11
- const { currentIndex, subscribeSensors, isRTL } = useController();
12
+ const { currentIndex, subscribeSensors } = useController();
12
13
  const { publish } = useEvents();
13
- React.useEffect(() => subscribeSensors("onKeyUp", (event) => {
14
- if (event.code === "ArrowLeft") {
15
- publish(isRTL ? "next" : "prev");
14
+ const isRTL = useLatest(useRTL());
15
+ React.useEffect(() => subscribeSensors("onKeyDown", (event) => {
16
+ if (event.key === "ArrowLeft") {
17
+ publish(isRTL.current ? "next" : "prev");
16
18
  }
17
- else if (event.code === "ArrowRight") {
18
- publish(isRTL ? "prev" : "next");
19
+ else if (event.key === "ArrowRight") {
20
+ publish(isRTL.current ? "prev" : "next");
19
21
  }
20
22
  }), [subscribeSensors, publish, isRTL]);
21
23
  return (React.createElement(React.Fragment, null,
@@ -7,5 +7,6 @@ export declare const label: (labels: Labels | undefined, lbl: string) => string;
7
7
  export declare const cleanup: (...cleaners: (() => void)[]) => () => void;
8
8
  export declare const makeUseContext: <T>(name: string, contextName: string, context: React.Context<T | null>) => () => T;
9
9
  export declare const hasWindow: () => boolean;
10
+ export declare const isDefined: <T = any>(x: T | undefined) => x is T;
10
11
  export declare const adjustDevicePixelRatio: (value: number) => number;
11
- export declare const isRTL: (node: HTMLElement) => boolean;
12
+ export declare const round: (value: number, decimals?: number) => number;
@@ -17,5 +17,9 @@ export const makeUseContext = (name, contextName, context) => () => {
17
17
  return ctx;
18
18
  };
19
19
  export const hasWindow = () => typeof window !== "undefined";
20
+ export const isDefined = (x) => typeof x !== "undefined";
20
21
  export const adjustDevicePixelRatio = (value) => hasWindow() ? Math.round(value / (window.devicePixelRatio || 1)) : value;
21
- export const isRTL = (node) => window.getComputedStyle(node).direction === "rtl";
22
+ export const round = (value, decimals = 0) => {
23
+ const factor = 10 ** decimals;
24
+ return Math.round((value + Number.EPSILON) * factor) / factor;
25
+ };
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import { cssClass, cssVar, makeUseContext } from "../core/utils.js";
2
+ import { cssClass, cssVar, isDefined, makeUseContext } from "../core/utils.js";
3
3
  import { useEvents } from "../core/contexts/Events.js";
4
4
  import { createModule } from "../core/index.js";
5
5
  const defaultTextAlign = "start";
@@ -27,7 +27,7 @@ export const CaptionsComponent = ({ children }) => {
27
27
  const { subscribe } = useEvents();
28
28
  const [toolbarWidth, setToolbarWidth] = React.useState();
29
29
  React.useEffect(() => subscribe("toolbar-width", (topic, event) => {
30
- if (typeof event === "undefined" || typeof event === "number") {
30
+ if (!isDefined(event) || typeof event === "number") {
31
31
  setToolbarWidth(event);
32
32
  }
33
33
  }), [subscribe]);
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import { Component, LightboxProps, Plugin, Render } from "../types.js";
2
+ import { Component, LightboxProps, Plugin } from "../types.js";
3
3
  declare module "../types.js" {
4
4
  interface LightboxProps {
5
5
  /** if `true`, enter fullscreen mode automatically when the lightbox opens */
@@ -37,9 +37,8 @@ declare global {
37
37
  }
38
38
  export declare const FullscreenContainer: Component;
39
39
  /** Fullscreen button props */
40
- export declare type FullscreenButtonProps = Pick<LightboxProps, "labels"> & {
40
+ export declare type FullscreenButtonProps = Pick<LightboxProps, "labels" | "render"> & {
41
41
  auto: boolean;
42
- render: Render;
43
42
  };
44
43
  /** Fullscreen button */
45
44
  export declare const FullscreenButton: React.FC<FullscreenButtonProps>;
@@ -1,7 +1,7 @@
1
1
  import * as React from "react";
2
- import { Component, LightboxProps, Plugin } from "../types.js";
2
+ import { Component, DeepNonNullable, LightboxProps, Plugin } from "../types.js";
3
3
  import { ContainerRect } from "../core/index.js";
4
- declare type Position = "top" | "bottom" | "start" | "end";
4
+ export declare type Position = "top" | "bottom" | "start" | "end";
5
5
  declare module "../types.js" {
6
6
  interface LightboxProps {
7
7
  /** Thumbnails plugin settings */
@@ -20,18 +20,19 @@ declare module "../types.js" {
20
20
  padding?: number;
21
21
  /** gap between thumbnails */
22
22
  gap?: number;
23
+ /** `object-fit` setting */
24
+ imageFit?: ImageFit;
23
25
  };
24
26
  }
25
27
  interface Render {
26
- thumbnail: ({ slide, rect }: {
28
+ thumbnail?: ({ slide, rect, render, imageFit, }: {
27
29
  slide: Slide;
28
30
  rect: ContainerRect;
31
+ render: Render;
32
+ imageFit: ImageFit;
29
33
  }) => React.ReactNode;
30
34
  }
31
35
  }
32
- declare type DeepNonNullable<T> = NonNullable<{
33
- [K in keyof T]-?: NonNullable<T[K]>;
34
- }>;
35
36
  declare type ThumbnailsInternal = DeepNonNullable<LightboxProps["thumbnails"]>;
36
37
  declare type ThumbnailsTrackProps = Pick<LightboxProps, "slides" | "carousel" | "animation" | "render"> & {
37
38
  container: React.RefObject<HTMLDivElement>;
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import { clsx, createIcon, createModule, cssClass, cssVar, ImageSlide, isRTL, useEnhancedEffect, useEvents, } from "../core/index.js";
2
+ import { clsx, createIcon, createModule, cssClass, cssVar, ImageSlide, useEnhancedEffect, useEvents, useLatest, useMotionPreference, useRTL, } from "../core/index.js";
3
3
  const defaultThumbnailsProps = {
4
4
  position: "bottom",
5
5
  width: 120,
@@ -8,6 +8,7 @@ const defaultThumbnailsProps = {
8
8
  borderRadius: 4,
9
9
  padding: 4,
10
10
  gap: 16,
11
+ imageFit: "contain",
11
12
  };
12
13
  const VideoThumbnailIcon = createIcon("VideoThumbnail", React.createElement("path", { d: "M10 16.5l6-4.5-6-4.5v9zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" }));
13
14
  const UnknownThumbnailIcon = createIcon("UnknownThumbnail", React.createElement("path", { d: "M23 18V6c0-1.1-.9-2-2-2H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2zM8.5 12.5l2.5 3.01L14.5 11l4.5 6H5l3.5-4.5z" }));
@@ -16,9 +17,9 @@ const cssThumbnailPrefix = (value) => cssPrefix(`thumbnail${value ? `_${value}`
16
17
  const getSlide = (slides, index) => slides[((index % slides.length) + slides.length) % slides.length];
17
18
  const isHorizontal = (position) => ["top", "bottom"].includes(position);
18
19
  const boxSize = (thumbnails, dimension, includeGap) => dimension + 2 * (thumbnails.border + thumbnails.padding) + (includeGap ? thumbnails.gap : 0);
19
- const renderThumbnail = ({ slide, render, rect }) => {
20
+ const renderThumbnail = ({ slide, render, rect, imageFit }) => {
20
21
  var _a;
21
- const customThumbnail = (_a = render.thumbnail) === null || _a === void 0 ? void 0 : _a.call(render, { slide, rect });
22
+ const customThumbnail = (_a = render.thumbnail) === null || _a === void 0 ? void 0 : _a.call(render, { slide, render, rect, imageFit });
22
23
  if (customThumbnail) {
23
24
  return customThumbnail;
24
25
  }
@@ -31,11 +32,11 @@ const renderThumbnail = ({ slide, render, rect }) => {
31
32
  }
32
33
  }
33
34
  else if ("src" in slide) {
34
- return React.createElement(ImageSlide, { slide: slide, rect: rect });
35
+ return React.createElement(ImageSlide, { slide: slide, render: render, rect: rect, imageFit: imageFit });
35
36
  }
36
37
  return React.createElement(UnknownThumbnailIcon, { className: thumbnailIconClass });
37
38
  };
38
- const Thumbnail = ({ rect, slide, onClick, active, fadeIn, fadeOut, placeholder, render, }) => (React.createElement("button", { type: "button", className: clsx(cssClass(cssThumbnailPrefix()), active && cssClass(cssThumbnailPrefix("active")), fadeIn && cssClass(cssThumbnailPrefix("fadein")), fadeOut && cssClass(cssThumbnailPrefix("fadeout")), placeholder && cssClass(cssThumbnailPrefix("placeholder"))), style: {
39
+ const Thumbnail = ({ rect, slide, onClick, active, fadeIn, fadeOut, placeholder, render, imageFit, }) => (React.createElement("button", { type: "button", className: clsx(cssClass(cssThumbnailPrefix()), active && cssClass(cssThumbnailPrefix("active")), fadeIn && cssClass(cssThumbnailPrefix("fadein")), fadeOut && cssClass(cssThumbnailPrefix("fadeout")), placeholder && cssClass(cssThumbnailPrefix("placeholder"))), style: {
39
40
  ...(fadeIn
40
41
  ? {
41
42
  [cssVar(cssThumbnailPrefix("fadein_duration"))]: `${fadeIn.duration}ms`,
@@ -48,13 +49,16 @@ const Thumbnail = ({ rect, slide, onClick, active, fadeIn, fadeOut, placeholder,
48
49
  [cssVar(cssThumbnailPrefix("fadeout_delay"))]: `${fadeOut.delay}ms`,
49
50
  }
50
51
  : null),
51
- }, onClick: onClick }, slide && renderThumbnail({ slide, render, rect })));
52
+ }, onClick: onClick }, slide && renderThumbnail({ slide, render, rect, imageFit })));
52
53
  export const ThumbnailsTrack = ({ container, startingIndex, slides, carousel, animation, render, thumbnails, thumbnailRect, }) => {
53
54
  const track = React.useRef(null);
54
55
  const [state, setState] = React.useState({
55
56
  index: startingIndex,
56
57
  offset: 0,
57
58
  });
59
+ const { publish, subscribe } = useEvents();
60
+ const reduceMotion = useLatest(useMotionPreference());
61
+ const isRTL = useLatest(useRTL());
58
62
  const refs = React.useRef({
59
63
  state,
60
64
  thumbnails,
@@ -67,12 +71,6 @@ export const ThumbnailsTrack = ({ container, startingIndex, slides, carousel, an
67
71
  refs.current.carousel = carousel;
68
72
  refs.current.animation = animation;
69
73
  const animationRef = React.useRef();
70
- const { publish, subscribe } = useEvents();
71
- React.useEffect(() => {
72
- if (track.current) {
73
- refs.current.isRTL = isRTL(track.current);
74
- }
75
- }, []);
76
74
  React.useEffect(() => subscribe("controller-swipe", (_, event) => {
77
75
  if (event && typeof event === "object" && "globalIndex" in event) {
78
76
  const { current } = refs;
@@ -106,7 +104,7 @@ export const ThumbnailsTrack = ({ container, startingIndex, slides, carousel, an
106
104
  animationRef.current = (_d = (_c = track.current).animate) === null || _d === void 0 ? void 0 : _d.call(_c, isHorizontal(current.thumbnails.position)
107
105
  ? [
108
106
  {
109
- transform: `translate3d(${(current.isRTL ? -1 : 1) *
107
+ transform: `translate3d(${(isRTL.current ? -1 : 1) *
110
108
  boxSize(current.thumbnails, current.thumbnails.width, true) *
111
109
  state.offset +
112
110
  current.animationOffset}px, 0, 0)`,
@@ -119,7 +117,7 @@ export const ThumbnailsTrack = ({ container, startingIndex, slides, carousel, an
119
117
  current.animationOffset}px, 0)`,
120
118
  },
121
119
  { transform: "translate3d(0, 0, 0)" },
122
- ], animationDuration);
120
+ ], reduceMotion.current ? 0 : animationDuration);
123
121
  if (animationRef.current) {
124
122
  animationRef.current.onfinish = () => {
125
123
  animationRef.current = undefined;
@@ -130,7 +128,7 @@ export const ThumbnailsTrack = ({ container, startingIndex, slides, carousel, an
130
128
  }
131
129
  current.animationOffset = 0;
132
130
  }
133
- }, [state.index, state.offset]);
131
+ }, [state.index, state.offset, isRTL, reduceMotion]);
134
132
  const { index, offset } = state;
135
133
  const { finite, preload } = carousel;
136
134
  const items = [];
@@ -171,25 +169,24 @@ export const ThumbnailsTrack = ({ container, startingIndex, slides, carousel, an
171
169
  publish("prev", index - slideIndex);
172
170
  }
173
171
  };
172
+ const { width, height, border, borderRadius, padding, gap, imageFit } = thumbnails;
174
173
  return (React.createElement("div", { className: clsx(cssClass(cssPrefix("container")), cssClass("flex_center")), style: {
175
- ...(thumbnails.width !== defaultThumbnailsProps.width
176
- ? { [cssVar(cssThumbnailPrefix("width"))]: `${boxSize(thumbnails, thumbnails.width)}px` }
177
- : null),
178
- ...(thumbnails.height !== defaultThumbnailsProps.height
179
- ? { [cssVar(cssThumbnailPrefix("height"))]: `${boxSize(thumbnails, thumbnails.height)}px` }
174
+ ...(width !== defaultThumbnailsProps.width
175
+ ? { [cssVar(cssThumbnailPrefix("width"))]: `${boxSize(thumbnails, width)}px` }
180
176
  : null),
181
- ...(thumbnails.border !== defaultThumbnailsProps.border
182
- ? { [cssVar(cssThumbnailPrefix("border"))]: `${thumbnails.border}px` }
177
+ ...(height !== defaultThumbnailsProps.height
178
+ ? { [cssVar(cssThumbnailPrefix("height"))]: `${boxSize(thumbnails, height)}px` }
183
179
  : null),
184
- ...(thumbnails.borderRadius !== defaultThumbnailsProps.borderRadius
185
- ? { [cssVar(cssThumbnailPrefix("border_radius"))]: `${thumbnails.borderRadius}px` }
180
+ ...(border !== defaultThumbnailsProps.border
181
+ ? { [cssVar(cssThumbnailPrefix("border"))]: `${border}px` }
186
182
  : null),
187
- ...(thumbnails.padding !== defaultThumbnailsProps.padding
188
- ? { [cssVar(cssThumbnailPrefix("padding"))]: `${thumbnails.padding}px` }
183
+ ...(borderRadius !== defaultThumbnailsProps.borderRadius
184
+ ? { [cssVar(cssThumbnailPrefix("border_radius"))]: `${borderRadius}px` }
189
185
  : null),
190
- ...(thumbnails.gap !== defaultThumbnailsProps.gap
191
- ? { [cssVar(cssThumbnailPrefix("gap"))]: `${thumbnails.gap}px` }
186
+ ...(padding !== defaultThumbnailsProps.padding
187
+ ? { [cssVar(cssThumbnailPrefix("padding"))]: `${padding}px` }
192
188
  : null),
189
+ ...(gap !== defaultThumbnailsProps.gap ? { [cssVar(cssThumbnailPrefix("gap"))]: `${gap}px` } : null),
193
190
  } },
194
191
  React.createElement("nav", { ref: track, className: cssClass(cssPrefix("track")) }, items.map(({ slide, index: slideIndex, placeholder }) => {
195
192
  var _a;
@@ -213,11 +210,11 @@ export const ThumbnailsTrack = ({ container, startingIndex, slides, carousel, an
213
210
  : -offset - (slideIndex - (index + preload))) * fadeAnimationDuration,
214
211
  }
215
212
  : undefined;
216
- return (React.createElement(Thumbnail, { key: slideIndex, rect: thumbnailRect, slide: slide, render: render, active: slideIndex === index, fadeIn: fadeIn, fadeOut: fadeOut, placeholder: Boolean(placeholder), onClick: handleClick(slideIndex) }));
213
+ return (React.createElement(Thumbnail, { key: slideIndex, rect: thumbnailRect, slide: slide, imageFit: imageFit, render: render, active: slideIndex === index, fadeIn: fadeIn, fadeOut: fadeOut, placeholder: Boolean(placeholder), onClick: handleClick(slideIndex) }));
217
214
  }))));
218
215
  };
219
- export const ThumbnailsComponent = ({ thumbnails: originalThumbnails, slides, index, carousel, animation, render, children, }) => {
220
- const thumbnails = { ...defaultThumbnailsProps, ...originalThumbnails };
216
+ export const ThumbnailsComponent = ({ thumbnails: thumbnailsProps, slides, index, carousel, animation, render, children, }) => {
217
+ const thumbnails = { ...defaultThumbnailsProps, ...thumbnailsProps };
221
218
  const ref = React.useRef(null);
222
219
  const track = (React.createElement(ThumbnailsTrack, { container: ref, slides: slides, thumbnails: thumbnails, carousel: carousel, animation: animation, render: render, startingIndex: index, thumbnailRect: { width: thumbnails.width, height: thumbnails.height } }));
223
220
  return (React.createElement("div", { ref: ref, className: clsx(cssClass(cssPrefix()), cssClass(cssPrefix(`${thumbnails.position}`)), cssClass("fullsize")) },
@@ -226,10 +223,10 @@ export const ThumbnailsComponent = ({ thumbnails: originalThumbnails, slides, in
226
223
  (thumbnails.position === "end" || thumbnails.position === "bottom") && track));
227
224
  };
228
225
  export const Thumbnails = ({ augment, contains, append, addParent }) => {
229
- augment(({ thumbnails: originalThumbnails, ...restProps }) => ({
226
+ augment(({ thumbnails, ...restProps }) => ({
230
227
  thumbnails: {
231
228
  ...defaultThumbnailsProps,
232
- ...originalThumbnails,
229
+ ...thumbnails,
233
230
  },
234
231
  ...restProps,
235
232
  }));