diff --git a/docs/demos/portal.md b/docs/demos/portal.md new file mode 100644 index 00000000..5611bd1d --- /dev/null +++ b/docs/demos/portal.md @@ -0,0 +1,8 @@ +--- +title: Portal +nav: + title: Demo + path: /demo +--- + + diff --git a/docs/examples/portal.tsx b/docs/examples/portal.tsx new file mode 100644 index 00000000..a1b66576 --- /dev/null +++ b/docs/examples/portal.tsx @@ -0,0 +1,110 @@ +/* eslint no-console:0 */ + +import Trigger from 'rc-trigger'; +import React from 'react'; +import { createPortal } from 'react-dom'; +import '../../assets/index.less'; + +const builtinPlacements = { + left: { + points: ['cr', 'cl'], + }, + right: { + points: ['cl', 'cr'], + }, + top: { + points: ['bc', 'tc'], + }, + bottom: { + points: ['tc', 'bc'], + }, + topLeft: { + points: ['bl', 'tl'], + }, + topRight: { + points: ['br', 'tr'], + }, + bottomRight: { + points: ['tr', 'br'], + }, + bottomLeft: { + points: ['tl', 'bl'], + }, +}; + +const popupBorderStyle = { + border: '1px solid red', + padding: 10, + background: 'rgba(255, 0, 0, 0.1)', +}; + +const PortalPopup = () => + createPortal( +
{ + console.log('Portal Down', e); + e.stopPropagation(); + e.preventDefault(); + }} + > + i am a portal element +
, + document.body, + ); + +const Test = () => { + const buttonRef = React.useRef(null); + React.useEffect(() => { + const button = buttonRef.current; + if (button) { + button.addEventListener('mousedown', (e) => { + console.log('button natives down'); + e.stopPropagation(); + e.preventDefault(); + }); + } + }, []); + + return ( +
+ + i am a click popup + +
+ } + onPopupVisibleChange={(visible) => { + console.log('visible change:', visible); + }} + > + + + + + + + ); +}; + +export default Test; diff --git a/src/Popup/index.tsx b/src/Popup/index.tsx index e5d64d42..a1052ab2 100644 --- a/src/Popup/index.tsx +++ b/src/Popup/index.tsx @@ -20,6 +20,7 @@ export interface PopupProps { onMouseEnter?: React.MouseEventHandler; onMouseLeave?: React.MouseEventHandler; onPointerEnter?: React.MouseEventHandler; + onMouseDownCapture?: React.MouseEventHandler; zIndex?: number; mask?: boolean; @@ -105,6 +106,7 @@ const Popup = React.forwardRef((props, ref) => { onMouseEnter, onMouseLeave, onPointerEnter, + onMouseDownCapture, ready, offsetX, @@ -255,6 +257,7 @@ const Popup = React.forwardRef((props, ref) => { onMouseLeave={onMouseLeave} onPointerEnter={onPointerEnter} onClick={onClick} + onMouseDownCapture={onMouseDownCapture} > {arrow && ( (null); // =========================== Align ============================ - const [mousePos, setMousePos] = React.useState<[x: number, y: number] | null>(null); + const [mousePos, setMousePos] = React.useState< + [x: number, y: number] | null + >(null); const setMousePosByEvent = ( event: Pick, @@ -720,6 +722,14 @@ export function generateTrigger( fresh={fresh} // Click onClick={onPopupClick} + onMouseDownCapture={() => { + // Additional check for click to hide + // Since `createPortal` will not included in the popup element + // So we use capture to handle this + if (clickToHide) { + triggerOpen(true); + } + }} // Mask mask={mask} // Motion diff --git a/tests/basic.test.jsx b/tests/basic.test.jsx index aa4e87d4..e5eb0aa3 100644 --- a/tests/basic.test.jsx +++ b/tests/basic.test.jsx @@ -3,7 +3,7 @@ import { act, cleanup, fireEvent, render } from '@testing-library/react'; import { spyElementPrototypes } from 'rc-util/lib/test/domHook'; import React, { StrictMode, createRef } from 'react'; -import ReactDOM from 'react-dom'; +import ReactDOM, { createPortal } from 'react-dom'; import Trigger from '../src'; import { awaitFakeTimer, placementAlignMap } from './util'; @@ -107,7 +107,7 @@ describe('Trigger.Basic', () => { expect(document.querySelector('.x-content').textContent).toBe('tooltip2'); trigger(container, '.target'); - expect(isPopupHidden).toBeTruthy(); + expect(isPopupHidden()).toBeTruthy(); }); it('click works with function', () => { @@ -1198,4 +1198,35 @@ describe('Trigger.Basic', () => { trigger(container, '.target'); expect(document.querySelector('.x-content').textContent).toBe('false'); }); + + it('createPortal should not close', async () => { + const Portal = () => + createPortal(
, document.body); + + const Demo = () => { + return ( + <> + }> +
+ +
+ + ); + }; + + const { container } = render(); + fireEvent.click(container.querySelector('.target')); + await awaitFakeTimer(); + expect(isPopupHidden()).toBeFalsy(); + + // Click portal should not close + fireEvent.click(document.querySelector('.portal')); + await awaitFakeTimer(); + expect(isPopupHidden()).toBeFalsy(); + + // Click outside to close + fireEvent.mouseDown(container.querySelector('.outer')); + await awaitFakeTimer(); + expect(isPopupHidden()).toBeTruthy(); + }); });