diff --git a/docs/examples/shadow.tsx b/docs/examples/shadow.tsx index 6a1f5a29..562f6d5b 100644 --- a/docs/examples/shadow.tsx +++ b/docs/examples/shadow.tsx @@ -58,13 +58,21 @@ const Demo = () => { export default () => { React.useEffect(() => { + const wrapperHost = document.createElement('div'); + const wrapperShadowRoot = wrapperHost.attachShadow({ + mode: 'open', + delegatesFocus: false, + }); + document.body.appendChild(wrapperHost); + const host = document.createElement('div'); - document.body.appendChild(host); + wrapperShadowRoot.appendChild(host); host.style.background = 'rgba(255,0,0,0.1)'; const shadowRoot = host.attachShadow({ mode: 'open', delegatesFocus: false, }); + const container = document.createElement('div'); shadowRoot.appendChild(container); diff --git a/src/hooks/useWinClick.ts b/src/hooks/useWinClick.ts index f33f05b0..c6acc482 100644 --- a/src/hooks/useWinClick.ts +++ b/src/hooks/useWinClick.ts @@ -19,8 +19,11 @@ export default function useWinClick( // Click to hide is special action since click popup element should not hide React.useEffect(() => { if (clickToHide && popupEle && (!mask || maskClosable)) { - const onTriggerClose = ({ target }: MouseEvent) => { - if (openRef.current && !inPopupOrChild(target)) { + const onTriggerClose = (e: MouseEvent) => { + if ( + openRef.current && + !inPopupOrChild(e.composedPath?.()?.[0] || e.target) + ) { triggerOpen(false); } }; diff --git a/src/index.tsx b/src/index.tsx index 4a7b990f..f59b0386 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -272,18 +272,19 @@ export function generateTrigger( const originChildProps = child?.props || {}; const cloneProps: typeof originChildProps = {}; - const inPopupOrChild = useEvent((ele: any) => { + const inPopupOrChild = useEvent((ele: EventTarget) => { const childDOM = targetEle; return ( - childDOM?.contains(ele) || + childDOM?.contains(ele as HTMLElement) || getShadowRoot(childDOM)?.host === ele || ele === childDOM || - popupEle?.contains(ele) || + popupEle?.contains(ele as HTMLElement) || getShadowRoot(popupEle)?.host === ele || ele === popupEle || Object.values(subPopupElements.current).some( - (subPopupEle) => subPopupEle?.contains(ele) || ele === subPopupEle, + (subPopupEle) => + subPopupEle?.contains(ele as HTMLElement) || ele === subPopupEle, ) ); }); diff --git a/tests/shadow.test.tsx b/tests/shadow.test.tsx index 9cf4b069..b7e05802 100644 --- a/tests/shadow.test.tsx +++ b/tests/shadow.test.tsx @@ -55,6 +55,34 @@ describe('Trigger.Shadow', () => { return shadowRoot; }; + const renderMultiLevelShadow = (props?: any) => { + const noRelatedSpan = document.createElement('span'); + document.body.appendChild(noRelatedSpan); + + const wrapperHost = document.createElement('div'); + const wrapperShadowRoot = wrapperHost.attachShadow({ + mode: 'open', + delegatesFocus: false, + }); + document.body.appendChild(wrapperHost); + + const host = document.createElement('div'); + wrapperShadowRoot.appendChild(host); + + const shadowRoot = host.attachShadow({ + mode: 'open', + delegatesFocus: false, + }); + const container = document.createElement('div'); + shadowRoot.appendChild(container); + + act(() => { + createRoot(container).render(); + }); + + return shadowRoot; + }; + it('popup not in the same shadow', async () => { const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); const shadowRoot = renderShadow(); @@ -103,4 +131,70 @@ describe('Trigger.Shadow', () => { expect(errSpy).not.toHaveBeenCalled(); errSpy.mockRestore(); }); + + it('click on target in shadow should not close popup', async () => { + const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const shadowRoot = renderShadow({ + getPopupContainer: (item: HTMLElement) => item.parentElement, + autoDestroy: true, + }); + + await awaitFakeTimer(); + + // Click to show + fireEvent.click(shadowRoot.querySelector('.target')); + await awaitFakeTimer(); + expect(shadowRoot.querySelector('.bamboo')).toBeTruthy(); + + // Click on target + fireEvent.mouseDown(shadowRoot.querySelector('.bamboo')); + await awaitFakeTimer(); + expect(shadowRoot.querySelector('.bamboo')).toBeTruthy(); + + expect(errSpy).not.toHaveBeenCalled(); + errSpy.mockRestore(); + }); + + it('click on target with multilevel shadows should not close popup', async () => { + const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const shadowRoot = renderMultiLevelShadow({ + getPopupContainer: (item: HTMLElement) => item.parentElement, + autoDestroy: true, + }); + + await awaitFakeTimer(); + + // Click to show + fireEvent.click(shadowRoot.querySelector('.target')); + await awaitFakeTimer(); + expect(shadowRoot.querySelector('.bamboo')).toBeTruthy(); + + // Click outside to hide + fireEvent.mouseDown(document.body.firstChild); + await awaitFakeTimer(); + expect(shadowRoot.querySelector('.bamboo')).toBeFalsy(); + + // Click to show again + fireEvent.click(shadowRoot.querySelector('.target')); + await awaitFakeTimer(); + expect(shadowRoot.querySelector('.bamboo')).toBeTruthy(); + + // Click in side shadow to hide + fireEvent.mouseDown(shadowRoot.querySelector('.little')); + await awaitFakeTimer(); + expect(shadowRoot.querySelector('.bamboo')).toBeFalsy(); + + // Click to show again + fireEvent.click(shadowRoot.querySelector('.target')); + await awaitFakeTimer(); + expect(shadowRoot.querySelector('.bamboo')).toBeTruthy(); + + // Click on target should not hide + fireEvent.mouseDown(shadowRoot.querySelector('.bamboo')); + await awaitFakeTimer(); + expect(shadowRoot.querySelector('.bamboo')).toBeTruthy(); + + expect(errSpy).not.toHaveBeenCalled(); + errSpy.mockRestore(); + }); });