Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/components/dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import useLockStyle from './hooks/useLockStyle';
import { parseValueToPx } from './utils';
import type { StyledProps } from '../common';
import type { DialogInstance, TdDialogProps } from './type';
import useDialogResize from './hooks/useDialogResize';

export interface DialogProps extends TdDialogProps, StyledProps {
isPlugin?: boolean; // 是否以插件形式调用
Expand All @@ -39,6 +40,8 @@ const Dialog = forwardRef<DialogInstance, DialogProps>((originalProps, ref) => {
const [state, setState] = useSetState<DialogProps>({ isPlugin: false, ...restProps });
const [local] = useLocaleReceiver('dialog');

const dragResizing = useRef(false);

const {
className,
dialogClassName,
Expand Down Expand Up @@ -77,6 +80,13 @@ const Dialog = forwardRef<DialogInstance, DialogProps>((originalProps, ref) => {
useLockStyle({ preventScrollThrough, visible, mode, showInAttachedElement });
useDialogEsc(visible, wrapRef);
useDialogPosition(visible, dialogCardRef);

useDialogResize({
dialogCardRef,
sizeDraggableProps: mode === 'modeless' ? props.sizeDraggable : false,
onDragResizeChange(resizing) { dragResizing.current = resizing; },
});

useDialogDrag({
dialogCardRef,
canDraggable: draggable && mode === 'modeless',
Expand Down Expand Up @@ -113,6 +123,10 @@ const Dialog = forwardRef<DialogInstance, DialogProps>((originalProps, ref) => {
}

const onMaskClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (dragResizing.current) {
return;
}

if (showOverlay && (closeOnOverlayClick ?? local.closeOnOverlayClick)) {
// 判断点击事件初次点击是否为内容区域
if (contentClickRef.current) {
Expand Down
2 changes: 1 addition & 1 deletion packages/components/dialog/defaultProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ export const dialogDefaultProps: TdDialogProps = {
preventScrollThrough: true,
showInAttachedElement: false,
showOverlay: true,
lazy: true,
sizeDraggable: false,
};
1 change: 1 addition & 0 deletions packages/components/dialog/dialog.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ placement | String | top | options: top/center | N
preventScrollThrough | Boolean | true | \- | N
showInAttachedElement | Boolean | false | \- | N
showOverlay | Boolean | true | \- | N
sizeDraggable | Boolean / Object | false | allow resizing drawer width/height, set max or min to limit size.。Typescript:`boolean \| DialogSizeDragLimit` `interface DialogSizeDragLimit { maxWidth: number\|undefined, minWidth: number\|undefined, maxHeight: number\|undefined, minHeight: number\|undefined }`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/dialog/type.ts) | N
theme | String | default | options: default/info/warning/danger/success | N
top | String / Number | - | \- | N
visible | Boolean | - | \- | N
Expand Down
1 change: 1 addition & 0 deletions packages/components/dialog/dialog.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ placement | String | top | 对话框位置,内置两种:垂直水平居中
preventScrollThrough | Boolean | true | 防止滚动穿透 | N
showInAttachedElement | Boolean | false | 仅在挂载元素中显示抽屉,默认在浏览器可视区域显示。父元素需要有定位属性,如:position: relative | N
showOverlay | Boolean | true | 是否显示遮罩层 | N
sizeDraggable | Boolean / Object | false | 弹窗大小可拖拽调整。`dialogSizeDragLimit.maxWidth`、`dialogSizeDragLimit.minWidth`、`dialogSizeDragLimit.maxHeight`、`dialogSizeDragLimit.minHeight` 用于控制拖拽尺寸大小限制。。TS 类型:`boolean \| DialogSizeDragLimit` `interface DialogSizeDragLimit { maxWidth: number\|undefined, minWidth: number\|undefined, maxHeight: number\|undefined, minHeight: number\|undefined }`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/dialog/type.ts) | N
theme | String | default | 对话框风格。可选项:default/info/warning/danger/success | N
top | String / Number | - | 用于弹框具体窗口顶部的距离,优先级大于 placement | N
visible | Boolean | - | 控制对话框是否显示 | N
Expand Down
22 changes: 20 additions & 2 deletions packages/components/dialog/hooks/useDialogDrag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ interface DialogDragProps {
canDraggable?: boolean;
}

const RESERVED_BORDER_WIDTH = 8; // 保留边框宽度,避免拖拽时误操作

const useDialogDrag = (props: DialogDragProps) => {
const { dialogCardRef, canDraggable } = props;

Expand All @@ -15,18 +17,31 @@ const useDialogDrag = (props: DialogDragProps) => {

const dragOffset = useRef({ x: 0, y: 0 });

const moving = useRef(false);

useMouseEvent(dialogCardRef, {
enabled: canDraggable,
onDown: (e) => {
const { offsetLeft, offsetTop, offsetWidth, offsetHeight, style } = dialogCardRef.current;
if (offsetWidth > screenWidth || offsetHeight > screenHeight) return;
if (offsetWidth > screenWidth || offsetHeight > screenHeight)
return;

if (e.clientX - offsetLeft <= RESERVED_BORDER_WIDTH || e.clientX - offsetLeft >= offsetWidth - RESERVED_BORDER_WIDTH)
return;
if (e.clientY - offsetTop <= RESERVED_BORDER_WIDTH || e.clientY - offsetTop >= offsetHeight - RESERVED_BORDER_WIDTH)
return;

style.cursor = 'move';
moving.current = true;
dragOffset.current = {
x: e.clientX - offsetLeft,
y: e.clientY - offsetTop,
};
},
onMove: (e) => {
if (!moving.current)
return;

const { offsetWidth, offsetHeight, style } = dialogCardRef.current;
let diffX = e.clientX - dragOffset.current.x;
let diffY = e.clientY - dragOffset.current.y;
Expand All @@ -40,7 +55,10 @@ const useDialogDrag = (props: DialogDragProps) => {
style.top = `${diffY}px`;
},
onUp: () => {
dialogCardRef.current.style.cursor = 'default';
if (moving.current) {
moving.current = false;
dialogCardRef.current.style.cursor = 'default';
}
},
});
};
Expand Down
217 changes: 217 additions & 0 deletions packages/components/dialog/hooks/useDialogResize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import React from "react";
import useMouseEvent from "../../hooks/useMouseEvent";
import { DialogSizeDragLimit } from "../type";


const RESIZE_BORDER_WIDTH = 8; // 边框宽度,拖拽时的感应区域


interface DialogResizeProps {
dialogCardRef: React.MutableRefObject<HTMLDivElement | null>;
sizeDraggableProps: boolean | DialogSizeDragLimit;
onDragResizeChange: (resizing: boolean) => void;
}


type ResizeDirection = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw' | false;


function mouseOnBorder(
e: React.MouseEvent,
dialogCardRef: React.MutableRefObject<HTMLDivElement | null>
): ResizeDirection {
const mouseX = e.clientX;
const mouseY = e.clientY;
const { left, top, width, height } = dialogCardRef.current.getBoundingClientRect();

const borderWidth = RESIZE_BORDER_WIDTH;
const onLeftBorder = mouseX >= left - borderWidth && mouseX <= left + borderWidth;
const onRightBorder = mouseX >= left + width - borderWidth && mouseX <= left + width + borderWidth;
const onTopBorder = mouseY >= top - borderWidth && mouseY <= top + borderWidth;
const onBottomBorder = mouseY >= top + height - borderWidth && mouseY <= top + height + borderWidth;
if (onLeftBorder && onTopBorder)
return 'nw';
if (onRightBorder && onBottomBorder)
return 'se';
if (onRightBorder && onTopBorder)
return 'ne';
if (onLeftBorder && onBottomBorder)
return 'sw';
if (onLeftBorder)
return 'w';
if (onRightBorder)
return 'e';
if (onTopBorder)
return 'n';
if (onBottomBorder)
return 's';
return false;
}


const useDialogResize = (props: DialogResizeProps) => {
const { dialogCardRef, sizeDraggableProps } = props;

const validWindow = typeof window === 'object';

const getWindowHeight = () => (validWindow ? window.innerHeight || document.documentElement.clientHeight : undefined);
const getWindowWidth = () => (validWindow ? window.innerWidth || document.documentElement.clientWidth : undefined);

const dialogSize = React.useRef({ x: 0, y: 0, width: 0, height: 0 });

const resizingDirection = React.useRef<ResizeDirection>(false);

const minWidth = React.useRef(0);
const maxWidth = React.useRef(Number.MAX_VALUE);
const minHeight = React.useRef<number|undefined>(undefined); // If undefined, set to current height on first drag (lower than which causes buttons overflow).
const maxHeight = React.useRef(Number.MAX_VALUE);


React.useEffect(() => {
if (sizeDraggableProps === undefined || typeof sizeDraggableProps === 'boolean') {
minWidth.current = undefined;
maxWidth.current = Number.MAX_VALUE;
minHeight.current = 0;
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

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

Inconsistent handling of undefined values. Line 72 sets minWidth.current = undefined but line 74 sets minHeight.current = 0. Both should handle undefined consistently or there should be a clear reason for the difference.

Suggested change
minHeight.current = 0;
minHeight.current = undefined;

Copilot uses AI. Check for mistakes.
maxHeight.current = Number.MAX_VALUE;
return;
}

if (sizeDraggableProps.maxHeight !== undefined) {
maxHeight.current = sizeDraggableProps.maxHeight;
}

if (sizeDraggableProps.minHeight !== undefined) {
minHeight.current = sizeDraggableProps.minHeight;
}

if (sizeDraggableProps.maxWidth !== undefined) {
maxWidth.current = sizeDraggableProps.maxWidth;
}

if (sizeDraggableProps.minWidth !== undefined) {
minWidth.current = sizeDraggableProps.minWidth;
}

}, [sizeDraggableProps]);


useMouseEvent(dialogCardRef, {
enabled: sizeDraggableProps !== undefined && sizeDraggableProps !== false,
alwaysEmitOnMove: sizeDraggableProps !== undefined && sizeDraggableProps !== false,
onDown: (e: React.MouseEvent) => {
if (minHeight.current === undefined && dialogCardRef.current) {
minHeight.current = dialogCardRef.current.offsetHeight;
}

resizingDirection.current = mouseOnBorder(e, dialogCardRef);
if (resizingDirection.current !== false) {
dialogSize.current = {
x: dialogCardRef.current.offsetLeft,
y: dialogCardRef.current.offsetTop,
width: dialogCardRef.current.offsetWidth,
height: dialogCardRef.current.offsetHeight,
};

props.onDragResizeChange(true);

e.stopPropagation();
e.preventDefault();
}
},
onMove: (e: React.MouseEvent) => {
// Check whether we should update cursor style.
const direction = mouseOnBorder(e, dialogCardRef);
if (direction) {
let cursor = '';
if (direction === 'n' || direction === 's')
cursor = `${direction}-resize`;
else if (direction === 'e' || direction === 'w')
cursor = `${direction}-resize`;
else if (direction === 'ne' || direction === 'sw')
cursor = 'nesw-resize';
else if (direction === 'nw' || direction === 'se')
cursor = 'nwse-resize';
dialogCardRef.current.style.cursor = cursor;
} else if (resizingDirection.current === false)
dialogCardRef.current.style.cursor = 'default';

if (resizingDirection.current === false)
return;


// Do resize.
const dir = resizingDirection.current;
const {style} = dialogCardRef.current;

style.position = 'absolute';

const mouseX = e.clientX;
const mouseY = e.clientY;

if (mouseX <= 4 || mouseY <= 4 || mouseX >= getWindowWidth() - 4 || mouseY >= getWindowHeight() - 4) {
return; // Just ignore.
}

let nextHeight = dialogCardRef.current.offsetHeight;
let nextWidth = dialogCardRef.current.offsetWidth;
let nextTop = dialogCardRef.current.offsetTop;
let nextLeft = dialogCardRef.current.offsetLeft;


if (dir.includes('n')) { // upper
const deltaY = mouseY - dialogSize.current.y;
nextHeight = dialogSize.current.height - deltaY;
nextTop = dialogSize.current.y + deltaY;
}
else if (dir.includes('s')) { // lower
const deltaY = mouseY - dialogSize.current.y - dialogSize.current.height;
nextHeight = dialogSize.current.height + deltaY;
}

if (dir.includes('w')) { // left
const deltaX = mouseX - dialogSize.current.x;
nextWidth = dialogSize.current.width - deltaX;
nextLeft = dialogSize.current.x + deltaX;
}
else if (dir.includes('e')) { // right
const deltaX = mouseX - dialogSize.current.x - dialogSize.current.width;
nextWidth = dialogSize.current.width + deltaX;
}


style.left = `${nextLeft}px`;
style.width = `${nextWidth}px`;
dialogSize.current.x = nextLeft;
dialogSize.current.width = nextWidth;


if (nextHeight >= minHeight.current) {
style.top = `${nextTop}px`;
style.height = `${nextHeight}px`;
dialogSize.current.y = nextTop;
dialogSize.current.height = nextHeight;
}

e.stopPropagation();
e.preventDefault();

},
onUp: (e: React.MouseEvent) => {

if (resizingDirection.current === false)
return;

resizingDirection.current = false;

// prevent triggering overlay click.
setTimeout(() => {
props.onDragResizeChange(false);
}, 0);

e.stopPropagation();
e.preventDefault();
},
});
};

export default useDialogResize;
12 changes: 12 additions & 0 deletions packages/components/dialog/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ export interface TdDialogProps {
* @default true
*/
showOverlay?: boolean;
/**
* 弹窗大小可拖拽调整。`dialogSizeDragLimit.maxWidth`、`dialogSizeDragLimit.minWidth`、`dialogSizeDragLimit.maxHeight`、`dialogSizeDragLimit.minHeight` 用于控制拖拽尺寸大小限制。
* @default false
*/
sizeDraggable?: boolean | DialogSizeDragLimit;
/**
* 对话框风格
* @default default
Expand Down Expand Up @@ -230,6 +235,13 @@ export interface DialogInstance {
update: (props: DialogOptions) => void;
}

export interface DialogSizeDragLimit {
maxWidth: number | undefined;
minWidth: number | undefined;
maxHeight: number | undefined;
minHeight: number | undefined;
}

export type DialogEventSource = 'esc' | 'close-btn' | 'cancel' | 'overlay';

export interface DialogCloseContext {
Expand Down
4 changes: 3 additions & 1 deletion packages/components/hooks/useMouseEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ type MouseEventOptions = {
onUp?: (e: MouseCallback, ctx: MouseContext) => void;
onEnter?: (e: MouseCallback, ctx: MouseContext) => void;
onLeave?: (e: MouseCallback, ctx: MouseContext) => void;

alwaysEmitOnMove?: boolean
};

const useMouseEvent = (elementRef: React.RefObject<HTMLElement>, options: MouseEventOptions) => {
Expand Down Expand Up @@ -66,7 +68,7 @@ const useMouseEvent = (elementRef: React.RefObject<HTMLElement>, options: MouseE
};

const handleMouseMove = (e: MouseEventLike) => {
if (!isMovingRef.current) return;
if (!isMovingRef.current && !options.alwaysEmitOnMove) return;
e.preventDefault();
emitMouseChange(e, options.onMove);
};
Expand Down
Loading