yet-another-react-lightbox-lite 1.0.0 → 1.1.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 CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  Lightweight React lightbox component. This is a trimmed-down version of the
4
4
  [yet-another-react-lightbox](https://github.com/igordanchenko/yet-another-react-lightbox)
5
- that provides essential lightbox features and slick UX with just 2.8KB bundle
5
+ that provides essential lightbox features and slick UX with just 3KB bundle
6
6
  size.
7
7
 
8
8
  ## Overview
9
9
 
10
- [![NPM Version](https://img.shields.io/npm/v/yet-another-react-lightbox-lite.svg?color=blue)](https://www.npmjs.com/package/yet-another-react-lightbox)
11
- [![Bundle Size](https://img.shields.io/bundlephobia/minzip/yet-another-react-lightbox-lite.svg?color=blue)](https://bundlephobia.com/package/yet-another-react-lightbox)
12
- [![License MIT](https://img.shields.io/npm/l/yet-another-react-lightbox-lite.svg?color=blue)](https://github.com/igordanchenko/yet-another-react-lightbox/blob/main/LICENSE)
10
+ [![NPM Version](https://img.shields.io/npm/v/yet-another-react-lightbox-lite.svg?color=blue)](https://www.npmjs.com/package/yet-another-react-lightbox-lite)
11
+ [![Bundle Size](https://img.shields.io/bundlephobia/minzip/yet-another-react-lightbox-lite.svg?color=blue)](https://bundlephobia.com/package/yet-another-react-lightbox-lite)
12
+ [![License MIT](https://img.shields.io/npm/l/yet-another-react-lightbox-lite.svg?color=blue)](https://github.com/igordanchenko/yet-another-react-lightbox-lite/blob/main/LICENSE)
13
13
 
14
14
  - **Built for React:** works with React 18+
15
15
  - **UX:** supports keyboard, mouse, touchpad and touchscreen navigation
@@ -203,6 +203,32 @@ Custom UI labels / translations.
203
203
  />
204
204
  ```
205
205
 
206
+ ### controller
207
+
208
+ Type: `object`
209
+
210
+ Controller settings.
211
+
212
+ - `closeOnPullUp` - if `true`, close the lightbox on pull-up gesture (default:
213
+ `true`)
214
+ - `closeOnPullDown` - if `true`, close the lightbox on pull-down gesture
215
+ (default: `true`)
216
+ - `closeOnBackdropClick` - if `true`, close the lightbox when the backdrop is
217
+ clicked (default: `true`)
218
+
219
+ Usage example:
220
+
221
+ ```tsx
222
+ <Lightbox
223
+ // ...
224
+ controller={{
225
+ closeOnPullUp: false,
226
+ closeOnPullDown: false,
227
+ closeOnBackdropClick: false,
228
+ }}
229
+ />
230
+ ```
231
+
206
232
  ### render
207
233
 
208
234
  Type: `object`
@@ -315,6 +341,41 @@ Render custom `Next` icon.
315
341
 
316
342
  Render custom `Close` icon.
317
343
 
344
+ ### styles
345
+
346
+ Type: `{ [key in Slot]?: SlotCSSProperties }`
347
+
348
+ Customization slots styles allow you to specify custom CSS styles or override
349
+ `--yarll__*` CSS variables by passing your custom styles through to the
350
+ corresponding lightbox elements.
351
+
352
+ Supported customization slots:
353
+
354
+ - `portal` - lightbox portal (root)
355
+ - `carousel` - lightbox carousel
356
+ - `slide` - lightbox slide
357
+ - `image` - lightbox slide image
358
+ - `button` - lightbox button
359
+ - `icon` - lightbox icon
360
+
361
+ Usage example:
362
+
363
+ ```tsx
364
+ <Lightbox
365
+ // ...
366
+ styles={{
367
+ portal: { "--yarll__backdrop_color": "rgba(0, 0, 0, 0.6)" },
368
+ }}
369
+ />
370
+ ```
371
+
372
+ ### className
373
+
374
+ Type: `string`
375
+
376
+ CSS class of the lightbox root element. You can use this class name to provide
377
+ module-scoped style overrides.
378
+
318
379
  ## Custom Slide Attributes
319
380
 
320
381
  You can add custom slide attributes with the following module augmentation.
@@ -420,6 +481,31 @@ export default function App() {
420
481
  }
421
482
  ```
422
483
 
484
+ ## Body Scroll Lock
485
+
486
+ By default, the lightbox hides the browser window scrollbar and prevents
487
+ document `<body>` from scrolling underneath the lightbox by assigning the
488
+ `height: 100%; overflow: hidden;` styles to the document `<body>` element.
489
+
490
+ If this behavior causes undesired side effects in your case, and you prefer not
491
+ to use this feature, you can turn it off by assigning the
492
+ `yarll__no_scroll_lock` class to the lightbox.
493
+
494
+ ```tsx
495
+ <Lightbox
496
+ // ..
497
+ className="yarll__no_scroll_lock"
498
+ />
499
+ ```
500
+
501
+ However, if you keep the body scroll lock feature on, you may notice a visual
502
+ layout shift of some fixed-positioned page elements when the lightbox opens. To
503
+ address this, you can assign the `yarll__fixed` CSS class to your
504
+ fixed-positioned elements to keep them in place. Please note that the
505
+ fixed-positioned element container should not have its own border or padding
506
+ styles. If that's the case, you can always add an extra wrapper that just
507
+ defines the fixed position without visual styles.
508
+
423
509
  ## License
424
510
 
425
511
  MIT © 2024 [Igor Danchenko](https://github.com/igordanchenko)
package/dist/index.d.ts CHANGED
@@ -13,6 +13,12 @@ interface LightboxProps {
13
13
  labels?: Labels;
14
14
  /** custom render functions */
15
15
  render?: Render;
16
+ /** controller settings */
17
+ controller?: Controller;
18
+ /** customization slots styles */
19
+ styles?: SlotStyles;
20
+ /** CSS class of the lightbox root element */
21
+ className?: string;
16
22
  }
17
23
  /** Slide */
18
24
  type Slide = SlideTypes[SlideTypeKey];
@@ -88,6 +94,40 @@ interface RenderSlideProps {
88
94
  /** if `true`, the slide is the current slide in the viewport */
89
95
  current: boolean;
90
96
  }
97
+ /** Controller settings */
98
+ type Controller = {
99
+ /** if `true`, close the lightbox on pull-up gesture (default: `true`) */
100
+ closeOnPullUp?: boolean;
101
+ /** if `true`, close the lightbox on pull-down gesture (default: `true`) */
102
+ closeOnPullDown?: boolean;
103
+ /** if `true`, close the lightbox when the backdrop is clicked (default: `true`) */
104
+ closeOnBackdropClick?: boolean;
105
+ };
106
+ /** Customization slots */
107
+ interface SlotType {
108
+ /** lightbox portal (root) customization slot */
109
+ portal: "portal";
110
+ /** lightbox carousel customization slot */
111
+ carousel: "carousel";
112
+ /** lightbox slide customization slot */
113
+ slide: "slide";
114
+ /** lightbox slide image customization slot */
115
+ image: "image";
116
+ /** lightbox button customization slot */
117
+ button: "button";
118
+ /** lightbox icon customization slot */
119
+ icon: "icon";
120
+ }
121
+ /** Customization slots */
122
+ type Slot = SlotType[keyof SlotType];
123
+ /** Customization slot CSS properties */
124
+ interface SlotCSSProperties extends React.CSSProperties {
125
+ [key: `--yarll__${string}`]: string | number;
126
+ }
127
+ /** Customization slots styles */
128
+ type SlotStyles = {
129
+ [key in Slot]?: SlotCSSProperties;
130
+ };
91
131
  /** Rect */
92
132
  type Rect = {
93
133
  width: number;
@@ -100,4 +140,4 @@ type RenderFunction<T = void> = [T] extends [void] ? () => React.ReactNode : (pr
100
140
 
101
141
  declare function Lightbox({ slides, index, setIndex, ...rest }: LightboxProps): react_jsx_runtime.JSX.Element | null;
102
142
 
103
- export { type Callback, type GenericSlide, type ImageSource, type Label, type Labels, type LightboxProps, type Rect, type Render, type RenderFunction, type RenderSlideProps, type Slide, type SlideImage, type SlideTypeKey, type SlideTypes, Lightbox as default };
143
+ export { type Callback, type Controller, type GenericSlide, type ImageSource, type Label, type Labels, type LightboxProps, type Rect, type Render, type RenderFunction, type RenderSlideProps, type Slide, type SlideImage, type SlideTypeKey, type SlideTypes, type Slot, type SlotStyles, type SlotType, Lightbox as default };
package/dist/index.js CHANGED
@@ -41,6 +41,7 @@ function LightboxContextProvider({ children, ...props }) {
41
41
  }
42
42
 
43
43
  function ImageSlide({ slide, rect }) {
44
+ const { styles } = useLightboxContext();
44
45
  const { width, height } = slide.srcSet?.[0] ?? slide;
45
46
  const imageAspectRatio = width && height ? width / height : undefined;
46
47
  const srcSet = slide.srcSet
@@ -50,11 +51,11 @@ function ImageSlide({ slide, rect }) {
50
51
  const sizes = imageAspectRatio
51
52
  ? `${imageAspectRatio < rect.width / rect.height ? Math.round(imageAspectRatio * rect.height) : rect.width}px`
52
53
  : undefined;
53
- return (jsx("img", { draggable: false, className: cssClass("slide_image"), srcSet: srcSet, sizes: sizes, src: slide.src, alt: slide.alt }));
54
+ return (jsx("img", { draggable: false, style: styles?.image, className: cssClass("slide_image"), srcSet: srcSet, sizes: sizes, src: slide.src, alt: slide.alt }));
54
55
  }
55
56
 
56
57
  function Carousel() {
57
- const { slides, index, render: { slide: renderSlide, slideHeader, slideFooter } = {} } = useLightboxContext();
58
+ const { slides, index, styles, render: { slide: renderSlide, slideHeader, slideFooter } = {} } = useLightboxContext();
58
59
  const [rect, setRect] = useState();
59
60
  const observer = useRef();
60
61
  const handleRef = useCallback((node) => {
@@ -69,7 +70,7 @@ function Carousel() {
69
70
  updateRect();
70
71
  }
71
72
  }, []);
72
- return (jsx("div", { ref: handleRef, className: cssClass("carousel"), children: rect &&
73
+ return (jsx("div", { ref: handleRef, style: styles?.carousel, className: cssClass("carousel"), children: rect &&
73
74
  Array.from({ length: 5 }).map((_, i) => {
74
75
  const slideIndex = index - 2 + i;
75
76
  if (slideIndex < 0 || slideIndex >= slides.length)
@@ -77,7 +78,7 @@ function Carousel() {
77
78
  const slide = slides[slideIndex];
78
79
  const current = slideIndex === index;
79
80
  const context = { slide, rect, current };
80
- return (jsxs("div", { role: "group", "aria-roledescription": "slide", className: cssClass("slide"), hidden: !current, children: [slideHeader?.(context), renderSlide?.(context) ?? jsx(ImageSlide, { ...context }), slideFooter?.(context)] }, slide.key ?? `${slideIndex}-${slide.src}`));
81
+ return (jsxs("div", { role: "group", "aria-roledescription": "slide", className: cssClass("slide"), hidden: !current, style: styles?.slide, children: [slideHeader?.(context), renderSlide?.(context) ?? jsx(ImageSlide, { ...context }), slideFooter?.(context)] }, slide.key ?? `${slideIndex}-${slide.src}`));
81
82
  }) }));
82
83
  }
83
84
 
@@ -115,9 +116,9 @@ function Controller({ setIndex, children }) {
115
116
  }
116
117
 
117
118
  function Button({ icon: Icon, renderIcon, label, onClick, disabled, className }) {
118
- const { labels } = useLightboxContext();
119
+ const { labels, styles } = useLightboxContext();
119
120
  const buttonLabel = translateLabel(labels, label);
120
- return (jsx("button", { type: "button", title: buttonLabel, "aria-label": buttonLabel, onClick: onClick, disabled: disabled, className: clsx(cssClass("button"), className), children: renderIcon?.() ?? jsx(Icon, { className: cssClass("icon") }) }));
121
+ 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") }) }));
121
122
  }
122
123
 
123
124
  function svgIcon(name, children) {
@@ -147,6 +148,12 @@ function useSensors() {
147
148
  const wheelCooldownMomentum = useRef(null);
148
149
  const activePointer = useRef(null);
149
150
  const { prev, next, close } = useController();
151
+ const { closeOnPullUp, closeOnPullDown, closeOnBackdropClick } = {
152
+ closeOnPullUp: true,
153
+ closeOnPullDown: true,
154
+ closeOnBackdropClick: true,
155
+ ...useLightboxContext().controller,
156
+ };
150
157
  return useMemo(() => {
151
158
  const onKeyDown = (event) => {
152
159
  switch (event.key) {
@@ -173,8 +180,9 @@ function useSensors() {
173
180
  const onPointerUp = (event) => {
174
181
  if (event.pointerId === activePointer.current?.pointerId) {
175
182
  const dx = event.clientX - activePointer.current.clientX;
183
+ const dy = event.clientY - activePointer.current.clientY;
176
184
  const deltaX = Math.abs(dx);
177
- const deltaY = Math.abs(event.clientY - activePointer.current.clientY);
185
+ const deltaY = Math.abs(dy);
178
186
  if (deltaX > 50 && deltaX > 1.2 * deltaY) {
179
187
  if (dx > 0) {
180
188
  prev();
@@ -183,8 +191,9 @@ function useSensors() {
183
191
  next();
184
192
  }
185
193
  }
186
- else if ((deltaY > 50 && deltaY > 1.2 * deltaX) ||
187
- (activePointer.current.target instanceof HTMLElement &&
194
+ else if ((deltaY > 50 && deltaY > 1.2 * deltaX && ((closeOnPullUp && dy < 0) || (closeOnPullDown && dy > 0))) ||
195
+ (closeOnBackdropClick &&
196
+ activePointer.current.target instanceof HTMLElement &&
188
197
  activePointer.current.target.className.split(" ").includes(cssClass("slide")))) {
189
198
  close();
190
199
  }
@@ -229,7 +238,7 @@ function useSensors() {
229
238
  onPointerCancel: onPointerUp,
230
239
  onWheel,
231
240
  };
232
- }, [prev, next, close]);
241
+ }, [prev, next, close, closeOnPullUp, closeOnPullDown, closeOnBackdropClick]);
233
242
  }
234
243
 
235
244
  function setAttribute(element, attribute, value) {
@@ -245,6 +254,7 @@ function setAttribute(element, attribute, value) {
245
254
  };
246
255
  }
247
256
  function Portal({ children }) {
257
+ const { styles, className } = useLightboxContext();
248
258
  const cleanup = useRef([]);
249
259
  const [mounted, setMounted] = useState(false);
250
260
  const [visible, setVisible] = useState(false);
@@ -307,7 +317,7 @@ function Portal({ children }) {
307
317
  }
308
318
  }, [handleCleanup]);
309
319
  return mounted
310
- ? createPortal(jsx("div", { "aria-modal": true, role: "dialog", "aria-roledescription": "carousel", tabIndex: -1, ref: handleRef, className: clsx(cssClass("portal"), !visible && cssClass("portal_closed")), onTransitionEnd: onTransitionEnd.current, onFocus: (event) => {
320
+ ? createPortal(jsx("div", { "aria-modal": true, role: "dialog", "aria-roledescription": "carousel", tabIndex: -1, ref: handleRef, style: styles?.portal, className: clsx(cssClass("portal"), !visible && cssClass("portal_closed"), className), onTransitionEnd: onTransitionEnd.current, onFocus: (event) => {
311
321
  if (!restoreFocus.current) {
312
322
  restoreFocus.current = event.relatedTarget;
313
323
  }
package/dist/styles.css CHANGED
@@ -1 +1 @@
1
- body:has(>.yarll__portal){overscroll-behavior:none}body:has(>.yarll__portal):not(.yarll__no_scroll_lock){height:100%;overflow:hidden;padding-right:var(--yarll__scrollbar-width,0)}.yarll__portal{align-items:center;display:flex;flex-direction:column;inset:0;justify-content:center;outline:none;overflow:hidden;overscroll-behavior:none;position:fixed;touch-action:none;-moz-user-select:none;user-select:none;-webkit-user-select:none;z-index:var(--yarll__portal_zindex,9999);-webkit-touch-callout:none;background-color:var(--yarll__backdrop_color,#000);color:var(--yarll__color,#fff);opacity:1;transition:var(--yarll__fade_transition,opacity .3s ease)}.yarll__portal_closed{opacity:0}.yarll__portal *{box-sizing:border-box}.yarll__carousel{align-self:stretch;flex:1;margin:var(--yarll__carousel_margin,16px);position:relative}.yarll__slide{align-items:center;display:flex;flex-direction:column;inset:0;justify-content:center;position:absolute}.yarll__slide[hidden]{display:none}.yarll__slide_image{display:block;flex:1;max-height:100%;max-width:100%;min-height:0;min-width:0;-o-object-fit:contain;object-fit:contain}.yarll__button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--yarll__button_background_color,transparent);border:0;color:var(--yarll__button_color,var(--yarll__button_color,hsla(0,0%,100%,.8)));cursor:pointer;filter:var(--yarll__button_filter,drop-shadow(2px 2px 2px rgba(0,0,0,.8)));margin:0;padding:var(--yarll__button_padding,8px)}.yarll__button:focus-visible{box-shadow:var(--yarll__button_focus_box_shadow,0 0 0 4px #fff);color:var(--yarll__button_color_active,#fff);outline:var(--yarll__button_focus_outline,6px double #000)}@supports not selector(:focus-visible){.yarll__button:focus{box-shadow:var(--yarll__button_focus_box_shadow,0 0 0 4px #fff);color:var(--yarll__button_color_active,#fff);outline:var(--yarll__button_focus_outline,6px double #000)}}@media (hover:hover){.yarll__button:focus-visible:hover,.yarll__button:focus:hover,.yarll__button:hover{color:var(--yarll__button_color_active,#fff)}}.yarll__button_close{position:absolute;right:8px;top:8px}.yarll__button_prev{left:8px}.yarll__button_next{right:8px}.yarll__button_next,.yarll__button_prev{padding:var(--yarll__navigation_button_padding,24px 8px);position:absolute}.yarll__button:disabled{color:var(--yarll__button_color_disabled,var(--yarll__button_color_disabled,hsla(0,0%,100%,.4)));cursor:default}.yarll__icon{display:block;height:var(--yarll__icon_size,32px);width:var(--yarll__icon_size,32px)}
1
+ body:has(>.yarll__portal){overscroll-behavior:none}body:has(>.yarll__portal:not(.yarll__no_scroll_lock)){height:100%;overflow:hidden;padding-right:var(--yarll__scrollbar-width,0)}body:has(>.yarll__portal:not(.yarll__no_scroll_lock)) .yarll_fixed{padding-right:var(--yarll__scrollbar-width,0)}.yarll__portal{align-items:center;display:flex;flex-direction:column;inset:0;justify-content:center;outline:none;overflow:hidden;overscroll-behavior:none;position:fixed;touch-action:none;-moz-user-select:none;user-select:none;-webkit-user-select:none;z-index:var(--yarll__portal_zindex,9999);-webkit-touch-callout:none;background-color:var(--yarll__backdrop_color,#000);color:var(--yarll__color,#fff);opacity:1;transition:var(--yarll__fade_transition,opacity .3s ease)}.yarll__portal_closed{opacity:0}.yarll__portal *{box-sizing:border-box}.yarll__carousel{align-self:stretch;flex:1;margin:var(--yarll__carousel_margin,16px);position:relative}.yarll__slide{align-items:center;display:flex;flex-direction:column;inset:0;justify-content:center;position:absolute}.yarll__slide[hidden]{display:none}.yarll__slide_image{display:block;flex:1;max-height:100%;max-width:100%;min-height:0;min-width:0;-o-object-fit:contain;object-fit:contain}.yarll__button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--yarll__button_background_color,transparent);border:0;color:var(--yarll__button_color,var(--yarll__button_color,hsla(0,0%,100%,.8)));cursor:pointer;filter:var(--yarll__button_filter,drop-shadow(2px 2px 2px rgba(0,0,0,.8)));margin:0;padding:var(--yarll__button_padding,8px)}.yarll__button:focus-visible{box-shadow:var(--yarll__button_focus_box_shadow,0 0 0 4px #fff);color:var(--yarll__button_color_active,#fff);outline:var(--yarll__button_focus_outline,6px double #000)}@supports not selector(:focus-visible){.yarll__button:focus{box-shadow:var(--yarll__button_focus_box_shadow,0 0 0 4px #fff);color:var(--yarll__button_color_active,#fff);outline:var(--yarll__button_focus_outline,6px double #000)}}@media (hover:hover){.yarll__button:focus-visible:hover,.yarll__button:focus:hover,.yarll__button:hover{color:var(--yarll__button_color_active,#fff)}}.yarll__button_close{position:absolute;right:8px;top:8px}.yarll__button_prev{left:8px}.yarll__button_next{right:8px}.yarll__button_next,.yarll__button_prev{padding:var(--yarll__navigation_button_padding,24px 8px);position:absolute}.yarll__button:disabled{color:var(--yarll__button_color_disabled,var(--yarll__button_color_disabled,hsla(0,0%,100%,.4)));cursor:default}.yarll__icon{display:block;height:var(--yarll__icon_size,32px);width:var(--yarll__icon_size,32px)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yet-another-react-lightbox-lite",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Lightweight React lightbox component",
5
5
  "author": "Igor Danchenko",
6
6
  "license": "MIT",