Skip to content

Commit b784f8f

Browse files
authored
fix: Area cal should skip static parent (#352)
* refactor: extract area cal * docs: add demo * chore: align fix * test: fin test case
1 parent 41b40a4 commit b784f8f

File tree

6 files changed

+227
-37
lines changed

6 files changed

+227
-37
lines changed

docs/demos/static-scroll.md

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

docs/examples/inside.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React from 'react';
33
import '../../assets/index.less';
44
import Trigger from '../../src';
55

6-
const builtinPlacements = {
6+
export const builtinPlacements = {
77
top: {
88
points: ['bc', 'tc'],
99
overflow: {

docs/examples/static-scroll.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/* eslint no-console:0 */
2+
import Trigger from 'rc-trigger';
3+
import React from 'react';
4+
import '../../assets/index.less';
5+
import { builtinPlacements } from './inside';
6+
7+
export default () => {
8+
return (
9+
<React.StrictMode>
10+
<div
11+
style={{
12+
background: 'rgba(0, 0, 255, 0.1)',
13+
margin: `64px`,
14+
height: 200,
15+
overflow: 'auto',
16+
// Must have for test
17+
position: 'static',
18+
}}
19+
>
20+
<Trigger
21+
arrow
22+
popup={
23+
<div
24+
style={{
25+
background: 'yellow',
26+
border: '1px solid blue',
27+
width: 200,
28+
height: 60,
29+
opacity: 0.9,
30+
}}
31+
>
32+
Popup
33+
</div>
34+
}
35+
popupStyle={{ boxShadow: '0 0 5px red' }}
36+
popupVisible
37+
builtinPlacements={builtinPlacements}
38+
popupPlacement="top"
39+
stretch="minWidth"
40+
getPopupContainer={(e) => e.parentElement!}
41+
>
42+
<span
43+
style={{
44+
background: 'green',
45+
color: '#FFF',
46+
paddingBlock: 30,
47+
paddingInline: 70,
48+
opacity: 0.9,
49+
transform: 'scale(0.6)',
50+
display: 'inline-block',
51+
}}
52+
>
53+
Target
54+
</span>
55+
</Trigger>
56+
{new Array(20).fill(null).map((_, index) => (
57+
<h1 key={index} style={{ width: '200vw' }}>
58+
Placeholder Line {index}
59+
</h1>
60+
))}
61+
</div>
62+
</React.StrictMode>
63+
);
64+
};

src/hooks/useAlign.ts

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,12 @@ import type {
99
AlignPointTopBottom,
1010
AlignType,
1111
} from '../interface';
12-
import { collectScroller, getWin } from '../util';
12+
import { collectScroller, getVisibleArea, getWin, toNum } from '../util';
1313

1414
type Rect = Record<'x' | 'y' | 'width' | 'height', number>;
1515

1616
type Points = [topBottom: AlignPointTopBottom, leftRight: AlignPointLeftRight];
1717

18-
function toNum(num: number) {
19-
return Number.isNaN(num) ? 1 : num;
20-
}
21-
2218
function splitPoints(points: string = ''): Points {
2319
return [points[0] as any, points[1] as any];
2420
}
@@ -174,7 +170,7 @@ export default function useAlign(
174170
const targetWidth = targetRect.width;
175171

176172
// Get bounding of visible area
177-
const visibleArea =
173+
let visibleArea =
178174
placementInfo.htmlRegion === 'scroll'
179175
? // Scroll region should take scrollLeft & scrollTop into account
180176
{
@@ -190,36 +186,7 @@ export default function useAlign(
190186
bottom: clientHeight,
191187
};
192188

193-
(scrollerList || []).forEach((ele) => {
194-
if (ele instanceof HTMLBodyElement) {
195-
return;
196-
}
197-
198-
const eleRect = ele.getBoundingClientRect();
199-
const {
200-
offsetHeight: eleOutHeight,
201-
clientHeight: eleInnerHeight,
202-
offsetWidth: eleOutWidth,
203-
clientWidth: eleInnerWidth,
204-
} = ele;
205-
206-
const scaleX = toNum(
207-
Math.round((eleRect.width / eleOutWidth) * 1000) / 1000,
208-
);
209-
const scaleY = toNum(
210-
Math.round((eleRect.height / eleOutHeight) * 1000) / 1000,
211-
);
212-
213-
const eleScrollWidth = (eleOutWidth - eleInnerWidth) * scaleX;
214-
const eleScrollHeight = (eleOutHeight - eleInnerHeight) * scaleY;
215-
const eleRight = eleRect.x + eleRect.width - eleScrollWidth;
216-
const eleBottom = eleRect.y + eleRect.height - eleScrollHeight;
217-
218-
visibleArea.left = Math.max(visibleArea.left, eleRect.left);
219-
visibleArea.top = Math.max(visibleArea.top, eleRect.top);
220-
visibleArea.right = Math.min(visibleArea.right, eleRight);
221-
visibleArea.bottom = Math.min(visibleArea.bottom, eleBottom);
222-
});
189+
visibleArea = getVisibleArea(visibleArea, scrollerList);
223190

224191
// Reset back
225192
popupElement.style.left = originLeft;

src/util.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ export function getWin(ele: HTMLElement) {
6969
return ele.ownerDocument.defaultView;
7070
}
7171

72+
/**
73+
* Get all the scrollable parent elements of the element
74+
* @param ele The element to be detected
75+
* @param areaOnly Only return the parent which will cut visible area
76+
*/
7277
export function collectScroller(ele: HTMLElement) {
7378
const scrollerList: HTMLElement[] = [];
7479
let current = ele?.parentElement;
@@ -86,3 +91,60 @@ export function collectScroller(ele: HTMLElement) {
8691

8792
return scrollerList;
8893
}
94+
95+
export function toNum(num: number) {
96+
return Number.isNaN(num) ? 1 : num;
97+
}
98+
99+
export interface VisibleArea {
100+
left: number;
101+
top: number;
102+
right: number;
103+
bottom: number;
104+
}
105+
106+
export function getVisibleArea(
107+
initArea: VisibleArea,
108+
scrollerList?: HTMLElement[],
109+
) {
110+
const visibleArea = { ...initArea };
111+
112+
(scrollerList || []).forEach((ele) => {
113+
if (ele instanceof HTMLBodyElement) {
114+
return;
115+
}
116+
117+
// Skip if static position which will not affect visible area
118+
const { position } = getWin(ele).getComputedStyle(ele);
119+
if (position === 'static') {
120+
return;
121+
}
122+
123+
const eleRect = ele.getBoundingClientRect();
124+
const {
125+
offsetHeight: eleOutHeight,
126+
clientHeight: eleInnerHeight,
127+
offsetWidth: eleOutWidth,
128+
clientWidth: eleInnerWidth,
129+
} = ele;
130+
131+
const scaleX = toNum(
132+
Math.round((eleRect.width / eleOutWidth) * 1000) / 1000,
133+
);
134+
const scaleY = toNum(
135+
Math.round((eleRect.height / eleOutHeight) * 1000) / 1000,
136+
);
137+
138+
const eleScrollWidth = (eleOutWidth - eleInnerWidth) * scaleX;
139+
const eleScrollHeight = (eleOutHeight - eleInnerHeight) * scaleY;
140+
const eleRight = eleRect.x + eleRect.width - eleScrollWidth;
141+
const eleBottom = eleRect.y + eleRect.height - eleScrollHeight;
142+
143+
visibleArea.left = Math.max(visibleArea.left, eleRect.x);
144+
visibleArea.top = Math.max(visibleArea.top, eleRect.y);
145+
visibleArea.right = Math.min(visibleArea.right, eleRight);
146+
visibleArea.bottom = Math.min(visibleArea.bottom, eleBottom);
147+
});
148+
149+
return visibleArea;
150+
}

tests/flip.test.tsx

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { act, cleanup, render } from '@testing-library/react';
22
import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
33
import Trigger from '../src';
4+
import { getVisibleArea } from '../src/util';
45

56
const builtinPlacements = {
67
top: {
@@ -270,4 +271,92 @@ describe('Trigger.Align', () => {
270271
top: `0px`,
271272
});
272273
});
274+
275+
// Static parent should not affect popup position
276+
// https://github.com/ant-design/ant-design/issues/41644
277+
it('static parent should not affect popup position', async () => {
278+
/*
279+
280+
********************
281+
* *
282+
* ************** *
283+
* * Affect * *
284+
* * ********** * *
285+
* * * Not * * *
286+
* * ********** * *
287+
* *
288+
* ************** *
289+
* *
290+
********************
291+
292+
*/
293+
const initArea = {
294+
left: 0,
295+
right: 500,
296+
top: 0,
297+
bottom: 500,
298+
};
299+
300+
// Affected area
301+
const affectEle = document.createElement('div');
302+
document.body.appendChild(affectEle);
303+
304+
affectEle.style.position = 'absolute';
305+
Object.defineProperties(affectEle, {
306+
offsetHeight: {
307+
get: () => 300,
308+
},
309+
offsetWidth: {
310+
get: () => 300,
311+
},
312+
clientHeight: {
313+
get: () => 300,
314+
},
315+
clientWidth: {
316+
get: () => 300,
317+
},
318+
});
319+
affectEle.getBoundingClientRect = () =>
320+
({
321+
x: 100,
322+
y: 100,
323+
width: 300,
324+
height: 300,
325+
} as any);
326+
327+
// Skip area
328+
const skipEle = document.createElement('div');
329+
document.body.appendChild(skipEle);
330+
331+
skipEle.style.position = 'static';
332+
Object.defineProperties(skipEle, {
333+
offsetHeight: {
334+
get: () => 100,
335+
},
336+
offsetWidth: {
337+
get: () => 100,
338+
},
339+
clientHeight: {
340+
get: () => 100,
341+
},
342+
clientWidth: {
343+
get: () => 100,
344+
},
345+
});
346+
skipEle.getBoundingClientRect = () =>
347+
({
348+
x: 200,
349+
y: 200,
350+
width: 100,
351+
height: 100,
352+
} as any);
353+
354+
const visibleArea = getVisibleArea(initArea, [affectEle, skipEle]);
355+
expect(visibleArea).toEqual({
356+
left: 100,
357+
right: 400,
358+
top: 100,
359+
bottom: 400,
360+
});
361+
});
273362
});

0 commit comments

Comments
 (0)