diff --git a/packages/components/popup/Popup.tsx b/packages/components/popup/Popup.tsx index 595acdec43..dc295b3ab4 100644 --- a/packages/components/popup/Popup.tsx +++ b/packages/components/popup/Popup.tsx @@ -112,6 +112,7 @@ const Popup = forwardRef((originalProps, ref const { triggerElementIsString, getTriggerElement, getTriggerNode, getPopupProps } = useTrigger({ triggerElement, + popupElement, content, disabled, trigger, @@ -225,7 +226,9 @@ const Popup = forwardRef((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); diff --git a/packages/components/popup/__tests__/popup.test.tsx b/packages/components/popup/__tests__/popup.test.tsx index 260f7362b3..0b29d8a2b7 100644 --- a/packages/components/popup/__tests__/popup.test.tsx +++ b/packages/components/popup/__tests__/popup.test.tsx @@ -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( - {wrappedPopupText}} - > -
{wrappedTriggerElement}
-
- } - > - {triggerElement} - , - ); - - // 初始时,所有浮层都不存在 - 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( @@ -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( + {wrappedPopupText}} + > +
{wrappedTriggerElement}
+
+ } + > + {triggerElement} + , + ); + + 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(); + }); + }); +}); diff --git a/packages/components/popup/hooks/useTrigger.tsx b/packages/components/popup/hooks/useTrigger.tsx index 95b0f2bb13..dc3c7249b7 100644 --- a/packages/components/popup/hooks/useTrigger.tsx +++ b/packages/components/popup/hooks/useTrigger.tsx @@ -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'; @@ -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'; @@ -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' }), @@ -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; diff --git a/packages/tdesign-react/.changelog/pr-4099.md b/packages/tdesign-react/.changelog/pr-4099.md new file mode 100644 index 0000000000..dd4526f2b3 --- /dev/null +++ b/packages/tdesign-react/.changelog/pr-4099.md @@ -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))