Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ Native image attributes are also supported.
| wheel | Enable mouse wheel zoom | boolean | true |
| onOpenChange | Callback when preview open state changes | `(open: boolean) => void` | - |
| onTransform | Callback when transform changes | `(info: { transform: TransformType; action: TransformAction }) => void` | - |
| rebound | Whether to rebound the image to the visible area after dragging ends | boolean | true |

### Image.PreviewGroup

Expand Down
6 changes: 6 additions & 0 deletions src/Preview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ export interface InternalPreviewConfig {

// Operation
movable?: boolean;
/** Whether to rebound the image to the visible area after dragging. Default is true. */
rebound?: boolean;
icons?: OperationIcons;
closeIcon?: React.ReactNode;

Expand Down Expand Up @@ -176,6 +178,7 @@ const Preview: React.FC<PreviewProps> = props => {
imageInfo,
fallback,
movable = true,
rebound = true,
onClose,
open,
afterOpenChange,
Expand Down Expand Up @@ -217,11 +220,13 @@ const Preview: React.FC<PreviewProps> = props => {
imgRef,
minScale,
maxScale,
rebound,
onTransform,
);
const { isMoving, onMouseDown, onWheel } = useMouseEvent(
imgRef,
movable,
rebound,
open,
scaleStep,
transform,
Expand All @@ -232,6 +237,7 @@ const Preview: React.FC<PreviewProps> = props => {
const { isTouching, onTouchStart, onTouchMove, onTouchEnd } = useTouchEvent(
imgRef,
movable,
rebound,
open,
minScale,
transform,
Expand Down
5 changes: 4 additions & 1 deletion src/hooks/useImageTransform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export default function useImageTransform(
imgRef: React.MutableRefObject<HTMLImageElement>,
minScale: number,
maxScale: number,
rebound: boolean,
onTransform: (info: { transform: TransformType; action: TransformAction }) => void,
) {
const frame = useRef(null);
Expand Down Expand Up @@ -134,7 +135,9 @@ export default function useImageTransform(
const mergedWidth = offsetWidth * newScale;
const mergedHeight = offsetHeight * newScale;
const { width: clientWidth, height: clientHeight } = getClientSize();
if (mergedWidth <= clientWidth && mergedHeight <= clientHeight) {
// Keep the dropped position when `rebound` is disabled, instead of
// resetting the image back to the viewport center.
if (mergedWidth <= clientWidth && mergedHeight <= clientHeight && rebound) {
newX = 0;
newY = 0;
}
Expand Down
4 changes: 4 additions & 0 deletions src/hooks/useMouseEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
export default function useMouseEvent(
imgRef: React.MutableRefObject<HTMLImageElement>,
movable: boolean,
rebound: boolean,
open: boolean,
scaleStep: number,
transform: TransformType,
Expand Down Expand Up @@ -64,6 +65,9 @@ export default function useMouseEvent(
const hasChangedPosition = x !== transformX && y !== transformY;
if (!hasChangedPosition) return;

// Keep the image at the dropped position when `rebound` is disabled
if (!rebound) return;

const width = imgRef.current.offsetWidth * scale;
const height = imgRef.current.offsetHeight * scale;
// eslint-disable-next-line @typescript-eslint/no-shadow
Expand Down
5 changes: 5 additions & 0 deletions src/hooks/useTouchEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ function getCenter(oldPoint1: Point, oldPoint2: Point, newPoint1: Point, newPoin
export default function useTouchEvent(
imgRef: React.MutableRefObject<HTMLImageElement>,
movable: boolean,
rebound: boolean,
open: boolean,
minScale: number,
transform: TransformType,
Expand Down Expand Up @@ -132,6 +133,7 @@ export default function useTouchEvent(

const onTouchEnd = () => {
if (!open) return;
const { eventType } = touchPointInfo.current;

if (isTouching) {
setIsTouching(false);
Expand All @@ -144,6 +146,9 @@ export default function useTouchEvent(
return updateTransform({ x: 0, y: 0, scale: minScale }, 'touchZoom');
}

// Keep the image at the dropped position when `rebound` is disabled
if (!rebound && eventType === 'move') return;

const width = imgRef.current.offsetWidth * scale;
const height = imgRef.current.offsetHeight * scale;
// eslint-disable-next-line @typescript-eslint/no-shadow
Expand Down
159 changes: 159 additions & 0 deletions tests/preview.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,165 @@ describe('Preview', () => {
jest.restoreAllMocks();
});

it('rebound disabled keeps the dropped position', () => {
const clientWidthMock = jest
.spyOn(document.documentElement, 'clientWidth', 'get')
.mockImplementation(() => 1080);
const clientHeightMock = jest
.spyOn(document.documentElement, 'clientHeight', 'get')
.mockImplementation(() => 760);

const imgEleMock = spyElementPrototypes(HTMLImageElement, {
offsetWidth: { get: () => 200 },
offsetHeight: { get: () => 100 },
getBoundingClientRect: () => ({ left: 0, top: 0 }),
});

const { container } = render(
<Image
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
preview={{ rebound: false }}
/>,
);

fireEvent.click(container.querySelector('.rc-image'));

// Drag the image within the visible area (image is smaller than the
// viewport, so the default behaviour would rebound it back to the origin)
fireMouseEvent('mouseDown', document.querySelector('.rc-image-preview-img'), {
pageX: 0,
pageY: 0,
button: 0,
});
fireMouseEvent('mouseMove', window, { pageX: 80, pageY: 60 });

expect(document.querySelector('.rc-image-preview-img')).toHaveStyle({
transform: 'translate3d(80px, 60px, 0) scale3d(1, 1, 1) rotate(0deg)',
});

// Without rebound, the image stays where it was dropped
fireMouseEvent('mouseUp', window);

expect(document.querySelector('.rc-image-preview-img')).toHaveStyle({
transform: 'translate3d(80px, 60px, 0) scale3d(1, 1, 1) rotate(0deg)',
});

clientWidthMock.mockRestore();
clientHeightMock.mockRestore();
imgEleMock.mockRestore();
jest.restoreAllMocks();
});

it('rebound defaults to true', () => {
const clientWidthMock = jest
.spyOn(document.documentElement, 'clientWidth', 'get')
.mockImplementation(() => 1080);
const clientHeightMock = jest
.spyOn(document.documentElement, 'clientHeight', 'get')
.mockImplementation(() => 760);

const imgEleMock = spyElementPrototypes(HTMLImageElement, {
offsetWidth: { get: () => 200 },
offsetHeight: { get: () => 100 },
getBoundingClientRect: () => ({ left: 0, top: 0 }),
});

const { container } = render(
<Image src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png" />,
);

fireEvent.click(container.querySelector('.rc-image'));

fireMouseEvent('mouseDown', document.querySelector('.rc-image-preview-img'), {
pageX: 0,
pageY: 0,
button: 0,
});
fireMouseEvent('mouseMove', window, { pageX: 80, pageY: 60 });

// When `rebound` is omitted, the default (true) still rebounds the image
fireMouseEvent('mouseUp', window);

expect(document.querySelector('.rc-image-preview-img')).toHaveStyle({
transform: 'translate3d(0px, 0px, 0) scale3d(1, 1, 1) rotate(0deg)',
});

clientWidthMock.mockRestore();
clientHeightMock.mockRestore();
imgEleMock.mockRestore();
jest.restoreAllMocks();
});

it('rebound disabled keeps the position when zooming back to min scale', () => {
const { container } = render(
<Image
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
preview={{ rebound: false }}
/>,
);
fireEvent.click(container.querySelector('.rc-image'));
act(() => {
jest.runAllTimers();
});
const img = document.querySelector('.rc-image-preview-img');

// Zoom in to 1.5x
fireEvent.click(document.querySelectorAll('.rc-image-preview-actions-action')[5]);
act(() => {
jest.runAllTimers();
});

// Drag the image to a non-centered position
fireMouseEvent('mouseDown', img, { pageX: 0, pageY: 0, button: 0 });
fireMouseEvent('mouseMove', window, { pageX: 100, pageY: 80 });
fireMouseEvent('mouseUp', window);

// Zoom out back to the minimum scale (1x)
fireEvent.click(document.querySelectorAll('.rc-image-preview-actions-action')[4]);
act(() => {
jest.runAllTimers();
});

// Without rebound, the image keeps the dropped position instead of
// jumping back to the viewport center
const transform = img.getAttribute('style') || '';
expect(transform).toContain('scale3d(1, 1, 1)');
expect(transform).not.toContain('translate3d(0px, 0px,');
});

it('rebound enabled resets to center when zooming back to min scale', () => {
const { container } = render(
<Image src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png" />,
);
fireEvent.click(container.querySelector('.rc-image'));
act(() => {
jest.runAllTimers();
});
const img = document.querySelector('.rc-image-preview-img');

// Zoom in to 1.5x
fireEvent.click(document.querySelectorAll('.rc-image-preview-actions-action')[5]);
act(() => {
jest.runAllTimers();
});

// Drag the image to a non-centered position
fireMouseEvent('mouseDown', img, { pageX: 0, pageY: 0, button: 0 });
fireMouseEvent('mouseMove', window, { pageX: 100, pageY: 80 });
fireMouseEvent('mouseUp', window);

// Zoom out back to the minimum scale (1x)
fireEvent.click(document.querySelectorAll('.rc-image-preview-actions-action')[4]);
act(() => {
jest.runAllTimers();
});

// With rebound (default true), the image jumps back to the center
expect(img).toHaveStyle({
transform: 'translate3d(0px, 0px, 0) scale3d(1, 1, 1) rotate(0deg)',
});
});

it('PreviewGroup render', () => {
const { container } = render(
<Image.PreviewGroup
Expand Down
94 changes: 94 additions & 0 deletions tests/previewTouch.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,100 @@ describe('Touch Events', () => {
});
});

it('touch move does not rebound when rebound is disabled', () => {
const { container } = render(
<Image
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
preview={{ rebound: false }}
/>,
);

fireEvent.click(container.querySelector('.rc-image'));

const previewImgDom = document.querySelector('.rc-image-preview-img');

fireEvent.touchStart(previewImgDom, {
touches: [{ clientX: 0, clientY: 0 }],
});
fireEvent.touchMove(previewImgDom, {
touches: [{ clientX: 50, clientY: 50 }],
});

act(() => {
jest.runAllTimers();
});

expect(previewImgDom).toHaveStyle({
transform: 'translate3d(50px, 50px, 0) scale3d(1, 1, 1) rotate(0deg)',
transitionDuration: '0s',
});

fireEvent.touchEnd(previewImgDom);

act(() => {
jest.runAllTimers();
});

// Keep the dropped position instead of rebounding to the center
expect(previewImgDom).toHaveStyle({
transform: 'translate3d(50px, 50px, 0) scale3d(1, 1, 1) rotate(0deg)',
});
});

it('touch zoom still corrects position when rebound is disabled', () => {
const imgEleMock = spyElementPrototypes(HTMLImageElement, {
offsetWidth: { get: () => 2000 },
offsetHeight: { get: () => 1000 },
getBoundingClientRect: () => ({ left: 10, top: 10 }),
});

const { container } = render(
<Image
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
preview={{ rebound: false }}
/>,
);

fireEvent.click(container.querySelector('.rc-image'));

const previewImgDom = document.querySelector('.rc-image-preview-img');

fireEvent.touchStart(previewImgDom, {
touches: [
{ clientX: 40, clientY: 40 },
{ clientX: 60, clientY: 60 },
],
});
fireEvent.touchMove(previewImgDom, {
touches: [
{ clientX: 30, clientY: 30 },
{ clientX: 70, clientY: 70 },
],
});

act(() => {
jest.runAllTimers();
});

expect(previewImgDom).toHaveStyle({
transform: 'translate3d(-50px, -50px, 0) scale3d(2, 2, 1) rotate(0deg)',
});

fireEvent.touchEnd(previewImgDom);

act(() => {
jest.runAllTimers();
});

// `rebound` only skips single-finger dragging; the two-finger zoom still
// corrects the image back into the visible area.
expect(previewImgDom).toHaveStyle({
transform: 'translate3d(2000px, 616px, 0) scale3d(2, 2, 1) rotate(0deg)',
});

imgEleMock.mockRestore();
});

it('touch zoom', () => {
const { container } = render(
<Image src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png" />,
Expand Down
Loading