diff --git a/package.json b/package.json
index 9b3dbeedb..b7cbee70d 100644
--- a/package.json
+++ b/package.json
@@ -83,6 +83,7 @@
"@types/lodash-es": "^4.17.12",
"@types/react": "^18.0.0",
"@types/react-syntax-highlighter": "~15.5.13",
+ "@types/react-resizable": "^3.0.8",
"@types/shortid": "^0.0.31",
"@types/showdown": "^1.9.0",
"@types/testing-library__jest-dom": "^5.14.5",
@@ -125,6 +126,7 @@
"react-syntax-highlighter": "~15.5.0",
"remark-gfm": "~3.0.1",
"react-draggable": "~4.4.6",
+ "react-resizable": "^3.0.5",
"shortid": "^2.2.16",
"showdown": "^1.9.0"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9f05053e5..d1e2e4d3c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -17,6 +17,7 @@ specifiers:
'@types/jest': ^29.2.3
'@types/lodash-es': ^4.17.12
'@types/react': ^18.0.0
+ '@types/react-resizable': ^3.0.8
'@types/react-syntax-highlighter': ~15.5.13
'@types/shortid': ^0.0.31
'@types/showdown': ^1.9.0
@@ -47,6 +48,7 @@ specifiers:
react-dom: ^18.0.0
react-draggable: ~4.4.6
react-markdown: ~8.0.6
+ react-resizable: ^3.0.5
react-syntax-highlighter: ~15.5.0
react-test-renderer: ^18.2.0
remark-gfm: ~3.0.1
@@ -72,6 +74,7 @@ dependencies:
rc-virtual-list: 3.11.2_react-dom@18.2.0+react@18.2.0
react-draggable: 4.4.6_react-dom@18.2.0+react@18.2.0
react-markdown: 8.0.7_d51bdd6a322172e118eec6adc1172a28
+ react-resizable: 3.0.5_react-dom@18.2.0+react@18.2.0
react-syntax-highlighter: 15.5.0_react@18.2.0
remark-gfm: 3.0.1
shortid: 2.2.16
@@ -88,6 +91,7 @@ devDependencies:
'@types/jest': 29.5.5
'@types/lodash-es': 4.17.12
'@types/react': 18.2.25
+ '@types/react-resizable': 3.0.8
'@types/react-syntax-highlighter': 15.5.13
'@types/shortid': 0.0.31
'@types/showdown': 1.9.4
@@ -2818,6 +2822,12 @@ packages:
'@types/react': 18.2.25
dev: true
+ /@types/react-resizable/3.0.8:
+ resolution: {integrity: sha512-Pcvt2eGA7KNXldt1hkhVhAgZ8hK41m0mp89mFgQi7LAAEZiaLgm4fHJ5zbJZ/4m2LVaAyYrrRRv1LHDcrGQanA==}
+ dependencies:
+ '@types/react': 18.2.25
+ dev: true
+
/@types/react-syntax-highlighter/15.5.13:
resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==}
dependencies:
@@ -2877,20 +2887,20 @@ packages:
/@types/unist/2.0.8:
resolution: {integrity: sha512-d0XxK3YTObnWVp6rZuev3c49+j4Lo8g4L1ZRm9z5L0xpoZycUPshHgczK5gsUMaZOstjVYYi09p5gYvUtfChYw==}
- /@types/yargs-parser/21.0.1:
- resolution: {integrity: sha512-axdPBuLuEJt0c4yI5OZssC19K2Mq1uKdrfZBzuxLvaztgqUtFYZUNw7lETExPYJR9jdEoIg4mb7RQKRQzOkeGQ==}
+ /@types/yargs-parser/21.0.3:
+ resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
dev: true
/@types/yargs/16.0.6:
resolution: {integrity: sha512-oTP7/Q13GSPrgcwEwdlnkoZSQ1Hg9THe644qq8PG6hhJzjZ3qj1JjEFPIwWV/IXVs5XGIVqtkNOS9kh63WIJ+A==}
dependencies:
- '@types/yargs-parser': 21.0.1
+ '@types/yargs-parser': 21.0.3
dev: true
/@types/yargs/17.0.28:
resolution: {integrity: sha512-N3e3fkS86hNhtk6BEnc0rj3zcehaxx8QWhCROJkqpl5Zaoi7nAic3jH8q94jVD3zu5LGk+PUB6KAiDmimYOEQw==}
dependencies:
- '@types/yargs-parser': 21.0.1
+ '@types/yargs-parser': 21.0.3
dev: true
/@typescript-eslint/eslint-plugin/5.30.0_ae1d9fe1980ced49e1fd252e1d4332fe:
@@ -6425,7 +6435,7 @@ packages:
is-string: 1.0.7
is-typed-array: 1.1.12
is-weakref: 1.0.2
- object-inspect: 1.12.3
+ object-inspect: 1.13.4
object-keys: 1.1.1
object.assign: 4.1.4
regexp.prototype.flags: 1.5.1
@@ -8902,7 +8912,7 @@ packages:
is-string: 1.0.7
is-symbol: 1.0.4
isarray: 2.0.5
- object-inspect: 1.12.3
+ object-inspect: 1.13.4
object.entries: 1.1.7
object.getprototypeof: 1.0.5
which-boxed-primitive: 1.0.2
@@ -11331,8 +11341,9 @@ packages:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
- /object-inspect/1.12.3:
- resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==}
+ /object-inspect/1.13.4:
+ resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
+ engines: {node: '>= 0.4'}
dev: true
/object-is/1.1.5:
@@ -13760,6 +13771,18 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /react-resizable/3.0.5_react-dom@18.2.0+react@18.2.0:
+ resolution: {integrity: sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==}
+ peerDependencies:
+ react: '>= 16.3'
+ dependencies:
+ prop-types: 15.8.1
+ react: 18.2.0
+ react-draggable: 4.4.6_react-dom@18.2.0+react@18.2.0
+ transitivePeerDependencies:
+ - react-dom
+ dev: false
+
/react-router-dom/6.3.0_react-dom@18.1.0+react@18.1.0:
resolution: {integrity: sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==}
peerDependencies:
@@ -14519,7 +14542,7 @@ packages:
dependencies:
call-bind: 1.0.2
get-intrinsic: 1.2.1
- object-inspect: 1.12.3
+ object-inspect: 1.13.4
dev: true
/signal-exit/3.0.7:
diff --git a/src/modal/__tests__/__snapshots__/handle.test.tsx.snap b/src/modal/__tests__/__snapshots__/handle.test.tsx.snap
new file mode 100644
index 000000000..1dcc16280
--- /dev/null
+++ b/src/modal/__tests__/__snapshots__/handle.test.tsx.snap
@@ -0,0 +1,10 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Handler Component should match snapshot 1`] = `
+
+
+
+`;
diff --git a/src/modal/__tests__/__snapshots__/modal.test.tsx.snap b/src/modal/__tests__/__snapshots__/modal.test.tsx.snap
index ea4a52a9a..2cdec5a4a 100644
--- a/src/modal/__tests__/__snapshots__/modal.test.tsx.snap
+++ b/src/modal/__tests__/__snapshots__/modal.test.tsx.snap
@@ -17,7 +17,7 @@ exports[`Test Modal Component Should Match snapshot 1`] = `
aria-modal="true"
class="ant-modal dtc-modal"
role="dialog"
- style="width: 520px;"
+ style="height: auto; width: 520px;"
>
`;
+
+exports[`Test Modal Component Should support banner Should match snapshot for draggable modal 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Test Modal Component Should support banner Should match snapshot for resizable modal 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/modal/__tests__/handle.test.tsx b/src/modal/__tests__/handle.test.tsx
new file mode 100644
index 000000000..5cc2f85e5
--- /dev/null
+++ b/src/modal/__tests__/handle.test.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import '@testing-library/jest-dom/extend-expect';
+
+import Handler from '../handle';
+
+describe('Handler Component', () => {
+ it('should match snapshot', () => {
+ const { asFragment } = render(
);
+ expect(asFragment()).toMatchSnapshot();
+ });
+
+ it('should forward ref to the div element', () => {
+ const ref = React.createRef
();
+ render();
+ expect(ref.current).toBeInstanceOf(HTMLDivElement);
+ expect(ref.current).toHaveClass('dt-modal-resize-handle handle-y');
+ });
+});
diff --git a/src/modal/__tests__/modal.test.tsx b/src/modal/__tests__/modal.test.tsx
index b7a4cebfa..135d9ee0a 100644
--- a/src/modal/__tests__/modal.test.tsx
+++ b/src/modal/__tests__/modal.test.tsx
@@ -1,10 +1,23 @@
import React from 'react';
-import { cleanup, render } from '@testing-library/react';
+import { cleanup, fireEvent, render } from '@testing-library/react';
import { alert, modal } from 'ant-design-testing';
import '@testing-library/jest-dom';
+import { IFloatProps } from '../../float';
import Modal from '../';
+function dragFromTo(
+ ele: HTMLElement,
+ from: NonNullable,
+ to: NonNullable
+) {
+ fireEvent.mouseDown(ele, { clientX: from.x, clientY: from.y });
+ fireEvent.mouseMove(document, { clientX: to.x, clientY: to.y });
+ return {
+ mouseUp: () => fireEvent.mouseUp(ele, { clientX: to.x, clientY: to.y }),
+ };
+}
+
describe('Test Modal Component', () => {
beforeEach(() => {
cleanup();
@@ -94,5 +107,107 @@ describe('Test Modal Component', () => {
expect(getByText('banner')).toBeInTheDocument();
expect(alert.query(container)?.classList.contains('ant-alert-error')).toBeTruthy();
});
+
+ it('Should match snapshot for draggable modal', () => {
+ const { asFragment } = render(
+
+ test
+
+ );
+ expect(asFragment()).toMatchSnapshot();
+ });
+
+ it('Should support draggable modal and update position on drag', () => {
+ const fn = jest.fn();
+ const { container } = render(
+
+ test
+
+ );
+
+ const modalElement = modal
+ .query(container)!
+ .querySelector('.ant-modal-header')!;
+
+ dragFromTo(modalElement, { x: 0, y: 0 }, { x: 100, y: 100 }).mouseUp();
+
+ expect(fn).toHaveBeenCalledWith({ x: 100, y: 100 });
+ });
+
+ it('Should match snapshot for resizable modal', () => {
+ const { asFragment } = render(
+
+ test
+
+ );
+ expect(asFragment()).toMatchSnapshot();
+ });
+
+ it('Should call onRectChange for resizable modal', () => {
+ const onRectChange = jest.fn();
+ const { container } = render(
+
+ test
+
+ );
+ const eHandle = modal.query(container)!.querySelector('.handle-e')!;
+ dragFromTo(eHandle, { x: 620, y: 300 }, { x: 700, y: 300 }).mouseUp();
+ expect(onRectChange).toBeCalledWith({ width: 700, height: 600 });
+ });
+
+ it('Should support resizable and draggable modal', () => {
+ const onRectChange = jest.fn();
+ const onPositionChange = jest.fn();
+ const { container } = render(
+
+ test
+
+ );
+ const eHandle = modal.query(container)!.querySelector('.handle-e')!;
+ dragFromTo(eHandle, { x: 620, y: 300 }, { x: 700, y: 300 }).mouseUp();
+ expect(onRectChange).toBeCalledWith({ width: 700, height: 600 });
+ expect(onPositionChange).not.toBeCalled();
+
+ const wHandle = modal.query(container)!.querySelector('.handle-w')!;
+ dragFromTo(wHandle, { x: 620, y: 300 }, { x: 400, y: 300 }).mouseUp();
+ expect(onRectChange).toBeCalledWith({ width: 700, height: 600 });
+ expect(onPositionChange).toBeCalledWith({ x: -170, y: 50 });
+ });
});
});
diff --git a/src/modal/demos/draggable.tsx b/src/modal/demos/draggable.tsx
new file mode 100644
index 000000000..19d332ee7
--- /dev/null
+++ b/src/modal/demos/draggable.tsx
@@ -0,0 +1,46 @@
+import React, { useState } from 'react';
+import { Button, Space, Tooltip } from 'antd';
+import { Modal, TinyTag } from 'dt-react-component';
+
+export default function Basic() {
+ const [visible, setVisible] = useState(false);
+ const [position, setPosition] = useState({ x: 120, y: 120 });
+
+ return (
+ <>
+
+ Draggable Modal
+
+ e.stopPropagation()}
+ value="Cancel"
+ style={{ color: '#1D78FF' }}
+ />
+
+
+ }
+ draggable={{
+ bounds: 'body',
+ }}
+ position={position}
+ onPositionChange={({ x, y }) => setPosition({ x, y })}
+ visible={visible}
+ onCancel={() => setVisible(false)}
+ onOk={() => setVisible(false)}
+ >
+
+ {Array.from({ length: 300 }).map((_, i) => (
+ -
+ {i}
+
+ ))}
+
+
+
+ >
+ );
+}
diff --git a/src/modal/demos/resizable.tsx b/src/modal/demos/resizable.tsx
new file mode 100644
index 000000000..a0ef4da12
--- /dev/null
+++ b/src/modal/demos/resizable.tsx
@@ -0,0 +1,37 @@
+import React, { useState } from 'react';
+import { Button } from 'antd';
+import { Modal, Resize, useModal } from 'dt-react-component';
+import type { RectState } from 'dt-react-component/modal';
+
+export default function Basic() {
+ const modal = useModal();
+ const [rect, setRect] = useState({ width: 520, height: 520 });
+ const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });
+
+ return (
+ setSize({ width: window.innerWidth, height: window.innerHeight })}>
+ modal.close()}
+ onOk={() => modal.close()}
+ >
+
+ {Array.from({ length: 300 }).map((_, i) => (
+ -
+ {i}
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/src/modal/demos/size.tsx b/src/modal/demos/size.tsx
index 6d419e0e6..894f9f21f 100644
--- a/src/modal/demos/size.tsx
+++ b/src/modal/demos/size.tsx
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Button, Space } from 'antd';
import { Modal } from 'dt-react-component';
-import { IModalProps } from 'dt-react-component/modal';
+import type { IModalProps } from 'dt-react-component/modal';
export default function Size() {
const [visible, setVisible] = useState(false);
diff --git a/src/modal/demos/window.tsx b/src/modal/demos/window.tsx
new file mode 100644
index 000000000..f465b8de2
--- /dev/null
+++ b/src/modal/demos/window.tsx
@@ -0,0 +1,68 @@
+import React, { useRef, useState } from 'react';
+import { Button } from 'antd';
+import { Modal, Resize } from 'dt-react-component';
+import type { RectState, ResizeHandle } from 'dt-react-component/modal';
+
+export default function Basic() {
+ const [visible, setVisible] = useState(false);
+ const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });
+
+ const [position, setPosition] = useState({ x: 120, y: 120 });
+ const [rect, setRect] = useState({ width: 520, height: 520 });
+
+ const resizeDirection = useRef('e');
+
+ // 限制 resize 超出当前屏幕
+ const getMaxConstraints = (): [number, number] => {
+ switch (resizeDirection.current) {
+ case 'e':
+ return [size.width - position.x, size.height];
+ case 'n':
+ return [size.width, position.y + rect.height];
+ case 's':
+ return [size.width, size.height - position.y];
+ case 'w':
+ return [position.x + rect.width, size.height];
+ case 'ne':
+ return [size.width - position.x, position.y + rect.height];
+ case 'nw':
+ return [position.x + rect.width, position.y + rect.height];
+ case 'se':
+ return [size.width - position.x, size.height - position.y];
+ case 'sw':
+ return [position.x + rect.width, size.height - position.y];
+ default:
+ return [0, 0];
+ }
+ };
+
+ return (
+ setSize({ width: window.innerWidth, height: window.innerHeight })}>
+ {
+ resizeDirection.current = data.handle;
+ },
+ }}
+ rect={rect}
+ draggable={{
+ bounds: 'body',
+ }}
+ position={position}
+ onPositionChange={setPosition}
+ onRectChange={setRect}
+ onCancel={() => setVisible(false)}
+ onOk={() => setVisible(false)}
+ >
+ <>Just Dtstack It>
+
+
+
+ );
+}
diff --git a/src/modal/handle/index.scss b/src/modal/handle/index.scss
new file mode 100644
index 000000000..c7c362d46
--- /dev/null
+++ b/src/modal/handle/index.scss
@@ -0,0 +1,65 @@
+.dt-modal-resize-handle {
+ position: absolute;
+ z-index: 1;
+ pointer-events: initial;
+ &.handle-w {
+ cursor: col-resize;
+ left: -5px;
+ bottom: 0;
+ width: 10px;
+ height: 100%;
+ }
+ &.handle-e {
+ cursor: col-resize;
+ right: -5px;
+ bottom: 0;
+ width: 10px;
+ height: 100%;
+ }
+ &.handle-n {
+ cursor: row-resize;
+ left: 0;
+ top: -5px;
+ width: 100%;
+ height: 10px;
+ }
+ &.handle-s {
+ cursor: row-resize;
+ left: 0;
+ bottom: -5px;
+ width: 100%;
+ height: 10px;
+ }
+ &.handle-se {
+ cursor: se-resize;
+ right: 0;
+ bottom: -5px;
+ width: 10px;
+ height: 10px;
+ z-index: 11;
+ }
+ &.handle-ne {
+ cursor: ne-resize;
+ right: 0;
+ top: -5px;
+ width: 10px;
+ height: 10px;
+ z-index: 11;
+ }
+ &.handle-nw {
+ cursor: nw-resize;
+ left: 0;
+ top: -5px;
+ width: 10px;
+ height: 10px;
+ z-index: 11;
+ }
+ &.handle-sw {
+ cursor: sw-resize;
+ left: 0;
+ bottom: -5px;
+ width: 10px;
+ height: 10px;
+ z-index: 11;
+ }
+}
diff --git a/src/modal/handle/index.tsx b/src/modal/handle/index.tsx
new file mode 100644
index 000000000..6a571cfdb
--- /dev/null
+++ b/src/modal/handle/index.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+
+import './index.scss';
+
+const Handler = React.forwardRef((props, ref) => {
+ const { handleAxis, ...restProps } = props;
+ return (
+
+ );
+});
+
+export default Handler;
diff --git a/src/modal/index.md b/src/modal/index.md
index d50924168..00309066b 100644
--- a/src/modal/index.md
+++ b/src/modal/index.md
@@ -11,6 +11,7 @@ toc: content
- 使用模态框时,使用该组件替换 antd 的 Modal
- 支持 size 属性来快速设置宽度;限制 Modal 的高度为 600px,超出内部滚动
- 支持 banner 属性来快速实现 Modal 内部提示
+- 支持 draggable 和 resizable 属性来实现拖拽和调整大小
## 示例
@@ -18,18 +19,20 @@ toc: content
+
+
+
## API
-### AlertProps
-
-[AlertProps](https://4x-ant-design.antgroup.com/components/alert-cn/#API)
-
-| 参数 | 说明 | 类型 | 默认值 |
-| ------ | ---- | --------------------------------------------- | --------- |
-| size | 尺寸 | `'small' \| 'default' \| 'middle' \| 'large'` | `default` |
-| banner | 提示 | `React.ReactNode \| AlertProps` | |
-
-:::info
-其余参数继承 antd4.x 的 [Modal](https://4x.ant.design/components/modal-cn/#API)
-:::
+| 参数 | 说明 | 类型 | 默认值 |
+| ---------------- | ----------------------------------------- | ------------------------------------------------------------ | --------- |
+| size | 尺寸 | `'small' \| 'default' \| 'middle' \| 'large'` | `default` |
+| banner | 提示 | `React.ReactNode \| AlertProps` | |
+| draggable | 是否可拖拽 | `IFloatProps['draggable']` | `false` |
+| resizable | 是否可调整大小 | `MergeOption>` | `false` |
+| rect | 初始宽高(仅开启 resizable 的情况下生效) | `{ width: number; height: number }` | |
+| position | 初始位置(仅开启 draggable 的情况下生效) | `{ x: number; y: number}` | |
+| onPositionChange | 位置变化时的回调 | `(data: { x: number; y: number}) => void` | |
+| onRectChange | 尺寸变化时的回调 | `(data: { width: number; height: number }) => void` | |
+| ...rest | 其他继承自 antd Modal 的属性 | [ModalProps](https://4x.ant.design/components/modal-cn/#API) | |
diff --git a/src/modal/index.scss b/src/modal/index.scss
index 78543bf4f..db75dff2e 100644
--- a/src/modal/index.scss
+++ b/src/modal/index.scss
@@ -1,8 +1,22 @@
$modal-max-height: 80vh;
.dtc-modal {
+ &__draggable {
+ top: 0;
+ left: 0;
+ margin: 0;
+ }
+ &__resizable {
+ padding: 0;
+ }
+ // resizable 的时候会设置尺寸,所以不需要最大尺寸
+ &:not(&__resizable) {
+ .ant-modal-content {
+ max-height: $modal-max-height;
+ }
+ }
.ant-modal-content {
- max-height: $modal-max-height;
+ height: 100%;
display: flex;
flex-direction: column;
.ant-modal-body {
diff --git a/src/modal/index.tsx b/src/modal/index.tsx
index ebf29995d..000817bfa 100644
--- a/src/modal/index.tsx
+++ b/src/modal/index.tsx
@@ -25,6 +25,7 @@ Object.assign(Modal, {
config,
});
-export { IModalProps } from './modal';
+export type { IModalProps, RectState } from './modal';
+export type { ResizeHandle } from 'react-resizable';
export default Modal;
diff --git a/src/modal/modal.tsx b/src/modal/modal.tsx
index 98d79a571..4606632fd 100644
--- a/src/modal/modal.tsx
+++ b/src/modal/modal.tsx
@@ -1,13 +1,25 @@
-import React from 'react';
-import { Alert, type AlertProps, Modal as AntdModal, type ModalProps } from 'antd';
+import React, { useMemo } from 'react';
+import { Resizable, type ResizableProps } from 'react-resizable';
+import { Alert, type AlertProps, Modal, type ModalProps } from 'antd';
import classNames from 'classnames';
import { omit } from 'lodash-es';
+import Float, { type IFloatProps } from '../float';
+import useMergeOption, { type MergeOption } from '../useMergeOption';
+import Handler from './handle';
import './index.scss';
+export type RectState = { width: number; height: number };
+
export interface IModalProps extends ModalProps {
size?: 'small' | 'default' | 'middle' | 'large';
banner?: AlertProps['message'] | Omit;
+ draggable?: IFloatProps['draggable'];
+ resizable?: MergeOption>;
+ rect?: RectState;
+ position?: IFloatProps['position'];
+ onPositionChange?: (data: NonNullable) => void;
+ onRectChange?: (data: RectState) => void;
}
const getWidthFromSize = (size: IModalProps['size']) => {
@@ -22,22 +34,104 @@ const isValidBanner = (banner: IModalProps['banner']): banner is AlertProps['mes
return true;
};
-export default function Modal({
+export default function InternalModal({
bodyStyle,
banner,
size = 'default',
children,
width,
className,
+ draggable = false,
+ position,
+ resizable = false,
+ rect,
+ onRectChange,
+ onPositionChange,
+ modalRender,
...rest
}: IModalProps) {
- const finalWidth = width ?? getWidthFromSize(size);
+ const mergedDraggable = useMergeOption(draggable, { handle: '.ant-modal-header' });
+ const mergedResizable = useMergeOption(resizable, {
+ axis: 'both',
+ resizeHandles: ['s', 'w', 'e', 'n', 'ne', 'nw', 'sw', 'se'],
+ width: 400,
+ height: 400,
+ minConstraints: [400, 400],
+ handle: ,
+ });
+
+ const final = useMemo(() => {
+ if (mergedResizable.disabled)
+ return { width: width ?? getWidthFromSize(size), height: 'auto' };
+ return {
+ width: rect?.width || mergedResizable.options.width || 0,
+ height: rect?.height || mergedResizable.options.height || 0,
+ };
+ }, [mergedResizable, width, size, rect]);
+
+ const handleResize: ResizableProps['onResize'] = (e, data) => {
+ mergedResizable.options.onResize?.(e, data);
+
+ const nextSize = { width: data.size.width, height: data.size.height };
+ onRectChange?.(nextSize);
+
+ if (mergedDraggable.disabled || !position) return;
+ const vertical = data.handle.includes('n');
+ const horizontal = data.handle.includes('w');
+ const offsetY = vertical ? nextSize.height - (final.height as number) : 0;
+ const offsetX = horizontal ? nextSize.width - (final.width as number) : 0;
+ const after = {
+ x: position.x - offsetX,
+ y: position.y - offsetY,
+ };
+ // Prevent unnecessary update
+ if (after.x === position.x && after.y === position.y) return;
+ onPositionChange?.(after);
+ };
+
+ const handleRenderModal = (modal: React.ReactNode) => {
+ const container = modalRender?.(modal) || modal;
+ let child = <>{container}>;
+ if (!mergedResizable.disabled) {
+ child = (
+
+ {child}
+
+ );
+ }
+ if (!mergedDraggable.disabled) {
+ child = (
+ onPositionChange?.({ x, y })}
+ >
+ {child}
+
+ );
+ }
+ return child;
+ };
return (
-
{banner && (
@@ -49,6 +143,6 @@ export default function Modal({
/>
)}
-
+
);
}