Skip to content

Commit 8acfe24

Browse files
fix(Dialog): animation does not originate from the click position (#4165)
* fix(Dialog): the animation effect does not originate from the click position * fix: drag failed * fix: position * chore: stash changelog [ci skip] --------- Co-authored-by: tdesign-bot <tdesign@tencent.com>
1 parent bf782d7 commit 8acfe24

4 files changed

Lines changed: 62 additions & 80 deletions

File tree

packages/components/dialog/Dialog.tsx

Lines changed: 9 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { isUndefined } from 'lodash-es';
55
import log from '@tdesign/common-js/log/index';
66
import { pxCompat } from '@tdesign/common-js/utils/helper';
77

8-
import { canUseDocument } from '../_util/dom';
98
import Portal from '../common/Portal';
109
import useAttach from '../hooks/useAttach';
1110
import useConfig from '../hooks/useConfig';
@@ -23,28 +22,6 @@ import useLockStyle from './hooks/useLockStyle';
2322
import type { StyledProps } from '../common';
2423
import type { DialogInstance, TdDialogProps } from './type';
2524

26-
type MousePosition = { x: number; y: number } | null;
27-
28-
let mousePosition: MousePosition;
29-
30-
const getClickPosition = (e: MouseEvent) => {
31-
mousePosition = {
32-
x: e.pageX,
33-
y: e.pageY,
34-
};
35-
// 100ms 内发生过点击事件,则从点击位置动画展示
36-
// 否则直接 zoom 展示
37-
// 这样可以兼容非点击方式展开
38-
setTimeout(() => {
39-
mousePosition = null;
40-
}, 100);
41-
};
42-
43-
// 只有点击事件支持从鼠标位置动画展开
44-
if (canUseDocument) {
45-
document.documentElement.addEventListener('click', getClickPosition, true);
46-
}
47-
4825
export interface DialogProps extends TdDialogProps, StyledProps {
4926
isPlugin?: boolean; // 是否以插件形式调用
5027
}
@@ -103,12 +80,11 @@ const Dialog = forwardRef<DialogInstance, DialogProps>((originalProps, ref) => {
10380
const isFullScreen = mode === 'full-screen';
10481

10582
const dialogAttach = useAttach('dialog', attach);
106-
const [animationVisible, setAnimationVisible] = useState(visible);
10783
const [dialogAnimationVisible, setDialogAnimationVisible] = useState(false);
10884

109-
const { focusTopDialog } = useDialogEsc(visible, wrapRef);
11085
useLockStyle({ preventScrollThrough, visible, mode, showInAttachedElement });
111-
useDialogPosition(visible, dialogCardRef);
86+
const { activateDialog } = useDialogEsc(visible, wrapRef);
87+
const { applyTransform } = useDialogPosition(dialogCardRef);
11288
const { isInputInteracting } = useDialogDrag({
11389
dialogCardRef,
11490
canDraggable: !isFullScreen && draggable,
@@ -121,15 +97,14 @@ const Dialog = forwardRef<DialogInstance, DialogProps>((originalProps, ref) => {
12197
}, [props, setState]);
12298

12399
useEffect(() => {
124-
if (dialogAnimationVisible) {
125-
wrapRef.current?.focus();
126-
if (mousePosition && dialogCardRef.current) {
127-
const offsetX = mousePosition.x - dialogCardRef.current.offsetLeft;
128-
const offsetY = mousePosition.y - dialogCardRef.current.offsetTop;
100+
if (!dialogCardRef.current) return;
101+
dialogCardRef.current.style.display = dialogAnimationVisible ? 'block' : 'none';
129102

130-
dialogCardRef.current.style.transformOrigin = `${offsetX}px ${offsetY}px`;
131-
}
103+
if (dialogAnimationVisible) {
104+
activateDialog();
105+
applyTransform();
132106
}
107+
// eslint-disable-next-line react-hooks/exhaustive-deps
133108
}, [dialogAnimationVisible]);
134109

135110
useImperativeHandle(ref, () => ({
@@ -200,30 +175,23 @@ const Dialog = forwardRef<DialogInstance, DialogProps>((originalProps, ref) => {
200175
// Portal Animation
201176
const onAnimateStart = () => {
202177
onBeforeOpen?.();
203-
setAnimationVisible(true);
204178
if (!wrapRef.current) return;
205179
wrapRef.current.style.display = 'block';
206180
};
207181

208182
const onAnimateLeave = () => {
209183
onClosed?.();
210-
setAnimationVisible(false);
211-
focusTopDialog();
212184
if (!wrapRef.current) return;
213185
wrapRef.current.style.display = 'none';
214186
};
215187

216188
// Dialog Animation
217189
const onInnerAnimateStart = () => {
218190
setDialogAnimationVisible(true);
219-
if (!dialogCardRef.current) return;
220-
dialogCardRef.current.style.display = 'block';
221191
};
222192

223193
const onInnerAnimateLeave = () => {
224194
setDialogAnimationVisible(false);
225-
if (!dialogCardRef.current) return;
226-
dialogCardRef.current.style.display = 'none';
227195
};
228196

229197
const renderMask = () => {
@@ -265,7 +233,7 @@ const Dialog = forwardRef<DialogInstance, DialogProps>((originalProps, ref) => {
265233
[`${componentCls}__ctx--absolute`]: showInAttachedElement,
266234
[`${componentCls}__ctx--modeless`]: isModeless,
267235
})}
268-
style={{ zIndex, display: animationVisible ? undefined : 'none' }}
236+
style={{ zIndex }}
269237
onKeyDown={handleKeyDown}
270238
tabIndex={0}
271239
>

packages/components/dialog/hooks/useDialogEsc.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,29 @@ const dialogStack: MutableRefObject<HTMLDivElement>[] = [];
77
const useDialogEsc = (visible: boolean, dialog: MutableRefObject<HTMLDivElement>) => {
88
const addedToStackRef = useRef<boolean>(false);
99

10-
// 关闭动画完成后调用,聚焦顶层 dialog
1110
const focusTopDialog = useCallback(() => {
1211
const lastDialog = dialogStack[dialogStack.length - 1];
1312
if (lastDialog?.current) {
1413
lastDialog.current.focus();
1514
}
1615
}, []);
1716

18-
// 每次渲染都执行,确保捕捉到的 current 不为 null
19-
useEffect(() => {
20-
if (visible && dialog?.current && !addedToStackRef.current) {
17+
const activateDialog = useCallback(() => {
18+
if (dialog?.current && !addedToStackRef.current) {
2119
dialogStack.push(dialog);
2220
addedToStackRef.current = true;
23-
dialog.current.focus();
21+
focusTopDialog();
2422
}
25-
});
23+
}, [dialog, focusTopDialog]);
2624

27-
// 处理 visible 变化
2825
useEffect(() => {
2926
if (!visible && addedToStackRef.current) {
3027
const index = dialogStack.indexOf(dialog);
3128
if (index > -1) {
3229
dialogStack.splice(index, 1);
3330
}
3431
addedToStackRef.current = false;
32+
focusTopDialog();
3533
}
3634

3735
return () => {
@@ -42,9 +40,10 @@ const useDialogEsc = (visible: boolean, dialog: MutableRefObject<HTMLDivElement>
4240
}
4341
}
4442
};
45-
}, [visible, dialog]);
43+
// eslint-disable-next-line react-hooks/exhaustive-deps
44+
}, [visible]);
4645

47-
return { focusTopDialog };
46+
return { activateDialog };
4847
};
4948

5049
export default useDialogEsc;
Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,46 @@
1-
import { useEffect, useRef } from 'react';
1+
import { useCallback } from 'react';
22

3-
import useIsomorphicLayoutEffect from '../../hooks/useLayoutEffect';
3+
import { canUseDocument } from '../../_util/dom';
44

55
import type { MutableRefObject } from 'react';
66

7-
export default function useDialogPosition(visible: boolean, dialogCardRef: MutableRefObject<HTMLElement>) {
8-
const mousePosRef = useRef(null);
9-
10-
const getClickPosition = (e: MouseEvent) => {
11-
mousePosRef.current = {
12-
x: e.clientX,
13-
y: e.clientY,
14-
};
15-
setTimeout(() => {
16-
mousePosRef.current = null;
17-
}, 100);
7+
// 模块级别的鼠标位置记录,确保 Plugin 模式也能捕获到点击位置
8+
let mousePosition: { x: number; y: number } | null = null;
9+
10+
const getClickPosition = (e: MouseEvent) => {
11+
mousePosition = {
12+
x: e.clientX,
13+
y: e.clientY,
1814
};
15+
// 100ms 内发生过点击事件,则从点击位置动画展示
16+
// 否则直接 zoom 展示
17+
// 这样可以兼容非点击方式展开
18+
setTimeout(() => {
19+
mousePosition = null;
20+
}, 100);
21+
};
22+
23+
if (canUseDocument) {
24+
document.addEventListener('click', getClickPosition, true);
25+
}
26+
27+
export default function useDialogPosition(dialogCardRef: MutableRefObject<HTMLElement>) {
28+
const applyTransform = useCallback(() => {
29+
const el = dialogCardRef.current;
30+
if (!el || !mousePosition) return;
31+
32+
const { x, y } = mousePosition;
33+
34+
const parentRect = el.offsetParent?.getBoundingClientRect() || { left: 0, top: 0 };
35+
36+
const top = parentRect.top + el.offsetTop;
37+
const left = parentRect.left + el.offsetLeft;
38+
39+
const offsetX = x - left;
40+
const offsetY = y - top;
41+
42+
el.style.transformOrigin = `${offsetX}px ${offsetY}px`;
43+
}, [dialogCardRef]);
1944

20-
useIsomorphicLayoutEffect(() => {
21-
document.addEventListener('click', getClickPosition, true);
22-
return () => {
23-
document.removeEventListener('click', getClickPosition, true);
24-
};
25-
}, []);
26-
27-
useEffect(() => {
28-
if (!visible) return;
29-
// 动画渲染初始位置
30-
if (mousePosRef.current && dialogCardRef.current) {
31-
// eslint-disable-next-line
32-
dialogCardRef.current.style.transformOrigin = `${mousePosRef.current.x - dialogCardRef.current.offsetLeft}px ${
33-
mousePosRef.current.y - dialogCardRef.current.offsetTop
34-
}px`;
35-
}
36-
}, [visible, dialogCardRef]);
45+
return { applyTransform };
3746
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
pr_number: 4165
3+
contributor: RylanBot
4+
---
5+
6+
- fix(Dialog): 修复弹出动画未从点击位置展开的问题 @RylanBot ([#4165](https://github.com/Tencent/tdesign-react/pull/4165))

0 commit comments

Comments
 (0)