From b88ff569d8cda3118270bcb278c2c32f4b4108ac Mon Sep 17 00:00:00 2001 From: liamli-0822 Date: Tue, 4 Mar 2025 14:42:07 +0800 Subject: [PATCH 1/5] feat: add ref forwarding to internal img element. Closes #373 --- src/Image.tsx | 27 ++++++++++---- tests/ref.test.tsx | 89 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 tests/ref.test.tsx diff --git a/src/Image.tsx b/src/Image.tsx index 868eec71..2cb543ff 100644 --- a/src/Image.tsx +++ b/src/Image.tsx @@ -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'; @@ -69,11 +69,11 @@ export interface ImageProps onError?: (e: React.SyntheticEvent) => void; } -interface CompoundedComponent

extends React.FC

{ +interface CompoundedComponent

extends React.ForwardRefExoticComponent

> { PreviewGroup: typeof PreviewGroup; } -const ImageInternal: CompoundedComponent = props => { +const ImageInternal = forwardRef((props, ref) => { const { src: imgSrc, alt, @@ -180,6 +180,22 @@ const ImageInternal: CompoundedComponent = props => { onClick?.(e); }; + // ========================== Combined Ref ========================== + const handleRef = (img: HTMLImageElement | null) => { + if (img) { + getImgRef(img); + + // 处理外部传入的 ref + if (ref) { + if (typeof ref === 'function') { + ref(img); + } else { + ref.current = img; + } + } + } + }; + // =========================== Render =========================== return ( <> @@ -206,7 +222,7 @@ const ImageInternal: CompoundedComponent = props => { height, ...style, }} - ref={getImgRef} + ref={handleRef} {...srcAndOnload} width={width} height={height} @@ -257,8 +273,7 @@ const ImageInternal: CompoundedComponent = props => { )} ); -}; - +}) as CompoundedComponent; ImageInternal.PreviewGroup = PreviewGroup; if (process.env.NODE_ENV !== 'production') { diff --git a/tests/ref.test.tsx b/tests/ref.test.tsx new file mode 100644 index 00000000..7dfa0a38 --- /dev/null +++ b/tests/ref.test.tsx @@ -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(); + const { container } = render( + 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( + , + ); + + // 确保回调 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(); + render( + , + ); + + // 确保可以通过 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(); + const { rerender } = render( + , + ); + + const initialImgElement = ref.current; + expect(initialImgElement).not.toBeNull(); + + // 重新渲染组件,但保持 ref 不变 + rerender( + updated alt, + ); + + // 确保 ref 引用的还是同一个 img 元素 + expect(ref.current).toBe(initialImgElement); + expect(ref.current?.alt).toBe('updated alt'); + }); +}); \ No newline at end of file From 0b6f3154bff975938c82db0c0a89947db5950de5 Mon Sep 17 00:00:00 2001 From: liamli-0822 Date: Mon, 17 Mar 2025 15:34:16 +0800 Subject: [PATCH 2/5] Update src/Image.tsx optimized the logic of the handleRef function Co-authored-by: afc163 --- src/Image.tsx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Image.tsx b/src/Image.tsx index 2cb543ff..87ccd3f2 100644 --- a/src/Image.tsx +++ b/src/Image.tsx @@ -182,17 +182,17 @@ const ImageInternal = forwardRef((props, ref) => { // ========================== Combined Ref ========================== const handleRef = (img: HTMLImageElement | null) => { - if (img) { - getImgRef(img); - - // 处理外部传入的 ref - if (ref) { - if (typeof ref === 'function') { - ref(img); - } else { - ref.current = img; - } - } + if (!img) { + return; + } + + getImgRef(img); + + // 处理外部传入的 ref + if (typeof ref === 'function') { + ref(img); + } else if (ref) { + ref.current = img; } }; From 338cb861d90fc2b3a5d247e8e27c51be8fb5ce16 Mon Sep 17 00:00:00 2001 From: liamli-0822 Date: Mon, 24 Mar 2025 10:17:32 +0800 Subject: [PATCH 3/5] feat: add nativeElement property to Image component ref --- src/Image.tsx | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/Image.tsx b/src/Image.tsx index 2cb543ff..2649d7a8 100644 --- a/src/Image.tsx +++ b/src/Image.tsx @@ -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, forwardRef } from 'react'; +import { useContext, useMemo, useState, forwardRef, useImperativeHandle, useRef } from 'react'; import type { PreviewProps, ToolbarRenderInfoType } from './Preview'; import Preview from './Preview'; import PreviewGroup from './PreviewGroup'; @@ -69,11 +69,16 @@ export interface ImageProps onError?: (e: React.SyntheticEvent) => void; } -interface CompoundedComponent

extends React.ForwardRefExoticComponent

> { +// 定义 ImageRef 接口,只包含 nativeElement 属性 +export interface ImageRef { + nativeElement: HTMLImageElement | null; +} + +interface CompoundedComponent

extends React.ForwardRefExoticComponent

> { PreviewGroup: typeof PreviewGroup; } -const ImageInternal = forwardRef((props, ref) => { +const ImageInternal = forwardRef((props, ref) => { const { src: imgSrc, alt, @@ -96,6 +101,14 @@ const ImageInternal = forwardRef((props, ref) => { ...otherProps } = props; + // 创建内部引用来跟踪 image 元素 + const imageElementRef = useRef(null); + + // 使用 useImperativeHandle 暴露自定义 ref 对象 + useImperativeHandle(ref, () => ({ + nativeElement: imageElementRef.current, + })); + const isCustomPlaceholder = placeholder && placeholder !== true; const { src: previewSrc, @@ -180,19 +193,14 @@ const ImageInternal = forwardRef((props, ref) => { onClick?.(e); }; - // ========================== Combined Ref ========================== + // ========================== Image Ref ========================== const handleRef = (img: HTMLImageElement | null) => { if (img) { - getImgRef(img); + // 保存到内部引用 + imageElementRef.current = img; - // 处理外部传入的 ref - if (ref) { - if (typeof ref === 'function') { - ref(img); - } else { - ref.current = img; - } - } + // 调用原来的 getImgRef + getImgRef(img); } }; @@ -280,4 +288,4 @@ if (process.env.NODE_ENV !== 'production') { ImageInternal.displayName = 'Image'; } -export default ImageInternal; +export default ImageInternal; \ No newline at end of file From 14a74e3ae25cfa8db2cadf91da8bfcf2f275d7b2 Mon Sep 17 00:00:00 2001 From: liamli-0822 Date: Mon, 24 Mar 2025 10:33:17 +0800 Subject: [PATCH 4/5] update tests --- tests/ref.test.tsx | 74 +++++++++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/tests/ref.test.tsx b/tests/ref.test.tsx index 7dfa0a38..265af045 100644 --- a/tests/ref.test.tsx +++ b/tests/ref.test.tsx @@ -1,11 +1,11 @@ import { act, fireEvent, render } from '@testing-library/react'; import React from 'react'; -import Image from '../src'; +import Image, { ImageRef } from '../src'; describe('Image ref forwarding', () => { // 测试对象类型的 ref - it('should forward object ref to internal img element', () => { - const ref = React.createRef(); + it('should provide access to internal img element via nativeElement', () => { + const ref = React.createRef(); const { container } = render( { />, ); - // 确保 ref.current 指向正确的 img 元素 + // 确保 ref.current.nativeElement 指向正确的 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'); + expect(ref.current?.nativeElement).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 imgElement: HTMLImageElement | null = null; - const callbackRef = (el: HTMLImageElement | null) => { - imgElement = el; + let imgRef: ImageRef | null = null; + const callbackRef = (el: ImageRef | null) => { + imgRef = el; }; const { container } = render( @@ -35,14 +36,15 @@ describe('Image ref forwarding', () => { />, ); - // 确保回调 ref 被调用,且指向正确的 img 元素 - expect(imgElement).not.toBeNull(); - expect(imgElement).toBe(container.querySelector('.rc-image-img')); + // 确保回调 ref 被调用,且 nativeElement 指向正确的 img 元素 + expect(imgRef).not.toBeNull(); + expect(imgRef?.nativeElement).not.toBeNull(); + expect(imgRef?.nativeElement).toBe(container.querySelector('.rc-image-img')); }); - // 测试 ref 能够访问 img 元素的属性和方法 - it('should allow access to img element properties and methods', () => { - const ref = React.createRef(); + // 测试通过 nativeElement 访问 img 元素的属性和方法 + it('should allow access to img element properties and methods via nativeElement', () => { + const ref = React.createRef(); render( { />, ); - // 确保可以通过 ref 访问 img 元素的属性 - expect(ref.current?.width).toBe(200); - expect(ref.current?.height).toBe(100); + // 确保可以通过 ref.nativeElement 访问 img 元素的属性 + expect(ref.current?.nativeElement?.width).toBe(200); + expect(ref.current?.nativeElement?.height).toBe(100); // 可以测试调用 img 元素的方法 // 注意:某些方法可能在 jsdom 环境中不可用,根据实际情况调整 }); - // 测试 ref 在组件重新渲染时保持稳定 - it('should maintain stable ref across re-renders', () => { - const ref = React.createRef(); + // 测试 ref.nativeElement 在组件重新渲染时保持稳定 + it('should maintain stable nativeElement reference across re-renders', () => { + const ref = React.createRef(); const { rerender } = render( { />, ); - const initialImgElement = ref.current; + const initialImgElement = ref.current?.nativeElement; expect(initialImgElement).not.toBeNull(); // 重新渲染组件,但保持 ref 不变 @@ -82,8 +84,26 @@ describe('Image ref forwarding', () => { />, ); - // 确保 ref 引用的还是同一个 img 元素 - expect(ref.current).toBe(initialImgElement); - expect(ref.current?.alt).toBe('updated alt'); + // 确保 ref.nativeElement 引用的还是同一个 img 元素 + expect(ref.current?.nativeElement).toBe(initialImgElement); + expect(ref.current?.nativeElement?.alt).toBe('updated alt'); }); -}); \ No newline at end of file + + // 测试 ref 不能直接访问 img 元素属性 + it('should not allow direct access to img element properties', () => { + const ref = React.createRef(); + render( + , + ); + + // 确保 ref.current 不是 HTMLImageElement + expect(ref.current).not.toBeNull(); + // @ts-ignore - 故意测试运行时行为 + expect(ref.current.tagName).toBeUndefined(); + // @ts-ignore - 故意测试运行时行为 + expect(ref.current.src).toBeUndefined(); + }); +}); From b04e15e7b53a0546303c38bc06bd6b9fb63de544 Mon Sep 17 00:00:00 2001 From: liamli-0822 Date: Mon, 24 Mar 2025 11:09:05 +0800 Subject: [PATCH 5/5] update tests --- tests/ref.test.tsx | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/tests/ref.test.tsx b/tests/ref.test.tsx index 265af045..a21adc82 100644 --- a/tests/ref.test.tsx +++ b/tests/ref.test.tsx @@ -16,7 +16,6 @@ describe('Image ref forwarding', () => { // 确保 ref.current.nativeElement 指向正确的 img 元素 expect(ref.current).not.toBeNull(); - expect(ref.current?.nativeElement).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'); @@ -38,7 +37,6 @@ describe('Image ref forwarding', () => { // 确保回调 ref 被调用,且 nativeElement 指向正确的 img 元素 expect(imgRef).not.toBeNull(); - expect(imgRef?.nativeElement).not.toBeNull(); expect(imgRef?.nativeElement).toBe(container.querySelector('.rc-image-img')); }); @@ -89,21 +87,4 @@ describe('Image ref forwarding', () => { expect(ref.current?.nativeElement?.alt).toBe('updated alt'); }); - // 测试 ref 不能直接访问 img 元素属性 - it('should not allow direct access to img element properties', () => { - const ref = React.createRef(); - render( - , - ); - - // 确保 ref.current 不是 HTMLImageElement - expect(ref.current).not.toBeNull(); - // @ts-ignore - 故意测试运行时行为 - expect(ref.current.tagName).toBeUndefined(); - // @ts-ignore - 故意测试运行时行为 - expect(ref.current.src).toBeUndefined(); - }); });