Skip to content
Merged
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
5 changes: 4 additions & 1 deletion packages/components/popup/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ const Popup = forwardRef<PopupInstanceFunctions, PopupProps>((originalProps, ref

const { triggerElementIsString, getTriggerElement, getTriggerNode, getPopupProps } = useTrigger({
triggerElement,
popupElement,
content,
disabled,
trigger,
Expand Down Expand Up @@ -225,7 +226,9 @@ const Popup = forwardRef<PopupInstanceFunctions, PopupProps>((originalProps, ref

function handleExited() {
setIsOverlayHover(false);
!destroyOnClose && popupElement && (popupElement.style.display = 'none');
if (!destroyOnClose && popupElement) popupElement.style.display = 'none';
// 如果是 destroyOnClose 需要重置 popupElement 否则影响二次操作的判断
else setPopupElement(null);
}
function handleEnter() {
setIsOverlayHover(true);
Expand Down
160 changes: 104 additions & 56 deletions packages/components/popup/__tests__/popup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,62 +264,6 @@ describe('Popup 组件测试', () => {
expect(popupContainer).toBeNull();
});

test('测试浮层嵌套', async () => {
const wrappedTriggerElement = '嵌套触发元素';
const wrappedPopupTestId = 'wrapped-popup-test-id';
const wrappedPopupText = '嵌套弹出层内容';
const { getByText, queryByTestId } = render(
<Popup
placement="top"
trigger="click"
destroyOnClose
content={
<Popup
placement="top"
trigger="click"
destroyOnClose
content={<div data-testid={wrappedPopupTestId}>{wrappedPopupText}</div>}
>
<div data-testid={popupTestId}>{wrappedTriggerElement}</div>
</Popup>
}
>
{triggerElement}
</Popup>,
);

// 初始时,所有浮层都不存在
const popupElement1 = await waitFor(() => queryByTestId(popupTestId));
const wrappedPopupElement1 = await waitFor(() => queryByTestId(wrappedPopupTestId));
expect(popupElement1).toBeNull();
expect(wrappedPopupElement1).toBeNull();

// 触发浮层和嵌套浮层
act(() => {
fireEvent.click(getByText(triggerElement));
});
act(() => {
fireEvent.click(getByText(wrappedTriggerElement));
});

// 所有浮层都展示出来
const popupElement2 = await waitFor(() => queryByTestId(popupTestId));
const wrappedPopupElement2 = await waitFor(() => queryByTestId(wrappedPopupTestId));
expect(popupElement2).not.toBeNull();
expect(wrappedPopupElement2).not.toBeNull();

// 嵌套元素的浮层触发 mouseDown,不应该关闭任何浮层
act(() => {
fireEvent.mouseDown(queryByTestId(wrappedPopupTestId));
});

// 所有浮层都展示出来
const popupElement3 = await waitFor(() => queryByTestId(popupTestId));
const wrappedPopupElement3 = await waitFor(() => queryByTestId(wrappedPopupTestId));
expect(popupElement3).not.toBeNull();
expect(wrappedPopupElement3).not.toBeNull();
});

test('异常情况:浮层隐藏时点击其他地方,浮层不可以展示出来', async () => {
const testClassName = 'test-class-name';
render(
Expand Down Expand Up @@ -395,3 +339,107 @@ describe('Popup 组件测试', () => {
});
});
});

describe('Popup 嵌套组件测试', () => {
const popupTestId = 'popup-test-id';
const wrappedPopupTestId = 'wrapped-popup-test-id';
const triggerElement = '外层触发元素';
const wrappedTriggerElement = '内层触发元素';
const wrappedPopupText = '内层浮层内容';

const renderNestedPopup = (trigger: 'click' | 'hover') =>
render(
<Popup
placement="top"
trigger={trigger}
destroyOnClose
content={
<Popup
placement="top"
trigger={trigger}
destroyOnClose
content={<div data-testid={wrappedPopupTestId}>{wrappedPopupText}</div>}
>
<div data-testid={popupTestId}>{wrappedTriggerElement}</div>
</Popup>
}
>
{triggerElement}
</Popup>,
);

test('trigger="click"', async () => {
const { getByText, queryByTestId } = renderNestedPopup('click');

// 初始状态,浮层不存在
expect(queryByTestId(popupTestId)).toBeNull();
expect(queryByTestId(wrappedPopupTestId)).toBeNull();

// click 外层触发器
act(() => {
fireEvent.click(getByText(triggerElement));
});
const popupElement = await waitFor(() => queryByTestId(popupTestId));
expect(popupElement).not.toBeNull();

// click 内层触发器
act(() => {
fireEvent.click(getByText(wrappedTriggerElement));
});
const wrappedPopupElement = await waitFor(() => queryByTestId(wrappedPopupTestId));
expect(wrappedPopupElement).not.toBeNull();
expect(wrappedPopupElement).toHaveTextContent(wrappedPopupText);

// mouseDown 内层内容不关闭
act(() => {
fireEvent.mouseDown(queryByTestId(wrappedPopupTestId) as HTMLElement);
});
await waitFor(() => {
expect(popupElement).not.toBeNull();
expect(wrappedPopupElement).not.toBeNull();
});
});

test('trigger="hover"', async () => {
const { getByText, getByTestId, queryByTestId } = renderNestedPopup('hover');

// 初始状态,浮层不存在
expect(queryByTestId(popupTestId)).toBeNull();
expect(queryByTestId(wrappedPopupTestId)).toBeNull();

// hover 外层触发器
act(() => {
fireEvent.mouseEnter(getByText(triggerElement));
});
const popupElement = await waitFor(() => queryByTestId(popupTestId));
expect(popupElement).not.toBeNull();

// hover 内层触发器
act(() => {
fireEvent.mouseEnter(getByTestId(popupTestId));
});
const wrappedPopupElement = await waitFor(() => queryByTestId(wrappedPopupTestId));
expect(wrappedPopupElement).not.toBeNull();
expect(wrappedPopupElement).toHaveTextContent(wrappedPopupText);

// mouseLeave 内层触发器
act(() => {
fireEvent.mouseLeave(getByTestId(popupTestId));
});

// 等待内层浮层销毁
await waitFor(() => {
expect(queryByTestId(wrappedPopupTestId)).toBeNull();
});

// mouseLeave 外层触发器
act(() => {
fireEvent.mouseLeave(getByText(triggerElement));
});

// 等待外层浮层销毁
await waitFor(() => {
expect(queryByTestId(popupTestId)).toBeNull();
});
});
});
39 changes: 14 additions & 25 deletions packages/components/popup/hooks/useTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { canUseDocument } from '../../_util/dom';
import { off, on } from '../../_util/listener';
import { composeRefs, getNodeRef, getRefDom, supportNodeRef } from '../../_util/ref';
import useConfig from '../../hooks/useConfig';
import useResizeObserver from '../../hooks/useResizeObserver';

const ESC_KEY = 'Escape';

Expand All @@ -13,7 +12,16 @@ const isEventFromDisabledElement = (e: Event | React.SyntheticEvent, container:
return !!(disabledEl && container.contains(disabledEl));
};

export default function useTrigger({ triggerElement, content, disabled, trigger, visible, onVisibleChange, delay }) {
export default function useTrigger({
triggerElement,
content,
disabled,
trigger,
visible,
onVisibleChange,
delay,
popupElement,
}) {
const { classPrefix } = useConfig();

const triggerElementIsString = typeof triggerElement === 'string';
Expand Down Expand Up @@ -53,8 +61,10 @@ export default function useTrigger({ triggerElement, content, disabled, trigger,
const handleMouseLeave = (e: MouseEvent | React.MouseEvent) => {
if (trigger !== 'hover' || hasPopupMouseDown.current) return;
const relatedTarget = e.relatedTarget as HTMLElement;
const isMovingToContent = relatedTarget?.closest?.(`.${classPrefix}-popup`);
if (isMovingToContent) return;
const closestPopup = relatedTarget?.closest?.(`.${classPrefix}-popup`);

const isMovingToCurrentPopup = popupElement ? popupElement?.isEqualNode?.(closestPopup) : closestPopup;
if (isMovingToCurrentPopup) return;
callFuncWithDelay({
delay: exitDelay,
callback: () => onVisibleChange(false, { e, trigger: 'trigger-element-hover' }),
Expand Down Expand Up @@ -216,27 +226,6 @@ export default function useTrigger({ triggerElement, content, disabled, trigger,
};
}, [visible, classPrefix, getTriggerElement]);

useResizeObserver(
triggerRef,
(entries) => {
entries.forEach((entry) => {
// 嵌套使用
// 针对父 Popup 关闭时,trigger 隐藏的场景
if (entry.contentRect.width === 0 && entry.contentRect.height === 0) {
const element = entry.target as HTMLElement;
// 检查元素是否真的被隐藏(完全通过判断尺寸为 0x0,会误判 inline 元素)
const computedStyle = window.getComputedStyle(element);
const isHidden =
computedStyle.display === 'none' || computedStyle.visibility === 'hidden' || computedStyle.opacity === '0';
if (isHidden) {
onVisibleChange(false, { trigger: 'document' });
}
}
});
},
visible && shouldToggle,
);

function getTriggerNode(children: React.ReactNode) {
if (triggerElementIsString) return;

Expand Down
7 changes: 7 additions & 0 deletions packages/tdesign-react/.changelog/pr-4099.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
pr_number: 4099
contributor: uyarn
---

- fix(Popup): 修复组件嵌套使用时的关闭逻辑 @uyarn ([#4099](https://github.com/Tencent/tdesign-react/pull/4099))
- chore(Popup): 优化开启 `destroyOnClose` 时的内部状态,确保逻辑正常 @uyarn ([#4099](https://github.com/Tencent/tdesign-react/pull/4099))
Loading