Skip to content

Commit

Permalink
feat: PreviewGroup support fallback (#266)
Browse files Browse the repository at this point in the history
* feat: PreviewGroup support fallback

* fix: optimize code
  • Loading branch information
linxianxi authored Jun 29, 2023
1 parent 61eccf2 commit 1bb2309
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 52 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ export default () => (
| preview | boolean \| [PreviewGroupType](#PreviewGroupType) | true | Whether to show preview, <br> current: If Preview the show img index, default 0 |
| previewPrefixCls | string | rc-image-preview | Preview classname prefix |
| icons | { [iconKey]?: ReactNode } | - | Icons in the top operation bar, iconKey: 'rotateLeft' \| 'rotateRight' \| 'zoomIn' \| 'zoomOut' \| 'close' \| 'left' \| 'right' |
| fallback | string | - | Load failed src |
| items | (string \| { src: string, alt: string, crossOrigin: string, ... })[] | - | preview group |

### PreviewGroupType

Expand All @@ -125,8 +127,7 @@ export default () => (
| maxScale | number | 50 | Max scale |
| forceRender | boolean | - | Force render preview |
| getContainer | string \| HTMLElement \| (() => HTMLElement) \| false | document.body | Return the mount node for preview |
| items | (string \| { src: string, alt: string, crossOrigin: string, ... })[] | - | preview group |
| countRender | (current: number, total: number) => React.ReactNode | - | Customize count |
| countRender | (current: number, total: number) => ReactNode | - | Customize count |
| imageRender | (originalNode: React.ReactNode, info: { transform: [TransformType](#TransformType), current: number }) => React.ReactNode | - | Customize image |
| toolbarRender | (originalNode: React.ReactNode, info: [ToolbarRenderInfoType](#ToolbarRenderInfoType)) => React.ReactNode | - | Customize toolbar |
| onVisibleChange | (visible: boolean, prevVisible: boolean, current: number) => void | - | Callback when visible is changed |
Expand Down
55 changes: 11 additions & 44 deletions src/Image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import { getOffset } from 'rc-util/lib/Dom/css';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import type { GetContainer } from 'rc-util/lib/PortalWrapper';
import * as React from 'react';
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useContext, useMemo, useState } from 'react';
import { COMMON_PROPS } from './common';
import { PreviewGroupContext } from './context';
import type { TransformType } from './hooks/useImageTransform';
import useRegisterImage from './hooks/useRegisterImage';
import useStatus from './hooks/useStatus';
import type { ImageElementProps } from './interface';
import type { PreviewProps, ToolbarRenderInfoType } from './Preview';
import Preview from './Preview';
import PreviewGroup from './PreviewGroup';
import { isImageValid } from './util';

export interface ImagePreviewType
extends Omit<
Expand Down Expand Up @@ -65,8 +65,6 @@ interface CompoundedComponent<P> extends React.FC<P> {
PreviewGroup: typeof PreviewGroup;
}

type ImageStatus = 'normal' | 'error' | 'loading';

const ImageInternal: CompoundedComponent<ImageProps> = props => {
const {
src: imgSrc,
Expand Down Expand Up @@ -111,58 +109,26 @@ const ImageInternal: CompoundedComponent<ImageProps> = props => {
value: previewVisible,
onChange: onPreviewVisibleChange,
});
const [status, setStatus] = useState<ImageStatus>(isCustomPlaceholder ? 'loading' : 'normal');
const [getImgRef, srcAndOnload, status] = useStatus({
src: imgSrc,
isCustomPlaceholder,
fallback,
});
const [mousePosition, setMousePosition] = useState<null | { x: number; y: number }>(null);
const isError = status === 'error';

const groupContext = useContext(PreviewGroupContext);

const canPreview = !!preview;

const isLoaded = useRef(false);

const onLoad = () => {
setStatus('normal');
};

const onPreviewClose = () => {
setShowPreview(false);
setMousePosition(null);
};

const getImgRef = (img?: HTMLImageElement) => {
isLoaded.current = false;
if (status !== 'loading') return;
if (img?.complete && (img.naturalWidth || img.naturalHeight)) {
isLoaded.current = true;
onLoad();
}
};

// https://github.com/react-component/image/pull/187
useEffect(() => {
isImageValid(imgSrc).then(isValid => {
if (!isValid) {
setStatus('error');
}
});
}, [imgSrc]);

useEffect(() => {
if (isError) {
setStatus('normal');
}
if (isCustomPlaceholder && !isLoaded.current) {
setStatus('loading');
}
}, [imgSrc]);

const wrapperClass = cn(prefixCls, wrapperClassName, rootClassName, {
[`${prefixCls}-error`]: isError,
[`${prefixCls}-error`]: status === 'error',
});

const mergedSrc = isError && fallback ? fallback : src;

// ========================= ImageProps =========================
const imgCommonProps = useMemo(
() => {
Expand Down Expand Up @@ -232,7 +198,7 @@ const ImageInternal: CompoundedComponent<ImageProps> = props => {
...style,
}}
ref={getImgRef}
{...(isError && fallback ? { src: fallback } : { onLoad, src: imgSrc })}
{...srcAndOnload}
width={width}
height={height}
onError={onError}
Expand Down Expand Up @@ -263,8 +229,9 @@ const ImageInternal: CompoundedComponent<ImageProps> = props => {
prefixCls={previewPrefixCls}
onClose={onPreviewClose}
mousePosition={mousePosition}
src={mergedSrc}
src={src}
alt={alt}
fallback={fallback}
getContainer={getPreviewContainer}
icons={icons}
scaleStep={scaleStep}
Expand Down
39 changes: 33 additions & 6 deletions src/Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { PreviewGroupContext } from './context';
import getFixScaleEleTransPosition from './getFixScaleEleTransPosition';
import type { TransformAction, TransformType } from './hooks/useImageTransform';
import useImageTransform from './hooks/useImageTransform';
import useStatus from './hooks/useStatus';
import Operations from './Operations';
import { BASE_SCALE_RATIO, WHEEL_MAX_SCALE_RATIO } from './previewConfig';

Expand Down Expand Up @@ -38,6 +39,7 @@ export interface PreviewProps extends Omit<IDialogPropTypes, 'onClose'> {
imgCommonProps?: React.ImgHTMLAttributes<HTMLImageElement>;
src?: string;
alt?: string;
fallback?: string;
rootClassName?: string;
icons?: {
rotateLeft?: React.ReactNode;
Expand Down Expand Up @@ -67,11 +69,35 @@ export interface PreviewProps extends Omit<IDialogPropTypes, 'onClose'> {
onChange?: (current, prev) => void;
}

interface PreviewImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
fallback?: string;
imgRef: React.MutableRefObject<HTMLImageElement>;
}

const PreviewImage: React.FC<PreviewImageProps> = ({ fallback, src, imgRef, ...props }) => {
const [getImgRef, srcAndOnload] = useStatus({
src,
fallback,
});

return (
<img
ref={ref => {
imgRef.current = ref;
getImgRef(ref);
}}
{...props}
{...srcAndOnload}
/>
);
};

const Preview: React.FC<PreviewProps> = props => {
const {
prefixCls,
src,
alt,
fallback,
onClose,
visible,
icons = {},
Expand Down Expand Up @@ -304,23 +330,24 @@ const Preview: React.FC<PreviewProps> = props => {
}, [visible, showLeftOrRightSwitches, current]);

const imgNode = (
<img
<PreviewImage
{...imgCommonProps}
width={props.width}
height={props.height}
onWheel={onWheel}
onMouseDown={onMouseDown}
onDoubleClick={onDoubleClick}
ref={imgRef}
imgRef={imgRef}
className={`${prefixCls}-img`}
src={src}
alt={alt}
style={{
transform: `translate3d(${transform.x}px, ${transform.y}px, 0) scale3d(${
transform.flipX ? '-' : ''
}${scale}, ${transform.flipY ? '-' : ''}${scale}, 1) rotate(${rotate}deg)`,
transitionDuration: !enableTransition && '0s',
}}
fallback={fallback}
src={src}
onWheel={onWheel}
onMouseDown={onMouseDown}
onDoubleClick={onDoubleClick}
/>
);

Expand Down
3 changes: 3 additions & 0 deletions src/PreviewGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface GroupConsumerProps {
previewPrefixCls?: string;
icons?: PreviewProps['icons'];
items?: (string | ImageElementProps)[];
fallback?: string;
preview?: boolean | PreviewGroupPreview;
children?: React.ReactNode;
}
Expand All @@ -43,6 +44,7 @@ const Group: React.FC<GroupConsumerProps> = ({
icons = {},
items,
preview,
fallback,
}) => {
const {
visible: previewVisible,
Expand Down Expand Up @@ -138,6 +140,7 @@ const Group: React.FC<GroupConsumerProps> = ({
mousePosition={mousePosition}
imgCommonProps={imgCommonProps}
src={src}
fallback={fallback}
icons={icons}
minScale={minScale}
maxScale={maxScale}
Expand Down
55 changes: 55 additions & 0 deletions src/hooks/useStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useEffect, useRef, useState } from 'react';
import { isImageValid } from '../util';

type ImageStatus = 'normal' | 'error' | 'loading';

export default function useStatus({
src,
isCustomPlaceholder,
fallback,
}: {
src: string;
isCustomPlaceholder?: boolean;
fallback?: string;
}) {
const [status, setStatus] = useState<ImageStatus>(isCustomPlaceholder ? 'loading' : 'normal');
const isLoaded = useRef(false);

const isError = status === 'error';

// https://github.com/react-component/image/pull/187
useEffect(() => {
isImageValid(src).then(isValid => {
if (!isValid) {
setStatus('error');
}
});
}, [src]);

useEffect(() => {
if (isCustomPlaceholder && !isLoaded.current) {
setStatus('loading');
} else if (isError) {
setStatus('normal');
}
}, [src]);

const onLoad = () => {
setStatus('normal');
};

const getImgRef = (img?: HTMLImageElement) => {
isLoaded.current = false;
if (status !== 'loading') {
return;
}
if (img?.complete && (img.naturalWidth || img.naturalHeight)) {
isLoaded.current = true;
onLoad();
}
};

const srcAndOnload = isError && fallback ? { src: fallback } : { onLoad, src };

return [getImgRef, srcAndOnload, status] as const;
}
17 changes: 17 additions & 0 deletions tests/fallback.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,23 @@ describe('Fallback', () => {
expect(container.querySelector('img').src).toEqual(fallback);
});

it('PreviewGroup Fallback correct', async () => {
const { container } = render(
<Image.PreviewGroup fallback={fallback}>
<Image src="abc" />
</Image.PreviewGroup>,
);

fireEvent.click(container.querySelector('.rc-image-img'));

await act(async () => {
jest.runAllTimers();
await Promise.resolve();
});

expect(document.querySelector('.rc-image-preview-img')).toHaveAttribute('src', fallback);
});

it('should not show preview', () => {
const { container } = render(<Image src="abc" fallback={fallback} />);

Expand Down

1 comment on commit 1bb2309

@vercel
Copy link

@vercel vercel bot commented on 1bb2309 Jun 29, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.