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
4 changes: 2 additions & 2 deletions packages/components/cascader/Cascader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ const Cascader: React.FC<CascaderProps> = (originalProps) => {
const { setVisible, visible, inputVal, setInputVal } = cascaderContext;

const updateScrollTop = (content: HTMLDivElement) => {
const cascaderMenuList = content.querySelectorAll(`.${COMPONENT_NAME}__menu`);
const cascaderMenuList = content?.querySelectorAll(`.${COMPONENT_NAME}__menu`);
requestAnimationFrame(() => {
cascaderMenuList.forEach((menu: HTMLDivElement) => {
cascaderMenuList?.forEach((menu: HTMLDivElement) => {
const firstSelectedNode: HTMLDivElement =
menu?.querySelector(`.${classPrefix}-is-selected`) || menu?.querySelector(`.${classPrefix}-is-expanded`);
if (!firstSelectedNode || !menu) return;
Expand Down
8 changes: 4 additions & 4 deletions packages/components/cascader/__tests__/cascader.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ describe('Cascader 组件测试', () => {
const spy = vi.spyOn(selectInputProps, 'onInputChange');
render(<Cascader options={options} selectInputProps={selectInputProps} filterable />);
// 模拟用户键盘输入 "test" ,一共会触发四次 onInputChange
userEvent.type(document.querySelector('input'), enterText);
await userEvent.type(document.querySelector('input'), enterText);
await mockTimeout(() => expect(spy).toHaveBeenCalledTimes(enterText.length));
});

Expand Down Expand Up @@ -244,17 +244,17 @@ describe('Cascader 组件测试', () => {
);
// 搜索 子选项一 ,共有两个结果,成功匹配的内容应该高亮
fireEvent.focus(getByPlaceholderText(placeholder));
userEvent.type(getByPlaceholderText(placeholder), filterContent);
await userEvent.type(getByPlaceholderText(placeholder), filterContent);
await mockTimeout(() =>
expect(document.querySelector(popupSelector).querySelectorAll('.t-cascader__item-label--filter').length).toBe(2),
);
// 清空搜索项,无匹配任何高亮内容
userEvent.type(getByPlaceholderText(placeholder), '{backspace}{backspace}{backspace}{backspace}');
await userEvent.type(getByPlaceholderText(placeholder), '{backspace}{backspace}{backspace}{backspace}');
await mockTimeout(() =>
expect(document.querySelector(popupSelector).querySelectorAll('.t-cascader__item-label--filter').length).toBe(0),
);
// 匹配不到任何内容
userEvent.type(getByPlaceholderText(placeholder), 'null');
await userEvent.type(getByPlaceholderText(placeholder), 'null');
await mockTimeout(() => expect(getByText('暂无数据')).toBeInTheDocument());
});

Expand Down
21 changes: 14 additions & 7 deletions packages/components/common/Portal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { forwardRef, useMemo, useImperativeHandle } from 'react';
import React, { forwardRef, useImperativeHandle, useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { AttachNode, AttachNodeReturnValue } from '../common';
import { canUseDocument } from '../_util/dom';
Expand Down Expand Up @@ -40,26 +40,33 @@ export function getAttach(attach: PortalProps['attach'], triggerNode?: HTMLEleme
const Portal = forwardRef((props: PortalProps, ref) => {
const { attach, children, triggerNode } = props;
const { classPrefix } = useConfig();

const container = useMemo(() => {
const [container] = useState(() => {
if (!canUseDocument) return null;
const el = document.createElement('div');
el.className = `${classPrefix}-portal-wrapper`;
return el;
}, [classPrefix]);
});
const [mounted, setMounted] = useState(false);

useIsomorphicLayoutEffect(() => {
if (!mounted) return;

const parentElement = getAttach(attach, triggerNode);
parentElement?.appendChild?.(container);

return () => {
parentElement?.removeChild?.(container);
};
}, [container, attach, triggerNode]);
}, [container, attach, triggerNode, mounted]);

useEffect(() => {
if (!mounted) {
setMounted(true);
}
}, [mounted]);

useImperativeHandle(ref, () => container);

return canUseDocument ? createPortal(children, container) : null;
return canUseDocument && mounted ? createPortal(children, container) : null;
});

Portal.displayName = 'Portal';
Expand Down
2 changes: 1 addition & 1 deletion packages/components/date-picker/base/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ const DatePickerHeader = (props: DatePickerHeaderProps) => {
// eslint-disable-next-line no-param-reassign
content.scrollTop = content.scrollHeight - 30 * 10;
} else {
const firstSelectedNode: HTMLDivElement = content.querySelector(`.${classPrefix}-is-selected`);
const firstSelectedNode: HTMLDivElement = content?.querySelector(`.${classPrefix}-is-selected`);

if (firstSelectedNode) {
const { paddingBottom } = getComputedStyle(firstSelectedNode);
Expand Down
66 changes: 54 additions & 12 deletions packages/components/dialog/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { forwardRef, useEffect, useRef, useImperativeHandle } from 'react';
import React, { forwardRef, useEffect, useRef, useImperativeHandle, useState } from 'react';
import { CSSTransition } from 'react-transition-group';
import classNames from 'classnames';
import log from '@tdesign/common-js/log/index';
Expand All @@ -13,11 +13,33 @@ import { dialogDefaultProps } from './defaultProps';
import DialogCard from './DialogCard';
import useDialogEsc from './hooks/useDialogEsc';
import useLockStyle from './hooks/useLockStyle';
import useDialogPosition from './hooks/useDialogPosition';
import useDialogDrag from './hooks/useDialogDrag';
import { parseValueToPx } from './utils';
import useDefaultProps from '../hooks/useDefaultProps';
import useAttach from '../hooks/useAttach';
import { canUseDocument } from '../_util/dom';

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 @@ -72,10 +94,12 @@ const Dialog = forwardRef<DialogInstance, DialogProps>((originalProps, ref) => {
} = state;

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

useLockStyle({ preventScrollThrough, visible, mode, showInAttachedElement });
useDialogEsc(visible, wrapRef);
useDialogPosition(visible, dialogCardRef);

const { onDialogMoveStart } = useDialogDrag({
dialogCardRef,
contentClickRef,
Expand All @@ -90,6 +114,18 @@ const Dialog = forwardRef<DialogInstance, DialogProps>((originalProps, ref) => {
setState((prevState) => ({ ...prevState, ...props }));
}, [props, setState, isPlugin]);

useEffect(() => {
if (dialogAnimationVisible) {
wrapRef.current?.focus();
if (mousePosition && dialogCardRef.current) {
const offsetX = mousePosition.x - dialogCardRef.current.offsetLeft;
const offsetY = mousePosition.y - dialogCardRef.current.offsetTop;

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

useImperativeHandle(ref, () => ({
show() {
setState({ visible: true });
Expand Down Expand Up @@ -151,25 +187,30 @@ const Dialog = forwardRef<DialogInstance, DialogProps>((originalProps, ref) => {
}
};

const onAnimateLeave = () => {
onClosed?.();

// Portal Animation
const onAnimateStart = () => {
onBeforeOpen?.();
setAnimationVisible(true);
if (!wrapRef.current) return;
wrapRef.current.style.display = 'none';
wrapRef.current.style.display = 'block';
};

const onAnimateStart = () => {
const onAnimateLeave = () => {
onClosed?.();
setAnimationVisible(false);
if (!wrapRef.current) return;
onBeforeOpen?.();
wrapRef.current.style.display = 'block';
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';
};
Expand All @@ -191,6 +232,7 @@ const Dialog = forwardRef<DialogInstance, DialogProps>((originalProps, ref) => {
</CSSTransition>
) : null;
};

return (
<CSSTransition
in={visible}
Expand All @@ -201,7 +243,7 @@ const Dialog = forwardRef<DialogInstance, DialogProps>((originalProps, ref) => {
nodeRef={portalRef}
onEnter={onAnimateStart}
onEntered={onOpened}
onExit={() => onBeforeClose?.()}
onExit={onBeforeClose}
onExited={onAnimateLeave}
>
<Portal attach={dialogAttach} ref={portalRef}>
Expand All @@ -211,7 +253,7 @@ const Dialog = forwardRef<DialogInstance, DialogProps>((originalProps, ref) => {
[`${componentCls}__ctx--fixed`]: !showInAttachedElement,
[`${componentCls}__ctx--absolute`]: showInAttachedElement,
})}
style={{ zIndex, display: 'none' }}
style={{ zIndex, display: animationVisible ? undefined : 'none' }}
onKeyDown={handleKeyDown}
tabIndex={0}
>
Expand Down
19 changes: 13 additions & 6 deletions packages/components/dialog/__tests__/dialog.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { render, fireEvent, mockTimeout } from '@test/utils';
import { render, fireEvent, mockTimeout, vi } from '@test/utils';
import userEvent from '@testing-library/user-event';
import Dialog from '../index';
import { DialogPlugin } from '../plugin';
Expand All @@ -22,6 +22,7 @@ function DialogDemo(props) {
return (
<>
<div onClick={handleClick}>Open Dialog Modal</div>
<div onClick={handleClose}>Close Dialog Modal</div>
<Dialog
header="Basic Modal"
visible={visible}
Expand Down Expand Up @@ -56,19 +57,25 @@ describe('Dialog组件测试', () => {
});

test('EscCloseDialog', async () => {
const { getByText } = render(<DialogDemo mode="modal" draggable={false} />);
fireEvent.click(getByText('Open Dialog Modal'));
const onEscKeydown = vi.fn();
const { getByText } = render(<DialogDemo mode="modal" draggable={false} onEscKeydown={onEscKeydown} />);

await fireEvent.click(getByText('Open Dialog Modal'));
expect(document.querySelector('.t-dialog__modal')).toBeInTheDocument();
await user.keyboard('{Escape}');
await mockTimeout(() => expect(document.querySelector('.t-dialog__modal')).not.toBeInTheDocument(), 400);
expect(onEscKeydown).toHaveBeenCalled();
});

test('EnterConfirm', async () => {
const { getByText } = render(<DialogDemo mode="modal" draggable={false} />);
const onConfirm = vi.fn();
const { getByText } = render(<DialogDemo mode="modal" draggable={false} onConfirm={onConfirm} />);

expect(document.querySelector('.t-dialog__modal')).not.toBeInTheDocument();

fireEvent.click(getByText('Open Dialog Modal'));
expect(document.querySelector('.t-dialog__modal')).toBeInTheDocument();
await user.keyboard('{Enter}');
await mockTimeout(() => expect(document.querySelector('.t-dialog__modal')).not.toBeInTheDocument(), 400);
expect(onConfirm).toHaveBeenCalled();
});

test('DraggableDialog', () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/components/popup/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,10 @@ const Popup = forwardRef<PopupRef, PopupProps>((originalProps, ref) => {
// 下拉展开时更新内部滚动条
useEffect(() => {
if (!triggerRef.current) triggerRef.current = getTriggerDom();
if (visible) {
if (visible && popupElement) {
updateScrollTop?.(contentRef.current);
}
}, [visible, updateScrollTop, getTriggerDom]);
}, [visible, popupElement, updateScrollTop, getTriggerDom]);

function handleExited() {
!destroyOnClose && popupElement && (popupElement.style.display = 'none');
Expand Down
3 changes: 1 addition & 2 deletions packages/components/textarea/Textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,7 @@ const Textarea = forwardRef<TextareaRefInterface, TextareaProps>((originalProps,

useEffect(() => {
handleAutoFocus();
adjustTextareaHeight();
}, [handleAutoFocus, adjustTextareaHeight]);
}, [handleAutoFocus]);

useEffect(() => {
if (allowInputOverMax) {
Expand Down
2 changes: 1 addition & 1 deletion test/snap/__snapshots__/csr.test.jsx.snap

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion test/snap/__snapshots__/ssr.test.jsx.snap

Large diffs are not rendered by default.