Skip to content

Commit 062842d

Browse files
authored
feat: add visibleFirst (#383)
* docs: demo first * chore: preparation * chore: adjust logic * chore: adjust to get recommend region * adjust: measure recommend region * chore: adjust default logic * chore: all the logic * test: all test
1 parent 1bb406c commit 062842d

File tree

8 files changed

+560
-41
lines changed

8 files changed

+560
-41
lines changed

.prettierrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
"semi": true,
44
"singleQuote": true,
55
"tabWidth": 2,
6-
"trailingComma": "all"
6+
"trailingComma": "all",
7+
"jsxSingleQuote": false
78
}

docs/demos/visible-fallback.md

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

docs/examples/visible-fallback.tsx

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/* eslint no-console:0 */
2+
import type { AlignType, TriggerRef } from 'rc-trigger';
3+
import Trigger from 'rc-trigger';
4+
import React from 'react';
5+
import '../../assets/index.less';
6+
7+
const builtinPlacements: Record<string, AlignType> = {
8+
top: {
9+
points: ['bc', 'tc'],
10+
overflow: {
11+
adjustX: true,
12+
adjustY: true,
13+
},
14+
offset: [0, 0],
15+
htmlRegion: 'visibleFirst',
16+
},
17+
bottom: {
18+
points: ['tc', 'bc'],
19+
overflow: {
20+
adjustX: true,
21+
adjustY: true,
22+
},
23+
offset: [0, 0],
24+
htmlRegion: 'visibleFirst',
25+
},
26+
};
27+
28+
export default () => {
29+
const [enoughTop, setEnoughTop] = React.useState(true);
30+
31+
const triggerRef = React.useRef<TriggerRef>();
32+
33+
React.useEffect(() => {
34+
triggerRef.current?.forceAlign();
35+
}, [enoughTop]);
36+
37+
return (
38+
<React.StrictMode>
39+
<p>`visibleFirst` should not show in hidden region if still scrollable</p>
40+
41+
<label>
42+
<input
43+
type="checkbox"
44+
checked={enoughTop}
45+
onChange={() => setEnoughTop((v) => !v)}
46+
/>
47+
Enough Top (Placement: bottom)
48+
</label>
49+
50+
<div
51+
style={{
52+
position: 'absolute',
53+
left: '50%',
54+
top: `calc(100vh - 100px - 90px - 50px)`,
55+
transform: 'translateX(-50%)',
56+
boxShadow: '0 0 1px blue',
57+
overflow: 'hidden',
58+
width: 500,
59+
height: 1000,
60+
}}
61+
>
62+
<Trigger
63+
arrow
64+
action="click"
65+
popupVisible
66+
ref={triggerRef}
67+
popup={
68+
<div
69+
style={{
70+
background: 'yellow',
71+
border: '1px solid blue',
72+
width: 300,
73+
height: 100,
74+
opacity: 0.9,
75+
boxSizing: 'border-box',
76+
}}
77+
>
78+
Should Always place bottom
79+
</div>
80+
}
81+
getPopupContainer={(n) => n.parentNode as any}
82+
popupStyle={{ boxShadow: '0 0 5px red' }}
83+
popupPlacement={enoughTop ? 'bottom' : 'top'}
84+
builtinPlacements={builtinPlacements}
85+
stretch="minWidth"
86+
>
87+
<span
88+
style={{
89+
background: 'green',
90+
color: '#FFF',
91+
opacity: 0.9,
92+
display: 'flex',
93+
alignItems: 'center',
94+
justifyContent: 'center',
95+
width: 100,
96+
height: 100,
97+
position: 'absolute',
98+
left: '50%',
99+
top: enoughTop ? 200 : 90,
100+
transform: 'translateX(-50%)',
101+
}}
102+
>
103+
Target
104+
</span>
105+
</Trigger>
106+
</div>
107+
</React.StrictMode>
108+
);
109+
};

src/hooks/useAlign.ts

Lines changed: 119 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -210,23 +210,39 @@ export default function useAlign(
210210
const targetWidth = targetRect.width;
211211

212212
// Get bounding of visible area
213-
let visibleArea =
214-
placementInfo.htmlRegion === 'scroll'
215-
? // Scroll region should take scrollLeft & scrollTop into account
216-
{
217-
left: -scrollLeft,
218-
top: -scrollTop,
219-
right: scrollWidth - scrollLeft,
220-
bottom: scrollHeight - scrollTop,
221-
}
222-
: {
223-
left: 0,
224-
top: 0,
225-
right: clientWidth,
226-
bottom: clientHeight,
227-
};
228-
229-
visibleArea = getVisibleArea(visibleArea, scrollerList);
213+
const visibleRegion = {
214+
left: 0,
215+
top: 0,
216+
right: clientWidth,
217+
bottom: clientHeight,
218+
};
219+
220+
const scrollRegion = {
221+
left: -scrollLeft,
222+
top: -scrollTop,
223+
right: scrollWidth - scrollLeft,
224+
bottom: scrollHeight - scrollTop,
225+
};
226+
227+
let { htmlRegion } = placementInfo;
228+
const VISIBLE = 'visible' as const;
229+
const VISIBLE_FIRST = 'visibleFirst' as const;
230+
if (htmlRegion !== 'scroll' && htmlRegion !== VISIBLE_FIRST) {
231+
htmlRegion = VISIBLE;
232+
}
233+
const isVisibleFirst = htmlRegion === VISIBLE_FIRST;
234+
235+
const scrollRegionArea = getVisibleArea(scrollRegion, scrollerList);
236+
const visibleRegionArea = getVisibleArea(visibleRegion, scrollerList);
237+
238+
const visibleArea =
239+
htmlRegion === VISIBLE ? visibleRegionArea : scrollRegionArea;
240+
241+
// When set to `visibleFirst`,
242+
// the check `adjust` logic will use `visibleRegion` for check first.
243+
const adjustCheckVisibleArea = isVisibleFirst
244+
? visibleRegionArea
245+
: visibleArea;
230246

231247
// Reset back
232248
popupElement.style.left = originLeft;
@@ -279,17 +295,21 @@ export default function useAlign(
279295

280296
// ============== Intersection ===============
281297
// Get area by position. Used for check if flip area is better
282-
function getIntersectionVisibleArea(offsetX: number, offsetY: number) {
298+
function getIntersectionVisibleArea(
299+
offsetX: number,
300+
offsetY: number,
301+
area = visibleArea,
302+
) {
283303
const l = popupRect.x + offsetX;
284304
const t = popupRect.y + offsetY;
285305

286306
const r = l + popupWidth;
287307
const b = t + popupHeight;
288308

289-
const visibleL = Math.max(l, visibleArea.left);
290-
const visibleT = Math.max(t, visibleArea.top);
291-
const visibleR = Math.min(r, visibleArea.right);
292-
const visibleB = Math.min(b, visibleArea.bottom);
309+
const visibleL = Math.max(l, area.left);
310+
const visibleT = Math.max(t, area.top);
311+
const visibleR = Math.min(r, area.right);
312+
const visibleB = Math.min(b, area.bottom);
293313

294314
return Math.max(0, (visibleR - visibleL) * (visibleB - visibleT));
295315
}
@@ -299,6 +319,13 @@ export default function useAlign(
299319
nextOffsetY,
300320
);
301321

322+
// As `visibleFirst`, we prepare this for check
323+
const originIntersectionRecommendArea = getIntersectionVisibleArea(
324+
nextOffsetX,
325+
nextOffsetY,
326+
visibleRegionArea,
327+
);
328+
302329
// ========================== Overflow ===========================
303330
const targetAlignPointTL = getAlignPoint(targetRect, ['t', 'l']);
304331
const popupAlignPointTL = getAlignPoint(popupRect, ['t', 'l']);
@@ -338,7 +365,8 @@ export default function useAlign(
338365
if (
339366
needAdjustY &&
340367
popupPoints[0] === 't' &&
341-
(nextPopupBottom > visibleArea.bottom || prevFlipRef.current.bt)
368+
(nextPopupBottom > adjustCheckVisibleArea.bottom ||
369+
prevFlipRef.current.bt)
342370
) {
343371
let tmpNextOffsetY: number = nextOffsetY;
344372

@@ -349,9 +377,23 @@ export default function useAlign(
349377
targetAlignPointTL.y - popupAlignPointBR.y - popupOffsetY;
350378
}
351379

380+
const newVisibleArea = getIntersectionVisibleArea(
381+
nextOffsetX,
382+
tmpNextOffsetY,
383+
);
384+
const newVisibleRecommendArea = getIntersectionVisibleArea(
385+
nextOffsetX,
386+
tmpNextOffsetY,
387+
visibleRegionArea,
388+
);
389+
352390
if (
353-
getIntersectionVisibleArea(nextOffsetX, tmpNextOffsetY) >=
354-
originIntersectionVisibleArea
391+
// Of course use larger one
392+
newVisibleArea > originIntersectionVisibleArea ||
393+
(newVisibleArea === originIntersectionVisibleArea &&
394+
(!isVisibleFirst ||
395+
// Choose recommend one
396+
newVisibleRecommendArea >= originIntersectionRecommendArea))
355397
) {
356398
prevFlipRef.current.bt = true;
357399
nextOffsetY = tmpNextOffsetY;
@@ -369,7 +411,7 @@ export default function useAlign(
369411
if (
370412
needAdjustY &&
371413
popupPoints[0] === 'b' &&
372-
(nextPopupY < visibleArea.top || prevFlipRef.current.tb)
414+
(nextPopupY < adjustCheckVisibleArea.top || prevFlipRef.current.tb)
373415
) {
374416
let tmpNextOffsetY: number = nextOffsetY;
375417

@@ -380,9 +422,23 @@ export default function useAlign(
380422
targetAlignPointBR.y - popupAlignPointTL.y - popupOffsetY;
381423
}
382424

425+
const newVisibleArea = getIntersectionVisibleArea(
426+
nextOffsetX,
427+
tmpNextOffsetY,
428+
);
429+
const newVisibleRecommendArea = getIntersectionVisibleArea(
430+
nextOffsetX,
431+
tmpNextOffsetY,
432+
visibleRegionArea,
433+
);
434+
383435
if (
384-
getIntersectionVisibleArea(nextOffsetX, tmpNextOffsetY) >=
385-
originIntersectionVisibleArea
436+
// Of course use larger one
437+
newVisibleArea > originIntersectionVisibleArea ||
438+
(newVisibleArea === originIntersectionVisibleArea &&
439+
(!isVisibleFirst ||
440+
// Choose recommend one
441+
newVisibleRecommendArea >= originIntersectionRecommendArea))
386442
) {
387443
prevFlipRef.current.tb = true;
388444
nextOffsetY = tmpNextOffsetY;
@@ -406,7 +462,8 @@ export default function useAlign(
406462
if (
407463
needAdjustX &&
408464
popupPoints[1] === 'l' &&
409-
(nextPopupRight > visibleArea.right || prevFlipRef.current.rl)
465+
(nextPopupRight > adjustCheckVisibleArea.right ||
466+
prevFlipRef.current.rl)
410467
) {
411468
let tmpNextOffsetX: number = nextOffsetX;
412469

@@ -417,9 +474,23 @@ export default function useAlign(
417474
targetAlignPointTL.x - popupAlignPointBR.x - popupOffsetX;
418475
}
419476

477+
const newVisibleArea = getIntersectionVisibleArea(
478+
tmpNextOffsetX,
479+
nextOffsetY,
480+
);
481+
const newVisibleRecommendArea = getIntersectionVisibleArea(
482+
tmpNextOffsetX,
483+
nextOffsetY,
484+
visibleRegionArea,
485+
);
486+
420487
if (
421-
getIntersectionVisibleArea(tmpNextOffsetX, nextOffsetY) >=
422-
originIntersectionVisibleArea
488+
// Of course use larger one
489+
newVisibleArea > originIntersectionVisibleArea ||
490+
(newVisibleArea === originIntersectionVisibleArea &&
491+
(!isVisibleFirst ||
492+
// Choose recommend one
493+
newVisibleRecommendArea >= originIntersectionRecommendArea))
423494
) {
424495
prevFlipRef.current.rl = true;
425496
nextOffsetX = tmpNextOffsetX;
@@ -437,7 +508,7 @@ export default function useAlign(
437508
if (
438509
needAdjustX &&
439510
popupPoints[1] === 'r' &&
440-
(nextPopupX < visibleArea.left || prevFlipRef.current.lr)
511+
(nextPopupX < adjustCheckVisibleArea.left || prevFlipRef.current.lr)
441512
) {
442513
let tmpNextOffsetX: number = nextOffsetX;
443514

@@ -448,9 +519,23 @@ export default function useAlign(
448519
targetAlignPointBR.x - popupAlignPointTL.x - popupOffsetX;
449520
}
450521

522+
const newVisibleArea = getIntersectionVisibleArea(
523+
tmpNextOffsetX,
524+
nextOffsetY,
525+
);
526+
const newVisibleRecommendArea = getIntersectionVisibleArea(
527+
tmpNextOffsetX,
528+
nextOffsetY,
529+
visibleRegionArea,
530+
);
531+
451532
if (
452-
getIntersectionVisibleArea(tmpNextOffsetX, nextOffsetY) >=
453-
originIntersectionVisibleArea
533+
// Of course use larger one
534+
newVisibleArea > originIntersectionVisibleArea ||
535+
(newVisibleArea === originIntersectionVisibleArea &&
536+
(!isVisibleFirst ||
537+
// Choose recommend one
538+
newVisibleRecommendArea >= originIntersectionRecommendArea))
454539
) {
455540
prevFlipRef.current.lr = true;
456541
nextOffsetX = tmpNextOffsetX;

src/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ export function generateTrigger(
386386

387387
useLayoutEffect(() => {
388388
triggerAlign();
389-
}, [mousePos]);
389+
}, [mousePos, popupPlacement]);
390390

391391
// When no builtinPlacements and popupAlign changed
392392
useLayoutEffect(() => {

src/interface.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,17 @@ export interface AlignType {
5252
autoArrow?: boolean;
5353
/**
5454
* Config visible region check of html node. Default `visible`:
55-
* - `visible`: The visible region of user browser window. Use `clientHeight` for check.
56-
* - `scroll`: The whole region of the html scroll area. Use `scrollHeight` for check.
55+
* - `visible`:
56+
* The visible region of user browser window.
57+
* Use `clientHeight` for check.
58+
* If `visible` region not satisfy, fallback to `scroll`.
59+
* - `scroll`:
60+
* The whole region of the html scroll area.
61+
* Use `scrollHeight` for check.
62+
* - `visibleFirst`:
63+
* Similar to `visible`, but if `visible` region not satisfy, fallback to `scroll`.
5764
*/
58-
htmlRegion?: 'visible' | 'scroll';
65+
htmlRegion?: 'visible' | 'scroll' | 'visibleFirst';
5966
/**
6067
* Whether use css right instead of left to position
6168
*/

0 commit comments

Comments
 (0)