Skip to content

Commit a5e4285

Browse files
authored
feat: Auto adjust support visible region calculation (#328)
* docs: init demo * chore: support visible area overflow * feat: support validate range * test: more test case * test: update test case
1 parent 7ee6faf commit a5e4285

File tree

8 files changed

+334
-57
lines changed

8 files changed

+334
-57
lines changed

docs/demos/inside.md

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

docs/examples/inside.tsx

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
"classnames": "^2.3.2",
6666
"rc-align": "^4.0.0",
6767
"rc-motion": "^2.0.0",
68-
"rc-resize-observer": "^1.3.0",
68+
"rc-resize-observer": "^1.3.1",
6969
"rc-util": "^5.27.1"
7070
},
7171
"peerDependencies": {

src/hooks/useAlign.ts

Lines changed: 75 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
AlignPointTopBottom,
88
AlignType,
99
} from '../interface';
10-
import { getWin } from '../util';
10+
import { collectScroller, getWin } from '../util';
1111

1212
type Rect = Record<'x' | 'y' | 'width' | 'height', number>;
1313

@@ -107,6 +107,15 @@ export default function useAlign(
107107
});
108108
const alignCountRef = React.useRef(0);
109109

110+
const scrollerList = React.useMemo(() => {
111+
if (!popupEle) {
112+
return [];
113+
}
114+
115+
return collectScroller(popupEle);
116+
}, [popupEle]);
117+
118+
// ========================= Align =========================
110119
const onAlign = useEvent(() => {
111120
if (popupEle && target && open) {
112121
const popupElement = popupEle;
@@ -146,6 +155,41 @@ export default function useAlign(
146155
const popupHeight = popupRect.height;
147156
const popupWidth = popupRect.width;
148157

158+
// Get bounding of visible area
159+
const visibleArea = {
160+
left: 0,
161+
top: 0,
162+
right: clientWidth,
163+
bottom: clientHeight,
164+
};
165+
166+
(scrollerList || []).forEach((ele) => {
167+
const eleRect = ele.getBoundingClientRect();
168+
const {
169+
offsetHeight: eleOutHeight,
170+
clientHeight: eleInnerHeight,
171+
offsetWidth: eleOutWidth,
172+
clientWidth: eleInnerWidth,
173+
} = ele;
174+
175+
const scaleX = toNum(
176+
Math.round((eleRect.width / eleOutWidth) * 1000) / 1000,
177+
);
178+
const scaleY = toNum(
179+
Math.round((eleRect.height / eleOutHeight) * 1000) / 1000,
180+
);
181+
182+
const scrollWidth = (eleOutWidth - eleInnerWidth) * scaleX;
183+
const scrollHeight = (eleOutHeight - eleInnerHeight) * scaleY;
184+
const eleRight = eleRect.x + eleRect.width - scrollWidth;
185+
const eleBottom = eleRect.y + eleRect.height - scrollHeight;
186+
187+
visibleArea.left = Math.max(visibleArea.left, eleRect.left);
188+
visibleArea.top = Math.max(visibleArea.top, eleRect.top);
189+
visibleArea.right = Math.min(visibleArea.right, eleRight);
190+
visibleArea.bottom = Math.min(visibleArea.bottom, eleBottom);
191+
});
192+
149193
// Reset back
150194
popupElement.style.left = originLeft;
151195
popupElement.style.top = originTop;
@@ -206,7 +250,7 @@ export default function useAlign(
206250
if (
207251
needAdjustY &&
208252
popupPoints[0] === 't' &&
209-
nextPopupBottom > clientHeight
253+
nextPopupBottom > visibleArea.bottom
210254
) {
211255
nextOffsetY = targetAlignPointTL.y - popupAlignPointBR.y - popupOffsetY;
212256

@@ -217,7 +261,11 @@ export default function useAlign(
217261
}
218262

219263
// Top to Bottom
220-
if (needAdjustY && popupPoints[0] === 'b' && nextPopupY < 0) {
264+
if (
265+
needAdjustY &&
266+
popupPoints[0] === 'b' &&
267+
nextPopupY < visibleArea.top
268+
) {
221269
nextOffsetY = targetAlignPointBR.y - popupAlignPointTL.y - popupOffsetY;
222270

223271
nextAlignInfo.points = [
@@ -237,7 +285,7 @@ export default function useAlign(
237285
if (
238286
needAdjustX &&
239287
popupPoints[1] === 'l' &&
240-
nextPopupRight > clientWidth
288+
nextPopupRight > visibleArea.right
241289
) {
242290
nextOffsetX = targetAlignPointTL.x - popupAlignPointBR.x - popupOffsetX;
243291

@@ -248,7 +296,11 @@ export default function useAlign(
248296
}
249297

250298
// Left to Right
251-
if (needAdjustX && popupPoints[1] === 'r' && nextPopupX < 0) {
299+
if (
300+
needAdjustX &&
301+
popupPoints[1] === 'r' &&
302+
nextPopupX < visibleArea.left
303+
) {
252304
nextOffsetX = targetAlignPointBR.x - popupAlignPointTL.x - popupOffsetX;
253305

254306
nextAlignInfo.points = [
@@ -261,41 +313,43 @@ export default function useAlign(
261313
const numShiftX = shiftX === true ? 0 : shiftX;
262314
if (typeof numShiftX === 'number') {
263315
// Left
264-
if (nextPopupX < 0) {
265-
nextOffsetX -= nextPopupX;
316+
if (nextPopupX < visibleArea.left) {
317+
nextOffsetX -= nextPopupX - visibleArea.left;
266318

267-
if (targetRect.x + targetRect.width < numShiftX) {
268-
nextOffsetX += targetRect.x + targetRect.width - numShiftX;
319+
if (targetRect.x + targetRect.width < visibleArea.left + numShiftX) {
320+
nextOffsetX +=
321+
targetRect.x - visibleArea.left + targetRect.width - numShiftX;
269322
}
270323
}
271324

272325
// Right
273-
if (nextPopupRight > clientWidth) {
274-
nextOffsetX -= nextPopupRight - clientWidth;
326+
if (nextPopupRight > visibleArea.right) {
327+
nextOffsetX -= nextPopupRight - visibleArea.right;
275328

276-
if (targetRect.x > clientWidth - numShiftX) {
277-
nextOffsetX += targetRect.x - clientWidth + numShiftX;
329+
if (targetRect.x > visibleArea.right - numShiftX) {
330+
nextOffsetX += targetRect.x - visibleArea.right + numShiftX;
278331
}
279332
}
280333
}
281334

282335
const numShiftY = shiftY === true ? 0 : shiftY;
283336
if (typeof numShiftY === 'number') {
284337
// Top
285-
if (nextPopupY < 0) {
286-
nextOffsetY -= nextPopupY;
338+
if (nextPopupY < visibleArea.top) {
339+
nextOffsetY -= nextPopupY - visibleArea.top;
287340

288-
if (targetRect.y + targetRect.height < numShiftY) {
289-
nextOffsetY += targetRect.y + targetRect.height - numShiftY;
341+
if (targetRect.y + targetRect.height < visibleArea.top + numShiftY) {
342+
nextOffsetY +=
343+
targetRect.y - visibleArea.top + targetRect.height - numShiftY;
290344
}
291345
}
292346

293347
// Bottom
294-
if (nextPopupBottom > clientHeight) {
295-
nextOffsetY -= nextPopupBottom - clientHeight;
348+
if (nextPopupBottom > visibleArea.bottom) {
349+
nextOffsetY -= nextPopupBottom - visibleArea.bottom;
296350

297-
if (targetRect.y > clientHeight - numShiftY) {
298-
nextOffsetY += targetRect.y - clientHeight + numShiftY;
351+
if (targetRect.y > visibleArea.bottom - numShiftY) {
352+
nextOffsetY += targetRect.y - visibleArea.bottom + numShiftY;
299353
}
300354
}
301355
}

src/hooks/useWatch.ts

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,5 @@
11
import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
2-
import { getWin } from '../util';
3-
4-
function collectScroller(ele: HTMLElement) {
5-
const scrollerList: HTMLElement[] = [];
6-
let current = ele?.parentElement;
7-
8-
const scrollStyle = ['hidden', 'scroll', 'auto'];
9-
10-
while (current) {
11-
const { overflowX, overflowY } = getWin(current).getComputedStyle(current);
12-
if (scrollStyle.includes(overflowX) || scrollStyle.includes(overflowY)) {
13-
scrollerList.push(current);
14-
}
15-
16-
current = current.parentElement;
17-
}
18-
19-
return scrollerList;
20-
}
2+
import { collectScroller, getWin } from '../util';
213

224
export default function useWatch(
235
open: boolean,

src/util.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,6 @@ function isPointsEq(
1717
return a1[0] === a2[0] && a1[1] === a2[1];
1818
}
1919

20-
export function getAlignFromPlacement(
21-
builtinPlacements: BuildInPlacements,
22-
placementStr: string,
23-
align: AlignType,
24-
): AlignType {
25-
const baseAlign = builtinPlacements[placementStr] || {};
26-
return {
27-
...baseAlign,
28-
...align,
29-
};
30-
}
31-
3220
export function getAlignPopupClassName(
3321
builtinPlacements: BuildInPlacements,
3422
prefixCls: string,
@@ -41,7 +29,9 @@ export function getAlignPopupClassName(
4129

4230
for (let i = 0; i < placements.length; i += 1) {
4331
const placement = placements[i];
44-
if (isPointsEq(builtinPlacements[placement]?.points, points, isAlignPoint)) {
32+
if (
33+
isPointsEq(builtinPlacements[placement]?.points, points, isAlignPoint)
34+
) {
4535
return `${prefixCls}-placement-${placement}`;
4636
}
4737
}
@@ -77,4 +67,22 @@ export function getMotion(
7767

7868
export function getWin(ele: HTMLElement) {
7969
return ele.ownerDocument.defaultView;
80-
}
70+
}
71+
72+
export function collectScroller(ele: HTMLElement) {
73+
const scrollerList: HTMLElement[] = [];
74+
let current = ele?.parentElement;
75+
76+
const scrollStyle = ['hidden', 'scroll', 'auto'];
77+
78+
while (current) {
79+
const { overflowX, overflowY } = getWin(current).getComputedStyle(current);
80+
if (scrollStyle.includes(overflowX) || scrollStyle.includes(overflowY)) {
81+
scrollerList.push(current);
82+
}
83+
84+
current = current.parentElement;
85+
}
86+
87+
return scrollerList;
88+
}

0 commit comments

Comments
 (0)