Skip to content

feat: add ref forwarding to internal img element. Closes #372 #379

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
27 changes: 21 additions & 6 deletions src/Image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { GetContainer } from '@rc-component/util/lib/PortalWrapper';
import useMergedState from '@rc-component/util/lib/hooks/useMergedState';
import cn from 'classnames';
import * as React from 'react';
import { useContext, useMemo, useState } from 'react';
import { useContext, useMemo, useState, forwardRef } from 'react';
import type { PreviewProps, ToolbarRenderInfoType } from './Preview';
import Preview from './Preview';
import PreviewGroup from './PreviewGroup';
Expand Down Expand Up @@ -69,11 +69,11 @@ export interface ImageProps
onError?: (e: React.SyntheticEvent<HTMLImageElement, Event>) => void;
}

interface CompoundedComponent<P> extends React.FC<P> {
interface CompoundedComponent<P> extends React.ForwardRefExoticComponent<P & React.RefAttributes<HTMLImageElement>> {
PreviewGroup: typeof PreviewGroup;
}

const ImageInternal: CompoundedComponent<ImageProps> = props => {
const ImageInternal = forwardRef<HTMLImageElement, ImageProps>((props, ref) => {
const {
src: imgSrc,
alt,
Expand Down Expand Up @@ -180,6 +180,22 @@ const ImageInternal: CompoundedComponent<ImageProps> = props => {
onClick?.(e);
};

// ========================== Combined Ref ==========================
const handleRef = (img: HTMLImageElement | null) => {
if (!img) {
return;
}

getImgRef(img);

// 处理外部传入的 ref
if (typeof ref === 'function') {
ref(img);
} else if (ref) {
ref.current = img;
}
};

// =========================== Render ===========================
return (
<>
Expand All @@ -206,7 +222,7 @@ const ImageInternal: CompoundedComponent<ImageProps> = props => {
height,
...style,
}}
ref={getImgRef}
ref={handleRef}
{...srcAndOnload}
width={width}
height={height}
Expand Down Expand Up @@ -257,8 +273,7 @@ const ImageInternal: CompoundedComponent<ImageProps> = props => {
)}
</>
);
};

}) as CompoundedComponent<ImageProps>;
ImageInternal.PreviewGroup = PreviewGroup;

if (process.env.NODE_ENV !== 'production') {
Expand Down
89 changes: 89 additions & 0 deletions tests/ref.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { act, fireEvent, render } from '@testing-library/react';
import React from 'react';
import Image from '../src';

describe('Image ref forwarding', () => {
// 测试对象类型的 ref
it('should forward object ref to internal img element', () => {
const ref = React.createRef<HTMLImageElement>();
const { container } = render(
<Image
ref={ref}
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
alt="test image"
/>,
);

// 确保 ref.current 指向正确的 img 元素
expect(ref.current).not.toBeNull();
expect(ref.current).toBe(container.querySelector('.rc-image-img'));
expect(ref.current?.tagName).toBe('IMG');
expect(ref.current?.alt).toBe('test image');
});

// 测试回调类型的 ref
it('should work with callback ref', () => {
let imgElement: HTMLImageElement | null = null;
const callbackRef = (el: HTMLImageElement | null) => {
imgElement = el;
};

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

// 确保回调 ref 被调用,且指向正确的 img 元素
expect(imgElement).not.toBeNull();
expect(imgElement).toBe(container.querySelector('.rc-image-img'));
});

// 测试 ref 能够访问 img 元素的属性和方法
it('should allow access to img element properties and methods', () => {
const ref = React.createRef<HTMLImageElement>();
render(
<Image
ref={ref}
width={200}
height={100}
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
/>,
);

// 确保可以通过 ref 访问 img 元素的属性
expect(ref.current?.width).toBe(200);
expect(ref.current?.height).toBe(100);

// 可以测试调用 img 元素的方法
// 注意:某些方法可能在 jsdom 环境中不可用,根据实际情况调整
});

// 测试 ref 在组件重新渲染时保持稳定
it('should maintain stable ref across re-renders', () => {
const ref = React.createRef<HTMLImageElement>();
const { rerender } = render(
<Image
ref={ref}
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
/>,
);

const initialImgElement = ref.current;
expect(initialImgElement).not.toBeNull();

// 重新渲染组件,但保持 ref 不变
rerender(
<Image
ref={ref}
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
alt="updated alt"
/>,
);

// 确保 ref 引用的还是同一个 img 元素
expect(ref.current).toBe(initialImgElement);
expect(ref.current?.alt).toBe('updated alt');
});
});