Skip to content

Commit

Permalink
Merge branch 'develop' into feature/popup
Browse files Browse the repository at this point in the history
  • Loading branch information
anlyyao committed Aug 21, 2024
2 parents 32815f5 + 7e07f83 commit 30b5ae1
Show file tree
Hide file tree
Showing 74 changed files with 5,829 additions and 849 deletions.
7 changes: 6 additions & 1 deletion site/mobile/mobile.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default {
{
title: 'Image 图片',
name: 'image',
component: () => import('tdesign-mobile-react/image/_example/index.jsx'),
component: () => import('tdesign-mobile-react/image/_example/index.tsx'),
},
{
title: 'Overlay 遮罩层',
Expand Down Expand Up @@ -222,5 +222,10 @@ export default {
name: 'result',
component: () => import('tdesign-mobile-react/result/_example/index.tsx'),
},
{
title: 'Link 链接',
name: 'link',
component: () => import('tdesign-mobile-react/link/_example/index.tsx'),
},
],
};
7 changes: 6 additions & 1 deletion site/web/site.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,18 @@ export default {
path: '/mobile-react/components/fab',
component: () => import('tdesign-mobile-react/fab/fab.md'),
},

{
title: 'Icon 图标',
name: 'icon',
path: '/mobile-react/components/icon',
component: () => import('tdesign-mobile-react/icon/icon.md'),
},
{
title: 'Link 链接',
name: 'link',
path: '/mobile-react/components/link',
component: () => import('tdesign-mobile-react/link/link.md'),
},
],
},
{
Expand Down
174 changes: 106 additions & 68 deletions src/image/Image.tsx
Original file line number Diff line number Diff line change
@@ -1,94 +1,132 @@
import React, { FC, useCallback, useMemo, useRef, useState } from 'react';
import ClassNames from 'classnames';
import { CloseIcon, EllipsisIcon } from 'tdesign-icons-react';
import { useInViewport } from 'ahooks';
import useConfig from '../_util/useConfig';
import React, { Fragment, useRef, useMemo, useState, useEffect } from 'react';
import type { SyntheticEvent } from 'react';
import classNames from 'classnames';
import { CloseIcon as TdCloseIcon } from 'tdesign-icons-react';
import Loading from '../loading';
import observe from '../_common/js/utils/observe';
import { StyledProps } from '../common';
import { TdImageProps } from './type';
import { imageDefaultProps } from './defaultProps';
import parseTNode from '../_util/parseTNode';
import useDefaultProps from '../hooks/useDefaultProps';
import { usePrefixClass } from '../hooks/useClass';

export interface ImageProps extends TdImageProps, Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'loading' | 'onError' | 'onLoad'> {}
export interface ImageProps extends TdImageProps, StyledProps {}

const Image: FC<ImageProps> = React.memo((props) => {
const Image: React.FC<ImageProps> = (props) => {
const {
className,
style,
src,
alt,
fit = 'fill',
onLoad,
loading,
onError,
error,
fallback,
fit,
lazy,
shape = 'round',
loading,
shape,
position,
error,
className,
style,
...others
} = props;

const [isLoad, setIsLoad] = useState(true);
const [isError, setIsError] = useState(false);
const ref = useRef<HTMLImageElement>(null);
const hasLoad = useRef<boolean>(false);
referrerpolicy,
srcset,
onError,
onLoad,
} = useDefaultProps<ImageProps>(props, imageDefaultProps);

// 统一配置信息
const { classPrefix } = useConfig();
const imageClass = usePrefixClass('image');
const imageRef = useRef<HTMLDivElement>(null);

// 观察元素是否在可见区域
const [isInViewport] = useInViewport(ref);
const [imageSrc, setImageSrc] = useState(src);
const [isLoaded, setIsLoaded] = useState(false);
const [isError, setIsError] = useState(false);
const [shouldLoad, setShouldLoad] = useState(!lazy);

const prefix = useMemo(() => `${classPrefix}-image`, [classPrefix]);
const rootClasses = classNames(
{
[`${imageClass}`]: true,
[`${imageClass}--${shape}`]: true,
},
className,
);

// Loading Element
const LoadingStatus = useMemo(() => {
if (!isLoad) return null;

return loading || <EllipsisIcon />;
}, [isLoad, loading]);
if (!(isError || !shouldLoad) && !isLoaded) return loading || <Loading theme="dots" inheritColor={true} />;
}, [isError, shouldLoad, isLoaded, loading]);

// Loading Failed Element
const FailedStatus = useMemo(() => {
if (!isError) return null;

return error || <CloseIcon />;
if (isError) return error || <TdCloseIcon size="22px" />;
}, [isError, error]);

// Image Src
const imgSrc = useMemo(() => {
if (!lazy || hasLoad.current) return src;

if (isInViewport) return src;

return '';
}, [src, lazy, isInViewport]);
const handleLoadImage = () => {
setShouldLoad(true);
};

useEffect(() => {
if (!lazy || !imageRef?.current) {
return;
}

const handleUnObserve = (element) => {
const observer = observe(element, null, handleLoadImage, 0);
return () => {
observer && observer.unobserve(element);
};
};

return handleUnObserve(imageRef.current);
}, [lazy, imageRef]);

// 图片加载完成回调
const handleLoad = (e: SyntheticEvent<HTMLImageElement>) => {
setIsLoaded(true);
onLoad?.({ e });
};

// 图片加载失败回调
const handleError = (e: SyntheticEvent<HTMLImageElement>) => {
setIsError(true);
if (fallback) {
setImageSrc(fallback);
}
onError?.({ e });
};

const renderMask = () =>
LoadingStatus || FailedStatus ? (
<div className={`${imageClass}__mask`}>{parseTNode(LoadingStatus || FailedStatus)}</div>
) : null;

const renderImage = () => (
<img
className={classNames(`${imageClass}__img`, `${imageClass}--fit-${fit}`, `${imageClass}--position-${position}`)}
src={imageSrc}
alt={alt}
referrerPolicy={referrerpolicy}
onLoad={handleLoad}
onError={handleError}
/>
);

// Get ClassName By Prefix
const getClass = useCallback((cls: string) => `${prefix}__${cls}`, [prefix]);
const renderImageSrcset = () => (
<picture>
{Object.entries(srcset).map(([type, url]) => (
<source key={url} type={type} srcSet={url} />
))}
{src && renderImage()}
</picture>
);

return (
<div className={ClassNames(prefix, `${prefix}--${shape}`, { [className]: className })} ref={ref}>
{LoadingStatus || FailedStatus ? <div className={getClass('status')}>{LoadingStatus || FailedStatus}</div> : null}
<img
{...others}
className={getClass('img')}
src={imgSrc}
alt={alt}
style={{ objectFit: fit, objectPosition: position, width: 'inherit', height: 'inherit', ...style }}
onLoad={() => {
hasLoad.current = true;
setIsLoad(false);
setIsError(false);
onLoad && onLoad();
}}
onError={() => {
if (imgSrc) {
hasLoad.current = true;
setIsLoad(false);
setIsError(true);
onError && onError();
}
}}
/>
<div ref={imageRef} className={rootClasses} style={style}>
{renderMask()}
{!isError && shouldLoad && (
<Fragment>{srcset && Object.keys(srcset).length ? renderImageSrcset() : renderImage()}</Fragment>
)}
</div>
);
});
};

Image.displayName = 'Image';

export default Image;
60 changes: 60 additions & 0 deletions src/image/_example/base.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import { Image } from 'tdesign-mobile-react';

const imageSrc = 'https://tdesign.gtimg.com/demo/demo-image-1.png';

export default function BaseImage() {
return (
<div className="image-example">
<div className="image-example-title">不同填充模式的图片</div>
<div className="image-example-desc">提供 fill、contain、cover、none、scale-down 5 种填充类型。</div>
<div className="image-group">
<div className="image-demo">
<p className="image-demo-tip">fill</p>
<Image
className="image-container"
style={{ width: '72px', height: '72px' }}
fit="fill"
src={imageSrc}
></Image>
</div>
<div className="image-demo">
<p className="image-demo-tip">contain</p>
<Image
className="image-container"
style={{ width: '72px', height: '72px' }}
fit="contain"
src={imageSrc}
></Image>
</div>
<div className="image-demo">
<p className="image-demo-tip">cover</p>
<Image
className="image-container"
style={{ width: '72px', height: '72px' }}
fit="cover"
src={imageSrc}
></Image>
</div>
<div className="image-demo">
<p className="image-demo-tip">none</p>
<Image
className="image-container"
style={{ width: '72px', height: '72px' }}
fit="none"
src={imageSrc}
></Image>
</div>
<div className="image-demo">
<p className="image-demo-tip">scale-down</p>
<Image
className="image-container"
style={{ width: '72px', height: '72px' }}
fit="scale-down"
src={imageSrc}
></Image>
</div>
</div>
</div>
);
}
24 changes: 0 additions & 24 deletions src/image/_example/crop.jsx

This file was deleted.

33 changes: 0 additions & 33 deletions src/image/_example/index.jsx

This file was deleted.

23 changes: 23 additions & 0 deletions src/image/_example/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import TDemoBlock from '../../../site/mobile/components/DemoBlock';
import TDemoHeader from '../../../site/mobile/components/DemoHeader';
import BaseImage from './base';
import PositionImage from './position';
import ShapeImage from './shape';
import StatusImage from './status';

export default function ImageDemo() {
return (
<>
<TDemoHeader title="Image 图片" summary="用于展示图片素材" />
<TDemoBlock title="01 组件类型" padding={true}>
<BaseImage />
<PositionImage />
<ShapeImage />
</TDemoBlock>
<TDemoBlock title="02 组件状态">
<StatusImage />
</TDemoBlock>
</>
);
}
Loading

0 comments on commit 30b5ae1

Please sign in to comment.