diff --git a/src/image-viewer/ImageViewer.tsx b/src/image-viewer/ImageViewer.tsx index cbe489cf5..30e408d03 100644 --- a/src/image-viewer/ImageViewer.tsx +++ b/src/image-viewer/ImageViewer.tsx @@ -1,21 +1,23 @@ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { createPortal } from 'react-dom'; import isFunction from 'lodash/isFunction'; -import { TdImageViewerProps } from './type'; import { ImageModal } from './ImageViewerModal'; -import { StyledProps, TNode } from '../common'; import { imageViewerDefaultProps } from './defaultProps'; +import type { TdImageViewerProps } from './type'; +import type { ImageModalProps } from './ImageViewerModal'; +import type { StyledProps, TNode } from '../common'; import useImageScale from './hooks/useImageScale'; import useList from './hooks/useList'; import useViewerScale from './hooks/useViewerScale'; import useControlled from '../hooks/useControlled'; import useDefaultProps from '../hooks/useDefaultProps'; +import { canUseDocument } from '../_util/dom'; export interface ImageViewerProps extends TdImageViewerProps, StyledProps {} const ImageViewer: React.FC = (originalProps) => { const props = useDefaultProps(originalProps, imageViewerDefaultProps); - const { mode, trigger, images, title, imageScale: imageScaleD, viewerScale: viewerScaleD } = props; + const { attach, mode, trigger, images, title, imageScale: imageScaleD, viewerScale: viewerScaleD } = props; const [visible, setVisible] = useControlled(props, 'visible', (visible, context) => { isFunction(props.onClose) && props.onClose(context); @@ -28,7 +30,7 @@ const ImageViewer: React.FC = (originalProps) => { const isMini = mode === 'modeless'; - const close = (context) => { + const close: ImageModalProps['onClose'] = (context) => { setVisible(false, context); setTimeout(() => setVisibled(false), 196); }; @@ -42,6 +44,21 @@ const ImageViewer: React.FC = (originalProps) => { // @ts-ignore TODO 待类型完善后移除 const uiImage: TNode = isFunction(trigger) ? trigger({ open, close, onOpen: open, onClose: close }) : trigger; + const attachElement = useMemo(() => { + if (!canUseDocument || !attach) return null; + + if (attach === 'body') { + return document.body; + } + + if (typeof attach === 'string') { + return document.querySelector(attach); + } + if (isFunction(attach)) { + return attach(); + } + }, [attach]); + return ( <> {uiImage} @@ -67,7 +84,7 @@ const ImageViewer: React.FC = (originalProps) => { onOpen={open} imageReferrerpolicy={props.imageReferrerpolicy} />, - document.body, + attachElement, )} ); diff --git a/src/image-viewer/ImageViewerModal.tsx b/src/image-viewer/ImageViewerModal.tsx index 809de58d0..cb7df7e43 100644 --- a/src/image-viewer/ImageViewerModal.tsx +++ b/src/image-viewer/ImageViewerModal.tsx @@ -371,7 +371,7 @@ const ImageViewerHeader = (props: ImageViewerHeaderProps) => { ); }; -interface ImageModalProps { +export interface ImageModalProps { title?: TNode; visible: boolean; closeOnOverlay: boolean; diff --git a/src/image-viewer/__tests__/image-viewer.test.tsx b/src/image-viewer/__tests__/image-viewer.test.tsx index 8a37301d9..7b9a774be 100644 --- a/src/image-viewer/__tests__/image-viewer.test.tsx +++ b/src/image-viewer/__tests__/image-viewer.test.tsx @@ -51,6 +51,50 @@ describe('ImageViewer', () => { const { getByText } = render(); expect(getByText(triggerText)).toBeTruthy(); }); + + test('base:attach is default=body', async () => { + const BasicImageViewer = () => { + const trigger = ({ open }) => {triggerText}; + return ; + }; + const { getByText } = render(); + + // 点击前,没有元素存在 + const imgContainer = document.body.querySelector('.t-image-viewer-preview-image'); + expect(imgContainer).toBeNull(); + + // 模拟鼠标点击 + act(() => { + fireEvent.click(getByText(triggerText)); + }); + + // 鼠标点击后,有元素 + const imgModal = document.body.querySelector('.t-image-viewer__modal-pic'); + expect(imgModal).toBeTruthy(); + }); + + test('base:attach is function', async () => { + const BasicImageViewer = () => { + const trigger = ({ open }) => {triggerText}; + return document.body} />; + }; + const { getByText } = render(); + + // 点击前,没有元素存在 + const imgContainer = document.body.querySelector('.t-image-viewer-preview-image'); + expect(imgContainer).toBeNull(); + + // 模拟鼠标点击 + act(() => { + fireEvent.click(getByText(triggerText)); + }); + + act(() => { + // 鼠标点击后,有元素 + const imgModal = document.body.querySelector('.t-image-viewer__modal-pic'); + expect(imgModal).toBeTruthy(); + }); + }); }); describe('ImageViewerMini', () => { diff --git a/src/image-viewer/defaultProps.ts b/src/image-viewer/defaultProps.ts index d6d4a553c..8c570763a 100644 --- a/src/image-viewer/defaultProps.ts +++ b/src/image-viewer/defaultProps.ts @@ -5,6 +5,7 @@ import { TdImageViewerProps } from './type'; export const imageViewerDefaultProps: TdImageViewerProps = { + attach: 'body', closeBtn: true, closeOnEscKeydown: true, draggable: undefined, diff --git a/src/image-viewer/image-viewer.en-US.md b/src/image-viewer/image-viewer.en-US.md index 5bd8dd6e0..4c0c0e880 100644 --- a/src/image-viewer/image-viewer.en-US.md +++ b/src/image-viewer/image-viewer.en-US.md @@ -8,6 +8,7 @@ name | type | default | description | required -- | -- | -- | -- | -- className | String | - | className of component | N style | Object | - | CSS(Cascading Style Sheets),Typescript:`React.CSSProperties` | N +attach | String / Function | 'body' | Typescript:`AttachNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N closeBtn | TNode | true | Typescript:`boolean \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N closeOnEscKeydown | Boolean | true | trigger image viewer close event on `ESC` keydown | N closeOnOverlay | Boolean | - | \- | N diff --git a/src/image-viewer/image-viewer.md b/src/image-viewer/image-viewer.md index a96dce116..c500458c9 100644 --- a/src/image-viewer/image-viewer.md +++ b/src/image-viewer/image-viewer.md @@ -8,6 +8,7 @@ -- | -- | -- | -- | -- className | String | - | 类名 | N style | Object | - | 样式,TS 类型:`React.CSSProperties` | N +attach | String / Function | 'body' | 制定挂载节点。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'body' 或 () => document.body。TS 类型:`AttachNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N closeBtn | TNode | true | 是否展示关闭按钮,值为 `true` 显示默认关闭按钮;值为 `false` 则不显示关闭按钮;也可以完全自定义关闭按钮。TS 类型:`boolean \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N closeOnEscKeydown | Boolean | true | 按下 ESC 时是否触发图片预览器关闭事件 | N closeOnOverlay | Boolean | - | 是否在点击遮罩层时,触发预览关闭 | N diff --git a/src/image-viewer/type.ts b/src/image-viewer/type.ts index dff6a9a6c..17d6584d5 100644 --- a/src/image-viewer/type.ts +++ b/src/image-viewer/type.ts @@ -4,10 +4,15 @@ * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC * */ -import { TNode } from '../common'; +import { TNode, AttachNode } from '../common'; import { MouseEvent, KeyboardEvent } from 'react'; export interface TdImageViewerProps { + /** + * 制定挂载节点。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'body' 或 () => document.body + * @default 'body' + */ + attach?: AttachNode; /** * 是否展示关闭按钮,值为 `true` 显示默认关闭按钮;值为 `false` 则不显示关闭按钮;也可以完全自定义关闭按钮 * @default true