Selaa lähdekoodia

feat: modal add modalRender prop (#2779)

* feat: modal add modalRender prop

* chore: Emphasis on parameters

---------

Co-authored-by: zhangyumei.0319 <[email protected]>
LonelySnowman 3 kuukautta sitten
vanhempi
sitoutus
974f969eba

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 465 - 433
content/show/modal/index-en-US.md


+ 36 - 5
content/show/modal/index.md

@@ -611,6 +611,35 @@ function Demo(props = {}) {
 }
 ```
 
+### 可拖拽 Modal
+
+通过 `modalRender` 自定义渲染 Modal 内容,可拖拽 Modal 通过 DragMove 组件实现。
+
+```jsx live=true hideInDSM
+import React, { useState } from 'react';
+import { ConfigProvider, Button, Modal, DragMove } from '@douyinfe/semi-ui';
+
+function Demo(props = {}) {
+    const [visible, setVisible] = useState(false);
+    return (
+        <div>
+            <Button onClick={() => setVisible(true)}>Open Modal</Button>
+            <Modal
+                title="可拖拽Modal"
+                visible={visible}
+                onCancel={() => setVisible(false)}
+                modalRender={(modal) => (
+                    <DragMove>{modal}</DragMove>
+                )}
+            >
+                <p>This is the content of a basic sidesheet.</p>
+                <p>Here is more content...</p>
+            </Modal>
+        </div>
+    );
+}
+```
+
 ## API 参考
 
 ### Modal
@@ -641,6 +670,7 @@ function Demo(props = {}) {
 | maskClosable | 是否允许通过点击遮罩来关闭对话框                                                                                          | boolean | true |
 | maskStyle | 遮罩的样式                                                                                                     | CSSProperties | 无 |
 | modalContentClass | 可用于设置对话框内容的样式类名 | string | 无 |
+| modalRender | 自定义渲染 Modal | (modal: ReactNode) => ReactNode | - |
 | motion | 动画效果开关                                                                                                    | boolean | true |
 | okButtonProps | 确认按钮的 props                                                                                               | [ButtonProps](/zh-CN/input/button#API参考) | 无 |
 | okText | 确认按钮的文字                                                                                                   | string | 无 |
@@ -682,6 +712,7 @@ function Demo(props = {}) {
 | maskClosable | 是否允许通过点击遮罩来关闭对话框 | boolean | true |
 | maskStyle | 遮罩的样式 | CSSProperties | 无 |
 | modalContentClass | 可用于设置对话框内容的样式类名 | string | 无 |
+| modalRender | 自定义渲染 Modal | (modal: ReactNode) => ReactNode | - |
 | okButtonProps | 确认按钮的 props | [ButtonProps](/zh-CN/input/button#API参考) | 无 |
 | okText | 确认按钮的文字 | string | 无 |
 | okType | 确认按钮的类型 | string | primary |
@@ -757,12 +788,12 @@ WAI-ARIA: https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/
 ## FAQ
 
 -  #### 为什么使用 LocaleProvider 后, Modal.confirm 确认、取消按钮的文本没有国际化?
-    Modal 使用 Portal 将浮层节点插入到 DOM 树中。但这个操作仅能改变节点在 DOM 树中的位置,无法改变节点在 React 节点树中的位置,LocalProvider是基于 Context 机制传递的,必须是从属的 React 子结点才可消费到 Local 相关 Context。因此命令式的 Modal 的内置文本无法自动适配国际化。
-    你可以通过 `okText` 和 `cancelText` 这两个属性来根据 Locale 重新设置 i18 的文本。   
-    在1.2版本之后,你也可以通过 Modal.useModal 方法来返回 modal 实体以及 contextHolder 节点。将 contextHolder 插入到你需要获取 context 位置,即可使 Modal 获取到对应的 Context,如 ConfigProvider 或者 LocaleProvider 的配置。
+   Modal 使用 Portal 将浮层节点插入到 DOM 树中。但这个操作仅能改变节点在 DOM 树中的位置,无法改变节点在 React 节点树中的位置,LocalProvider是基于 Context 机制传递的,必须是从属的 React 子结点才可消费到 Local 相关 Context。因此命令式的 Modal 的内置文本无法自动适配国际化。
+   你可以通过 `okText` 和 `cancelText` 这两个属性来根据 Locale 重新设置 i18 的文本。   
+   在1.2版本之后,你也可以通过 Modal.useModal 方法来返回 modal 实体以及 contextHolder 节点。将 contextHolder 插入到你需要获取 context 位置,即可使 Modal 获取到对应的 Context,如 ConfigProvider 或者 LocaleProvider 的配置。
 
 -  #### 为什么 title 和 content 的间距在命令式调用和非命令式调用下不同?
-    命令式调用场景下,标题和内容的相关性更强,所以用更近的距离表达这种强相关性,符合预期。用户如果不想要这种效果,可以自己做样式覆盖。
+   命令式调用场景下,标题和内容的相关性更强,所以用更近的距离表达这种强相关性,符合预期。用户如果不想要这种效果,可以自己做样式覆盖。
 
 <!-- ## 相关物料
 ```material
@@ -770,4 +801,4 @@ WAI-ARIA: https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/
 ``` -->
 
 ## 相关物料
-<semi-material-list code="1"></semi-material-list>
+<semi-material-list code="1"></semi-material-list>

+ 2 - 4
packages/semi-foundation/modal/variables.scss

@@ -48,7 +48,7 @@ $spacing-modal_content_fullscreen-top: 0px; // 模态框内容全屏顶部位置
 // Width/Height
 $width-modal_title: 100%; // 模态框标题宽度
 $width-modal_content: 100%; // 模态框内容宽度
-$height-modal_content: 100%; // 模态框内容高度
+$height-modal_content: max-content; // 模态框内容高度
 $width-modal_content-border: 1px; // 模态框内容描边宽度
 $width-modal_small: 448px; // 模态框宽度 - 小
 $width-modal_medium: 684px; // 模态框宽度 - 中
@@ -72,6 +72,4 @@ $width-modal_footer-border:0; // 模态框 footer 顶部描边宽度
 
 
 //shadow
-$shadow-modal_content: var(--semi-shadow-elevated);
-
-
+$shadow-modal_content: var(--semi-shadow-elevated);

+ 4 - 2
packages/semi-ui/modal/Modal.tsx

@@ -35,7 +35,8 @@ export interface ModalReactProps extends ModalProps {
     footer?: ReactNode;
     header?: ReactNode;
     onCancel?: (e: React.MouseEvent) => void | Promise<any>;
-    onOk?: (e: React.MouseEvent) => void | Promise<any>
+    onOk?: (e: React.MouseEvent) => void | Promise<any>;
+    modalRender?: (node: ReactNode) => ReactNode
 }
 
 
@@ -57,6 +58,7 @@ class Modal extends BaseComponent<ModalReactProps, ModalState> {
         maskClosable: PropTypes.bool,
         onCancel: PropTypes.func,
         onOk: PropTypes.func,
+        modalRender: PropTypes.func,
         afterClose: PropTypes.func,
         okButtonProps: PropTypes.object,
         cancelButtonProps: PropTypes.object,
@@ -286,7 +288,7 @@ class Modal extends BaseComponent<ModalReactProps, ModalState> {
                         autoFocus={true}
                         {...this.props.cancelButtonProps}
                         style={{
-                            ...footerFill ? { marginLeft: "unset" }:{},
+                            ...footerFill ? { marginLeft: "unset" } : {},
                             ...this.props.cancelButtonProps?.style
                         }}
                         x-semi-children-alias="cancelText"

+ 20 - 19
packages/semi-ui/modal/ModalContent.tsx

@@ -1,4 +1,4 @@
-import React, { CSSProperties } from 'react';
+import React, { CSSProperties, ReactNode } from 'react';
 import PropTypes from 'prop-types';
 import cls from 'classnames';
 import { cssClasses } from '@douyinfe/semi-foundation/modal/constants';
@@ -19,7 +19,8 @@ let uuid = 0;
 
 
 export interface ModalContentReactProps extends ModalContentProps {
-    children?: React.ReactNode
+    children?: React.ReactNode;
+    modalRender?: (node: ReactNode) => ReactNode
 }
 
 export default class ModalContent extends BaseComponent<ModalContentReactProps, ModalContentState> {
@@ -268,8 +269,22 @@ export default class ModalContent extends BaseComponent<ModalContentReactProps,
                 {props.footer}
             </div>
         ) : null;
+
+        const modalContentElement = (<div
+            role="dialog"
+            ref={this.modalDialogRef}
+            aria-modal="true"
+            aria-labelledby={`${cssClasses.DIALOG}-title`}
+            aria-describedby={`${cssClasses.DIALOG}-body`}
+            onAnimationEnd={props.onAnimationEnd}
+            className={cls([`${cssClasses.DIALOG}-content`,
+                props.contentClassName,
+                { [`${cssClasses.DIALOG}-content-fullScreen`]: props.isFullScreen }])}>
+            {header}
+            {body}
+            {footer}
+        </div>);
         const dialogElement = (
-            // eslint-disable-next-line jsx-a11y/no-static-element-interactions
             <div
                 key="dialog-element"
                 className={digCls}
@@ -277,23 +292,9 @@ export default class ModalContent extends BaseComponent<ModalContentReactProps,
                 style={{ ...props.style, ...style }}
                 id={this.dialogId}
             >
-                <div
-                    role="dialog"
-                    ref={this.modalDialogRef}
-                    aria-modal="true"
-                    aria-labelledby={`${cssClasses.DIALOG}-title`}
-                    aria-describedby={`${cssClasses.DIALOG}-body`}
-                    onAnimationEnd={props.onAnimationEnd}
-                    className={cls([`${cssClasses.DIALOG}-content`,
-                        props.contentClassName,
-                        { [`${cssClasses.DIALOG}-content-fullScreen`]: props.isFullScreen }])}>
-                    {header}
-                    {body}
-                    {footer}
-                </div>
+                {props?.modalRender ? props?.modalRender(modalContentElement) : modalContentElement}
             </div>
-        ); 
-        // return props.visible ? dialogElement : null;
+        );
         return dialogElement;
     };
 

+ 15 - 0
packages/semi-ui/modal/__test__/modal.test.js

@@ -321,4 +321,19 @@ describe('modal', () => {
         expect(modal.state().displayNone).toEqual(true);
         expect(modal.exists(`div.${BASE_CLASS_PREFIX}-modal`)).toEqual(true);
     });
+
+    it('modal render', () => {
+        const testClass = 'modal-render-test'
+        let com = getModal({
+            visible: true,
+            modalRender: (modal) => (
+                <div className={testClass}>
+                    {modal}
+                </div>
+            )
+        });
+        let modal = mount(com, { attachTo: document.getElementById('container') });
+        expect(modal.exists(`div.${testClass}`)).toEqual(true);
+        modal.unmount();
+    });
 })

+ 26 - 1
packages/semi-ui/modal/_story/modal.stories.jsx

@@ -1,7 +1,7 @@
 import React, { useState } from 'react';
 import en_GB from '../../locale/source/en_GB';
 
-import { Select, Modal, Button, Tooltip, Popover, ConfigProvider, Tag, Space } from '../../index';
+import { Select, Modal, Button, Tooltip, Popover, ConfigProvider, Tag, Space, DragMove } from '../../index';
 import CollapsibleInModal from './CollapsibleInModal';
 import DynamicContextDemo from './DynamicContext';
 
@@ -341,3 +341,28 @@ export const UseModalAfterClose = () => {
   );
 };
 UseModalAfterClose.storyName = "useModal afterClose";
+
+export const DraggableModal = () => {
+    const [visible, setVisible] = useState(false);
+    return (
+        <div>
+            <Button onClick={() => setVisible(true)}>Open Modal</Button>
+            <Modal
+                title="可拖拽Modal"
+                visible={visible}
+                onCancel={() => setVisible(false)}
+                modalRender={(modal) => (
+                    <DragMove>{modal}</DragMove>
+                )}
+            >
+                <p>This is the content of a basic sidesheet.</p>
+                <p>Here is more content...</p>
+            </Modal>
+        </div>
+    );
+};
+
+
+DraggableModal.story = {
+    name: 'draggable modal',
+};

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä