Skip to content

Commit f438cb5

Browse files
authored
feat: support both overflow & shift (#350)
* docs: init demo * chore: support both * test: add test case
1 parent a31202a commit f438cb5

File tree

4 files changed

+280
-8
lines changed

4 files changed

+280
-8
lines changed

docs/demos/large-popup.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
title: Large Popup
3+
nav:
4+
title: Demo
5+
path: /demo
6+
---
7+
8+
<code src="../examples/large-popup.tsx"></code>

docs/examples/large-popup.tsx

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/* eslint no-console:0 */
2+
import Trigger from 'rc-trigger';
3+
import React from 'react';
4+
import '../../assets/index.less';
5+
6+
const builtinPlacements = {
7+
top: {
8+
points: ['bc', 'tc'],
9+
overflow: {
10+
shiftY: true,
11+
adjustY: true,
12+
},
13+
offset: [0, -10],
14+
},
15+
bottom: {
16+
points: ['tc', 'bc'],
17+
overflow: {
18+
shiftY: true,
19+
adjustY: true,
20+
},
21+
offset: [0, 10],
22+
htmlRegion: 'scroll' as const,
23+
},
24+
};
25+
26+
export default () => {
27+
const containerRef = React.useRef<HTMLDivElement>();
28+
29+
React.useEffect(() => {
30+
console.clear();
31+
containerRef.current.scrollTop = document.defaultView.innerHeight * 0.75;
32+
}, []);
33+
34+
return (
35+
<React.StrictMode>
36+
<div
37+
id="demo-root"
38+
style={{ background: 'rgba(0, 0, 255, 0.1)', padding: 16 }}
39+
>
40+
<div
41+
ref={containerRef}
42+
style={{
43+
border: '1px solid red',
44+
padding: 10,
45+
height: '100vh',
46+
background: '#FFF',
47+
position: 'relative',
48+
overflow: 'auto',
49+
}}
50+
>
51+
<div
52+
style={{
53+
height: '200vh',
54+
paddingTop: `100vh`,
55+
display: 'flex',
56+
justifyContent: 'center',
57+
alignItems: 'start',
58+
}}
59+
>
60+
<Trigger
61+
arrow
62+
// forceRender
63+
action="click"
64+
popup={
65+
<div
66+
style={{
67+
background: 'yellow',
68+
border: '1px solid blue',
69+
width: 200,
70+
height: '75vh',
71+
opacity: 0.9,
72+
}}
73+
>
74+
Popup 75vh
75+
</div>
76+
}
77+
popupStyle={{ boxShadow: '0 0 5px red' }}
78+
popupVisible
79+
popupPlacement="top"
80+
builtinPlacements={builtinPlacements}
81+
>
82+
<span
83+
style={{
84+
background: 'green',
85+
color: '#FFF',
86+
paddingBlock: 30,
87+
paddingInline: 70,
88+
opacity: 0.9,
89+
transform: 'scale(0.6)',
90+
display: 'inline-block',
91+
}}
92+
>
93+
Target
94+
</span>
95+
</Trigger>
96+
</div>
97+
</div>
98+
</div>
99+
100+
{/* <div style={{ height: '100vh' }} /> */}
101+
</React.StrictMode>
102+
);
103+
};

src/hooks/useAlign.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ export default function useAlign(
286286
nextOffsetY,
287287
);
288288

289-
// ================ Overflow =================
289+
// ========================== Overflow ===========================
290290
const targetAlignPointTL = getAlignPoint(targetRect, ['t', 'l']);
291291
const popupAlignPointTL = getAlignPoint(popupRect, ['t', 'l']);
292292
const targetAlignPointBR = getAlignPoint(targetRect, ['b', 'r']);
@@ -302,10 +302,21 @@ export default function useAlign(
302302
return val >= 0;
303303
};
304304

305-
// >>>>>>>>>> Top & Bottom
306-
const nextPopupY = popupRect.y + nextOffsetY;
307-
const nextPopupBottom = nextPopupY + popupHeight;
305+
// Prepare position
306+
let nextPopupY: number;
307+
let nextPopupBottom: number;
308+
let nextPopupX: number;
309+
let nextPopupRight: number;
310+
311+
function syncNextPopupPosition() {
312+
nextPopupY = popupRect.y + nextOffsetY;
313+
nextPopupBottom = nextPopupY + popupHeight;
314+
nextPopupX = popupRect.x + nextOffsetX;
315+
nextPopupRight = nextPopupX + popupWidth;
316+
}
317+
syncNextPopupPosition();
308318

319+
// >>>>>>>>>> Top & Bottom
309320
const needAdjustY = supportAdjust(adjustY);
310321

311322
const sameTB = popupPoints[0] === targetPoints[0];
@@ -367,9 +378,6 @@ export default function useAlign(
367378
}
368379

369380
// >>>>>>>>>> Left & Right
370-
const nextPopupX = popupRect.x + nextOffsetX;
371-
const nextPopupRight = nextPopupX + popupWidth;
372-
373381
const needAdjustX = supportAdjust(adjustX);
374382

375383
// >>>>> Flip
@@ -431,7 +439,9 @@ export default function useAlign(
431439
}
432440
}
433441

434-
// >>>>> Shift
442+
// ============================ Shift ============================
443+
syncNextPopupPosition();
444+
435445
const numShiftX = shiftX === true ? 0 : shiftX;
436446
if (typeof numShiftX === 'number') {
437447
// Left
@@ -476,6 +486,7 @@ export default function useAlign(
476486
}
477487
}
478488

489+
// ============================ Arrow ============================
479490
// Arrow center align
480491
const popupLeft = popupRect.x + nextOffsetX;
481492
const popupRight = popupLeft + popupWidth;

tests/flipShift.test.tsx

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { act, cleanup, render } from '@testing-library/react';
2+
import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
3+
import Trigger from '../src';
4+
5+
/*
6+
***********
7+
****************** * *
8+
* Placement * * Popup *
9+
* ********** * * *
10+
* * Target * * * *
11+
* ********** * ***********
12+
* *
13+
* *
14+
******************
15+
16+
When `placement` is `top`. It will find should flip to bottom:
17+
18+
******************
19+
* *
20+
* ********** *
21+
* * Target * *
22+
* ********** * *********** top: 200
23+
* Placement * * *
24+
* * * Popup *
25+
****************** * *
26+
* *
27+
***********
28+
29+
When `placement` is `bottom`. It will find should shift to show in viewport:
30+
31+
******************
32+
* *
33+
* ********** * *********** top: 100
34+
* * Target * * * *
35+
* ********** * * Popup *
36+
* Placement * * *
37+
* * * *
38+
****************** ***********
39+
40+
*/
41+
42+
const builtinPlacements = {
43+
top: {
44+
points: ['bc', 'tc'],
45+
overflow: {
46+
adjustY: true,
47+
shiftY: true,
48+
},
49+
},
50+
bottom: {
51+
points: ['tc', 'bc'],
52+
overflow: {
53+
adjustY: true,
54+
shiftY: true,
55+
},
56+
},
57+
left: {
58+
points: ['cr', 'cl'],
59+
overflow: {
60+
adjustX: true,
61+
shiftX: true,
62+
},
63+
},
64+
right: {
65+
points: ['cl', 'cr'],
66+
overflow: {
67+
adjustX: true,
68+
shiftX: true,
69+
},
70+
},
71+
};
72+
73+
describe('Trigger.Flip+Shift', () => {
74+
beforeAll(() => {
75+
// Viewport size
76+
spyElementPrototypes(HTMLElement, {
77+
clientWidth: {
78+
get: () => 400,
79+
},
80+
clientHeight: {
81+
get: () => 400,
82+
},
83+
});
84+
85+
// Popup size
86+
spyElementPrototypes(HTMLDivElement, {
87+
getBoundingClientRect() {
88+
return {
89+
x: 0,
90+
y: 0,
91+
width: 100,
92+
height: 300,
93+
};
94+
},
95+
});
96+
spyElementPrototypes(HTMLSpanElement, {
97+
getBoundingClientRect() {
98+
return {
99+
x: 0,
100+
y: 100,
101+
width: 100,
102+
height: 100,
103+
};
104+
},
105+
});
106+
spyElementPrototypes(HTMLElement, {
107+
offsetParent: {
108+
get: () => document.body,
109+
},
110+
});
111+
});
112+
113+
beforeEach(() => {
114+
jest.useFakeTimers();
115+
});
116+
117+
afterEach(() => {
118+
cleanup();
119+
jest.useRealTimers();
120+
});
121+
122+
it('both work', async () => {
123+
render(
124+
<Trigger
125+
popupVisible
126+
popupPlacement="top"
127+
builtinPlacements={builtinPlacements}
128+
popup={<strong>trigger</strong>}
129+
>
130+
<span className="target" />
131+
</Trigger>,
132+
);
133+
134+
await act(async () => {
135+
await Promise.resolve();
136+
});
137+
138+
console.log(document.body.innerHTML);
139+
140+
expect(
141+
document.querySelector('.rc-trigger-popup-placement-bottom'),
142+
).toBeTruthy();
143+
144+
expect(
145+
document.querySelector('.rc-trigger-popup-placement-bottom'),
146+
).toHaveStyle({
147+
top: '100px',
148+
});
149+
});
150+
});

0 commit comments

Comments
 (0)