Skip to content

Commit c12f342

Browse files
committed
feat: add horizontal drag and zoom support (v1.2.0)
1 parent 946568f commit c12f342

4 files changed

Lines changed: 84 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## [1.2.0] - 2026-01-04
4+
5+
### Added
6+
7+
- **Horizontal Dragging**: Support for dragging images horizontally if they are wider than the container.
8+
- **Zoom Support**: New `zoom` prop to scale images, enabling 2D scrolling/positioning when zoomed in.
9+
310
All notable changes to this project will be documented in this file.
411

512
## [1.1.0] - 2026-01-04

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ A React component for creating draggable image views with automatic scaling and
44

55
## Features
66

7-
- **Vertical drag adjustment** for perfect image positioning
7+
- **Vertical and Horizontal drag adjustment** for perfect image positioning
8+
- **Zoom support** for detailed cropping and 2D scrolling
89
- **Automatic image scaling** while maintaining aspect ratio
910
- **Smooth animations and transitions**
1011
- **Accessibility first** with keyboard navigation
@@ -27,6 +28,7 @@ import ImageCropView from 'react-image-crop-drag';
2728

2829
function App() {
2930
const [isEditing, setIsEditing] = useState(false);
31+
const [zoom, setZoom] = useState(1);
3032

3133
const handleSave = (cropData) => {
3234
setIsEditing(false);
@@ -39,18 +41,18 @@ function App() {
3941
containerWidth={800}
4042
containerHeight={400}
4143
isEditing={isEditing}
44+
zoom={zoom}
4245
onSave={handleSave}
4346
/>
4447
);
4548
}
46-
4749
```
4850

4951
## Accessibility
5052

5153
This component is built with accessibility in mind:
5254

53-
- **Keyboard Support**: When in editing mode, the container is focusable. Use **Up/Down Arrow keys** to adjust the image position.
55+
- **Keyboard Support**: When in editing mode, the container is focusable. Use **Arrow keys** (Up/Down/Left/Right) to adjust the image position.
5456
- **Screen Readers**: Helper texts and ARIA attributes provide context to screen reader users.
5557
- **Touch Support**: Full touch support for dragging on mobile devices.
5658

@@ -63,6 +65,7 @@ This component is built with accessibility in mind:
6365
| `containerWidth` | number | No | 800 | Width of the container |
6466
| `containerHeight` | number | No | 400 | Height of the container |
6567
| `isEditing` | boolean | No | false | Enable/disable drag mode |
68+
| `zoom` | number | No | 1 | Zoom level (multiplier) |
6669
| `onSave` | function | No | - | Callback with crop data |
6770

6871
## Example Use Cases

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-image-crop-drag",
3-
"version": "1.1.0",
3+
"version": "1.2.0",
44
"description": "React component for draggable image crop view with vertical adjustment",
55
"main": "dist/index.js",
66
"module": "dist/index.esm.js",

src/ImageCropView.jsx

Lines changed: 70 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ const ImageCropView = ({
66
containerWidth = 800,
77
containerHeight = 400,
88
isEditing,
9+
zoom = 1,
910
}) => {
10-
const [position, setPosition] = useState({ y: 0 });
11+
const [position, setPosition] = useState({ x: 0, y: 0 });
1112
const [imageSize, setImageSize] = useState({ width: 0, height: 0 });
1213
const [isDragging, setIsDragging] = useState(false);
13-
const [startY, setStartY] = useState(0);
14+
const [startPos, setStartPos] = useState({ x: 0, y: 0 });
1415
const imageRef = useRef(null);
1516
const containerRef = useRef(null);
1617

@@ -22,21 +23,42 @@ const ImageCropView = ({
2223
let width = containerWidth;
2324
let height = containerWidth / aspectRatio;
2425

25-
26-
if (height < containerHeight) {
27-
height = containerHeight;
28-
width = containerHeight * aspectRatio;
26+
if (width < containerWidth || height < containerHeight) {
27+
const containerRatio = containerWidth / containerHeight;
28+
if (aspectRatio > containerRatio) {
29+
height = containerHeight;
30+
width = containerHeight * aspectRatio;
31+
} else {
32+
width = containerWidth;
33+
height = containerWidth / aspectRatio;
34+
}
2935
}
3036

37+
width *= zoom;
38+
height *= zoom;
39+
3140
setImageSize({ width, height });
32-
setPosition({ y: (containerHeight - height) / 2 });
41+
setPosition({
42+
x: (containerWidth - width) / 2,
43+
y: (containerHeight - height) / 2
44+
});
45+
};
46+
47+
image.onerror = (err) => {
48+
console.error("ImageCropView: Failed to load image", src, err);
3349
};
34-
}, [src, containerWidth, containerHeight]);
50+
}, [src, containerWidth, containerHeight, zoom]);
3551

36-
const constrainY = (y) => {
52+
const constrain = (x, y) => {
53+
const minX = containerWidth - imageSize.width;
54+
const maxX = 0;
3755
const minY = containerHeight - imageSize.height;
3856
const maxY = 0;
39-
return Math.min(maxY, Math.max(minY, y));
57+
58+
return {
59+
x: Math.min(maxX, Math.max(minX, x)),
60+
y: Math.min(maxY, Math.max(minY, y))
61+
};
4062
};
4163

4264
const handleMouseDown = (e) => {
@@ -46,16 +68,20 @@ const ImageCropView = ({
4668
}
4769
e.preventDefault();
4870
setIsDragging(true);
49-
setStartY(e.clientY - position.y);
71+
setStartPos({
72+
x: e.clientX - position.x,
73+
y: e.clientY - position.y
74+
});
5075
};
5176

5277
const handleMouseMove = (e) => {
5378
if (!isEditing || !isDragging) {
5479
return;
5580
}
5681

57-
const newY = e.clientY - startY;
58-
setPosition({ y: constrainY(newY) });
82+
const newX = e.clientX - startPos.x;
83+
const newY = e.clientY - startPos.y;
84+
setPosition(constrain(newX, newY));
5985
};
6086

6187
const handleMouseUp = () => {
@@ -65,15 +91,19 @@ const ImageCropView = ({
6591
const handleTouchStart = (e) => {
6692
if (!isEditing) return;
6793
setIsDragging(true);
68-
setStartY(e.touches[0].clientY - position.y);
94+
setStartPos({
95+
x: e.touches[0].clientX - position.x,
96+
y: e.touches[0].clientY - position.y
97+
});
6998
};
7099

71100
const handleTouchMove = (e) => {
72101
if (!isEditing || !isDragging) return;
73102
if (e.cancelable) e.preventDefault();
74103

75-
const newY = e.touches[0].clientY - startY;
76-
setPosition({ y: constrainY(newY) });
104+
const newX = e.touches[0].clientX - startPos.x;
105+
const newY = e.touches[0].clientY - startPos.y;
106+
setPosition(constrain(newX, newY));
77107
};
78108

79109
const handleTouchEnd = () => {
@@ -84,27 +114,35 @@ const ImageCropView = ({
84114
if (!isEditing) return;
85115

86116
const step = 20;
87-
let newY = position.y;
88-
89-
if (e.key === 'ArrowUp') {
90-
e.preventDefault();
91-
newY -= step;
92-
} else if (e.key === 'ArrowDown') {
93-
e.preventDefault();
94-
newY += step;
95-
} else {
96-
return;
117+
let { x: newX, y: newY } = position;
118+
119+
switch (e.key) {
120+
case 'ArrowUp':
121+
newY -= step;
122+
break;
123+
case 'ArrowDown':
124+
newY += step;
125+
break;
126+
case 'ArrowLeft':
127+
newX -= step;
128+
break;
129+
case 'ArrowRight':
130+
newX += step;
131+
break;
132+
default:
133+
return;
97134
}
98135

99-
setPosition({ y: constrainY(newY) });
136+
e.preventDefault();
137+
setPosition(constrain(newX, newY));
100138
};
101139

102140
return (
103141
<div
104142
ref={containerRef}
105143
role="application"
106144
aria-label="Image Cropper"
107-
aria-description="Use Up and Down arrow keys to adjust current image position."
145+
aria-description="Use Arrow keys to adjust image position."
108146
tabIndex={isEditing ? 0 : -1}
109147
onKeyDown={handleKeyDown}
110148
style={{
@@ -126,11 +164,12 @@ const ImageCropView = ({
126164
width: imageSize.width,
127165
height: imageSize.height,
128166
position: "absolute",
129-
left: "50%",
130-
transform: `translateX(-50%) translateY(${position.y}px)`,
167+
left: 0,
168+
top: 0,
169+
transform: `translate3d(${position.x}px, ${position.y}px, 0)`,
131170
transition: isDragging ? "none" : "transform 0.1s ease",
132171
userSelect: "none",
133-
objectFit: "cover",
172+
objectFit: "fill",
134173
pointerEvents: isEditing ? "auto" : "none",
135174
touchAction: "none"
136175
}}

0 commit comments

Comments
 (0)