react-lazy-img-observer 1.4.0 → 1.5.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
@@ -1,4 +1,10 @@
1
- # 📷 ImageLazy (v1.4.0)
1
+ # 📷 ImageLazy (v1.5.0)
2
+
3
+ ![npm](https://img.shields.io/npm/v/react-lazy-img-observer)
4
+ ![downloads](https://img.shields.io/npm/dw/react-lazy-img-observer)
5
+ ![license](https://img.shields.io/npm/l/react-lazy-img-observer)
6
+
7
+ ---
2
8
 
3
9
  **ImageLazy** is a lightweight and customizable React component for lazy loading images using the Intersection Observer API. It includes fallback handling, visual effects, and support for custom loaders and transitions — perfect for performance-oriented applications.
4
10
 
@@ -6,12 +12,80 @@
6
12
 
7
13
  ---
8
14
 
15
+ ## 📚 Contenido
16
+
17
+ - [📦 Instalación](#-installation--instalación)
18
+ - [✨ Características Nuevas](#-new-features--características-nuevas)
19
+ - [🚀 Uso Básico](#-usage--uso-básico)
20
+ - [📃 Tabla de Props](#-props-table--tabla-de-props)
21
+ - [🎨 Ejemplos Avanzados](#-advanced-example-with-spinner-and-fallback--ejemplo-avanzado-con-spinner-y-fallback)
22
+ - [💡 Uso de Props](#-prop-usage-with-examples--uso-de-props-con-ejemplos)
23
+ - [🔁 Efectos Visuales](#-effects--efectos-visuales)
24
+ - [🔧 Cómo funciona](#-how-it-works--cómo-funciona)
25
+ - [❓ FAQ](#-faq)
26
+ - [📊 Comparación](#-comparison-with-react-lazy-load-image-component--comparación-con-react-lazy-load-image-component)
27
+ - [📄 Licencia](#-license--licencia)
28
+ - [👤 Autor](#-author--autor)
29
+
30
+ ---
31
+
9
32
  ## 📦 Installation / Instalación
10
33
 
11
34
  ```bash
12
35
  npm install react-lazy-img-observer
13
36
  ```
14
37
 
38
+ ## ✨ New Features / Características Nuevas
39
+
40
+ - **Next.js 13+ Ready**: Full support for App Router with `"use client"` directive.
41
+ - **SSR Optimized**: Zero hydration mismatches or server warnings.
42
+ - **TypeScript Native**: Written in TS for perfect autocompletion.
43
+ - **Performance**: Improved threshold defaults and memoized styles.
44
+
45
+ ---
46
+
47
+ ## 📃 Props Table / Tabla de Props
48
+
49
+ | Prop | Tipo | Por Defecto | Descripción (ES) / Description (EN) |
50
+ | -------------------- | ----------------------------------------- | ----------- | --------------------------------------------------------------- |
51
+ | `src` | `string` | – | URL de la imagen / Image URL |
52
+ | `alt` | `string` | – | Texto alternativo / Alt text |
53
+ | `srcSet` | `string` | – | Set de imágenes responsive / Responsive image set |
54
+ | `sizes` | `string` | – | Tamaños para srcSet / Responsive sizes |
55
+ | `width` | `number` | – | Ancho fijo / Fixed width |
56
+ | `height` | `number` | – | Alto fijo / Fixed height |
57
+ | `id` | `string \| number` | – | ID del elemento / Element ID |
58
+ | `className` | `string` | – | Clase CSS personalizada / Custom CSS class |
59
+ | `title` | `string` | – | Atributo title del tag img / Tooltip text |
60
+ | `useTitleFromAlt` | `boolean` | `false` | Usa el alt como title / Use `alt` value as `title` |
61
+ | `extraData` | `ImgHTMLAttributes<HTMLImageElement>` | – | Props adicionales para img / Extra native props |
62
+ | `viewTransitionName` | `string` | – | Nombre para View Transitions API |
63
+ | `style` | `CSSProperties` | – | Estilos inline / Inline styles |
64
+ | `backgroundColor` | `string` | – | Color de fondo mientras carga / Background while loading |
65
+ | `animationDuration` | `string` | `0.9s` | Duración de la transición / Animation duration |
66
+ | `blurAmount` | `string` | `20px` | Desenfoque inicial / Initial blur effect |
67
+ | `fallbackSrc` | `string` | – | Imagen fallback si falla / Fallback image if loading fails |
68
+ | `threshold` | `number` | `0.5` | Umbral de visibilidad / Intersection threshold |
69
+ | `transitionType` | `'blur' \| 'fade' \| 'scale' \| 'custom'` | `'blur'` | Tipo de transición visual / Type of transition |
70
+ | `onLoadComplete` | `() => void` | – | Callback al cargar / Callback after image is fully loaded |
71
+ | `visibleByDefault` | `boolean` | `false` | Cargar sin lazy / Load immediately without waiting for viewport |
72
+ | `loadingComponent` | `ReactNode` | – | Componente mientras carga / Custom loader component |
73
+
74
+ ---
75
+
76
+ ---
77
+
78
+ ## 🔁 Effects / Efectos Visuales
79
+
80
+ | Effect | Description |
81
+ | -------- | ----------------------------------------------------------------------------------------- |
82
+ | `blur` | Apply blur and remove it after load / Aplicar desenfoque y eliminarlo después de cargar |
83
+ | `fade` | Fade in the image / Desvanecimiento de la imagen |
84
+ | `scale` | Slight zoom effect / Ligero efecto de zoom |
85
+ | `custom` | Use your own CSS (add class to image) / Utilice su propio CSS (agregue clase a la imagen) |
86
+
87
+ ---
88
+
15
89
  ---
16
90
 
17
91
  ## 🚀 Usage / Uso Básico
@@ -31,6 +105,12 @@ function App() {
31
105
  }
32
106
  ```
33
107
 
108
+ ### ✅ Explicación:
109
+
110
+ - `src`: URL de la imagen / Image URL.
111
+ - `alt`: Texto descriptivo para accesibilidad y SEO / Descriptive text for accessibility and SEO.
112
+ - `width` / `height`: Definen tamaño fijo (opcional pero recomendable para evitar reflow / Define fixed size (optional but recommended to avoid reflow).
113
+
34
114
  ---
35
115
 
36
116
  ## 🎨 Advanced Example with Spinner and Fallback / Ejemplo Avanzado con Spinner y Fallback
@@ -60,142 +140,271 @@ function GalleryImage() {
60
140
 
61
141
  ---
62
142
 
63
- ## 💫 Custom Transition with Your Own CSS / Transiciones Personalizadas con CSS Propio
143
+ ## 💡 Prop Usage with Examples / Uso de Props con Ejemplos
144
+
145
+ ### 📍 `fallbackSrc`
146
+
147
+ ```tsx
148
+ <ImageLazy
149
+ src="/img/main.jpg"
150
+ fallbackSrc="/img/fallback.jpg"
151
+ alt="Imagen principal"
152
+ />
153
+ ```
154
+
155
+ 👉 Si la imagen original falla, se muestra automáticamente la alternativa / If the original image fails, the alternative is automatically displayed.
156
+
157
+ ---
158
+
159
+ ### 🎨 `backgroundColor`
160
+
161
+ ```tsx
162
+ <ImageLazy
163
+ src="/img/pic.jpg"
164
+ alt="Imagen con fondo gris"
165
+ backgroundColor="#f0f0f0"
166
+ />
167
+ ```
168
+
169
+ 👉 Color visible mientras la imagen carga (mejora UX) / Color visible while image loads (UX improvement).
170
+
171
+ ---
172
+
173
+ ### 💫 `blurAmount` y `animationDuration`
64
174
 
65
175
  ```tsx
66
176
  <ImageLazy
67
- src="/img/circle.jpg"
68
- alt="Spinning Image"
69
- transitionType="custom"
70
- className="my-custom-transition"
177
+ src="/img/blurred.jpg"
178
+ alt="Efecto blur personalizado"
179
+ blurAmount="5px"
180
+ animationDuration="2s"
71
181
  />
72
182
  ```
73
183
 
74
- **CSS**
184
+ 👉 Controla el desenfoque inicial y duración del efecto / Controls the initial blur and duration of the effect.
185
+
186
+ ---
187
+
188
+ ### 🌀 `transitionType`
189
+
190
+ ```tsx
191
+ <ImageLazy src="/img/pic.jpg" alt="Fade in" transitionType="fade" />
192
+ <ImageLazy src="/img/pic.jpg" alt="Scale in" transitionType="scale" />
193
+ <ImageLazy src="/img/pic.jpg" alt="Custom CSS" transitionType="custom" className="img-fx" />
194
+ ```
195
+
196
+ **CSS (ejemplo avanzado):**
75
197
 
76
198
  ```css
77
- .my-custom-transition {
199
+ .img-fx {
78
200
  opacity: 0;
79
- transform: rotate(10deg) scale(0.95);
80
- transition: all 0.8s ease-in-out;
201
+ transform: scale(0.8) rotate(-2deg);
202
+ transition: all 0.8s ease;
81
203
  }
82
-
83
- .my-custom-transition.loaded {
204
+ .img-fx.loaded {
84
205
  opacity: 1;
85
- transform: rotate(0deg) scale(1);
206
+ transform: scale(1) rotate(0deg);
86
207
  }
87
208
  ```
88
209
 
210
+ 👉 Usa `custom` para controlar completamente la animación con tu propio CSS / Use `custom` to fully control the animation with your own CSS.
211
+
89
212
  ---
90
213
 
91
- ## 🧩 Props
214
+ ### 🔁 `visibleByDefault`
92
215
 
93
- | Prop | Type | Default | Description / Descripción |
94
- | -------------------- | ----------------------------------- | ------- | ---------------------------------------------------------- |
95
- | `src` | `string` | - | Image URL / URL de imagen ✅ |
96
- | `alt` | `string` | - | Alt text / Texto alternativo ✅ |
97
- | `width` | `number` | - | Width / Ancho |
98
- | `height` | `number` | - | Height / Alto |
99
- | `srcSet` | `string` | - | Responsive image set |
100
- | `sizes` | `string` | - | Responsive sizes |
101
- | `fallbackSrc` | `string` | - | Fallback if image fails / Imagen alternativa si falla |
102
- | `backgroundColor` | `string` | - | Background color before load |
103
- | `blurAmount` | `string` | `20px` | Initial blur / Desenfoque inicial |
104
- | `animationDuration` | `string` | `0.9s` | Transition duration / Duración de transición |
105
- | `threshold` | `number` | `0.5` | Intersection threshold / Umbral de visibilidad |
106
- | `transitionType` | `"blur"\|"fade"\|"scale"\|"custom"` | `blur` | Type of transition / Tipo de transición |
107
- | `loadingComponent` | `ReactNode` | - | Custom loader / Componente de carga personalizado ✅ |
108
- | `onLoadComplete` | `() => void` | - | Callback when loaded / Al terminar de cargar |
109
- | `visibleByDefault` | `boolean` | `false` | Skip lazy load / Saltar carga diferida si ya está en caché |
110
- | `viewTransitionName` | `string` | - | View Transitions API |
111
- | `extraData` | `ImgHTMLAttributes` | - | Additional attributes |
112
- | `style` | `CSSProperties` | - | Inline styles |
113
- | `className` | `string` | - | Custom CSS class |
114
- | `id` | `string`\|`number` | - | Element ID |
216
+ ```tsx
217
+ <ImageLazy src="/img/pic.jpg" alt="Carga inmediata" visibleByDefault={true} />
218
+ ```
219
+
220
+ 👉 Útil cuando no deseas esperar a que la imagen entre al viewport (ej: SSR o cuando ya está precargada / Useful when you don't want to wait for the image to enter the viewport (e.g. SSR or when it is already pre-loaded).
115
221
 
116
222
  ---
117
223
 
118
224
  ## 🔧 How it works / ¿Cómo funciona?
119
225
 
120
- - When `visibleByDefault` is `false` (default), the image loads only when it's in the viewport (IntersectionObserver).
121
- - If `visibleByDefault` is `true`, the image is loaded immediately (useful for SSR or cached images).
122
- - You can add a custom CSS transition, fallback, background color, and even a loading spinner.
226
+ - When `visibleByDefault` is `false` (default), the image loads only when it's in the viewport (IntersectionObserver)/Cuando `visibleByDefault` es `false` (predeterminado), la imagen se carga solo cuando está en la ventana gráfica (IntersectionObserver).
227
+ - If `visibleByDefault` is `true`, the image is loaded immediately (useful for SSR or cached images)/Si `visibleByDefault` es `true`, la imagen se carga inmediatamente (útil para SSR o imágenes almacenadas en caché).
228
+ - You can add a custom CSS transition, fallback, background color, and even a loading spinner/Puede agregar una transición CSS personalizada, una alternativa, un color de fondo e incluso un indicador de carga.
123
229
 
124
230
  ---
125
231
 
126
- ## 🔁 Effects / Efectos Visuales
232
+ ### 🧭 `loadingComponent`
233
+
234
+ ```tsx
235
+ <ImageLazy
236
+ src="/img/loading.jpg"
237
+ alt="Con spinner"
238
+ loadingComponent={<div className="spinner">Cargando...</div>}
239
+ />
240
+ ```
127
241
 
128
- | Effect | Description |
129
- | -------- | ------------------------------------- |
130
- | `blur` | Apply blur and remove it after load |
131
- | `fade` | Fade in the image |
132
- | `scale` | Slight zoom effect |
133
- | `custom` | Use your own CSS (add class to image) |
242
+ 👉 Muestra un componente personalizado mientras se carga la imagen / Display a custom component while the image is loading.
134
243
 
135
244
  ---
136
245
 
137
- ## 🌐 SSR Compatible / Compatible con SSR
246
+ ### 🧪 `onLoadComplete`
138
247
 
139
- ImageLazy is safe to use in SSR environments (like Next.js). It checks for `window` and disables IntersectionObserver logic when rendering on the server.
248
+ ```tsx
249
+ function handleLoad() {
250
+ console.log("¡Imagen completamente cargada!");
251
+ }
140
252
 
141
- ImageLazy es seguro para entornos con renderizado del lado del servidor como Next.js. Detecta si `window` está disponible antes de usar IntersectionObserver.
253
+ <ImageLazy
254
+ src="/img/event.jpg"
255
+ alt="Imagen con evento"
256
+ onLoadComplete={handleLoad}
257
+ />;
258
+ ```
259
+
260
+ ### 🔁 `onLoadComplete` con lógica avanzada / with advanced logic
261
+
262
+ ```tsx
263
+ const [estado, setEstado] = useState("cargando");
264
+
265
+ <ImageLazy
266
+ src="/img/camara.jpg"
267
+ alt="Avanzado"
268
+ onLoadComplete={() => setEstado("cargada")}
269
+ />
270
+ <p>Estado de imagen: {estado}</p>
271
+ ```
272
+
273
+ ### ✅ `onLoadComplete` con múltiples imágenes / with multiple images
274
+
275
+ ```tsx
276
+ const [cargas, setCargas] = useState(0);
277
+
278
+ const incrementar = () => setCargas((prev) => prev + 1);
279
+
280
+ <ImageLazy src="/img/uno.jpg" alt="uno" onLoadComplete={incrementar} />
281
+ <ImageLazy src="/img/dos.jpg" alt="dos" onLoadComplete={incrementar} />
282
+ <p>Imágenes cargadas: {cargas}</p>
283
+ ```
142
284
 
143
285
  ---
144
286
 
145
- ## 🖼 Spinner / Carga Personalizada
287
+ ### 🧷 `useTitleFromAlt`
146
288
 
147
- You can use `loadingComponent` to display a spinner while the image loads:
289
+ ```tsx
290
+ <ImageLazy
291
+ src="/img/title.jpg"
292
+ alt="Una imagen informativa"
293
+ useTitleFromAlt={true}
294
+ />
295
+ ```
296
+
297
+ 👉 Usa el valor de `alt` también como atributo `title` automáticamente. Útil para tooltips nativos / Automatically uses the `alt` value as the `title` attribute as well. Useful for native tooltips..
298
+
299
+ ---
300
+
301
+ ### 🖼 `viewTransitionName`
148
302
 
149
303
  ```tsx
150
304
  <ImageLazy
151
- src="/img/pic.webp"
152
- alt="Custom Spinner"
153
- loadingComponent={<div className="my-spinner" />}
305
+ src="/img/view.jpg"
306
+ alt="Transición de vista"
307
+ viewTransitionName="fade-image"
154
308
  />
155
309
  ```
156
310
 
157
- ```css
158
- .my-spinner {
159
- width: 40px;
160
- height: 40px;
161
- border: 4px solid #ccc;
162
- border-top-color: #000;
163
- border-radius: 50%;
164
- animation: spin 1s linear infinite;
165
- }
166
- @keyframes spin {
167
- to { transform: rotate(360deg); }
168
- }
311
+ 👉 Para integrar con la API de `view-transition` (Chrome ≥ 111) y hacer animaciones entre páginas con `<ViewTransition>` / To integrate with the `view-transition` API (Chrome ≥ 111) and make animations between pages with `<ViewTransition>`.
312
+
313
+ ---
314
+
315
+ ### 🏷️ `id` (uso en scroll o testing)
316
+
317
+ ```tsx
318
+ <ImageLazy
319
+ src="/img/anchor.jpg"
320
+ alt="Imagen anclada"
321
+ id="fotoPrincipal"
322
+ />
323
+ <a href="#fotoPrincipal">Ir a la imagen</a>
169
324
  ```
170
325
 
326
+ 👉 Permite identificar la imagen fácilmente para navegación, testing u operaciones DOM / Allows you to easily identify the image for navigation, testing, or DOM operations..
327
+
328
+ ---
329
+
330
+ ### 📐 `srcSet` y `sizes`
331
+
332
+ ```tsx
333
+ <ImageLazy
334
+ src="/img/400.jpg"
335
+ srcSet="/img/400.jpg 400w, /img/800.jpg 800w"
336
+ sizes="(max-width: 600px) 400px, 800px"
337
+ alt="Imagen responsive"
338
+ />
339
+ ```
340
+
341
+ 👉 Mejora el rendimiento en dispositivos móviles cargando imágenes más ligeras según el ancho de pantalla / Improves performance on mobile devices by loading images smaller based on screen width.
342
+
343
+ ---
344
+
345
+ ### 🧩 `extraData`, `className`, `style`
346
+
347
+ ```tsx
348
+ <ImageLazy
349
+ src="/img/extra.jpg"
350
+ alt="Con extras"
351
+ className="rounded shadow"
352
+ style={{ objectFit: "cover" }}
353
+ extraData={{ draggable: false, crossOrigin: "anonymous" }}
354
+ />
355
+ ```
356
+
357
+ 👉 Puedes personalizar todo el comportamiento nativo de `<img>` sin limitaciones / You can customize all native behavior of `<img>` without limitations.
358
+
359
+ ---
360
+
361
+ ## 🌐 SSR Compatible / Compatible con SSR
362
+
363
+ ImageLazy is safe to use in SSR environments (like Next.js). It checks for `window` and disables IntersectionObserver logic when rendering on the server.
364
+
365
+ ImageLazy es seguro para entornos con renderizado del lado del servidor como Next.js. Detecta si `window` está disponible antes de usar IntersectionObserver.
366
+
171
367
  ---
172
368
 
173
369
  ## 📊 Comparison with react-lazy-load-image-component / Comparación con react-lazy-load-image-component
174
370
 
175
- | Feature / Característica | `ImageLazy` ✅ | `react-lazy-load-image-component` ❌ |
176
- |----------------------------------------------|------------------------------------|--------------------------------------------------|
177
- | Lightweight & no external dependencies | ✅ Yes / Sí | ❌ No (más pesado y con múltiples componentes) |
178
- | SSR Friendly (Next.js compatible) | ✅ Yes / Sí | ✅ Yes / Sí |
179
- | Custom transitions via CSS (`custom`) | ✅ Yes / Sí | ❌ No |
180
- | Native spinner support (`loadingComponent`) | ✅ Yes / Sí | ❌ No (solo con workarounds) |
181
- | Fallback support (`fallbackSrc`) | ✅ Yes / Sí | ✅ Yes / Sí |
182
- | Responsive images (`srcSet` and `sizes`) | ✅ Yes / Sí | ✅ Yes / Sí |
183
- | Load other elements lazily | ❌ Images only / Solo imágenes | ✅ Yes (via `LazyLoadComponent`) |
184
- | Built-in effects | ✅ `blur`, `fade`, `scale`, custom | ✅ `blur`, `opacity`, `bw` |
185
- | Transition customization | ✅ Total freedom with CSS | ❌ Limited |
371
+ | Feature / Característica | `ImageLazy` ✅ | `react-lazy-load-image-component` ❌ |
372
+ | ------------------------------------------- | ---------------------------------- | ---------------------------------------------- |
373
+ | Lightweight & no external dependencies | ✅ Yes / Sí | ❌ No (más pesado y con múltiples componentes) |
374
+ | SSR Friendly (Next.js compatible) | ✅ Yes / Sí | ✅ Yes / Sí |
375
+ | Custom transitions via CSS (`custom`) | ✅ Yes / Sí | ❌ No |
376
+ | Native spinner support (`loadingComponent`) | ✅ Yes / Sí | ❌ No (solo con workarounds) |
377
+ | Fallback support (`fallbackSrc`) | ✅ Yes / Sí | ✅ Yes / Sí |
378
+ | Responsive images (`srcSet` and `sizes`) | ✅ Yes / Sí | ✅ Yes / Sí |
379
+ | Load other elements lazily | ❌ Images only / Solo imágenes | ✅ Yes (via `LazyLoadComponent`) |
380
+ | Built-in effects | ✅ `blur`, `fade`, `scale`, custom | ✅ `blur`, `opacity`, `bw` |
381
+ | Transition customization | ✅ Total freedom with CSS | ❌ Limited |
382
+
383
+ ---
384
+
385
+ ## ❓ FAQ
386
+
387
+ **¿Por qué mi imagen no se muestra?** / **Why isn't my image showing?**
388
+ Asegúrate de que `src` no sea `null`, y revisa si `visibleByDefault` está en `true` / Make sure `src` is not `null`, and check if `visibleByDefault` is set to `true`..
389
+
390
+ **¿Puedo usarlo en SSR como Next.js?** / **Can I use it in SSR like Next.js?**
391
+ Sí. Internamente revisa si `window` existe antes de ejecutar el observer / Yes. Internally it checks if `window` exists before running the observer.
392
+
393
+ **¿Cómo puedo aplicar una animación personalizada?** / **How ​​can I apply a custom animation?**
394
+ Usa `transitionType="custom"` y define tu clase con efectos CSS / Use `transitionType="custom"` and define your class with CSS effects.
186
395
 
187
396
  ---
188
397
 
189
398
  ## 📄 License / Licencia
190
399
 
191
- [ISC License](./LICENSE)
400
+ [MIT License](./LICENSE)
192
401
 
193
402
  ---
194
403
 
195
404
  ## 👤 Author / Autor
196
405
 
197
- **Percy Chuzon**\
198
- 📧 [contacto@percychuzon.com](mailto\:contacto@percychuzon.com)\
406
+ **Percy Chuzon**
407
+ 📧 [contacto@percychuzon.com](mailto:contacto@percychuzon.com)
199
408
  🌐 [https://percychuzon.com](https://percychuzon.com)
200
409
 
201
410
  ---
@@ -209,4 +418,3 @@ Si deseas más control, usa `transitionType="custom"` y aplica tus estilos con C
209
418
  ---
210
419
 
211
420
  Happy loading! 🎉 / ¡Carga feliz! 🎉
212
-
@@ -1,13 +1,31 @@
1
1
  "use strict";
2
+ "use client";
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
3
4
  exports.ImageLazy = void 0;
4
5
  const jsx_runtime_1 = require("react/jsx-runtime");
5
6
  const react_1 = require("react");
6
- const ImageLazy = ({ src, alt, srcSet, sizes, width, height, className, id, extraData, title, viewTransitionName, style, backgroundColor, animationDuration = "0.9s", blurAmount = "20px", fallbackSrc, threshold = 0.5, transitionType = "blur", onLoadComplete, visibleByDefault = false, loadingComponent, }) => {
7
+ const ImageLazy = ({ src, alt, srcSet, sizes, width, height, className, id, extraData, title, useTitleFromAlt = false, viewTransitionName, style, backgroundColor, animationDuration = "0.9s", blurAmount = "20px", fallbackSrc, threshold = 0.1, transitionType = "blur", onLoadComplete, visibleByDefault = false, loadingComponent, }) => {
7
8
  const imageRef = (0, react_1.useRef)(null);
8
9
  const [realSrc, setRealSrc] = (0, react_1.useState)(visibleByDefault ? src : null);
9
10
  const [loaded, setLoaded] = (0, react_1.useState)(false);
10
11
  const [hasError, setHasError] = (0, react_1.useState)(false);
12
+ (0, react_1.useEffect)(() => {
13
+ setLoaded(false);
14
+ setHasError(false);
15
+ setRealSrc(visibleByDefault ? src : null);
16
+ }, [src, visibleByDefault]);
17
+ (0, react_1.useEffect)(() => {
18
+ const isDev = typeof window !== "undefined" &&
19
+ window?.location?.hostname?.includes("localhost");
20
+ if (isDev) {
21
+ if (!alt) {
22
+ console.warn("[ImageLazy] ⚠️ Se recomienda definir el atributo 'alt' por accesibilidad.");
23
+ }
24
+ if (!src) {
25
+ console.warn("[ImageLazy] ⚠️ La prop 'src' está vacía o nula. La imagen no se cargará.");
26
+ }
27
+ }
28
+ }, [alt, src]);
11
29
  (0, react_1.useEffect)(() => {
12
30
  if (typeof window === "undefined" || visibleByDefault)
13
31
  return;
@@ -23,7 +41,7 @@ const ImageLazy = ({ src, alt, srcSet, sizes, width, height, className, id, extr
23
41
  }, { root: null, rootMargin: "0px", threshold });
24
42
  observer.observe(imageRef.current);
25
43
  return () => observer.disconnect();
26
- }, [src, threshold, visibleByDefault]);
44
+ }, [src, threshold, visibleByDefault, realSrc]);
27
45
  const handleLoad = () => {
28
46
  setLoaded(true);
29
47
  onLoadComplete?.();
@@ -37,31 +55,36 @@ const ImageLazy = ({ src, alt, srcSet, sizes, width, height, className, id, extr
37
55
  }
38
56
  };
39
57
  const isCustom = transitionType === "custom";
40
- const transitionStyles = isCustom
41
- ? {}
42
- : transitionType === "fade"
43
- ? {
58
+ const transitionStyles = (0, react_1.useMemo)(() => {
59
+ if (isCustom)
60
+ return {};
61
+ if (transitionType === "fade") {
62
+ return {
44
63
  opacity: loaded ? 1 : 0,
45
64
  transition: `opacity ${animationDuration}`,
46
- }
47
- : transitionType === "scale"
48
- ? {
49
- transform: loaded ? "scale(1)" : "scale(1.05)",
50
- opacity: loaded ? 1 : 0,
51
- transition: `transform ${animationDuration}, opacity ${animationDuration}`,
52
- }
53
- : {
54
- filter: loaded ? "none" : `blur(${blurAmount})`,
55
- transition: `filter ${animationDuration}`,
56
- };
57
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [!loaded && loadingComponent, !hasError && ((0, jsx_runtime_1.jsx)("img", { ref: imageRef, src: realSrc ?? "", alt: alt, title: title, className: className, width: width, height: height, loading: "lazy", id: id?.toString(), onLoad: handleLoad, onError: handleError, srcSet: srcSet, sizes: sizes, style: {
58
- backgroundColor: !loaded && backgroundColor ? backgroundColor : undefined,
59
- backgroundSize: "cover",
60
- backgroundPosition: "center",
61
- ...(viewTransitionName ? { viewTransitionName } : {}),
62
- ...transitionStyles,
63
- ...style,
64
- }, ...extraData }))] }));
65
+ };
66
+ }
67
+ if (transitionType === "scale") {
68
+ return {
69
+ transform: loaded ? "scale(1)" : "scale(1.05)",
70
+ opacity: loaded ? 1 : 0,
71
+ transition: `transform ${animationDuration}, opacity ${animationDuration}`,
72
+ };
73
+ }
74
+ return {
75
+ filter: loaded ? "none" : `blur(${blurAmount})`,
76
+ transition: `filter ${animationDuration}`,
77
+ };
78
+ }, [transitionType, loaded, animationDuration, blurAmount, isCustom]);
79
+ const finalStyle = (0, react_1.useMemo)(() => ({
80
+ backgroundColor: !loaded && backgroundColor ? backgroundColor : undefined,
81
+ backgroundSize: "cover",
82
+ backgroundPosition: "center",
83
+ ...(viewTransitionName ? { viewTransitionName } : {}),
84
+ ...transitionStyles,
85
+ ...style,
86
+ }), [loaded, backgroundColor, viewTransitionName, transitionStyles, style]);
87
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [!loaded && loadingComponent, (!hasError || realSrc === null) && ((0, jsx_runtime_1.jsx)("img", { ref: imageRef, src: realSrc ?? undefined, alt: alt, title: title ?? (useTitleFromAlt ? alt : undefined), className: className, width: width, height: height, id: id?.toString(), onLoad: handleLoad, onError: handleError, srcSet: realSrc ? srcSet : undefined, sizes: realSrc ? sizes : undefined, style: finalStyle, ...extraData }))] }));
65
88
  };
66
89
  exports.ImageLazy = ImageLazy;
67
90
  exports.default = ImageLazy;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ImageLazy=void 0;const jsx_runtime_1=require("react/jsx-runtime"),react_1=require("react"),ImageLazy=({src:e,alt:t,srcSet:r,sizes:n,width:a,height:o,className:i,id:s,extraData:c,title:u,viewTransitionName:l,style:d,backgroundColor:m,animationDuration:f="0.9s",blurAmount:g="20px",fallbackSrc:y,threshold:p=.5,transitionType:_="blur",onLoadComplete:b,visibleByDefault:x=!1,loadingComponent:h})=>{const v=(0,react_1.useRef)(null),[z,j]=(0,react_1.useState)(x?e:null),[I,S]=(0,react_1.useState)(!1),[w,L]=(0,react_1.useState)(!1);(0,react_1.useEffect)((()=>{if("undefined"==typeof window||x)return;if("undefined"==typeof IntersectionObserver||!v.current)return;const t=new IntersectionObserver((r=>{r.forEach((r=>{r.isIntersecting&&(j(e),t.unobserve(r.target))}))}),{root:null,rootMargin:"0px",threshold:p});return t.observe(v.current),()=>t.disconnect()}),[e,p,x]);const k="custom"===_?{}:"fade"===_?{opacity:I?1:0,transition:`opacity ${f}`}:"scale"===_?{transform:I?"scale(1)":"scale(1.05)",opacity:I?1:0,transition:`transform ${f}, opacity ${f}`}:{filter:I?"none":`blur(${g})`,transition:`filter ${f}`};return(0,jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment,{children:[!I&&h,!w&&(0,jsx_runtime_1.jsx)("img",{ref:v,src:z??"",alt:t,title:u,className:i,width:a,height:o,loading:"lazy",id:s?.toString(),onLoad:()=>{S(!0),b?.()},onError:()=>{y&&z!==y?j(y):L(!0)},srcSet:r,sizes:n,style:{backgroundColor:!I&&m?m:void 0,backgroundSize:"cover",backgroundPosition:"center",...l?{viewTransitionName:l}:{},...k,...d},...c})]})};exports.ImageLazy=ImageLazy,exports.default=ImageLazy;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ImageLazy=void 0;const jsx_runtime_1=require("react/jsx-runtime"),react_1=require("react"),ImageLazy=({src:e,alt:t,srcSet:r,sizes:a,width:o,height:n,className:i,id:s,extraData:c,title:l,useTitleFromAlt:u=!1,viewTransitionName:d,style:m,backgroundColor:f,animationDuration:g="0.9s",blurAmount:p="20px",fallbackSrc:_,threshold:y=.1,transitionType:b="blur",onLoadComplete:v,visibleByDefault:h=!1,loadingComponent:x})=>{const w=(0,react_1.useRef)(null),[L,z]=(0,react_1.useState)(h?e:null),[I,S]=(0,react_1.useState)(!1),[j,k]=(0,react_1.useState)(!1);(0,react_1.useEffect)((()=>{S(!1),k(!1),z(h?e:null)}),[e,h]),(0,react_1.useEffect)((()=>{"undefined"!=typeof window&&window?.location?.hostname?.includes("localhost")&&(t||console.warn("[ImageLazy] ⚠️ Se recomienda definir el atributo 'alt' por accesibilidad."),e||console.warn("[ImageLazy] ⚠️ La prop 'src' está vacía o nula. La imagen no se cargará."))}),[t,e]),(0,react_1.useEffect)((()=>{if("undefined"==typeof window||h)return;if("undefined"==typeof IntersectionObserver||!w.current)return;const t=new IntersectionObserver((r=>{r.forEach((r=>{r.isIntersecting&&(z(e),t.unobserve(r.target))}))}),{root:null,rootMargin:"0px",threshold:y});return t.observe(w.current),()=>t.disconnect()}),[e,y,h,L]);const E="custom"===b,$=(0,react_1.useMemo)((()=>E?{}:"fade"===b?{opacity:I?1:0,transition:`opacity ${g}`}:"scale"===b?{transform:I?"scale(1)":"scale(1.05)",opacity:I?1:0,transition:`transform ${g}, opacity ${g}`}:{filter:I?"none":`blur(${p})`,transition:`filter ${g}`}),[b,I,g,p,E]),C=(0,react_1.useMemo)((()=>({backgroundColor:!I&&f?f:void 0,backgroundSize:"cover",backgroundPosition:"center",...d?{viewTransitionName:d}:{},...$,...m})),[I,f,d,$,m]);return(0,jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment,{children:[!I&&x,(!j||null===L)&&(0,jsx_runtime_1.jsx)("img",{ref:w,src:L??void 0,alt:t,title:l??(u?t:void 0),className:i,width:o,height:n,id:s?.toString(),onLoad:()=>{S(!0),v?.()},onError:()=>{_&&L!==_?z(_):k(!0)},srcSet:L?r:void 0,sizes:L?a:void 0,style:C,...c})]})};exports.ImageLazy=ImageLazy,exports.default=ImageLazy;
@@ -1,10 +1,28 @@
1
+ "use client";
1
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from "react";
3
- const ImageLazy = ({ src, alt, srcSet, sizes, width, height, className, id, extraData, title, viewTransitionName, style, backgroundColor, animationDuration = "0.9s", blurAmount = "20px", fallbackSrc, threshold = 0.5, transitionType = "blur", onLoadComplete, visibleByDefault = false, loadingComponent, }) => {
3
+ import { useEffect, useRef, useState, useMemo } from "react";
4
+ const ImageLazy = ({ src, alt, srcSet, sizes, width, height, className, id, extraData, title, useTitleFromAlt = false, viewTransitionName, style, backgroundColor, animationDuration = "0.9s", blurAmount = "20px", fallbackSrc, threshold = 0.1, transitionType = "blur", onLoadComplete, visibleByDefault = false, loadingComponent, }) => {
4
5
  const imageRef = useRef(null);
5
6
  const [realSrc, setRealSrc] = useState(visibleByDefault ? src : null);
6
7
  const [loaded, setLoaded] = useState(false);
7
8
  const [hasError, setHasError] = useState(false);
9
+ useEffect(() => {
10
+ setLoaded(false);
11
+ setHasError(false);
12
+ setRealSrc(visibleByDefault ? src : null);
13
+ }, [src, visibleByDefault]);
14
+ useEffect(() => {
15
+ const isDev = typeof window !== "undefined" &&
16
+ window?.location?.hostname?.includes("localhost");
17
+ if (isDev) {
18
+ if (!alt) {
19
+ console.warn("[ImageLazy] ⚠️ Se recomienda definir el atributo 'alt' por accesibilidad.");
20
+ }
21
+ if (!src) {
22
+ console.warn("[ImageLazy] ⚠️ La prop 'src' está vacía o nula. La imagen no se cargará.");
23
+ }
24
+ }
25
+ }, [alt, src]);
8
26
  useEffect(() => {
9
27
  if (typeof window === "undefined" || visibleByDefault)
10
28
  return;
@@ -20,7 +38,7 @@ const ImageLazy = ({ src, alt, srcSet, sizes, width, height, className, id, extr
20
38
  }, { root: null, rootMargin: "0px", threshold });
21
39
  observer.observe(imageRef.current);
22
40
  return () => observer.disconnect();
23
- }, [src, threshold, visibleByDefault]);
41
+ }, [src, threshold, visibleByDefault, realSrc]);
24
42
  const handleLoad = () => {
25
43
  setLoaded(true);
26
44
  onLoadComplete?.();
@@ -34,31 +52,36 @@ const ImageLazy = ({ src, alt, srcSet, sizes, width, height, className, id, extr
34
52
  }
35
53
  };
36
54
  const isCustom = transitionType === "custom";
37
- const transitionStyles = isCustom
38
- ? {}
39
- : transitionType === "fade"
40
- ? {
55
+ const transitionStyles = useMemo(() => {
56
+ if (isCustom)
57
+ return {};
58
+ if (transitionType === "fade") {
59
+ return {
41
60
  opacity: loaded ? 1 : 0,
42
61
  transition: `opacity ${animationDuration}`,
43
- }
44
- : transitionType === "scale"
45
- ? {
46
- transform: loaded ? "scale(1)" : "scale(1.05)",
47
- opacity: loaded ? 1 : 0,
48
- transition: `transform ${animationDuration}, opacity ${animationDuration}`,
49
- }
50
- : {
51
- filter: loaded ? "none" : `blur(${blurAmount})`,
52
- transition: `filter ${animationDuration}`,
53
- };
54
- return (_jsxs(_Fragment, { children: [!loaded && loadingComponent, !hasError && (_jsx("img", { ref: imageRef, src: realSrc ?? "", alt: alt, title: title, className: className, width: width, height: height, loading: "lazy", id: id?.toString(), onLoad: handleLoad, onError: handleError, srcSet: srcSet, sizes: sizes, style: {
55
- backgroundColor: !loaded && backgroundColor ? backgroundColor : undefined,
56
- backgroundSize: "cover",
57
- backgroundPosition: "center",
58
- ...(viewTransitionName ? { viewTransitionName } : {}),
59
- ...transitionStyles,
60
- ...style,
61
- }, ...extraData }))] }));
62
+ };
63
+ }
64
+ if (transitionType === "scale") {
65
+ return {
66
+ transform: loaded ? "scale(1)" : "scale(1.05)",
67
+ opacity: loaded ? 1 : 0,
68
+ transition: `transform ${animationDuration}, opacity ${animationDuration}`,
69
+ };
70
+ }
71
+ return {
72
+ filter: loaded ? "none" : `blur(${blurAmount})`,
73
+ transition: `filter ${animationDuration}`,
74
+ };
75
+ }, [transitionType, loaded, animationDuration, blurAmount, isCustom]);
76
+ const finalStyle = useMemo(() => ({
77
+ backgroundColor: !loaded && backgroundColor ? backgroundColor : undefined,
78
+ backgroundSize: "cover",
79
+ backgroundPosition: "center",
80
+ ...(viewTransitionName ? { viewTransitionName } : {}),
81
+ ...transitionStyles,
82
+ ...style,
83
+ }), [loaded, backgroundColor, viewTransitionName, transitionStyles, style]);
84
+ return (_jsxs(_Fragment, { children: [!loaded && loadingComponent, (!hasError || realSrc === null) && (_jsx("img", { ref: imageRef, src: realSrc ?? undefined, alt: alt, title: title ?? (useTitleFromAlt ? alt : undefined), className: className, width: width, height: height, id: id?.toString(), onLoad: handleLoad, onError: handleError, srcSet: realSrc ? srcSet : undefined, sizes: realSrc ? sizes : undefined, style: finalStyle, ...extraData }))] }));
62
85
  };
63
86
  export default ImageLazy;
64
87
  export { ImageLazy };
@@ -1 +1 @@
1
- import{jsx as _jsx,Fragment as _Fragment,jsxs as _jsxs}from"react/jsx-runtime";import{useEffect,useRef,useState}from"react";const ImageLazy=({src:e,alt:t,srcSet:r,sizes:n,width:o,height:s,className:a,id:i,extraData:c,title:l,viewTransitionName:u,style:d,backgroundColor:f,animationDuration:m="0.9s",blurAmount:g="20px",fallbackSrc:p,threshold:y=.5,transitionType:b="blur",onLoadComplete:h,visibleByDefault:x=!1,loadingComponent:v})=>{const S=useRef(null),[j,w]=useState(x?e:null),[z,I]=useState(!1),[_,k]=useState(!1);useEffect((()=>{if("undefined"==typeof window||x)return;if("undefined"==typeof IntersectionObserver||!S.current)return;const t=new IntersectionObserver((r=>{r.forEach((r=>{r.isIntersecting&&(w(e),t.unobserve(r.target))}))}),{root:null,rootMargin:"0px",threshold:y});return t.observe(S.current),()=>t.disconnect()}),[e,y,x]);const L="custom"===b?{}:"fade"===b?{opacity:z?1:0,transition:`opacity ${m}`}:"scale"===b?{transform:z?"scale(1)":"scale(1.05)",opacity:z?1:0,transition:`transform ${m}, opacity ${m}`}:{filter:z?"none":`blur(${g})`,transition:`filter ${m}`};return _jsxs(_Fragment,{children:[!z&&v,!_&&_jsx("img",{ref:S,src:j??"",alt:t,title:l,className:a,width:o,height:s,loading:"lazy",id:i?.toString(),onLoad:()=>{I(!0),h?.()},onError:()=>{p&&j!==p?w(p):k(!0)},srcSet:r,sizes:n,style:{backgroundColor:!z&&f?f:void 0,backgroundSize:"cover",backgroundPosition:"center",...u?{viewTransitionName:u}:{},...L,...d},...c})]})};export default ImageLazy;export{ImageLazy};
1
+ import{jsx as _jsx,Fragment as _Fragment,jsxs as _jsxs}from"react/jsx-runtime";import{useEffect,useRef,useState,useMemo}from"react";const ImageLazy=({src:e,alt:t,srcSet:o,sizes:r,width:n,height:a,className:s,id:i,extraData:c,title:l,useTitleFromAlt:u=!1,viewTransitionName:d,style:m,backgroundColor:f,animationDuration:g="0.9s",blurAmount:p="20px",fallbackSrc:y,threshold:b=.1,transitionType:h="blur",onLoadComplete:v,visibleByDefault:w=!1,loadingComponent:x})=>{const S=useRef(null),[L,z]=useState(w?e:null),[I,j]=useState(!1),[E,_]=useState(!1);useEffect((()=>{j(!1),_(!1),z(w?e:null)}),[e,w]),useEffect((()=>{"undefined"!=typeof window&&window?.location?.hostname?.includes("localhost")&&(t||console.warn("[ImageLazy] ⚠️ Se recomienda definir el atributo 'alt' por accesibilidad."),e||console.warn("[ImageLazy] ⚠️ La prop 'src' está vacía o nula. La imagen no se cargará."))}),[t,e]),useEffect((()=>{if("undefined"==typeof window||w)return;if("undefined"==typeof IntersectionObserver||!S.current)return;const t=new IntersectionObserver((o=>{o.forEach((o=>{o.isIntersecting&&(z(e),t.unobserve(o.target))}))}),{root:null,rootMargin:"0px",threshold:b});return t.observe(S.current),()=>t.disconnect()}),[e,b,w,L]);const k="custom"===h,$=useMemo((()=>k?{}:"fade"===h?{opacity:I?1:0,transition:`opacity ${g}`}:"scale"===h?{transform:I?"scale(1)":"scale(1.05)",opacity:I?1:0,transition:`transform ${g}, opacity ${g}`}:{filter:I?"none":`blur(${p})`,transition:`filter ${g}`}),[h,I,g,p,k]),C=useMemo((()=>({backgroundColor:!I&&f?f:void 0,backgroundSize:"cover",backgroundPosition:"center",...d?{viewTransitionName:d}:{},...$,...m})),[I,f,d,$,m]);return _jsxs(_Fragment,{children:[!I&&x,(!E||null===L)&&_jsx("img",{ref:S,src:L??void 0,alt:t,title:l??(u?t:void 0),className:s,width:n,height:a,id:i?.toString(),onLoad:()=>{j(!0),v?.()},onError:()=>{y&&L!==y?z(y):_(!0)},srcSet:L?o:void 0,sizes:L?r:void 0,style:C,...c})]})};export default ImageLazy;export{ImageLazy};
@@ -8,6 +8,7 @@ export type ImagesLazy = {
8
8
  id?: number | string;
9
9
  className?: string;
10
10
  title?: string;
11
+ useTitleFromAlt?: boolean;
11
12
  extraData?: React.ImgHTMLAttributes<HTMLImageElement>;
12
13
  viewTransitionName?: string;
13
14
  style?: React.CSSProperties;
@@ -21,6 +22,6 @@ export type ImagesLazy = {
21
22
  visibleByDefault?: boolean;
22
23
  loadingComponent?: React.ReactNode;
23
24
  };
24
- declare const ImageLazy: ({ src, alt, srcSet, sizes, width, height, className, id, extraData, title, viewTransitionName, style, backgroundColor, animationDuration, blurAmount, fallbackSrc, threshold, transitionType, onLoadComplete, visibleByDefault, loadingComponent, }: ImagesLazy) => import("react/jsx-runtime").JSX.Element;
25
+ declare const ImageLazy: ({ src, alt, srcSet, sizes, width, height, className, id, extraData, title, useTitleFromAlt, viewTransitionName, style, backgroundColor, animationDuration, blurAmount, fallbackSrc, threshold, transitionType, onLoadComplete, visibleByDefault, loadingComponent, }: ImagesLazy) => import("react/jsx-runtime").JSX.Element;
25
26
  export default ImageLazy;
26
27
  export { ImageLazy };
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "react-lazy-img-observer",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "A React component for lazy loading images with Intersection Observer",
5
5
  "main": "dist/cjs/ImageLazy.min.js",
6
6
  "module": "dist/esm/ImageLazy.min.js",
7
7
  "types": "dist/types/ImageLazy.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
+ "types": "./dist/types/ImageLazy.d.ts",
10
11
  "import": "./dist/esm/ImageLazy.min.js",
11
- "require": "./dist/cjs/ImageLazy.min.js",
12
- "types": "./dist/types/ImageLazy.d.ts"
12
+ "require": "./dist/cjs/ImageLazy.min.js"
13
13
  }
14
14
  },
15
15
  "files": [
@@ -19,6 +19,7 @@
19
19
  ],
20
20
  "sideEffects": false,
21
21
  "scripts": {
22
+ "test": "vitest",
22
23
  "clean": "rimraf dist",
23
24
  "build": "npm run clean && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && terser dist/cjs/ImageLazy.js -o dist/cjs/ImageLazy.min.js --compress --mangle && terser dist/esm/ImageLazy.js -o dist/esm/ImageLazy.min.js --compress --mangle"
24
25
  },
@@ -52,17 +53,20 @@
52
53
  "bugs": {
53
54
  "url": "https://github.com/perch33/react-lazy-img-observer/issues"
54
55
  },
55
- "dependencies": {
56
- "terser": "^5.39.2"
57
- },
58
56
  "peerDependencies": {
59
57
  "react": ">=17.0.0",
60
58
  "react-dom": ">=17.0.0"
61
59
  },
62
60
  "devDependencies": {
61
+ "@testing-library/jest-dom": "^6.9.1",
62
+ "@testing-library/react": "^16.3.1",
63
63
  "@types/react": "^18.3.21",
64
64
  "@types/react-dom": "^18.3.7",
65
+ "@vitejs/plugin-react": "^5.1.2",
66
+ "jsdom": "^27.4.0",
65
67
  "rimraf": "^6.0.1",
66
- "typescript": "^5.8.3"
68
+ "terser": "^5.39.2",
69
+ "typescript": "^5.8.3",
70
+ "vitest": "^4.0.17"
67
71
  }
68
72
  }