Skip to content

Commit 355cd8c

Browse files
authored
fix: Only flip if space is enough (#343)
* chore: improve check logic only flip when has more space * test: add test case
1 parent 4eefd66 commit 355cd8c

File tree

3 files changed

+270
-25
lines changed

3 files changed

+270
-25
lines changed

docs/examples/container.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const builtinPlacements = {
5555
},
5656
};
5757

58-
const popupPlacement = 'bottom';
58+
const popupPlacement = 'right';
5959

6060
export default () => {
6161
console.log('Demo Render!');

src/hooks/useAlign.ts

Lines changed: 79 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,25 @@ export default function useAlign(
267267
let nextOffsetX = targetAlignPoint.x - popupAlignPoint.x + popupOffsetX;
268268
let nextOffsetY = targetAlignPoint.y - popupAlignPoint.y + popupOffsetY;
269269

270+
// ============== Intersection ===============
271+
// Get area by position. Used for check if flip area is better
272+
function getIntersectionVisibleArea(x: number, y: number) {
273+
const r = x + popupWidth;
274+
const b = y + popupHeight;
275+
276+
const visibleX = Math.max(x, visibleArea.left);
277+
const visibleY = Math.max(y, visibleArea.top);
278+
const visibleR = Math.min(r, visibleArea.right);
279+
const visibleB = Math.min(b, visibleArea.bottom);
280+
281+
return (visibleR - visibleX) * (visibleB - visibleY);
282+
}
283+
284+
const originIntersectionVisibleArea = getIntersectionVisibleArea(
285+
nextOffsetX,
286+
nextOffsetY,
287+
);
288+
270289
// ================ Overflow =================
271290
const targetAlignPointTL = getAlignPoint(targetRect, ['t', 'l']);
272291
const popupAlignPointTL = getAlignPoint(popupRect, ['t', 'l']);
@@ -297,17 +316,26 @@ export default function useAlign(
297316
popupPoints[0] === 't' &&
298317
nextPopupBottom > visibleArea.bottom
299318
) {
319+
let tmpNextOffsetY: number;
320+
300321
if (sameTB) {
301-
nextOffsetY -= popupHeight - targetHeight;
322+
tmpNextOffsetY -= popupHeight - targetHeight;
302323
} else {
303-
nextOffsetY =
324+
tmpNextOffsetY =
304325
targetAlignPointTL.y - popupAlignPointBR.y - popupOffsetY;
305326
}
306327

307-
nextAlignInfo.points = [
308-
reversePoints(popupPoints, 0),
309-
reversePoints(targetPoints, 0),
310-
];
328+
if (
329+
getIntersectionVisibleArea(nextOffsetX, tmpNextOffsetY) >
330+
originIntersectionVisibleArea
331+
) {
332+
nextOffsetY = tmpNextOffsetY;
333+
334+
nextAlignInfo.points = [
335+
reversePoints(popupPoints, 0),
336+
reversePoints(targetPoints, 0),
337+
];
338+
}
311339
}
312340

313341
// Top to Bottom
@@ -316,17 +344,26 @@ export default function useAlign(
316344
popupPoints[0] === 'b' &&
317345
nextPopupY < visibleArea.top
318346
) {
347+
let tmpNextOffsetY: number;
348+
319349
if (sameTB) {
320-
nextOffsetY += popupHeight - targetHeight;
350+
tmpNextOffsetY += popupHeight - targetHeight;
321351
} else {
322-
nextOffsetY =
352+
tmpNextOffsetY =
323353
targetAlignPointBR.y - popupAlignPointTL.y - popupOffsetY;
324354
}
325355

326-
nextAlignInfo.points = [
327-
reversePoints(popupPoints, 0),
328-
reversePoints(targetPoints, 0),
329-
];
356+
if (
357+
getIntersectionVisibleArea(nextOffsetX, tmpNextOffsetY) >
358+
originIntersectionVisibleArea
359+
) {
360+
nextOffsetY = tmpNextOffsetY;
361+
362+
nextAlignInfo.points = [
363+
reversePoints(popupPoints, 0),
364+
reversePoints(targetPoints, 0),
365+
];
366+
}
330367
}
331368

332369
// >>>>>>>>>> Left & Right
@@ -344,17 +381,26 @@ export default function useAlign(
344381
popupPoints[1] === 'l' &&
345382
nextPopupRight > visibleArea.right
346383
) {
384+
let tmpNextOffsetX: number;
385+
347386
if (sameLR) {
348-
nextOffsetX -= popupWidth - targetWidth;
387+
tmpNextOffsetX -= popupWidth - targetWidth;
349388
} else {
350-
nextOffsetX =
389+
tmpNextOffsetX =
351390
targetAlignPointTL.x - popupAlignPointBR.x - popupOffsetX;
352391
}
353392

354-
nextAlignInfo.points = [
355-
reversePoints(popupPoints, 1),
356-
reversePoints(targetPoints, 1),
357-
];
393+
if (
394+
getIntersectionVisibleArea(tmpNextOffsetX, nextOffsetY) >
395+
originIntersectionVisibleArea
396+
) {
397+
nextOffsetX = tmpNextOffsetX;
398+
399+
nextAlignInfo.points = [
400+
reversePoints(popupPoints, 1),
401+
reversePoints(targetPoints, 1),
402+
];
403+
}
358404
}
359405

360406
// Left to Right
@@ -363,17 +409,26 @@ export default function useAlign(
363409
popupPoints[1] === 'r' &&
364410
nextPopupX < visibleArea.left
365411
) {
412+
let tmpNextOffsetX: number;
413+
366414
if (sameLR) {
367-
nextOffsetX += popupWidth - targetWidth;
415+
tmpNextOffsetX += popupWidth - targetWidth;
368416
} else {
369-
nextOffsetX =
417+
tmpNextOffsetX =
370418
targetAlignPointBR.x - popupAlignPointTL.x - popupOffsetX;
371419
}
372420

373-
nextAlignInfo.points = [
374-
reversePoints(popupPoints, 1),
375-
reversePoints(targetPoints, 1),
376-
];
421+
if (
422+
getIntersectionVisibleArea(tmpNextOffsetX, nextOffsetY) >
423+
originIntersectionVisibleArea
424+
) {
425+
nextOffsetX = tmpNextOffsetX;
426+
427+
nextAlignInfo.points = [
428+
reversePoints(popupPoints, 1),
429+
reversePoints(targetPoints, 1),
430+
];
431+
}
377432
}
378433

379434
// >>>>> Shift

tests/flip.test.tsx

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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+
const builtinPlacements = {
6+
top: {
7+
points: ['bc', 'tc'],
8+
overflow: {
9+
adjustX: true,
10+
adjustY: true,
11+
},
12+
},
13+
bottom: {
14+
points: ['tc', 'bc'],
15+
overflow: {
16+
adjustX: true,
17+
adjustY: true,
18+
},
19+
},
20+
left: {
21+
points: ['cr', 'cl'],
22+
overflow: {
23+
adjustX: true,
24+
adjustY: true,
25+
},
26+
},
27+
right: {
28+
points: ['cl', 'cr'],
29+
overflow: {
30+
adjustX: true,
31+
adjustY: true,
32+
},
33+
},
34+
};
35+
36+
describe('Trigger.Align', () => {
37+
let spanRect = {
38+
x: 0,
39+
y: 0,
40+
width: 1,
41+
height: 1,
42+
};
43+
44+
beforeAll(() => {
45+
spyElementPrototypes(HTMLElement, {
46+
clientWidth: {
47+
get: () => 100,
48+
},
49+
clientHeight: {
50+
get: () => 100,
51+
},
52+
});
53+
54+
spyElementPrototypes(HTMLDivElement, {
55+
getBoundingClientRect() {
56+
return {
57+
x: 0,
58+
y: 0,
59+
width: 100,
60+
height: 100,
61+
};
62+
},
63+
});
64+
65+
spyElementPrototypes(HTMLSpanElement, {
66+
getBoundingClientRect() {
67+
return spanRect;
68+
},
69+
});
70+
71+
spyElementPrototypes(HTMLElement, {
72+
offsetParent: {
73+
get: () => document.body,
74+
},
75+
});
76+
});
77+
78+
beforeEach(() => {
79+
spanRect = {
80+
x: 0,
81+
y: 0,
82+
width: 1,
83+
height: 1,
84+
};
85+
jest.useFakeTimers();
86+
});
87+
88+
afterEach(() => {
89+
cleanup();
90+
jest.useRealTimers();
91+
});
92+
93+
describe('not flip if cant', () => {
94+
const list = [
95+
{
96+
placement: 'right',
97+
x: 10,
98+
className: '.rc-trigger-popup-placement-right',
99+
},
100+
{
101+
placement: 'left',
102+
x: 90,
103+
className: '.rc-trigger-popup-placement-left',
104+
},
105+
{
106+
placement: 'top',
107+
y: 90,
108+
className: '.rc-trigger-popup-placement-top',
109+
},
110+
{
111+
placement: 'bottom',
112+
y: 10,
113+
className: '.rc-trigger-popup-placement-bottom',
114+
},
115+
];
116+
117+
list.forEach(({ placement, x = 0, y = 0, className }) => {
118+
it(placement, async () => {
119+
spanRect.x = x;
120+
spanRect.y = y;
121+
122+
render(
123+
<Trigger
124+
popupVisible
125+
popupPlacement={placement}
126+
builtinPlacements={builtinPlacements}
127+
popup={<strong>trigger</strong>}
128+
>
129+
<span className="target" />
130+
</Trigger>,
131+
);
132+
133+
await act(async () => {
134+
await Promise.resolve();
135+
});
136+
137+
expect(document.querySelector(className)).toBeTruthy();
138+
});
139+
});
140+
});
141+
142+
describe('flip if can', () => {
143+
const list = [
144+
{
145+
placement: 'right',
146+
x: 90,
147+
className: '.rc-trigger-popup-placement-left',
148+
},
149+
{
150+
placement: 'left',
151+
x: 10,
152+
className: '.rc-trigger-popup-placement-right',
153+
},
154+
{
155+
placement: 'top',
156+
y: 10,
157+
className: '.rc-trigger-popup-placement-bottom',
158+
},
159+
{
160+
placement: 'bottom',
161+
y: 90,
162+
className: '.rc-trigger-popup-placement-top',
163+
},
164+
];
165+
166+
list.forEach(({ placement, x = 0, y = 0, className }) => {
167+
it(placement, async () => {
168+
spanRect.x = x;
169+
spanRect.y = y;
170+
171+
render(
172+
<Trigger
173+
popupVisible
174+
popupPlacement={placement}
175+
builtinPlacements={builtinPlacements}
176+
popup={<strong>trigger</strong>}
177+
>
178+
<span className="target" />
179+
</Trigger>,
180+
);
181+
182+
await act(async () => {
183+
await Promise.resolve();
184+
});
185+
186+
expect(document.querySelector(className)).toBeTruthy();
187+
});
188+
});
189+
});
190+
});

0 commit comments

Comments
 (0)