Skip to content

Commit a50295b

Browse files
authored
Merge pull request #28 from Mario-Duarte/feat/allow-for-custom-classes-to-be-added
2 parents 48f0f59 + 53f1c5a commit a50295b

19 files changed

Lines changed: 2441 additions & 180 deletions

.github/workflows/test.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Run Tests
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
branches: [master]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Set up Node.js
17+
uses: actions/setup-node@v4
18+
with:
19+
node-version: '20'
20+
21+
- name: Install dependencies
22+
run: npm ci
23+
24+
- name: Run tests
25+
run: npm test

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
20.19.4

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## New to v1.7.0
2+
3+
- Improved accessibility: `figure` now uses `role="button"` and `tabIndex=0` for keyboard navigation.
4+
- Added `theme` prop for custom root and image class styling.
5+
- Enhanced error handling: custom `errorContent` can be rendered on image load failure.
6+
- Refactored internal hooks for better testability and maintainability.
7+
- Updated documentation and examples.
8+
19
## New to v1.6.0
210

311
- Migrated build system to Vite for faster builds and modern development experience.

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ It will have the class `loading` while the image is been preloaded and `loaded`
3535

3636
Additionally it will have the class `fullview` while the user has not initiated the zoom and `zoomed` once the user has taped/clicked in.
3737

38-
## New to v1.6.0
38+
## New to v1.7.0
3939

40-
- Migrated build system to Vite for faster builds and modern development experience.
41-
- TypeScript declaration files are now generated automatically and included in the package.
42-
- Improved compatibility with React 17, 18, and 19 via updated peer dependencies.
43-
- CSS is now injected automatically with the build, simplifying usage and integration.
44-
- Internal codebase refactoring for better maintainability and performance.
40+
- Improved accessibility: `figure` now uses `role="button"` and `tabIndex=0` for keyboard navigation.
41+
- Added `theme` prop for custom root and image class styling.
42+
- Enhanced error handling: custom `errorContent` can be rendered on image load failure.
43+
- Refactored internal hooks for better testability and maintainability.
44+
- Updated documentation and examples.
4545

4646
[View changelog](https://github.com/Mario-Duarte/react-image-zooom/blob/master/CHANGELOG.md) for previous release notes.
4747

dist/react-image-zooom.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
(function(){"use strict";try{if(typeof document<"u"){var o=document.createElement("style");o.appendChild(document.createTextNode('@keyframes rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}figure.image-zooom{position:relative;display:inline-block;width:auto;min-height:25vh;background-position:50% 50%;background-color:#eee;margin:0;overflow:hidden;cursor:zoom-in;-webkit-user-select:none;user-select:none}figure.image-zooom.loaded{min-height:auto}figure.image-zooom.zoomed{cursor:zoom-out}figure.image-zooom:before{content:"";background-color:transparent;position:absolute;top:0;left:0;right:0;width:100%;height:100%;opacity:1;transition:opacity .2s ease-in-out;z-index:1}figure.image-zooom.loaded:before{opacity:0}figure.image-zooom:after{content:"";position:absolute;top:calc(50% - 25px);left:calc(50% - 25px);width:50px;height:50px;border-radius:50%;border:5px solid transparent;border-top-color:#333;border-right-color:#333;border-bottom-color:#333;animation:rotate 2s linear infinite;opacity:1;transition:opacity .2s ease-in-out;z-index:2}figure.image-zooom.loaded:after{opacity:0}img#imageZoom{opacity:1;display:block}figure.image-zooom.zoomed img#imageZoom{opacity:0}p.image-zooom-error{width:100%;text-align:center;border:1px solid #f8f8f8;padding:8px 16px;border-radius:8px;color:#555}')),document.head.appendChild(o)}}catch(e){console.error("vite-plugin-css-injected-by-js",e)}})();
2-
"use strict";const C=require("react/jsx-runtime"),s=require("react");function N(t,o){s.useEffect(()=>{const e=getComputedStyle(document.body).overflow||"auto";return t?document.body.style.overflow="hidden":document.body.style.overflow=e,()=>{document.body.style.overflow=e}},[t]),s.useEffect(()=>{if(!o)return;const e=n=>{t&&n.touches.length===1&&n.preventDefault()};return t&&o.addEventListener("touchmove",e,{passive:!1}),()=>{o.removeEventListener("touchmove",e)}},[t,o])}function V(t,o,e,n){return s.useMemo(()=>{if(!o||!e)return`${t}%`;const r=(n==null?void 0:n.clientWidth)||0;if(!r)return`${t}%`;const l=e/r*100;return`${l<100?t:l}%`},[t,o,e,n])}function X(t){return s.useCallback(o=>{if(!t)return;const e=t.getBoundingClientRect();let n,r;if((i=>"touches"in i)(o)){const i=o.touches[0];n=(i.clientX-e.x)/e.width*100,r=(i.clientY-e.y)/e.height*100}else n=(o.clientX-e.x)/e.width*100,r=(o.clientY-e.y)/e.height*100;return`${Math.max(0,Math.min(n,100))}% ${Math.max(0,Math.min(r,100))}%`},[t])}function Y(t,o){const[e,n]=s.useState({imgData:null,error:!1,naturalWidth:0,naturalHeight:0});return s.useEffect(()=>{if(n({imgData:null,error:!1,naturalWidth:0,naturalHeight:0}),!t){n(f=>({...f,error:!0}));return}const r=new Image,l=()=>{n({imgData:r.src,error:!1,naturalWidth:r.naturalWidth,naturalHeight:r.naturalHeight})},i=f=>{n(b=>({...b,error:!0})),o==null||o(f)};return r.addEventListener("load",l),r.addEventListener("error",i),r.src=t,()=>{r.removeEventListener("load",l),r.removeEventListener("error",i)}},[t,o]),e}function O({zoom:t="200",fullWidth:o=!1,alt:e="This is an imageZoom image",width:n="100%",height:r="auto",src:l,id:i,className:f,onError:b,errorContent:S}){const[u,y]=s.useState(!1),[p,h]=s.useState("50% 50%"),g=s.useRef(null),c=X(g.current),m=s.useRef(!1);N(u,g.current);const{imgData:d,error:$,naturalWidth:D}=Y(l,b),E=V(t,o,D,g.current),L=(a,M,k,x,v,q)=>{M&&(m.current=!0);const I=!k;x(I);const Z=I?q(a):"50% 50%";Z&&v(Z)},T=(a,M,k,x)=>{if(M){const v=x(a);v&&k(v)}},j=s.useCallback(a=>{if(m.current){m.current=!1;return}L(a,!1,u,y,h,c)},[u,c]),w=s.useCallback(a=>{a.touches.length===1&&L(a,!0,u,y,h,c)},[u,c]),z=s.useCallback(a=>{T(a,u,h,c)},[u,c]),H=s.useCallback(a=>{a.touches.length===1&&(a.preventDefault(),m.current=!0,u?T(a,u,h,c):w(a))},[w,u,c]),P=s.useCallback(()=>{m.current=!1,y(!1),h("50% 50%")},[]),W=s.useMemo(()=>({backgroundImage:`url(${u&&d?d:""})`,backgroundSize:E,backgroundPosition:p}),[u,d,E,p]);if($)return s.isValidElement(S)?S:C.jsx("p",{className:"image-zooom-error",children:"There was a problem loading your image"});const B=[d?"loaded":"loading",u?"zoomed":"fullView","image-zooom",f].filter(Boolean).join(" ");return C.jsx("figure",{ref:g,id:i,className:B,style:W,onClick:j,onMouseMove:z,onMouseLeave:P,onTouchStart:w,onTouchMove:H,onTouchEnd:P,role:"button","aria-label":`Zoomable image: ${e}`,tabIndex:0,children:d&&C.jsx("img",{loading:"lazy",id:"imageZoom",src:d,alt:e,width:n,height:r})})}module.exports=O;
2+
"use strict";const C=require("react/jsx-runtime"),s=require("react");function V(t,o){s.useEffect(()=>{const e=getComputedStyle(document.body).overflow||"auto";return t?document.body.style.overflow="hidden":document.body.style.overflow=e,()=>{document.body.style.overflow=e}},[t]),s.useEffect(()=>{if(!o)return;const e=n=>{t&&n.touches.length===1&&n.preventDefault()};return t&&o.addEventListener("touchmove",e,{passive:!1}),()=>{o.removeEventListener("touchmove",e)}},[t,o])}function X(t,o,e,n){return s.useMemo(()=>{if(!o||!e)return`${t}%`;const r=(n==null?void 0:n.clientWidth)||0;if(!r)return`${t}%`;const l=e/r*100;return`${l<100?t:l}%`},[t,o,e,n])}function Y(t){return s.useCallback(o=>{if(!t)return;const e=t.getBoundingClientRect();let n,r;if((i=>"touches"in i)(o)){const i=o.touches[0];n=(i.clientX-e.x)/e.width*100,r=(i.clientY-e.y)/e.height*100}else n=(o.clientX-e.x)/e.width*100,r=(o.clientY-e.y)/e.height*100;return`${Math.max(0,Math.min(n,100))}% ${Math.max(0,Math.min(r,100))}%`},[t])}function O(t,o){const[e,n]=s.useState({imgData:null,error:!1,naturalWidth:0,naturalHeight:0});return s.useEffect(()=>{if(n({imgData:null,error:!1,naturalWidth:0,naturalHeight:0}),!t){n(g=>({...g,error:!0}));return}const r=new Image,l=()=>{n({imgData:r.src,error:!1,naturalWidth:r.naturalWidth,naturalHeight:r.naturalHeight})},i=g=>{n(c=>({...c,error:!0})),o==null||o(g)};return r.addEventListener("load",l),r.addEventListener("error",i),r.src=t,()=>{r.removeEventListener("load",l),r.removeEventListener("error",i)}},[t,o]),e}function A({zoom:t="200",fullWidth:o=!1,alt:e="This is an imageZoom image",width:n="100%",height:r="auto",src:l,id:i,className:g,theme:c,onError:$,errorContent:S}){const[u,y]=s.useState(!1),[p,h]=s.useState("50% 50%"),v=s.useRef(null),d=Y(v.current),m=s.useRef(!1);V(u,v.current);const{imgData:f,error:D,naturalWidth:j}=O(l,$),E=X(t,o,j,v.current),L=(a,M,k,x,b,q)=>{M&&(m.current=!0);const I=!k;x(I);const Z=I?q(a):"50% 50%";Z&&b(Z)},T=(a,M,k,x)=>{if(M){const b=x(a);b&&k(b)}},z=s.useCallback(a=>{if(m.current){m.current=!1;return}L(a,!1,u,y,h,d)},[u,d]),w=s.useCallback(a=>{a.touches.length===1&&L(a,!0,u,y,h,d)},[u,d]),H=s.useCallback(a=>{T(a,u,h,d)},[u,d]),W=s.useCallback(a=>{a.touches.length===1&&(a.preventDefault(),m.current=!0,u?T(a,u,h,d):w(a))},[w,u,d]),P=s.useCallback(()=>{m.current=!1,y(!1),h("50% 50%")},[]),B=s.useMemo(()=>({backgroundImage:`url(${u&&f?f:""})`,backgroundSize:E,backgroundPosition:p}),[u,f,E,p]);if(D)return s.isValidElement(S)?S:C.jsx("p",{className:"image-zooom-error",children:"There was a problem loading your image"});const N=[f?"loaded":"loading",u?"zoomed":"fullView","image-zooom",c==null?void 0:c.root,g].filter(Boolean).join(" ");return C.jsx("figure",{ref:v,id:i,className:N,style:B,onClick:z,onMouseMove:H,onMouseLeave:P,onTouchStart:w,onTouchMove:W,onTouchEnd:P,role:"button","aria-label":`Zoomable image: ${e}`,tabIndex:0,children:f&&C.jsx("img",{loading:"lazy",id:"imageZoom",className:c==null?void 0:c.image,src:f,alt:e,width:n,height:r})})}module.exports=A;

dist/react-image-zooom.d.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { default as React, JSX } from 'react';
22

3+
type ImageZoomClasses = {
4+
root?: string;
5+
image?: string;
6+
};
37
interface ImageZoomProps {
48
zoom?: string | number;
59
fullWidth?: boolean;
@@ -8,10 +12,33 @@ interface ImageZoomProps {
812
height?: string | number;
913
src: string;
1014
id?: string;
15+
/**
16+
* @deprecated Use the `theme` prop instead for custom styling.
17+
*/
1118
className?: string;
12-
onError?: (error: ErrorEvent) => void;
19+
theme?: ImageZoomClasses;
20+
onError?: (error: Event | ErrorEvent) => void;
1321
errorContent?: React.ReactNode;
1422
}
15-
declare function ImageZoom({ zoom, fullWidth, alt, width, height, src, id, className, onError, errorContent, }: ImageZoomProps): JSX.Element;
23+
/**
24+
* Displays an image with interactive zoom functionality.
25+
*
26+
* The `ImageZoom` component allows users to zoom into an image by clicking or touching it.
27+
* It supports both mouse and touch events, custom styling via themes, and error handling for image loading.
28+
*
29+
* @param zoom - The zoom level or percentage for the image when zoomed in. Default is "200".
30+
* @param fullWidth - If true, the zoomed image will fill the container's width. Default is false.
31+
* @param alt - The alt text for the image. Default is "This is an imageZoom image".
32+
* @param width - The width of the image. Default is "100%".
33+
* @param height - The height of the image. Default is "auto".
34+
* @param src - The source URL of the image to display.
35+
* @param id - Optional ID for the figure element.
36+
* @param className - (Deprecated) Custom class name for styling. Use the `theme` prop instead.
37+
* @param theme - Optional theme object for custom styling.
38+
* @param onError - Optional callback for image load errors.
39+
* @param errorContent - Optional custom content to display when an error occurs.
40+
* @returns A JSX element rendering the zoomable image.
41+
*/
42+
declare function ImageZoom({ zoom, fullWidth, alt, width, height, src, id, className, theme, onError, errorContent, }: ImageZoomProps): JSX.Element;
1643
export default ImageZoom;
17-
export type { ImageZoomProps };
44+
export type { ImageZoomProps, ImageZoomClasses };

dist/react-image-zooom.mjs

Lines changed: 55 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
(function(){"use strict";try{if(typeof document<"u"){var o=document.createElement("style");o.appendChild(document.createTextNode('@keyframes rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}figure.image-zooom{position:relative;display:inline-block;width:auto;min-height:25vh;background-position:50% 50%;background-color:#eee;margin:0;overflow:hidden;cursor:zoom-in;-webkit-user-select:none;user-select:none}figure.image-zooom.loaded{min-height:auto}figure.image-zooom.zoomed{cursor:zoom-out}figure.image-zooom:before{content:"";background-color:transparent;position:absolute;top:0;left:0;right:0;width:100%;height:100%;opacity:1;transition:opacity .2s ease-in-out;z-index:1}figure.image-zooom.loaded:before{opacity:0}figure.image-zooom:after{content:"";position:absolute;top:calc(50% - 25px);left:calc(50% - 25px);width:50px;height:50px;border-radius:50%;border:5px solid transparent;border-top-color:#333;border-right-color:#333;border-bottom-color:#333;animation:rotate 2s linear infinite;opacity:1;transition:opacity .2s ease-in-out;z-index:2}figure.image-zooom.loaded:after{opacity:0}img#imageZoom{opacity:1;display:block}figure.image-zooom.zoomed img#imageZoom{opacity:0}p.image-zooom-error{width:100%;text-align:center;border:1px solid #f8f8f8;padding:8px 16px;border-radius:8px;color:#555}')),document.head.appendChild(o)}}catch(e){console.error("vite-plugin-css-injected-by-js",e)}})();
22
import { jsx as T } from "react/jsx-runtime";
3-
import { useEffect as P, useMemo as H, useCallback as d, useState as S, useRef as z, isValidElement as q } from "react";
4-
function A(t, o) {
3+
import { useEffect as P, useMemo as H, useCallback as f, useState as S, useRef as z, isValidElement as A } from "react";
4+
function F(t, o) {
55
P(() => {
66
const e = getComputedStyle(document.body).overflow || "auto";
77
return t ? document.body.style.overflow = "hidden" : document.body.style.overflow = e, () => {
@@ -17,7 +17,7 @@ function A(t, o) {
1717
};
1818
}, [t, o]);
1919
}
20-
function F(t, o, e, n) {
20+
function G(t, o, e, n) {
2121
return H(() => {
2222
if (!o || !e) return `${t}%`;
2323
const r = (n == null ? void 0 : n.clientWidth) || 0;
@@ -26,8 +26,8 @@ function F(t, o, e, n) {
2626
return `${u < 100 ? t : u}%`;
2727
}, [t, o, e, n]);
2828
}
29-
function G(t) {
30-
return d((o) => {
29+
function J(t) {
30+
return f((o) => {
3131
if (!t) return;
3232
const e = t.getBoundingClientRect();
3333
let n, r;
@@ -42,7 +42,7 @@ function G(t) {
4242
)}%`;
4343
}, [t]);
4444
}
45-
function J(t, o) {
45+
function K(t, o) {
4646
const [e, n] = S({
4747
imgData: null,
4848
error: !1,
@@ -51,7 +51,7 @@ function J(t, o) {
5151
});
5252
return P(() => {
5353
if (n({ imgData: null, error: !1, naturalWidth: 0, naturalHeight: 0 }), !t) {
54-
n((h) => ({ ...h, error: !0 }));
54+
n((g) => ({ ...g, error: !0 }));
5555
return;
5656
}
5757
const r = new Image(), u = () => {
@@ -61,108 +61,111 @@ function J(t, o) {
6161
naturalWidth: r.naturalWidth,
6262
naturalHeight: r.naturalHeight
6363
});
64-
}, s = (h) => {
65-
n((y) => ({ ...y, error: !0 })), o == null || o(h);
64+
}, s = (g) => {
65+
n((l) => ({ ...l, error: !0 })), o == null || o(g);
6666
};
6767
return r.addEventListener("load", u), r.addEventListener("error", s), r.src = t, () => {
6868
r.removeEventListener("load", u), r.removeEventListener("error", s);
6969
};
7070
}, [t, o]), e;
7171
}
72-
function U({
72+
function _({
7373
zoom: t = "200",
7474
fullWidth: o = !1,
7575
alt: e = "This is an imageZoom image",
7676
width: n = "100%",
7777
height: r = "auto",
7878
src: u,
7979
id: s,
80-
className: h,
81-
onError: y,
80+
className: g,
81+
theme: l,
82+
onError: W,
8283
errorContent: x
8384
}) {
84-
const [i, b] = S(!1), [E, f] = S("50% 50%"), g = z(null), l = G(g.current), m = z(!1);
85-
A(i, g.current);
86-
const { imgData: c, error: W, naturalWidth: B } = J(u, y), I = F(
85+
const [i, b] = S(!1), [E, h] = S("50% 50%"), v = z(null), c = J(v.current), m = z(!1);
86+
F(i, v.current);
87+
const { imgData: d, error: B, naturalWidth: N } = K(u, W), I = G(
8788
t,
8889
o,
89-
B,
90-
g.current
91-
), Z = (a, p, M, L, v, O) => {
90+
N,
91+
v.current
92+
), Z = (a, p, M, L, y, q) => {
9293
p && (m.current = !0);
9394
const C = !M;
9495
L(C);
95-
const D = C ? O(a) : "50% 50%";
96-
D && v(D);
96+
const D = C ? q(a) : "50% 50%";
97+
D && y(D);
9798
}, $ = (a, p, M, L) => {
9899
if (p) {
99-
const v = L(a);
100-
v && M(v);
100+
const y = L(a);
101+
y && M(y);
101102
}
102-
}, j = d(
103+
}, j = f(
103104
(a) => {
104105
if (m.current) {
105106
m.current = !1;
106107
return;
107108
}
108-
Z(a, !1, i, b, f, l);
109+
Z(a, !1, i, b, h, c);
109110
},
110-
[i, l]
111-
), w = d(
111+
[i, c]
112+
), w = f(
112113
(a) => {
113-
a.touches.length === 1 && Z(a, !0, i, b, f, l);
114+
a.touches.length === 1 && Z(a, !0, i, b, h, c);
114115
},
115-
[i, l]
116-
), N = d(
116+
[i, c]
117+
), V = f(
117118
(a) => {
118-
$(a, i, f, l);
119+
$(a, i, h, c);
119120
},
120-
[i, l]
121-
), V = d(
121+
[i, c]
122+
), X = f(
122123
(a) => {
123-
a.touches.length === 1 && (a.preventDefault(), m.current = !0, i ? $(a, i, f, l) : w(a));
124+
a.touches.length === 1 && (a.preventDefault(), m.current = !0, i ? $(a, i, h, c) : w(a));
124125
},
125-
[w, i, l]
126-
), k = d(() => {
127-
m.current = !1, b(!1), f("50% 50%");
128-
}, []), X = H(
126+
[w, i, c]
127+
), k = f(() => {
128+
m.current = !1, b(!1), h("50% 50%");
129+
}, []), Y = H(
129130
() => ({
130-
backgroundImage: `url(${i && c ? c : ""})`,
131+
backgroundImage: `url(${i && d ? d : ""})`,
131132
backgroundSize: I,
132133
backgroundPosition: E
133134
}),
134-
[i, c, I, E]
135+
[i, d, I, E]
135136
);
136-
if (W)
137-
return q(x) ? x : /* @__PURE__ */ T("p", { className: "image-zooom-error", children: "There was a problem loading your image" });
138-
const Y = [
139-
c ? "loaded" : "loading",
137+
if (B)
138+
return A(x) ? x : /* @__PURE__ */ T("p", { className: "image-zooom-error", children: "There was a problem loading your image" });
139+
const O = [
140+
d ? "loaded" : "loading",
140141
i ? "zoomed" : "fullView",
141142
"image-zooom",
142-
h
143+
l == null ? void 0 : l.root,
144+
g
143145
].filter(Boolean).join(" ");
144146
return /* @__PURE__ */ T(
145147
"figure",
146148
{
147-
ref: g,
149+
ref: v,
148150
id: s,
149-
className: Y,
150-
style: X,
151+
className: O,
152+
style: Y,
151153
onClick: j,
152-
onMouseMove: N,
154+
onMouseMove: V,
153155
onMouseLeave: k,
154156
onTouchStart: w,
155-
onTouchMove: V,
157+
onTouchMove: X,
156158
onTouchEnd: k,
157159
role: "button",
158160
"aria-label": `Zoomable image: ${e}`,
159161
tabIndex: 0,
160-
children: c && /* @__PURE__ */ T(
162+
children: d && /* @__PURE__ */ T(
161163
"img",
162164
{
163165
loading: "lazy",
164166
id: "imageZoom",
165-
src: c,
167+
className: l == null ? void 0 : l.image,
168+
src: d,
166169
alt: e,
167170
width: n,
168171
height: r
@@ -172,5 +175,5 @@ function U({
172175
);
173176
}
174177
export {
175-
U as default
178+
_ as default
176179
};

0 commit comments

Comments
 (0)