Skip to content
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

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

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
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
35 changes: 28 additions & 7 deletions src/Image.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import useMergedState from '@rc-component/util/lib/hooks/useMergedState';
import classnames from 'classnames';
import * as React from 'react';
import { useContext, useMemo, useState } from 'react';
import { useContext, useMemo, useState, forwardRef, useImperativeHandle, useRef } from 'react';
import type {
InternalPreviewConfig,
InternalPreviewSemanticName,
Expand Down Expand Up @@ -71,11 +71,16 @@ export interface ImageProps
onError?: (e: React.SyntheticEvent<HTMLImageElement, Event>) => void;
}

interface CompoundedComponent<P> extends React.FC<P> {
// 定义 ImageRef 接口,只包含 nativeElement 属性
export interface ImageRef {
nativeElement: HTMLImageElement | null;
}

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

const ImageInternal: CompoundedComponent<ImageProps> = props => {
const ImageInternal = forwardRef<ImageRef, ImageProps>((props, ref) => {
const {
// Misc
prefixCls = 'rc-image',
Expand Down Expand Up @@ -107,6 +112,13 @@ const ImageInternal: CompoundedComponent<ImageProps> = props => {
...otherProps
} = props;

// 创建内部引用来跟踪 image 元素
const imageElementRef = useRef<HTMLImageElement | null>(null);

// 使用 useImperativeHandle 暴露自定义 ref 对象
useImperativeHandle(ref, () => ({
nativeElement: imageElementRef.current,
}));
const groupContext = useContext(PreviewGroupContext);

// ========================== Preview ===========================
Expand Down Expand Up @@ -193,6 +205,16 @@ const ImageInternal: CompoundedComponent<ImageProps> = props => {
onClick?.(e);
};

// ========================== Image Ref ==========================
const handleRef = (img: HTMLImageElement | null) => {
if (!img) {
return;
}
// 保存到内部引用
imageElementRef.current = img;
getImgRef(img);
};
Comment on lines +208 to +216
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

集成内部和外部引用的处理函数

handleRef 函数巧妙地处理了内部引用的存储和对 getImgRef 的调用,确保了与现有功能的兼容性。但是,这里有一个潜在的问题:当 img 元素为 null 时直接返回了,但没有更新 imageElementRef.current。应该考虑在这种情况下将 imageElementRef.current 也设置为 null。

 const handleRef = (img: HTMLImageElement | null) => {
   if (!img) {
+    imageElementRef.current = null;
     return;
   }
   // 保存到内部引用
   imageElementRef.current = img;
   getImgRef(img);
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// ========================== Image Ref ==========================
const handleRef = (img: HTMLImageElement | null) => {
if (!img) {
return;
}
// 保存到内部引用
imageElementRef.current = img;
getImgRef(img);
};
// ========================== Image Ref ==========================
const handleRef = (img: HTMLImageElement | null) => {
if (!img) {
imageElementRef.current = null;
return;
}
// 保存到内部引用
imageElementRef.current = img;
getImgRef(img);
};


// =========================== Render ===========================
return (
<>
Expand Down Expand Up @@ -223,7 +245,7 @@ const ImageInternal: CompoundedComponent<ImageProps> = props => {
...styles.image,
...style,
}}
ref={getImgRef}
ref={handleRef}
{...srcAndOnload}
width={width}
height={height}
Expand Down Expand Up @@ -269,12 +291,11 @@ const ImageInternal: CompoundedComponent<ImageProps> = props => {
)}
</>
);
};

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

if (process.env.NODE_ENV !== 'production') {
ImageInternal.displayName = 'Image';
}

export default ImageInternal;
export default ImageInternal;
90 changes: 90 additions & 0 deletions tests/ref.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { act, fireEvent, render } from '@testing-library/react';
import React from 'react';
import Image, { ImageRef } from '../src';

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

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

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

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

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

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

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

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

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

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

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

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

});