-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: new component Image with built-in lazy loading option (#395)
* feat(intersectionobserver): add IntersectionObserver hook * feat(intersectionobserver): update IntersectionObserver unit test * feat(useintersectionobserver): use ref and add freezeOnceVisible * feat(useintersectionobserver): update test unit * feat(image): add image component * feat(image): update review comment and mock window.Image * feat(image): add image test
- Loading branch information
Showing
12 changed files
with
509 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`test Image should render correct 1`] = ` | ||
{ | ||
"asFragment": [Function], | ||
"baseElement": <body> | ||
<div> | ||
<div | ||
aria-busy="true" | ||
aria-live="polite" | ||
class="ant-spin ant-spin-spinning" | ||
> | ||
<span | ||
class="ant-spin-dot ant-spin-dot-spin" | ||
> | ||
<i | ||
class="ant-spin-dot-item" | ||
/> | ||
<i | ||
class="ant-spin-dot-item" | ||
/> | ||
<i | ||
class="ant-spin-dot-item" | ||
/> | ||
<i | ||
class="ant-spin-dot-item" | ||
/> | ||
</span> | ||
</div> | ||
</div> | ||
</body>, | ||
"container": <div> | ||
<div | ||
aria-busy="true" | ||
aria-live="polite" | ||
class="ant-spin ant-spin-spinning" | ||
> | ||
<span | ||
class="ant-spin-dot ant-spin-dot-spin" | ||
> | ||
<i | ||
class="ant-spin-dot-item" | ||
/> | ||
<i | ||
class="ant-spin-dot-item" | ||
/> | ||
<i | ||
class="ant-spin-dot-item" | ||
/> | ||
<i | ||
class="ant-spin-dot-item" | ||
/> | ||
</span> | ||
</div> | ||
</div>, | ||
"debug": [Function], | ||
"findAllByAltText": [Function], | ||
"findAllByDisplayValue": [Function], | ||
"findAllByLabelText": [Function], | ||
"findAllByPlaceholderText": [Function], | ||
"findAllByRole": [Function], | ||
"findAllByTestId": [Function], | ||
"findAllByText": [Function], | ||
"findAllByTitle": [Function], | ||
"findByAltText": [Function], | ||
"findByDisplayValue": [Function], | ||
"findByLabelText": [Function], | ||
"findByPlaceholderText": [Function], | ||
"findByRole": [Function], | ||
"findByTestId": [Function], | ||
"findByText": [Function], | ||
"findByTitle": [Function], | ||
"getAllByAltText": [Function], | ||
"getAllByDisplayValue": [Function], | ||
"getAllByLabelText": [Function], | ||
"getAllByPlaceholderText": [Function], | ||
"getAllByRole": [Function], | ||
"getAllByTestId": [Function], | ||
"getAllByText": [Function], | ||
"getAllByTitle": [Function], | ||
"getByAltText": [Function], | ||
"getByDisplayValue": [Function], | ||
"getByLabelText": [Function], | ||
"getByPlaceholderText": [Function], | ||
"getByRole": [Function], | ||
"getByTestId": [Function], | ||
"getByText": [Function], | ||
"getByTitle": [Function], | ||
"queryAllByAltText": [Function], | ||
"queryAllByDisplayValue": [Function], | ||
"queryAllByLabelText": [Function], | ||
"queryAllByPlaceholderText": [Function], | ||
"queryAllByRole": [Function], | ||
"queryAllByTestId": [Function], | ||
"queryAllByText": [Function], | ||
"queryAllByTitle": [Function], | ||
"queryByAltText": [Function], | ||
"queryByDisplayValue": [Function], | ||
"queryByLabelText": [Function], | ||
"queryByPlaceholderText": [Function], | ||
"queryByRole": [Function], | ||
"queryByTestId": [Function], | ||
"queryByText": [Function], | ||
"queryByTitle": [Function], | ||
"rerender": [Function], | ||
"unmount": [Function], | ||
} | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import React from 'react'; | ||
import { render, waitFor } from '@testing-library/react'; | ||
|
||
import Image from '..'; | ||
let originalImage: any = null; | ||
|
||
describe('test Image', () => { | ||
beforeEach(() => { | ||
originalImage = window.Image; | ||
// @ts-ignore | ||
window.Image = class { | ||
constructor() { | ||
setTimeout(() => { | ||
// @ts-ignore | ||
this.onload(); | ||
}, 100); | ||
} | ||
}; | ||
}); | ||
|
||
afterEach(() => { | ||
window.Image = originalImage; | ||
}); | ||
|
||
it('should render correct', async () => { | ||
const wrapper = render(<Image src="https://example.com/image.jpg" />); | ||
await waitFor(() => { | ||
expect(wrapper).toMatchSnapshot(); | ||
}); | ||
}); | ||
|
||
it('renders img when src is available', async () => { | ||
render(<Image src="https://example.com/image.jpg" />); | ||
await waitFor(() => { | ||
// 检查是否正确渲染 img 元素 | ||
const imgDom = document.querySelector('img'); | ||
expect(imgDom).not.toBeNull(); | ||
expect(imgDom?.src).toEqual('https://example.com/image.jpg'); | ||
}); | ||
}); | ||
|
||
it('renders img with correct props', async () => { | ||
const { container } = render( | ||
<Image | ||
src="https://example.com/image.jpg" | ||
height={200} | ||
width={100} | ||
className="test" | ||
style={{ backgroundColor: 'red' }} | ||
/> | ||
); | ||
await waitFor(() => { | ||
// 检查是否正确渲染 img 元素 | ||
const imgDom = container.querySelector('img'); | ||
expect(imgDom).not.toBeNull(); | ||
expect(imgDom?.height).toEqual(200); | ||
expect(imgDom?.width).toEqual(100); | ||
expect(imgDom?.className).toEqual('test'); | ||
expect((imgDom?.style as any)._values).toEqual({ 'background-color': 'red' }); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import React from 'react'; | ||
|
||
import Image from '..'; | ||
|
||
export default function Component() { | ||
return ( | ||
<div style={{ height: 200 }}> | ||
<Image | ||
height={200} | ||
width={200} | ||
src="https://dtstack.github.io/dt-react-component/static/empty_overview.43b0eedf.png" | ||
style={{ borderColor: 'red' }} | ||
/> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import React from 'react'; | ||
|
||
import Image from '..'; | ||
|
||
export default function Component() { | ||
return ( | ||
<div style={{ height: 200, overflow: 'scroll' }}> | ||
<div style={{ height: 300 }}>占位</div> | ||
<Image | ||
height={200} | ||
width={200} | ||
lazy | ||
src="https://dtstack.github.io/dt-react-component/static/empty_permission.35e2808b.png" | ||
/> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
--- | ||
title: Image 图片组件 | ||
group: 组件 | ||
toc: content | ||
--- | ||
|
||
# Image | ||
|
||
## 何时使用 | ||
|
||
展示图片 | ||
|
||
## 示例 | ||
|
||
<code src="./demos/basic.tsx" title="基础使用"></code> | ||
<code src="./demos/lazy.tsx" title="图片懒加载"></code> | ||
|
||
## API | ||
|
||
### Props | ||
|
||
| 参数 | 说明 | 类型 | 默认值 | | ||
| --------- | ------------------ | --------------------- | ------ | | ||
| src | 图片资源 | `string` | - | | ||
| lazy | 是否开启图片懒加载 | `boolean` | - | | ||
| className | 图片样式名 | `string` | | ||
| style | 图片样式 | `React.CSSProperties` | | ||
| width | 图片样式 | `number` | | ||
| height | 图片样式 | `number` | | ||
| loader | 图片样式 | `JSX.Element \| null` | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import React, { CSSProperties, useRef } from 'react'; | ||
import { Spin } from 'antd'; | ||
|
||
import useIntersectionObserver from '../useIntersectionObserver'; | ||
|
||
interface IProps { | ||
src: string; | ||
lazy?: boolean; | ||
alt?: string; | ||
className?: string; | ||
style?: CSSProperties; | ||
width?: number; | ||
height?: number; | ||
loader?: JSX.Element | null; | ||
} | ||
|
||
function loadImage(src: string) { | ||
return new Promise((resolve, reject) => { | ||
const i = new Image(); | ||
i.onload = () => resolve(true); | ||
i.onerror = reject; | ||
i.src = src; | ||
}); | ||
} | ||
|
||
export function useImage({ src }: { src: string }): { | ||
src: string | undefined; | ||
isLoading: boolean; | ||
} { | ||
const [loading, setLoading] = React.useState(true); | ||
const [value, setValue] = React.useState<string | undefined>(undefined); | ||
|
||
React.useEffect(() => { | ||
loadImage(src) | ||
.then(() => { | ||
setLoading(false); | ||
setValue(src); | ||
}) | ||
.finally(() => { | ||
setLoading(false); | ||
}); | ||
}, [src]); | ||
|
||
return { isLoading: loading, src: value }; | ||
} | ||
|
||
const ImageComponent = (props: IProps) => { | ||
const { lazy } = props; | ||
if (lazy) return <LazyImage {...props} />; | ||
return <NormalImage {...props} />; | ||
}; | ||
|
||
const LazyImage = (props: IProps) => { | ||
const { src, ...rest } = props; | ||
const imgRef = useRef<HTMLImageElement>(null); | ||
useIntersectionObserver(([entry]) => { | ||
const { target, isIntersecting } = entry; | ||
if (isIntersecting) { | ||
const _target = target as HTMLImageElement; | ||
_target.src = _target.dataset['src'] ?? ''; | ||
_target.onload = () => { | ||
_target.style.opacity = '1'; | ||
}; | ||
} | ||
}, imgRef); | ||
return <img ref={imgRef} {...rest} data-src={src} />; | ||
}; | ||
|
||
const NormalImage = (props: IProps) => { | ||
const { src: originSrc, loader = <Spin spinning />, ...rest } = props; | ||
const { src, isLoading } = useImage({ src: originSrc }); | ||
if (src) return <img {...rest} src={src} />; | ||
if (isLoading) return loader; | ||
return null; | ||
}; | ||
|
||
export default ImageComponent; |
59 changes: 59 additions & 0 deletions
59
src/useIntersectionObserver/__tests__/useIntersectionObserver.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { RefObject } from 'react'; | ||
import { act, renderHook } from '@testing-library/react-hooks'; | ||
|
||
import useIntersectionObserver from '../index'; | ||
|
||
describe('useIntersectionObserver', () => { | ||
let observeMock: jest.Mock; | ||
let disconnectMock: jest.Mock; | ||
|
||
beforeEach(() => { | ||
observeMock = jest.fn(); | ||
disconnectMock = jest.fn(); | ||
jest.spyOn(window, 'IntersectionObserver').mockImplementation( | ||
( | ||
_callback: IntersectionObserverCallback, | ||
_options?: IntersectionObserverInit | undefined | ||
) => ({ | ||
observe: observeMock, | ||
disconnect: disconnectMock, | ||
root: null, | ||
rootMargin: '', | ||
thresholds: [], | ||
takeRecords: jest.fn(), | ||
unobserve: jest.fn(), | ||
}) | ||
); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.restoreAllMocks(); | ||
}); | ||
|
||
it('should observe target element and disconnect on unmount', () => { | ||
const ref = { current: document.createElement('div') }; | ||
const callback = jest.fn(); | ||
const options = { threshold: 0, root: null, rootMargin: '0%' }; | ||
const { unmount } = renderHook(() => useIntersectionObserver(callback, ref, options)); | ||
expect(window.IntersectionObserver).toHaveBeenCalledWith(expect.any(Function), options); | ||
expect(observeMock).toHaveBeenCalledWith(ref.current); | ||
act(() => { | ||
unmount(); | ||
}); | ||
expect(disconnectMock).toHaveBeenCalled(); | ||
}); | ||
|
||
it('should not observe target element if not provided', () => { | ||
const callback = jest.fn(); | ||
const options = { threshold: 0, root: null, rootMargin: '0%' }; | ||
const { unmount } = renderHook(() => | ||
useIntersectionObserver(callback, null as unknown as RefObject<Element>, options) | ||
); | ||
expect(window.IntersectionObserver).toHaveBeenCalledWith(expect.any(Function), options); | ||
expect(observeMock).not.toHaveBeenCalled(); | ||
act(() => { | ||
unmount(); | ||
}); | ||
expect(callback).not.toHaveBeenCalled(); | ||
}); | ||
}); |
Oops, something went wrong.