Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
50 changes: 9 additions & 41 deletions packages/components/dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { isUndefined } from 'lodash-es';

import log from '@tdesign/common-js/log/index';
import { pxCompat } from '@tdesign/common-js/utils/helper';
import { canUseDocument } from '../_util/dom';
import Portal from '../common/Portal';
import useAttach from '../hooks/useAttach';
import useConfig from '../hooks/useConfig';
Expand All @@ -23,28 +22,6 @@ import useLockStyle from './hooks/useLockStyle';
import type { StyledProps } from '../common';
import type { DialogInstance, TdDialogProps } from './type';

type MousePosition = { x: number; y: number } | null;

let mousePosition: MousePosition;

const getClickPosition = (e: MouseEvent) => {
mousePosition = {
x: e.pageX,
y: e.pageY,
};
// 100ms 内发生过点击事件,则从点击位置动画展示
// 否则直接 zoom 展示
// 这样可以兼容非点击方式展开
setTimeout(() => {
mousePosition = null;
}, 100);
};

// 只有点击事件支持从鼠标位置动画展开
if (canUseDocument) {
document.documentElement.addEventListener('click', getClickPosition, true);
}

export interface DialogProps extends TdDialogProps, StyledProps {
isPlugin?: boolean; // 是否以插件形式调用
}
Expand Down Expand Up @@ -103,12 +80,11 @@ const Dialog = forwardRef<DialogInstance, DialogProps>((originalProps, ref) => {
const isFullScreen = mode === 'full-screen';

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

const { focusTopDialog } = useDialogEsc(visible, wrapRef);
useLockStyle({ preventScrollThrough, visible, mode, showInAttachedElement });
useDialogPosition(visible, dialogCardRef);
const { activateDialog } = useDialogEsc(visible, wrapRef);
const { applyTransform } = useDialogPosition(dialogCardRef);
const { isInputInteracting } = useDialogDrag({
dialogCardRef,
canDraggable: !isFullScreen && draggable,
Expand All @@ -121,15 +97,14 @@ const Dialog = forwardRef<DialogInstance, DialogProps>((originalProps, ref) => {
}, [props, setState]);

useEffect(() => {
if (dialogAnimationVisible) {
wrapRef.current?.focus();
if (mousePosition && dialogCardRef.current) {
const offsetX = mousePosition.x - dialogCardRef.current.offsetLeft;
const offsetY = mousePosition.y - dialogCardRef.current.offsetTop;
if (!dialogCardRef.current) return;
dialogCardRef.current.style.display = dialogAnimationVisible ? 'block' : 'none';

dialogCardRef.current.style.transformOrigin = `${offsetX}px ${offsetY}px`;
}
if (dialogAnimationVisible) {
activateDialog();
applyTransform();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dialogAnimationVisible]);

useImperativeHandle(ref, () => ({
Expand Down Expand Up @@ -200,30 +175,23 @@ const Dialog = forwardRef<DialogInstance, DialogProps>((originalProps, ref) => {
// Portal Animation
const onAnimateStart = () => {
onBeforeOpen?.();
setAnimationVisible(true);
if (!wrapRef.current) return;
wrapRef.current.style.display = 'block';
};

const onAnimateLeave = () => {
onClosed?.();
setAnimationVisible(false);
focusTopDialog();
if (!wrapRef.current) return;
wrapRef.current.style.display = 'none';
};

// Dialog Animation
const onInnerAnimateStart = () => {
setDialogAnimationVisible(true);
if (!dialogCardRef.current) return;
dialogCardRef.current.style.display = 'block';
};

const onInnerAnimateLeave = () => {
setDialogAnimationVisible(false);
if (!dialogCardRef.current) return;
dialogCardRef.current.style.display = 'none';
};

const renderMask = () => {
Expand Down Expand Up @@ -265,7 +233,7 @@ const Dialog = forwardRef<DialogInstance, DialogProps>((originalProps, ref) => {
[`${componentCls}__ctx--absolute`]: showInAttachedElement,
[`${componentCls}__ctx--modeless`]: isModeless,
})}
style={{ zIndex, display: animationVisible ? undefined : 'none' }}
style={{ zIndex }}
onKeyDown={handleKeyDown}
tabIndex={0}
>
Expand Down
19 changes: 9 additions & 10 deletions packages/components/dialog/hooks/useDialogEsc.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
import { MutableRefObject, useCallback, useEffect, useRef } from 'react';
import { type MutableRefObject, useCallback, useEffect, useRef } from 'react';

const dialogStack: MutableRefObject<HTMLDivElement>[] = [];

const useDialogEsc = (visible: boolean, dialog: MutableRefObject<HTMLDivElement>) => {
const addedToStackRef = useRef<boolean>(false);

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

// 每次渲染都执行,确保捕捉到的 current 不为 null
useEffect(() => {
if (visible && dialog?.current && !addedToStackRef.current) {
const activateDialog = useCallback(() => {
if (dialog?.current && !addedToStackRef.current) {
dialogStack.push(dialog);
addedToStackRef.current = true;
dialog.current.focus();
focusTopDialog();
}
});
}, [dialog, focusTopDialog]);

// 处理 visible 变化
useEffect(() => {
if (!visible && addedToStackRef.current) {
const index = dialogStack.indexOf(dialog);
if (index > -1) {
dialogStack.splice(index, 1);
}
addedToStackRef.current = false;
focusTopDialog();
}

return () => {
Expand All @@ -40,9 +38,10 @@ const useDialogEsc = (visible: boolean, dialog: MutableRefObject<HTMLDivElement>
}
}
};
}, [visible, dialog]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible]);

return { focusTopDialog };
return { activateDialog };
};

export default useDialogEsc;
71 changes: 40 additions & 31 deletions packages/components/dialog/hooks/useDialogPosition.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,43 @@
import { MutableRefObject, useEffect, useRef } from 'react';
import useIsomorphicLayoutEffect from '../../hooks/useLayoutEffect';

export default function useDialogPosition(visible: boolean, dialogCardRef: MutableRefObject<HTMLElement>) {
const mousePosRef = useRef(null);

const getClickPosition = (e: MouseEvent) => {
mousePosRef.current = {
x: e.clientX,
y: e.clientY,
};
setTimeout(() => {
mousePosRef.current = null;
}, 100);
import { type MutableRefObject, useCallback } from 'react';
import { canUseDocument } from '../../_util/dom';

// 模块级别的鼠标位置记录,确保 Plugin 模式也能捕获到点击位置
let mousePosition: { x: number; y: number } | null = null;

const getClickPosition = (e: MouseEvent) => {
mousePosition = {
x: e.clientX,
y: e.clientY,
};
// 100ms 内发生过点击事件,则从点击位置动画展示
// 否则直接 zoom 展示
// 这样可以兼容非点击方式展开
setTimeout(() => {
mousePosition = null;
}, 100);
};

if (canUseDocument) {
document.addEventListener('click', getClickPosition, true);
}

export default function useDialogPosition(dialogCardRef: MutableRefObject<HTMLElement>) {
const applyTransform = useCallback(() => {
const el = dialogCardRef.current;
if (!el || !mousePosition) return;

const { x, y } = mousePosition;

const parentRect = el.offsetParent?.getBoundingClientRect() || { left: 0, top: 0 };

const top = parentRect.top + el.offsetTop;
const left = parentRect.left + el.offsetLeft;

const offsetX = x - left;
const offsetY = y - top;

el.style.transformOrigin = `${offsetX}px ${offsetY}px`;
}, [dialogCardRef]);

useIsomorphicLayoutEffect(() => {
document.addEventListener('click', getClickPosition, true);
return () => {
document.removeEventListener('click', getClickPosition, true);
};
}, []);

useEffect(() => {
if (!visible) return;
// 动画渲染初始位置
if (mousePosRef.current && dialogCardRef.current) {
// eslint-disable-next-line
dialogCardRef.current.style.transformOrigin = `${mousePosRef.current.x - dialogCardRef.current.offsetLeft}px ${
mousePosRef.current.y - dialogCardRef.current.offsetTop
}px`;
}
}, [visible, dialogCardRef]);
return { applyTransform };
}
Loading