diff --git a/packages/ui/hiui/package.json b/packages/ui/hiui/package.json index 69e7ce2c4..a909c300a 100644 --- a/packages/ui/hiui/package.json +++ b/packages/ui/hiui/package.json @@ -68,6 +68,7 @@ "@hi-ui/form": "^4.0.6", "@hi-ui/grid": "^4.0.5", "@hi-ui/highlighter": "^4.0.5", + "@hi-ui/image": "^4.0.0-alpha.0", "@hi-ui/input": "^4.0.4", "@hi-ui/input-group": "^4.0.3", "@hi-ui/list": "^4.0.6", diff --git a/packages/ui/hiui/src/index.ts b/packages/ui/hiui/src/index.ts index 4e4e3e19f..1e10aa4aa 100644 --- a/packages/ui/hiui/src/index.ts +++ b/packages/ui/hiui/src/index.ts @@ -220,3 +220,6 @@ export { default as Anchor } from '@hi-ui/anchor' export * from '@hi-ui/back-top' export { default as BackTop } from '@hi-ui/back-top' + +export * from '@hi-ui/image' +export { default as Image } from '@hi-ui/image' diff --git a/packages/ui/image/README.md b/packages/ui/image/README.md new file mode 100644 index 000000000..460635659 --- /dev/null +++ b/packages/ui/image/README.md @@ -0,0 +1,11 @@ +# `@hi-ui/image` + +> TODO: description + +## Usage + +``` +const Image = require('@hi-ui/image'); + +// TODO: DEMONSTRATE API +``` diff --git a/packages/ui/image/__tests__/image.test.js b/packages/ui/image/__tests__/image.test.js new file mode 100644 index 000000000..b1ebeecca --- /dev/null +++ b/packages/ui/image/__tests__/image.test.js @@ -0,0 +1,5 @@ +const Image = require('../src') + +describe('@hi-ui/image', () => { + it('needs tests', () => {}) +}) diff --git a/packages/ui/image/hi-docs.config.mdx b/packages/ui/image/hi-docs.config.mdx new file mode 100644 index 000000000..0371563bd --- /dev/null +++ b/packages/ui/image/hi-docs.config.mdx @@ -0,0 +1,17 @@ +# Image 图片 + +展示和预览图片 + +## 何时使用 + +展示和预览图片时使用 + +常见于表单,作为表单的组件类型之一 + +## 使用示例 + + + +## Props + + diff --git a/packages/ui/image/jest.config.js b/packages/ui/image/jest.config.js new file mode 100644 index 000000000..e33c14b5d --- /dev/null +++ b/packages/ui/image/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../../jest.config') diff --git a/packages/ui/image/package.json b/packages/ui/image/package.json new file mode 100644 index 000000000..9893aaf85 --- /dev/null +++ b/packages/ui/image/package.json @@ -0,0 +1,61 @@ +{ + "name": "@hi-ui/image", + "version": "4.0.0-alpha.0", + "description": "A sub-package for @hi-ui/hiui.", + "keywords": [], + "author": "HiUI ", + "homepage": "https://github.com/XiaoMi/hiui/tree/master/packages/ui/image#readme", + "license": "MIT", + "directories": { + "lib": "lib", + "test": "__tests__" + }, + "files": [ + "lib" + ], + "main": "lib/cjs/index.js", + "module": "lib/esm/index.js", + "types": "lib/types/index.d.ts", + "typings": "lib/types/index.d.ts", + "exports": { + ".": { + "require": "./lib/cjs/index.js", + "default": "./lib/esm/index.js" + } + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/XiaoMi/hiui.git" + }, + "scripts": { + "test": "jest", + "clean": "rimraf lib", + "prebuild": "yarn clean", + "build:esm": "hi-build ./src/index.ts --format esm -d ./lib/esm", + "build:cjs": "hi-build ./src/index.ts --format cjs -d ./lib/cjs", + "build:types": "tsc --emitDeclarationOnly --declaration --declarationDir lib/types", + "build": "concurrently yarn:build:*" + }, + "bugs": { + "url": "https://github.com/XiaoMi/hiui/issues" + }, + "dependencies": { + "@hi-ui/classname": "^4.0.0", + "@hi-ui/env": "^4.0.0" + }, + "peerDependencies": { + "@hi-ui/core": ">=4.0.0", + "react": ">=16.8.6", + "react-dom": ">=16.8.6" + }, + "devDependencies": { + "@hi-ui/core": "^4.0.0", + "@hi-ui/core-css": "^4.0.0", + "@hi-ui/hi-build": "^4.0.0", + "react": "^17.0.1", + "react-dom": "^17.0.1" + } +} diff --git a/packages/ui/image/src/Image.tsx b/packages/ui/image/src/Image.tsx new file mode 100644 index 000000000..03aca26e4 --- /dev/null +++ b/packages/ui/image/src/Image.tsx @@ -0,0 +1,105 @@ +import React, { forwardRef, useMemo, useState } from 'react' +import { cx, getPrefixCls } from '@hi-ui/classname' +import { __DEV__ } from '@hi-ui/env' +import { HiBaseHTMLProps } from '@hi-ui/core' +import { useLatestCallback } from '@hi-ui/use-latest' +import Preview, { PreviewProps } from '@hi-ui/preview' +import { isObject } from '@hi-ui/type-assertion' +import { useImage, UseImageProps } from './use-image' + +const IMAGE_PREFIX = getPrefixCls('image') + +/** + * TODO: What is Image + */ +export const Image = forwardRef( + ( + { + prefixCls = IMAGE_PREFIX, + role = 'image', + className, + style, + src, + width, + height, + fallback, + placeholder, + preview, + onLoad, + onError, + onClick, + ...rest + }, + ref + ) => { + const { status, imageProps, getImageRef } = useImage({ + fallback, + placeholder, + onError, + src, + }) + + const [previewVisible, setPreViewVisible] = useState(false) + + const canPreviewMemo = useMemo(() => { + return preview && src && status !== 'error' + }, [preview, src, status]) + + const cls = cx(prefixCls, className, canPreviewMemo && `${prefixCls}--preview`) + + const handleClick = useLatestCallback((e) => { + onClick?.(e) + setPreViewVisible(true) + }) + + const handleClosePreview = useLatestCallback(() => { + setPreViewVisible(false) + }) + + return ( + <> +
+ + {status === 'loading' && ( + + )} +
+ {canPreviewMemo && ( + + )} + + ) + } +) + +export interface ImageProps extends Omit, 'placeholder'>, UseImageProps { + /** + * 图片预览参数 + */ + preview?: Partial | boolean +} + +if (__DEV__) { + Image.displayName = 'Image' +} diff --git a/packages/ui/image/src/index.ts b/packages/ui/image/src/index.ts new file mode 100644 index 000000000..996e0a400 --- /dev/null +++ b/packages/ui/image/src/index.ts @@ -0,0 +1,4 @@ +import './styles/index.scss' + +export * from './Image' +export { Image as default } from './Image' diff --git a/packages/ui/image/src/styles/image.scss b/packages/ui/image/src/styles/image.scss new file mode 100644 index 000000000..3e4e7dea7 --- /dev/null +++ b/packages/ui/image/src/styles/image.scss @@ -0,0 +1,24 @@ +@import '~@hi-ui/core-css/lib/index.scss'; + +$prefix: '#{$component-prefix}-image' !default; + +.#{$prefix} { + position: relative; + display: inline-block; + + &--preview { + cursor: pointer; + } + + &-img { + vertical-align: middle; + } + + &-placeholder { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + } +} diff --git a/packages/ui/image/src/styles/index.scss b/packages/ui/image/src/styles/index.scss new file mode 100644 index 000000000..30b72fac5 --- /dev/null +++ b/packages/ui/image/src/styles/index.scss @@ -0,0 +1 @@ +@import './image.scss'; diff --git a/packages/ui/image/src/types.ts b/packages/ui/image/src/types.ts new file mode 100644 index 000000000..dc1b406c1 --- /dev/null +++ b/packages/ui/image/src/types.ts @@ -0,0 +1 @@ +export type ImageStatus = 'normal' | 'error' | 'loading' diff --git a/packages/ui/image/src/use-image.ts b/packages/ui/image/src/use-image.ts new file mode 100644 index 000000000..3479be043 --- /dev/null +++ b/packages/ui/image/src/use-image.ts @@ -0,0 +1,74 @@ +import React, { useState, useEffect, useMemo } from 'react' +import { useLatestCallback, useLatestRef } from '@hi-ui/use-latest' +import { ImageStatus } from './types' + +export const useImage = ({ placeholder, fallback, onLoad, onError, src }: UseImageProps) => { + const isCustomPlaceholder = placeholder && placeholder !== true + const [status, setStatus] = useState(isCustomPlaceholder ? 'loading' : 'normal') + const isError = status === 'error' + const isLoaded = useLatestRef(false) + + const handleLoad = useLatestCallback((e) => { + onLoad?.(e) + setStatus('normal') + }) + + const handleError = useLatestCallback((e) => { + onError?.(e) + setStatus('error') + }) + + const imageProps = useMemo(() => { + return { + ...(isError && fallback + ? { src: fallback } + : { src, onLoad: handleLoad, onError: handleError }), + } + }, [fallback, handleError, handleLoad, src, isError]) + + const getImageRef = (img?: HTMLImageElement) => { + isLoaded.current = false + + if (status !== 'loading') return + + if (img?.complete && (img.naturalWidth || img.naturalHeight)) { + isLoaded.current = true + setStatus('normal') + } + } + + useEffect(() => { + if (isError) { + setStatus('normal') + } + + if (isCustomPlaceholder && !isLoaded.current) { + setStatus('loading') + } + }, [src]) + + return { + status, + imageProps, + getImageRef, + } +} + +export interface UseImageProps { + /** + * 图片地址 + */ + src?: string + /** + * 加载失败容错地址 + */ + fallback?: string + /** + * 加载占位 + */ + placeholder?: React.ReactNode + onLoad?: (e: React.SyntheticEvent) => void + onError?: (e: React.SyntheticEvent) => void +} + +export type useImageReturn = ReturnType diff --git a/packages/ui/image/stories/basic.stories.tsx b/packages/ui/image/stories/basic.stories.tsx new file mode 100644 index 000000000..16a6db0aa --- /dev/null +++ b/packages/ui/image/stories/basic.stories.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import Image from '../src' + +/** + * @title 基础用法 + */ +export const Basic = () => { + return ( + <> +

Basic

+
+ +
+ + ) +} diff --git a/packages/ui/image/stories/fallback.stories.tsx b/packages/ui/image/stories/fallback.stories.tsx new file mode 100644 index 000000000..b3ec0d15e --- /dev/null +++ b/packages/ui/image/stories/fallback.stories.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import Image from '../src' + +/** + * @title 容错处理 + * @desc 设置 fallback,当图片加载失败时,显示占位图 + */ +export const Fallback = () => { + return ( + <> +

Fallback

+
+ +
+ + ) +} diff --git a/packages/ui/image/stories/index.stories.tsx b/packages/ui/image/stories/index.stories.tsx new file mode 100644 index 000000000..aba89983e --- /dev/null +++ b/packages/ui/image/stories/index.stories.tsx @@ -0,0 +1,13 @@ +import React from 'react' +import Image from '../src' + +export * from './basic.stories' +export * from './placeholder.stories' +export * from './fallback.stories' +export * from './preview.stories' + +export default { + title: 'Data Display/Image', + component: Image, + decorators: [(story: Function) =>
{story()}
], +} diff --git a/packages/ui/image/stories/placeholder.stories.tsx b/packages/ui/image/stories/placeholder.stories.tsx new file mode 100644 index 000000000..f5b7e1d81 --- /dev/null +++ b/packages/ui/image/stories/placeholder.stories.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import Image from '../src' + +/** + * @title 渐进加载 + * @desc 当加载大图片时,设置 placeholder 为一个低质量图片,渐进加载 + */ +export const Placeholder = () => { + return ( + <> +

Placeholder

+
+ + } + /> +
+ + ) +} diff --git a/packages/ui/image/stories/preview.stories.tsx b/packages/ui/image/stories/preview.stories.tsx new file mode 100644 index 000000000..04565b869 --- /dev/null +++ b/packages/ui/image/stories/preview.stories.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import Image from '../src' + +/** + * @title 图片预览 + * @desc 点击预览图片,支持设置 Preview 组件所有参数 + */ +export const Preview = () => { + return ( + <> +

Preview

+
+ +
+ + ) +} diff --git a/packages/ui/image/tsconfig.json b/packages/ui/image/tsconfig.json new file mode 100644 index 000000000..f7bbdb2fe --- /dev/null +++ b/packages/ui/image/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["./src"] +}