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

ImageViewer preview component supports self-defined preview content #6825

Merged
merged 12 commits into from
Mar 7, 2025
16 changes: 15 additions & 1 deletion src/components/image-viewer/demos/demo1.less
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,25 @@

.footerButton {
font-size: var(--adm-font-size-4);
color: #ffffff;
color: #fff;
line-height: 1;
padding: 10px 16px;
background-color: rgba(153, 153, 153, 0.4);
border-radius: 100px;
display: inline-block;
cursor: pointer;
}
.image-render {
width: 100%;
height: 100%;
display: flex;
align-items: center;
}
.custom-render {
color: #1677ff;
background: #fff;
width: 200px;
height: 100px;
border-radius: 2px;
padding: 8px;
}
47 changes: 44 additions & 3 deletions src/components/image-viewer/demos/demo1.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import React, { useState } from 'react'
import { ImageViewer, Button } from 'antd-mobile'
import { Button, ImageViewer } from 'antd-mobile'
import { DemoBlock } from 'demos'
import { demoImage, demoImages } from './images'
import React, { useState } from 'react'
import styles from './demo1.less'
import { demoImage, demoImages } from './images'

const demoViewImages = [
'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/file/A*uYT7SZwhJnUAAAAAAAAAAAAADgCCAQ',
'https://images.unsplash.com/photo-1620476214170-1d8080f65cdb?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=3150&q=80',
]

// 单张图片预览
const Single = () => {
Expand Down Expand Up @@ -95,6 +100,38 @@ const ViewWithFooter = () => {
)
}

// 自定义预览内容
const ViewImageRender = () => {
const [visible, setVisible] = useState(false)
return (
<>
<Button
onClick={() => {
setVisible(true)
}}
>
显示图片
</Button>
<ImageViewer.Multi
images={demoViewImages}
visible={visible}
imageRender={(image, info) => {
if (info.index === 0)
return (
<div className={styles['image-render']}>
<video muted width='100%' controls src={image} />
</div>
)
}}
defaultIndex={0}
onClose={() => {
setVisible(false)
}}
/>
</>
)
}

export default () => {
return (
<>
Expand Down Expand Up @@ -134,6 +171,10 @@ export default () => {
<DemoBlock title='自定义底部额外内容'>
<ViewWithFooter />
</DemoBlock>

<DemoBlock title='自定义预览内容'>
<ViewImageRender />
</DemoBlock>
</>
)
}
18 changes: 11 additions & 7 deletions src/components/image-viewer/image-viewer.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
import classNames from 'classnames'
import type { FC, ReactNode } from 'react'
import React, {
forwardRef,
useCallback,
useImperativeHandle,
useRef,
useState,
useCallback,
} from 'react'
import type { FC, ReactNode } from 'react'
import { mergeProps } from '../../utils/with-default-props'
import {
GetContainer,
renderToContainer,
} from '../../utils/render-to-container'
import { mergeProps } from '../../utils/with-default-props'
import Mask from '../mask'
import SafeArea from '../safe-area'
import { Slide } from './slide'
import { Slides, SlidesRef } from './slides'
import classNames from 'classnames'

const classPrefix = `adm-image-viewer`

export type ImageViewerProps = {
image?: string
image: string
maxZoom?: number | 'auto'
getContainer?: GetContainer
visible?: boolean
onClose?: () => void
afterClose?: () => void
renderFooter?: (image: string) => ReactNode
imageRender?: (image: string, { index }: { index: number }) => ReactNode
Copy link
Member

Choose a reason for hiding this comment

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

主体应该没问题了,补一下测试用例吧~

classNames?: {
mask?: string
body?: string
Expand Down Expand Up @@ -57,11 +58,12 @@ export const ImageViewer: FC<ImageViewerProps> = p => {
props?.classNames?.body
)}
>
{props.image && (
{(props.image || typeof props.imageRender === 'function') && (
<Slide
image={props.image}
onTap={props.onClose}
maxZoom={props.maxZoom}
imageRender={props.imageRender}
/>
)}
</div>
Expand All @@ -80,12 +82,13 @@ export type MultiImageViewerRef = SlidesRef

export type MultiImageViewerProps = Omit<
ImageViewerProps,
'image' | 'renderFooter'
'image' | 'renderFooter' | 'imageRender'
> & {
images?: string[]
defaultIndex?: number
onIndexChange?: (index: number) => void
renderFooter?: (image: string, index: number) => ReactNode
imageRender?: (image: string, { index }: { index: number }) => ReactNode
}

const multiDefaultProps = {
Expand Down Expand Up @@ -140,6 +143,7 @@ export const MultiImageViewer = forwardRef<
images={props.images}
onTap={props.onClose}
maxZoom={props.maxZoom}
imageRender={props.imageRender}
/>
)}
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/components/image-viewer/index.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ You need to click on the picture to view the details and use it with the thumbna
| maxZoom | The maximum zoom ratio | `number \| 'auto'` | `3` |
| onClose | Triggered when it is closed | `() => void` | - |
| renderFooter | Render extra content on footer | `(image: string) => ReactNode` | - |
| imageRender | Custom rendering content | `(image: string, { index }: { index: number }) => ReactNode` |
| visible | Whether to show or hide | `boolean` | `false` |

## ImageViewer.Multi
Expand All @@ -34,6 +35,7 @@ On the basis of `ImageViewer`, the following props have been added:
| images | Url list of image resources | `string[]` | - |
| onIndexChange | Triggered when the picture is switched | `(index: number) => void` | - |
| renderFooter | Render extra content on footer | `(image: string, index: number) => ReactNode` | - |
| imageRender | Custom rendering content | `(image: string, { index }: { index: number }) => ReactNode` | - |

At the same time, the `image` prop is removed.

Expand Down
2 changes: 2 additions & 0 deletions src/components/image-viewer/index.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
| maxZoom | 最大缩放比例 | `number \| 'auto'` | `3` |
| onClose | 关闭时触发 | `() => void` | - |
| renderFooter | 渲染底部额外内容 | `(image: string) => ReactNode` | - |
| imageRender | 自定义渲染内容 | `(image: string, { index }: { index: number }) => ReactNode` | - |
| visible | 是否显示 | `boolean` | `false` |

## ImageViewer.Multi
Expand All @@ -32,6 +33,7 @@
| images | 图片资源的 url 列表 | `string[]` | - |
| onIndexChange | 切换图片时触发 | `(index: number) => void` | - |
| renderFooter | 渲染底部额外内容 | `(image: string, index: number) => ReactNode` | - |
| imageRender | 自定义渲染内容 | `(image: string, { index }: { index: number }) => ReactNode` | - |

其他属性同 `ImageViewer`,但是去掉了 `image` 属性。

Expand Down
34 changes: 22 additions & 12 deletions src/components/image-viewer/slide.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React, { useRef } from 'react'
import type { FC, MutableRefObject } from 'react'
import { useSpring, animated } from '@react-spring/web'
import { animated, useSpring } from '@react-spring/web'
import { useSize } from 'ahooks'
import { rubberbandIfOutOfBounds } from '../../utils/rubberband'
import { useDragAndPinch } from '../../utils/use-drag-and-pinch'
import type { FC, MutableRefObject, ReactNode } from 'react'
import React, { useRef } from 'react'
import { bound } from '../../utils/bound'
import type { Matrix } from '../../utils/matrix'
import * as mat from '../../utils/matrix'
import { rubberbandIfOutOfBounds } from '../../utils/rubberband'
import { useDragAndPinch } from '../../utils/use-drag-and-pinch'

const classPrefix = `adm-image-viewer`

Expand All @@ -16,10 +16,12 @@ type Props = {
onTap?: () => void
onZoomChange?: (zoom: number) => void
dragLockRef?: MutableRefObject<boolean>
imageRender?: (image: string, { index }: { index: number }) => ReactNode
index?: number
}

export const Slide: FC<Props> = props => {
const { dragLockRef, maxZoom } = props
const { dragLockRef, maxZoom, imageRender, index } = props
const initialMartix = useRef<boolean[]>([])
const controlRef = useRef<HTMLDivElement>(null)
const imgRef = useRef<HTMLImageElement>(null)
Expand Down Expand Up @@ -295,6 +297,10 @@ export const Slide: FC<Props> = props => {
}
)

const customRendering =
typeof imageRender === 'function' &&
imageRender(props.image, { index } as { index: number })

return (
<div className={`${classPrefix}-slide`}>
<div className={`${classPrefix}-control`} ref={controlRef}>
Expand All @@ -304,12 +310,16 @@ export const Slide: FC<Props> = props => {
matrix,
}}
>
<img
ref={imgRef}
src={props.image}
draggable={false}
alt={props.image}
/>
{customRendering ? (
customRendering
) : (
<img
ref={imgRef}
src={props.image}
draggable={false}
alt={props.image}
/>
)}
</animated.div>
</div>
</div>
Expand Down
16 changes: 12 additions & 4 deletions src/components/image-viewer/slides.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import React, { forwardRef, useImperativeHandle, useRef } from 'react'
import { animated, useSpring } from '@react-spring/web'
import { useDrag } from '@use-gesture/react'
import { useSpring, animated } from '@react-spring/web'
import { Slide } from './slide'
import { convertPx } from '../../utils/convert-px'
import React, {
ReactNode,
forwardRef,
useImperativeHandle,
useRef,
} from 'react'
import { bound } from '../../utils/bound'
import { convertPx } from '../../utils/convert-px'
import { Slide } from './slide'

const classPrefix = `adm-image-viewer`

Expand All @@ -13,6 +18,7 @@ export type SlidesType = {
maxZoom: number
defaultIndex: number
onIndexChange?: (index: number) => void
imageRender?: (image: string, { index }: { index: number }) => ReactNode
}
export type SlidesRef = {
swipeTo: (index: number, immediate?: boolean) => void
Expand Down Expand Up @@ -95,6 +101,8 @@ export const Slides = forwardRef<SlidesRef, SlidesType>((props, ref) => {
image={image}
onTap={props.onTap}
maxZoom={props.maxZoom}
imageRender={props.imageRender}
index={index}
onZoomChange={zoom => {
if (zoom !== 1) {
const index: number = Math.round(x.get() / slideWidth)
Expand Down
33 changes: 26 additions & 7 deletions src/components/image-viewer/tests/image-viewer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import React, { useRef, useState } from 'react'
import {
render,
testA11y,
act,
fireEvent,
waitFor,
mockDrag,
render,
screen,
testA11y,
userEvent,
mockDrag,
act,
waitFor,
} from 'testing'
import ImageViewer, { MultiImageViewerRef } from '../index'
import Button from '../../button'
import ImageViewer, { MultiImageViewerRef } from '../index'
import image from './image.json'
const classPrefix = `adm-image-viewer`

Expand All @@ -21,6 +21,11 @@ const demoImages = [
`data:image/avif;base64,${image.content}`,
]

const demoViewImages = [
'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/file/A*uYT7SZwhJnUAAAAAAAAAAAAADgCCAQ',
'https://images.unsplash.com/photo-1620476214170-1d8080f65cdb?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=3150&q=80',
]

const G = global as any

// `@react-spring/web` with `skipAnimation` not work in test env. Strange
Expand Down Expand Up @@ -165,7 +170,6 @@ describe('ImageViewer.Multi', () => {
})
await waitFor(() => expect(img).not.toBeVisible())
})

test('slide and slide with pinched should be work', async () => {
Object.defineProperty(window, 'innerWidth', {
value: 300,
Expand Down Expand Up @@ -228,6 +232,21 @@ describe('ImageViewer.Multi', () => {
await waitFor(() => expect(onIndexChange).toBeCalledWith(3))
expect(screen.getByText('4 / 4')).toBeInTheDocument()
})
test('rendering with imageRender', () => {
function App() {
return (
<ImageViewer.Multi
images={demoViewImages}
visible
imageRender={(image, info) => (
<div className={`customize-preview-node-${info.index}`} />
)}
/>
)
}
render(<App />)
expect(document.querySelector('.customize-preview-node-0')).toBeTruthy()
})
})

describe('ImageViewer', () => {
Expand Down
Loading