Ver Fonte

Fix/image portal (#2011)

* fix: Image /ImagePreview does not create a preview pop-up layer by default

* fix: Fix the problem that closeable API does not take effect

* feat: ImagePreview add previewCls and previewStyle API

* fix: Add the onClick API type declaration of the Image component

* docs: ImagePreview add previewCls & previewStyle API description, Image add onClick API description
YyumeiZhang há 1 ano atrás
pai
commit
f755977862

+ 3 - 0
content/show/image/index-en-US.md

@@ -470,6 +470,7 @@ import { Image, ImagePreview } from '@douyinfe/semi-ui';
 | crossOrigin      | Passthrough to the crossorigin of the native img tag | 'anonymous' \| 'use-credentials' |-| |
 | fallback         | Custom loading failed display content | ReactNode  | - | |
 | height           | Image display height                 | number            | - | |
+| onClick          | Click callback on image              | (event: Event) => void | - | |
 | onError          | Load error callback                  | (event: Event) => void | - | |
 | onLoad           | Load success callback                | (event: Event) => void | - | |
 | placeholder      | Placeholder content when the image is not loaded | ReactNode | - | |
@@ -511,6 +512,8 @@ import { Image, ImagePreview } from '@douyinfe/semi-ui';
 | preLoad          | Whether to enable preloading                                                                                                                                             | boolean | true | |
 | preLoadGap       | Preloaded step size                                                                                                                                                      | number         | 2 | |
 | previewTitle     | Custom preview title                                                                                                                                                     | ReactNode      | - | |
+| previewCls        | Custom preview style class name                                                                                                                                       | string           | - | |
+| previewStyle        | Custom preview style                                                                                                                                       | object           | - | |
 | prevTip          | Previous operation button prompt                                                                                                                                         | string  | "Previous" | |
 | renderHeader     | Custom render preview top info                                                                                                                                           |(info: reactNode) => ReactNode  | - | |
 | renderPreviewMenu | Custom render preview bottom menu information                                                                                                                            | (props: MenuProps) => ReactNode; | - | |

+ 3 - 0
content/show/image/index.md

@@ -471,6 +471,7 @@ import { Image, ImagePreview } from '@douyinfe/semi-ui';
 | crossOrigin       | 透传给原生 img 标签的 crossorigin         | 'anonymous'|'use-credentials'| - | |
 | fallback          | 加载失败容错地址或者自定义加载失败时的显示内容 | ReactNode  | - | |
 | height            | 图片显示高度                             | number            | - | |
+| onClick           | 点击图片的回调                            | (event: any) => void | - | |
 | onError           | 加载错误回调                              | (event: Event) => void | - | |
 | onLoad            | 加载成功回调                              | (event: Event) => void | - | |
 | placeholder       | 图片未加载时候的占位内容                   | ReactNode         | - | |
@@ -513,6 +514,8 @@ import { Image, ImagePreview } from '@douyinfe/semi-ui';
 | preLoad           | 是否开启预加载                                                                                                                                          | boolean        | true | |
 | preLoadGap        | 预加载的步长                                                                                                                                           | number         | 2 | |
 | previewTitle      | 自定义预览 title                                                                                                                                      | ReactNode      | - | |
+| previewCls        | 自定义预览样式类名                                                                                                                                       | string           | - | |
+| previewStyle        | 自定义预览样式                                                                                                                                       | object           | - | |
 | prevTip           | 上一步操作按钮提示                                                                                                                                        | string         | "上一步" | |
 | renderHeader      | 自定义渲染预览顶部信息                                                                                                                                      | (info: ReactNode) => ReactNode  | - | |
 | renderPreviewMenu | 自定义渲染预览底部菜单信息                                                                                                                                    | (props: MenuProps) => ReactNode;| - | |

+ 13 - 0
cypress/e2e/image.spec.js

@@ -559,4 +559,17 @@ describe('image', () => {
                     });
             });
     });
+
+    // API:previewCls, previewStyle,测试 preview 的 className 和 style 是否生效
+    it.only("previewCls & previewStyle", () => {
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=image--preview-cls-and-preview-style&args=&viewMode=storyi');
+        cy.wait(4000);
+        cy.get('.semi-image-img-preview').eq(0).click();
+        cy.get('.semi-image-preview').eq(0).should('have.class', 'test-preview');
+        cy.get('.semi-image-preview').eq(0).should('have.attr', 'style').should('contain', 'background: lightblue;');
+        cy.get('.semi-image-preview').click();
+        cy.get('.semi-image-img-preview').eq(1).click();
+        cy.get('.semi-image-preview').eq(0).should('have.class', 'test-imagePreview');
+        cy.get('.semi-image-preview').eq(0).should('have.attr', 'style').should('contain', 'background: lightgreen;');
+    });
 });

+ 42 - 1
packages/semi-ui/image/_story/image.stories.jsx

@@ -62,6 +62,7 @@ export const basicImage = () => {
     const [disableDownload, setDisableDownload] = useState(false);
     const [maskClosable, setMaskClosable] = useState(true);
     const [preview, setPreview] = useState(true);
+    const [closable, setClosable] = useState(true);
 
     const itemStyle = { display: 'flex', alignItems: 'center', flexShrink: 0, width: 'fit-content', margin: '10px 20px 0 0' };
     const menuStyle = { marginBottom: 20, display: 'flex', flexWrap: 'wrap' };
@@ -81,6 +82,10 @@ export const basicImage = () => {
                 <span >是否禁用下载:</span>
                 <Switch checked={disableDownload} checkedText="是" uncheckedText="否" onChange={setDisableDownload}/>
             </div>
+            <div style={itemStyle} id='closable'>
+                <span>是否显示预览关闭按钮:</span>
+                <Switch checked={closable} checkedText="是" uncheckedText="否" onChange={setClosable} />
+            </div>
             <div style={itemStyle} id='maskClosable'>
                 <span >点击遮罩层是否关闭预览:</span>
                 <Switch checked={maskClosable} checkedText="是" uncheckedText="否" onChange={setMaskClosable}/>
@@ -93,7 +98,8 @@ export const basicImage = () => {
             preview={preview ? {
                 closeOnEsc: escOut,
                 disableDownload,
-                maskClosable
+                maskClosable,
+                closable
             } : false}
         />
     </>
@@ -728,4 +734,39 @@ export const SmallHeightImage = () => {
             src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg"
         />
     </>
+}
+
+export const previewClsAndPreviewStyle = () => {
+   return <>
+        <span>1.previewCls为 test-preview, previewStyle 的 background 为 lightblue </span>
+        <br />
+        <Image 
+            width={360}
+            height={200}
+            src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg"
+            preview={{
+                previewCls: 'test-preview',
+                previewStyle: { background: 'lightblue' }
+            }}
+        />
+        <br />
+        <span>2.previewCls为 test-imagePreview, previewStyle 的 background 为 lightgreen </span>
+        <br />
+        <ImagePreview
+            previewCls='test-imagePreview'
+            previewStyle={{ background: 'lightgreen' }}
+        >
+            {srcList1.map((src, index) => {
+                return (
+                    <Image 
+                        key={index} 
+                        src={src} 
+                        width={200} 
+                        alt={`lamp${index + 1}`} 
+                        style={{ marginRight: 5 }}
+                    />
+                );
+            })}
+        </ImagePreview> 
+    </>
 }

+ 1 - 0
packages/semi-ui/image/_story/image.stories.tsx

@@ -73,6 +73,7 @@ export const BasicPreview = () => {
                             width={200}
                             alt={`lamp${index + 1}`}
                             data-test={'data-test'}
+                            onClick={()=>{}}
                         />
                 )})}
             </ImagePreview>

+ 7 - 2
packages/semi-ui/image/image.tsx

@@ -10,7 +10,7 @@ import { PreviewContext, PreviewContextProps } from "./previewContext";
 import ImageFoundation, { ImageAdapter } from "@douyinfe/semi-foundation/image/imageFoundation";
 import LocaleConsumer from "../locale/localeConsumer";
 import { Locale } from "../locale/interface";
-import { isBoolean, isObject, isUndefined } from "lodash";
+import { isBoolean, isObject, isUndefined, omit } from "lodash";
 import Skeleton from "../skeleton";
 import "@douyinfe/semi-foundation/image/image.scss";
 
@@ -31,6 +31,7 @@ export default class Image extends BaseComponent<ImageProps, ImageStates> {
         preview: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
         onLoad: PropTypes.func,
         onError: PropTypes.func,
+        onClick: PropTypes.func,
         crossOrigin: PropTypes.string,
         imageID: PropTypes.number,
     }
@@ -179,7 +180,11 @@ export default class Image extends BaseComponent<ImageProps, ImageStates> {
         const canPreview = loadStatus === "success" && preview && !this.isInGroup();
         const showPreviewCursor = preview && loadStatus === "success";
         const previewSrc = isObject(preview) ? ((preview as any).src ?? src) : src;
-        const previewProps = isObject(preview) ? preview : {};
+        const previewProps = isObject(preview) && canPreview ? { 
+            ...omit(preview, ['className', 'style', 'previewCls', 'previewStyle']), 
+            className: preview?.previewCls, 
+            style: preview?.previewStyle 
+        }: {} as any;
         return ( 
             <div
                 style={outerStyle}

+ 6 - 0
packages/semi-ui/image/interface.tsx

@@ -21,6 +21,7 @@ export interface ImageProps extends BaseProps{
     preview?: boolean | PreviewProps;
     onError?: (event: Event) => void;
     onLoad?: (event: Event) => void;
+    onClick?: (event: any) => void;
     crossOrigin?: "anonymous"| "use-credentials";
     children?: ReactNode;
     imageID?: number;
@@ -59,6 +60,8 @@ export interface PreviewProps extends BaseProps {
     crossOrigin?: "anonymous"| "use-credentials";
     maxZoom?: number;
     minZoom?: number;
+    previewCls?: string;
+    previewStyle?: React.CSSProperties;
     renderHeader?: (info: any) => ReactNode;
     renderPreviewMenu?: (props: MenuProps) => ReactNode;
     getPopupContainer?: () => HTMLElement;
@@ -75,6 +78,8 @@ export interface PreviewProps extends BaseProps {
     setDownloadName?: (src: string) => string
 }
 
+export interface PreviewInnerProps extends Omit<PreviewProps, "previewCls" | "previewStyle"> {}
+
 export interface MenuProps {
     min?: number;
     max?: number;
@@ -116,6 +121,7 @@ export interface SliderProps {
 }
 
 export interface HeaderProps {
+    closable: boolean;
     renderHeader?: (info: any) => ReactNode;
     title?: string;
     titleStyle?: React.CSSProperties;

+ 10 - 1
packages/semi-ui/image/preview.tsx

@@ -10,6 +10,7 @@ import { cssClasses } from "@douyinfe/semi-foundation/image/constants";
 import { isObject, isEqual } from "lodash";
 import "@douyinfe/semi-foundation/image/image.scss";
 import cls from "classnames";
+import { omit } from "lodash";
 
 const prefixCls = cssClasses.PREFIX;
 
@@ -39,6 +40,8 @@ export default class Preview extends BaseComponent<PreviewProps, PreviewState> {
         lazyLoadMargin: PropTypes.string,
         preLoad: PropTypes.bool,
         preLoadGap: PropTypes.number,
+        previewCls: PropTypes.string,
+        previewStyle: PropTypes.object,
         disableDownload: PropTypes.bool,
         zIndex: PropTypes.number,
         renderHeader: PropTypes.func,
@@ -60,6 +63,7 @@ export default class Preview extends BaseComponent<PreviewProps, PreviewState> {
         src: [],
         lazyLoad: true,
         lazyLoadMargin: "0px 100px 100px 0px",
+        closable: true
     };
 
     get adapter() {
@@ -193,6 +197,11 @@ export default class Preview extends BaseComponent<PreviewProps, PreviewState> {
 
     render() {
         const { src, className, style, lazyLoad, setDownloadName, ...restProps } = this.props;
+        const previewInnerProps = { 
+            ...omit(restProps, ['previewCls', 'previewStyle']), 
+            className: restProps?.previewCls, 
+            style: restProps?.previewStyle 
+        };
         const { currentIndex, visible } = this.state;
         const { srcListInChildren, newChildren, titles } = this.loopImageIndex();
         const srcArr = Array.isArray(src) ? src : (typeof src === "string" ? [src] : []);
@@ -216,7 +225,7 @@ export default class Preview extends BaseComponent<PreviewProps, PreviewState> {
                     {newChildren}
                 </div>
                 <PreviewInner
-                    {...restProps}
+                    {...previewInnerProps}
                     ref={this.previewRef}
                     src={finalSrcList}
                     currentIndex={currentIndex}

+ 3 - 3
packages/semi-ui/image/previewHeader.tsx

@@ -7,7 +7,7 @@ import { PreviewContext } from "./previewContext";
 
 const prefixCls = `${cssClasses.PREFIX}-preview-header`;
 
-const Header = forwardRef(({ onClose, titleStyle, className, renderHeader }: HeaderProps, ref: React.LegacyRef<HTMLElement>) => (
+const Header = forwardRef(({ onClose, titleStyle, className, renderHeader, closable }: HeaderProps, ref: React.LegacyRef<HTMLElement>) => (
     <PreviewContext.Consumer>
         {({ currentIndex, titles }) => {
             let title;
@@ -18,9 +18,9 @@ const Header = forwardRef(({ onClose, titleStyle, className, renderHeader }: Hea
                 <section ref={ref} className={cls(prefixCls, className)}>
                     <section className={`${prefixCls}-title`} style={titleStyle}>{renderHeader ? renderHeader(title) : title}</section>
                     {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
-                    <section className={`${prefixCls}-close`} onMouseUp={onClose}>
+                    {closable && <section className={`${prefixCls}-close`} onMouseUp={onClose}>
                         <IconClose />
-                    </section>
+                    </section>}
                 </section>
             );
         }}

+ 76 - 76
packages/semi-ui/image/previewInner.tsx

@@ -1,6 +1,6 @@
 import React, { CSSProperties } from "react";
 import BaseComponent from "../_base/baseComponent";
-import { PreviewProps as PreviewInnerProps, PreviewInnerStates } from "./interface";
+import { PreviewInnerProps, PreviewInnerStates } from "./interface";
 import PropTypes from "prop-types";
 import { cssClasses } from "@douyinfe/semi-foundation/image/constants";
 import cls from "classnames";
@@ -342,6 +342,7 @@ export default class PreviewInner extends BaseComponent<PreviewInnerProps, Previ
     render() {
         const {
             getPopupContainer,
+            closable,
             zIndex,
             visible,
             className,
@@ -389,83 +390,82 @@ export default class PreviewInner extends BaseComponent<PreviewInnerProps, Previ
         const showPrev = total !== 1 && (infinite || currentIndex !== 0);
         const showNext = total !== 1 && (infinite || currentIndex !== total - 1);
         return (
-            <Portal
+            visible && <Portal
                 getPopupContainer={getPopupContainer}
                 style={wrapperStyle}
-            >
-                {visible &&
-                    // eslint-disable-next-line jsx-a11y/mouse-events-have-key-events,jsx-a11y/no-static-element-interactions
-                    <div
-                        className={previewWrapperCls}
-                        style={style}
-                        onMouseDown={this.handleMouseDown}
-                        onMouseUp={this.handleMouseUp}
-                        ref={this.registryImageWrapRef}
-                        onMouseMove={this.handleMouseMove}
-                    >
-                        <Header ref={this.headerRef} className={cls(hideViewerCls)} onClose={this.handlePreviewClose} renderHeader={renderHeader} />
-                        <PreviewImage
-                            src={imgSrc[currentIndex]}
-                            onZoom={this.handleZoomImage}
-                            disableDownload={disableDownload}
-                            setRatio={this.handleAdjustRatio}
-                            zoom={zoom}
-                            ratio={ratio}
-                            rotation={rotation}
-                            crossOrigin={crossOrigin}
-                            onError={this.onImageError}
-                            onLoad={this.onImageLoad}
-                        />
-                        {showPrev && (
-                            // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
-                            <div
-                                ref={this.leftIconRef}
-                                className={cls(`${previewPrefixCls}-icon`, `${previewPrefixCls}-prev`, hideViewerCls)}
-                                onClick={(): void => this.handleSwitchImage("prev")}
-                            >
-                                <IconArrowLeft size="large" />
-                            </div>
-                        )}
-                        {showNext && (
-                            // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
-                            <div
-                                ref={this.rightIconRef}
-                                className={cls(`${previewPrefixCls}-icon`, `${previewPrefixCls}-next`, hideViewerCls)}
-                                onClick={(): void => this.handleSwitchImage("next")}
-                            >
-                                <IconArrowRight size="large" />
-                            </div>
-                        )}
-                        <Footer
-                            forwardRef={this.footerRef}
-                            className={hideViewerCls}
-                            totalNum={total}
-                            curPage={currentIndex + 1}
-                            disabledPrev={!showPrev}
-                            disabledNext={!showNext}
-                            zoom={zoom * 100}
-                            step={zoomStep * 100}
-                            showTooltip={showTooltip}
-                            ratio={ratio}
-                            prevTip={prevTip}
-                            nextTip={nextTip}
-                            zoomInTip={zoomInTip}
-                            zoomOutTip={zoomOutTip}
-                            rotateTip={rotateTip}
-                            downloadTip={downloadTip}
-                            disableDownload={disableDownload}
-                            adaptiveTip={adaptiveTip}
-                            originTip={originTip}
-                            onPrev={(): void => this.handleSwitchImage("prev")}
-                            onNext={(): void => this.handleSwitchImage("next")}
-                            onZoomIn={this.handleZoomImage}
-                            onZoomOut={this.handleZoomImage}
-                            onDownload={this.handleDownload}
-                            onRotate={this.handleRotateImage}
-                            onAdjustRatio={this.handleAdjustRatio}
-                            renderPreviewMenu={renderPreviewMenu}
-                        />
-                    </div>}
+            >  
+                {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
+                <div
+                    className={previewWrapperCls}
+                    style={style}
+                    onMouseDown={this.handleMouseDown}
+                    onMouseUp={this.handleMouseUp}
+                    ref={this.registryImageWrapRef}
+                    onMouseMove={this.handleMouseMove}
+                >
+                    <Header ref={this.headerRef} className={cls(hideViewerCls)} onClose={this.handlePreviewClose} renderHeader={renderHeader} closable={closable}/>
+                    <PreviewImage
+                        src={imgSrc[currentIndex]}
+                        onZoom={this.handleZoomImage}
+                        disableDownload={disableDownload}
+                        setRatio={this.handleAdjustRatio}
+                        zoom={zoom}
+                        ratio={ratio}
+                        rotation={rotation}
+                        crossOrigin={crossOrigin}
+                        onError={this.onImageError}
+                        onLoad={this.onImageLoad}
+                    />
+                    {showPrev && (
+                        // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
+                        <div
+                            ref={this.leftIconRef}
+                            className={cls(`${previewPrefixCls}-icon`, `${previewPrefixCls}-prev`, hideViewerCls)}
+                            onClick={(): void => this.handleSwitchImage("prev")}
+                        >
+                            <IconArrowLeft size="large" />
+                        </div>
+                    )}
+                    {showNext && (
+                        // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
+                        <div
+                            ref={this.rightIconRef}
+                            className={cls(`${previewPrefixCls}-icon`, `${previewPrefixCls}-next`, hideViewerCls)}
+                            onClick={(): void => this.handleSwitchImage("next")}
+                        >
+                            <IconArrowRight size="large" />
+                        </div>
+                    )}
+                    <Footer
+                        forwardRef={this.footerRef}
+                        className={hideViewerCls}
+                        totalNum={total}
+                        curPage={currentIndex + 1}
+                        disabledPrev={!showPrev}
+                        disabledNext={!showNext}
+                        zoom={zoom * 100}
+                        step={zoomStep * 100}
+                        showTooltip={showTooltip}
+                        ratio={ratio}
+                        prevTip={prevTip}
+                        nextTip={nextTip}
+                        zoomInTip={zoomInTip}
+                        zoomOutTip={zoomOutTip}
+                        rotateTip={rotateTip}
+                        downloadTip={downloadTip}
+                        disableDownload={disableDownload}
+                        adaptiveTip={adaptiveTip}
+                        originTip={originTip}
+                        onPrev={(): void => this.handleSwitchImage("prev")}
+                        onNext={(): void => this.handleSwitchImage("next")}
+                        onZoomIn={this.handleZoomImage}
+                        onZoomOut={this.handleZoomImage}
+                        onDownload={this.handleDownload}
+                        onRotate={this.handleRotateImage}
+                        onAdjustRatio={this.handleAdjustRatio}
+                        renderPreviewMenu={renderPreviewMenu}
+                    />
+                </div>
             </Portal>
         );
     }