Skip to content

Commit 0fa6243

Browse files
authored
fix: try to fix createPortal close case (#492)
* docs: add debug demo * fix: trigger open logic * test: add test case
1 parent 8abc4f9 commit 0fa6243

File tree

5 files changed

+165
-3
lines changed

5 files changed

+165
-3
lines changed

docs/demos/portal.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
title: Portal
3+
nav:
4+
title: Demo
5+
path: /demo
6+
---
7+
8+
<code src="../examples/portal.tsx"></code>

docs/examples/portal.tsx

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/* eslint no-console:0 */
2+
3+
import Trigger from 'rc-trigger';
4+
import React from 'react';
5+
import { createPortal } from 'react-dom';
6+
import '../../assets/index.less';
7+
8+
const builtinPlacements = {
9+
left: {
10+
points: ['cr', 'cl'],
11+
},
12+
right: {
13+
points: ['cl', 'cr'],
14+
},
15+
top: {
16+
points: ['bc', 'tc'],
17+
},
18+
bottom: {
19+
points: ['tc', 'bc'],
20+
},
21+
topLeft: {
22+
points: ['bl', 'tl'],
23+
},
24+
topRight: {
25+
points: ['br', 'tr'],
26+
},
27+
bottomRight: {
28+
points: ['tr', 'br'],
29+
},
30+
bottomLeft: {
31+
points: ['tl', 'bl'],
32+
},
33+
};
34+
35+
const popupBorderStyle = {
36+
border: '1px solid red',
37+
padding: 10,
38+
background: 'rgba(255, 0, 0, 0.1)',
39+
};
40+
41+
const PortalPopup = () =>
42+
createPortal(
43+
<div
44+
style={popupBorderStyle}
45+
onMouseDown={(e) => {
46+
console.log('Portal Down', e);
47+
e.stopPropagation();
48+
e.preventDefault();
49+
}}
50+
>
51+
i am a portal element
52+
</div>,
53+
document.body,
54+
);
55+
56+
const Test = () => {
57+
const buttonRef = React.useRef<HTMLButtonElement>(null);
58+
React.useEffect(() => {
59+
const button = buttonRef.current;
60+
if (button) {
61+
button.addEventListener('mousedown', (e) => {
62+
console.log('button natives down');
63+
e.stopPropagation();
64+
e.preventDefault();
65+
});
66+
}
67+
}, []);
68+
69+
return (
70+
<div
71+
style={{
72+
padding: 100,
73+
display: 'flex',
74+
flexDirection: 'column',
75+
alignItems: 'flex-start',
76+
gap: 100,
77+
}}
78+
>
79+
<Trigger
80+
popupPlacement="right"
81+
action={['click']}
82+
builtinPlacements={builtinPlacements}
83+
popup={
84+
<div style={popupBorderStyle}>
85+
i am a click popup
86+
<PortalPopup />
87+
</div>
88+
}
89+
onPopupVisibleChange={(visible) => {
90+
console.log('visible change:', visible);
91+
}}
92+
>
93+
<button>Click Me</button>
94+
</Trigger>
95+
96+
<button
97+
onMouseDown={(e) => {
98+
console.log('button down');
99+
e.stopPropagation();
100+
e.preventDefault();
101+
}}
102+
>
103+
Stop Pop & Prevent Default
104+
</button>
105+
<button ref={buttonRef}>Native Stop Pop & Prevent Default</button>
106+
</div>
107+
);
108+
};
109+
110+
export default Test;

src/Popup/index.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export interface PopupProps {
2020
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
2121
onMouseLeave?: React.MouseEventHandler<HTMLDivElement>;
2222
onPointerEnter?: React.MouseEventHandler<HTMLDivElement>;
23+
onMouseDownCapture?: React.MouseEventHandler<HTMLDivElement>;
2324
zIndex?: number;
2425

2526
mask?: boolean;
@@ -105,6 +106,7 @@ const Popup = React.forwardRef<HTMLDivElement, PopupProps>((props, ref) => {
105106
onMouseEnter,
106107
onMouseLeave,
107108
onPointerEnter,
109+
onMouseDownCapture,
108110

109111
ready,
110112
offsetX,
@@ -255,6 +257,7 @@ const Popup = React.forwardRef<HTMLDivElement, PopupProps>((props, ref) => {
255257
onMouseLeave={onMouseLeave}
256258
onPointerEnter={onPointerEnter}
257259
onClick={onClick}
260+
onMouseDownCapture={onMouseDownCapture}
258261
>
259262
{arrow && (
260263
<Arrow

src/index.tsx

+11-1
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,9 @@ export function generateTrigger(
381381
React.useState<VoidFunction>(null);
382382

383383
// =========================== Align ============================
384-
const [mousePos, setMousePos] = React.useState<[x: number, y: number] | null>(null);
384+
const [mousePos, setMousePos] = React.useState<
385+
[x: number, y: number] | null
386+
>(null);
385387

386388
const setMousePosByEvent = (
387389
event: Pick<React.MouseEvent, 'clientX' | 'clientY'>,
@@ -720,6 +722,14 @@ export function generateTrigger(
720722
fresh={fresh}
721723
// Click
722724
onClick={onPopupClick}
725+
onMouseDownCapture={() => {
726+
// Additional check for click to hide
727+
// Since `createPortal` will not included in the popup element
728+
// So we use capture to handle this
729+
if (clickToHide) {
730+
triggerOpen(true);
731+
}
732+
}}
723733
// Mask
724734
mask={mask}
725735
// Motion

tests/basic.test.jsx

+33-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { act, cleanup, fireEvent, render } from '@testing-library/react';
44
import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
55
import React, { StrictMode, createRef } from 'react';
6-
import ReactDOM from 'react-dom';
6+
import ReactDOM, { createPortal } from 'react-dom';
77
import Trigger from '../src';
88
import { awaitFakeTimer, placementAlignMap } from './util';
99

@@ -107,7 +107,7 @@ describe('Trigger.Basic', () => {
107107
expect(document.querySelector('.x-content').textContent).toBe('tooltip2');
108108

109109
trigger(container, '.target');
110-
expect(isPopupHidden).toBeTruthy();
110+
expect(isPopupHidden()).toBeTruthy();
111111
});
112112

113113
it('click works with function', () => {
@@ -1198,4 +1198,35 @@ describe('Trigger.Basic', () => {
11981198
trigger(container, '.target');
11991199
expect(document.querySelector('.x-content').textContent).toBe('false');
12001200
});
1201+
1202+
it('createPortal should not close', async () => {
1203+
const Portal = () =>
1204+
createPortal(<div className="portal" />, document.body);
1205+
1206+
const Demo = () => {
1207+
return (
1208+
<>
1209+
<Trigger action="click" popup={<Portal />}>
1210+
<div className="target" />
1211+
</Trigger>
1212+
<div className="outer" />
1213+
</>
1214+
);
1215+
};
1216+
1217+
const { container } = render(<Demo />);
1218+
fireEvent.click(container.querySelector('.target'));
1219+
await awaitFakeTimer();
1220+
expect(isPopupHidden()).toBeFalsy();
1221+
1222+
// Click portal should not close
1223+
fireEvent.click(document.querySelector('.portal'));
1224+
await awaitFakeTimer();
1225+
expect(isPopupHidden()).toBeFalsy();
1226+
1227+
// Click outside to close
1228+
fireEvent.mouseDown(container.querySelector('.outer'));
1229+
await awaitFakeTimer();
1230+
expect(isPopupHidden()).toBeTruthy();
1231+
});
12011232
});

0 commit comments

Comments
 (0)