@jekrch/react-viewport-lightbox 0.2.0 → 0.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 +30 -22
- package/dist/index.cjs +50 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -1
- package/dist/index.d.ts +58 -1
- package/dist/index.js +50 -9
- package/dist/index.js.map +1 -1
- package/dist/styles.css +60 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,28 +75,35 @@ animation and calls `onClose` after the exit completes.
|
|
|
75
75
|
|
|
76
76
|
## Props
|
|
77
77
|
|
|
78
|
-
| Prop | Type | Default | Description
|
|
79
|
-
| ---------------------- | --------------------------------------- | ---------- |
|
|
80
|
-
| `items` | `ViewerItem[]` | required | Images to display.
|
|
81
|
-
| `index` | `number` | required | Controlled index of the active item.
|
|
82
|
-
| `onIndexChange` | `(index: number) => void` | required | Called when navigation changes the index.
|
|
83
|
-
| `onNavigate` | `(direction: "prev" \| "next") => void` | optional | Fired when a slide starts (before `onIndexChange`), so overlays can animate out in sync with the image.
|
|
84
|
-
| `onClose` | `() => void` | required | Called after the exit animation completes.
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
78
|
+
| Prop | Type | Default | Description |
|
|
79
|
+
| ---------------------- | --------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
80
|
+
| `items` | `ViewerItem[]` | required | Images to display. |
|
|
81
|
+
| `index` | `number` | required | Controlled index of the active item. |
|
|
82
|
+
| `onIndexChange` | `(index: number) => void` | required | Called when navigation changes the index. |
|
|
83
|
+
| `onNavigate` | `(direction: "prev" \| "next") => void` | optional | Fired when a slide starts (before `onIndexChange`), so overlays can animate out in sync with the image. |
|
|
84
|
+
| `onClose` | `() => void` | required | Called after the exit animation completes. |
|
|
85
|
+
| `onEscape` | `() => boolean` | optional | Called on Escape before the viewer closes. Return `true` to mark the key handled and veto the default close (e.g. dismiss your own overlay first); `false`/`undefined` falls through to closing. |
|
|
86
|
+
| `getOriginRect` | `(index: number) => ViewerRect \| null` | optional | Enables a shared-element "zoom from thumbnail" open/close transition. Return the source element's rect (e.g. `el.getBoundingClientRect()`) for the given index, or `null` to fall back to the fade. Honors reduced-motion. |
|
|
87
|
+
| `zoom` | `boolean` | `true` | Enable wheel/pinch/double-tap zoom + pan. |
|
|
88
|
+
| `zoomToCursor` | `boolean` | `true` | Anchor wheel/pinch zoom on the pointer: scrolling zooms toward the cursor and a pinch zooms toward the gesture midpoint. Set `false` to zoom about the viewport center. |
|
|
89
|
+
| `showCounter` | `boolean` | `true` | Show the `index / total` counter. |
|
|
90
|
+
| `showZoomControls` | `boolean` | `true` | Show the built-in zoom in/out/reset buttons. Independent of `zoom` (the gestures): set `false` to keep zoom/pan while a consumer overlay owns the chrome. Auto-hidden on touch-primary devices and while content is shifted. |
|
|
91
|
+
| `disableNavigation` | `boolean` | `false` | Suppress built-in arrow-key navigation and swipe commit without tearing the viewer down (e.g. while an overlay handles left/right itself). Does not hide the on-screen nav buttons. |
|
|
92
|
+
| `loop` | `boolean` | `false` | Wrap around at the ends (buttons + arrow keys). |
|
|
93
|
+
| `closeOnBackdropClick` | `boolean` | `false` | Close the viewer when the empty area around the image is clicked. Image, bars, and controls are unaffected. |
|
|
94
|
+
| `renderHeader` | `(ctx) => ReactNode` | optional | Top-left title area. |
|
|
95
|
+
| `renderHeaderActions` | `(ctx) => ReactNode` | optional | Extra top-right buttons (before Close). |
|
|
96
|
+
| `renderNavStart` | `(ctx) => ReactNode` | optional | Pinned to the left edge of the nav row (e.g. a details toggle); costs no extra height, nav group stays centered. |
|
|
97
|
+
| `renderNavEnd` | `(ctx) => ReactNode` | optional | Pinned to the right edge of the nav row. |
|
|
98
|
+
| `navSlotPlacement` | `"edge" \| "inline"` | `"edge"` | Where `renderNavStart`/`renderNavEnd` sit: `"edge"` pins them to the row edges (nav group stays optically centered); `"inline"` places them directly flanking the arrows as one centered cluster. |
|
|
99
|
+
| `navHeight` | `number \| string` | `2.375rem` | Size of the prev/next nav arrows. A number is pixels; a string is used verbatim. Sets `--rvl-nav-height`, so it can also be themed in CSS. |
|
|
100
|
+
| `navInset` | `number \| string` | `1.3rem` | Gap between the bottom nav controls and the viewport's bottom edge (floored by the safe-area inset). Number is pixels; string verbatim. Sets `--rvl-nav-inset`. |
|
|
101
|
+
| `counterFontSize` | `number \| string` | `0.29×` | Overrides the counter font size (which otherwise scales with `navHeight`). Number is pixels; string verbatim. Sets `--rvl-counter-font-size`. |
|
|
102
|
+
| `renderFooter` | `(ctx) => ReactNode` | optional | Content below the nav row. |
|
|
103
|
+
| `renderOverlay` | `(ctx) => ReactNode` | optional | Drawers and graphs layered over the image. |
|
|
104
|
+
| `classNames` | `Partial<Record<ViewerSlot, string>>` | optional | Per-slot `className` overrides. |
|
|
105
|
+
| `icons` | `Partial<ViewerIcons>` | optional | Override `close`, `zoomIn`, `zoomOut`, `prev`, and `next`. |
|
|
106
|
+
| `ariaLabel` | `string` | item `alt` | Dialog label. |
|
|
100
107
|
|
|
101
108
|
### `ViewerItem`
|
|
102
109
|
|
|
@@ -195,6 +202,7 @@ interface ViewerContext<TData = unknown> {
|
|
|
195
202
|
index: number;
|
|
196
203
|
item: ViewerItem<TData>; // item.data holds your per-slide payload
|
|
197
204
|
total: number;
|
|
205
|
+
closing: boolean; // true once the exit animation starts, so overlays can fade out in step
|
|
198
206
|
|
|
199
207
|
hasPrev: boolean;
|
|
200
208
|
hasNext: boolean;
|
package/dist/index.cjs
CHANGED
|
@@ -592,16 +592,29 @@ function useBarMeasure(topBarRef, bottomBarRef, measureKey) {
|
|
|
592
592
|
var lockCount = 0;
|
|
593
593
|
var previousOverflow = "";
|
|
594
594
|
var previousPaddingRight = "";
|
|
595
|
+
var previousPosition = "";
|
|
596
|
+
var previousTop = "";
|
|
597
|
+
var previousWidth = "";
|
|
598
|
+
var lockedScrollY = 0;
|
|
595
599
|
function useBodyScrollLock(isLocked) {
|
|
596
600
|
react.useEffect(() => {
|
|
597
601
|
if (!isLocked) return;
|
|
598
602
|
if (typeof document === "undefined") return;
|
|
599
603
|
if (lockCount === 0) {
|
|
600
604
|
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
605
|
+
const rootGutter = window.getComputedStyle(document.documentElement).scrollbarGutter;
|
|
606
|
+
const reservesGutter = typeof rootGutter === "string" && rootGutter.includes("stable");
|
|
607
|
+
lockedScrollY = window.scrollY;
|
|
601
608
|
previousOverflow = document.body.style.overflow;
|
|
602
609
|
previousPaddingRight = document.body.style.paddingRight;
|
|
610
|
+
previousPosition = document.body.style.position;
|
|
611
|
+
previousTop = document.body.style.top;
|
|
612
|
+
previousWidth = document.body.style.width;
|
|
603
613
|
document.body.style.overflow = "hidden";
|
|
604
|
-
|
|
614
|
+
document.body.style.position = "fixed";
|
|
615
|
+
document.body.style.top = `-${lockedScrollY}px`;
|
|
616
|
+
document.body.style.width = "100%";
|
|
617
|
+
if (scrollbarWidth > 0 && !reservesGutter) {
|
|
605
618
|
const currentPaddingRight = parseFloat(window.getComputedStyle(document.body).paddingRight) || 0;
|
|
606
619
|
document.body.style.paddingRight = `${currentPaddingRight + scrollbarWidth}px`;
|
|
607
620
|
}
|
|
@@ -612,6 +625,10 @@ function useBodyScrollLock(isLocked) {
|
|
|
612
625
|
if (lockCount === 0) {
|
|
613
626
|
document.body.style.overflow = previousOverflow;
|
|
614
627
|
document.body.style.paddingRight = previousPaddingRight;
|
|
628
|
+
document.body.style.position = previousPosition;
|
|
629
|
+
document.body.style.top = previousTop;
|
|
630
|
+
document.body.style.width = previousWidth;
|
|
631
|
+
window.scrollTo(0, lockedScrollY);
|
|
615
632
|
}
|
|
616
633
|
};
|
|
617
634
|
}, [isLocked]);
|
|
@@ -749,16 +766,23 @@ function ImageViewer({
|
|
|
749
766
|
onIndexChange,
|
|
750
767
|
onNavigate,
|
|
751
768
|
onClose,
|
|
769
|
+
onEscape,
|
|
752
770
|
getOriginRect,
|
|
753
771
|
zoom = true,
|
|
754
772
|
zoomToCursor = true,
|
|
755
773
|
showCounter = true,
|
|
774
|
+
showZoomControls = true,
|
|
775
|
+
disableNavigation = false,
|
|
756
776
|
loop = false,
|
|
757
777
|
closeOnBackdropClick = false,
|
|
758
778
|
renderHeader,
|
|
759
779
|
renderHeaderActions,
|
|
760
780
|
renderNavStart,
|
|
761
781
|
renderNavEnd,
|
|
782
|
+
navSlotPlacement = "edge",
|
|
783
|
+
navHeight,
|
|
784
|
+
navInset,
|
|
785
|
+
counterFontSize,
|
|
762
786
|
renderFooter,
|
|
763
787
|
renderOverlay,
|
|
764
788
|
classNames,
|
|
@@ -803,7 +827,15 @@ function ImageViewer({
|
|
|
803
827
|
const { slideTrackRef, slideActive, slideAnimating, swipeOffset, commitSlide } = slide;
|
|
804
828
|
const gestures = useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoom, zoomToCursor);
|
|
805
829
|
react.useEffect(() => {
|
|
806
|
-
|
|
830
|
+
if (typeof window === "undefined" || !window.matchMedia) {
|
|
831
|
+
setIsTouchDevice("ontouchstart" in window || navigator.maxTouchPoints > 0);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
const mq = window.matchMedia("(hover: none) and (pointer: coarse)");
|
|
835
|
+
const update = () => setIsTouchDevice(mq.matches);
|
|
836
|
+
update();
|
|
837
|
+
mq.addEventListener("change", update);
|
|
838
|
+
return () => mq.removeEventListener("change", update);
|
|
807
839
|
}, []);
|
|
808
840
|
react.useEffect(() => {
|
|
809
841
|
const onResize = () => setViewportWidth(window.innerWidth);
|
|
@@ -922,16 +954,17 @@ function ImageViewer({
|
|
|
922
954
|
react.useEffect(() => {
|
|
923
955
|
const handler = (e) => {
|
|
924
956
|
if (e.key === "Escape") {
|
|
957
|
+
if (onEscape?.()) return;
|
|
925
958
|
handleClose();
|
|
926
959
|
return;
|
|
927
960
|
}
|
|
928
|
-
if (displayScale > 1) return;
|
|
961
|
+
if (disableNavigation || displayScale > 1) return;
|
|
929
962
|
if (e.key === "ArrowLeft" && hasPrev) navigate("prev");
|
|
930
963
|
if (e.key === "ArrowRight" && hasNext) navigate("next");
|
|
931
964
|
};
|
|
932
965
|
window.addEventListener("keydown", handler);
|
|
933
966
|
return () => window.removeEventListener("keydown", handler);
|
|
934
|
-
}, [handleClose, hasPrev, hasNext, displayScale, navigate]);
|
|
967
|
+
}, [handleClose, hasPrev, hasNext, displayScale, navigate, onEscape, disableNavigation]);
|
|
935
968
|
const setContentShift = react.useCallback((transform, animate = true) => {
|
|
936
969
|
setContentShiftState({ transform, animate });
|
|
937
970
|
}, []);
|
|
@@ -940,6 +973,7 @@ function ImageViewer({
|
|
|
940
973
|
index,
|
|
941
974
|
item,
|
|
942
975
|
total: items.length,
|
|
976
|
+
closing,
|
|
943
977
|
hasPrev,
|
|
944
978
|
hasNext,
|
|
945
979
|
goPrev: () => navigate("prev"),
|
|
@@ -972,6 +1006,12 @@ function ImageViewer({
|
|
|
972
1006
|
const reservedH = bottomBarH + IMG_PADDING * 2;
|
|
973
1007
|
const imgMaxHeight = `calc(100vh - ${reservedH}px)`;
|
|
974
1008
|
const imgStyle = { maxHeight: imgMaxHeight };
|
|
1009
|
+
const dim = (v) => typeof v === "number" ? `${v}px` : v;
|
|
1010
|
+
const rootStyle = {
|
|
1011
|
+
...navHeight != null && { "--rvl-nav-height": dim(navHeight) },
|
|
1012
|
+
...navInset != null && { "--rvl-nav-inset": dim(navInset) },
|
|
1013
|
+
...counterFontSize != null && { "--rvl-counter-font-size": dim(counterFontSize) }
|
|
1014
|
+
};
|
|
975
1015
|
if (gateEntry && !fullLoaded) imgStyle.opacity = 0;
|
|
976
1016
|
const totalDigits = String(items.length).length;
|
|
977
1017
|
const counterMinWidth = `${totalDigits * 2 * 0.6 + 1.5}em`;
|
|
@@ -981,7 +1021,7 @@ function ImageViewer({
|
|
|
981
1021
|
const nextItem = nextIndex >= 0 ? items[nextIndex] : null;
|
|
982
1022
|
const showAdjacent = slideActive || slideAnimating || swipeOffset !== 0;
|
|
983
1023
|
const adjacentOpacity = Math.min(1, Math.abs(swipeOffset) / (viewportWidth * 0.8 || 1));
|
|
984
|
-
const
|
|
1024
|
+
const showZoomCtrls = zoom && !isTouchDevice && showZoomControls && !contentShift.transform;
|
|
985
1025
|
const headerActions = renderHeaderActions?.(ctx);
|
|
986
1026
|
const navStart = renderNavStart?.(ctx);
|
|
987
1027
|
const navEnd = renderNavEnd?.(ctx);
|
|
@@ -996,6 +1036,7 @@ function ImageViewer({
|
|
|
996
1036
|
"aria-modal": "true",
|
|
997
1037
|
"aria-label": ariaLabel ?? item.alt ?? "Image viewer",
|
|
998
1038
|
tabIndex: -1,
|
|
1039
|
+
style: rootStyle,
|
|
999
1040
|
children: [
|
|
1000
1041
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1001
1042
|
"div",
|
|
@@ -1009,7 +1050,7 @@ function ImageViewer({
|
|
|
1009
1050
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: cx("rvl-header", cn("topBar")), children: renderHeader?.(ctx) }),
|
|
1010
1051
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rvl-header-actions", children: [
|
|
1011
1052
|
headerActions,
|
|
1012
|
-
|
|
1053
|
+
showZoomCtrls && isZoomed && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1013
1054
|
"button",
|
|
1014
1055
|
{
|
|
1015
1056
|
type: "button",
|
|
@@ -1026,7 +1067,7 @@ function ImageViewer({
|
|
|
1026
1067
|
]
|
|
1027
1068
|
}
|
|
1028
1069
|
),
|
|
1029
|
-
|
|
1070
|
+
showZoomCtrls && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1030
1071
|
"button",
|
|
1031
1072
|
{
|
|
1032
1073
|
type: "button",
|
|
@@ -1040,7 +1081,7 @@ function ImageViewer({
|
|
|
1040
1081
|
children: mergedIcons.zoomIn
|
|
1041
1082
|
}
|
|
1042
1083
|
),
|
|
1043
|
-
|
|
1084
|
+
showZoomCtrls && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1044
1085
|
"button",
|
|
1045
1086
|
{
|
|
1046
1087
|
type: "button",
|
|
@@ -1166,7 +1207,7 @@ function ImageViewer({
|
|
|
1166
1207
|
}
|
|
1167
1208
|
),
|
|
1168
1209
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { ref: bottomBarRef, className: cx("rvl-bar", "rvl-bottom-bar", cn("bottomBar")), children: [
|
|
1169
|
-
showNavRow && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rvl-nav-row", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rvl-nav-inner", children: [
|
|
1210
|
+
showNavRow && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rvl-nav-row", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx("rvl-nav-inner", navSlotPlacement === "inline" && "rvl-nav-inline"), children: [
|
|
1170
1211
|
navStart != null && /* @__PURE__ */ jsxRuntime.jsx("div", { className: cx("rvl-nav-start", cn("navStart")), children: navStart }),
|
|
1171
1212
|
hasNavGroup && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rvl-nav-group", children: [
|
|
1172
1213
|
/* @__PURE__ */ jsxRuntime.jsx(
|