Skip to content

Commit 076e070

Browse files
authored
feat: Stable popup align (#362)
* docs: update demo * fix: flip memo * test: add test case
1 parent ff874e3 commit 076e070

File tree

3 files changed

+229
-62
lines changed

3 files changed

+229
-62
lines changed

docs/examples/inside.tsx

Lines changed: 59 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,11 @@ export const builtinPlacements = {
7070
},
7171
};
7272

73-
const popupPlacement = 'leftBottom';
73+
const popupPlacement = 'top';
7474

7575
export default () => {
76+
const [popupHeight, setPopupHeight] = React.useState(60);
77+
7678
const containerRef = React.useRef<HTMLDivElement>();
7779

7880
React.useEffect(() => {
@@ -81,59 +83,70 @@ export default () => {
8183
}, []);
8284

8385
return (
84-
<div
85-
style={{
86-
position: 'absolute',
87-
inset: 64,
88-
overflow: `auto`,
89-
border: '1px solid red',
90-
}}
91-
ref={containerRef}
92-
>
86+
<>
87+
<div style={{ position: 'fixed', top: 0, left: 0 }}>
88+
<button
89+
onClick={() => {
90+
setPopupHeight(popupHeight === 60 ? 200 : 60);
91+
}}
92+
>
93+
Popup Height: {popupHeight}
94+
</button>
95+
</div>
9396
<div
9497
style={{
95-
width: `300vw`,
96-
height: `300vh`,
97-
display: 'flex',
98-
alignItems: 'center',
99-
justifyContent: 'center',
98+
position: 'absolute',
99+
inset: 64,
100+
overflow: `auto`,
101+
border: '1px solid red',
100102
}}
103+
ref={containerRef}
101104
>
102-
<Trigger
103-
arrow
104-
popup={
105-
<div
105+
<div
106+
style={{
107+
width: `300vw`,
108+
height: `300vh`,
109+
display: 'flex',
110+
alignItems: 'center',
111+
justifyContent: 'center',
112+
}}
113+
>
114+
<Trigger
115+
arrow
116+
popup={
117+
<div
118+
style={{
119+
background: 'yellow',
120+
border: '1px solid blue',
121+
width: 200,
122+
height: popupHeight,
123+
opacity: 0.9,
124+
}}
125+
>
126+
Popup
127+
</div>
128+
}
129+
popupVisible
130+
getPopupContainer={() => containerRef.current}
131+
popupPlacement={popupPlacement}
132+
builtinPlacements={builtinPlacements}
133+
>
134+
<span
106135
style={{
107-
background: 'yellow',
108-
border: '1px solid blue',
109-
width: 200,
110-
height: 60,
136+
display: 'inline-block',
137+
background: 'green',
138+
color: '#FFF',
139+
paddingBlock: 30,
140+
paddingInline: 70,
111141
opacity: 0.9,
142+
transform: 'scale(0.6)',
112143
}}
113144
>
114-
Popup
115-
</div>
116-
}
117-
popupVisible
118-
getPopupContainer={() => containerRef.current}
119-
popupPlacement={popupPlacement}
120-
builtinPlacements={builtinPlacements}
121-
>
122-
<span
123-
style={{
124-
display: 'inline-block',
125-
background: 'green',
126-
color: '#FFF',
127-
paddingBlock: 30,
128-
paddingInline: 70,
129-
opacity: 0.9,
130-
transform: 'scale(0.6)',
131-
}}
132-
>
133-
Target
134-
</span>
135-
</Trigger>
145+
Target
146+
</span>
147+
</Trigger>
148+
</div>
136149
</div>
137-
</div>
150+
</>
138151
);
139152
};

src/hooks/useAlign.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,24 @@ export default function useAlign(
113113
return collectScroller(popupEle);
114114
}, [popupEle]);
115115

116+
// ========================= Flip ==========================
117+
// We will memo flip info.
118+
// If size change to make flip, it will memo the flip info and use it in next align.
119+
const prevFlipRef = React.useRef<{
120+
tb?: boolean;
121+
bt?: boolean;
122+
lr?: boolean;
123+
rl?: boolean;
124+
}>({});
125+
126+
const resetFlipCache = () => {
127+
prevFlipRef.current = {};
128+
};
129+
130+
if (!open) {
131+
resetFlipCache();
132+
}
133+
116134
// ========================= Align =========================
117135
const onAlign = useEvent(() => {
118136
if (popupEle && target && open) {
@@ -295,7 +313,7 @@ export default function useAlign(
295313
if (
296314
needAdjustY &&
297315
popupPoints[0] === 't' &&
298-
nextPopupBottom > visibleArea.bottom
316+
(nextPopupBottom > visibleArea.bottom || prevFlipRef.current.bt)
299317
) {
300318
let tmpNextOffsetY: number = nextOffsetY;
301319

@@ -310,20 +328,23 @@ export default function useAlign(
310328
getIntersectionVisibleArea(nextOffsetX, tmpNextOffsetY) >=
311329
originIntersectionVisibleArea
312330
) {
331+
prevFlipRef.current.bt = true;
313332
nextOffsetY = tmpNextOffsetY;
314333

315334
nextAlignInfo.points = [
316335
reversePoints(popupPoints, 0),
317336
reversePoints(targetPoints, 0),
318337
];
338+
} else {
339+
prevFlipRef.current.bt = false;
319340
}
320341
}
321342

322343
// Top to Bottom
323344
if (
324345
needAdjustY &&
325346
popupPoints[0] === 'b' &&
326-
nextPopupY < visibleArea.top
347+
(nextPopupY < visibleArea.top || prevFlipRef.current.tb)
327348
) {
328349
let tmpNextOffsetY: number = nextOffsetY;
329350

@@ -338,12 +359,15 @@ export default function useAlign(
338359
getIntersectionVisibleArea(nextOffsetX, tmpNextOffsetY) >=
339360
originIntersectionVisibleArea
340361
) {
362+
prevFlipRef.current.tb = true;
341363
nextOffsetY = tmpNextOffsetY;
342364

343365
nextAlignInfo.points = [
344366
reversePoints(popupPoints, 0),
345367
reversePoints(targetPoints, 0),
346368
];
369+
} else {
370+
prevFlipRef.current.tb = false;
347371
}
348372
}
349373

@@ -357,7 +381,7 @@ export default function useAlign(
357381
if (
358382
needAdjustX &&
359383
popupPoints[1] === 'l' &&
360-
nextPopupRight > visibleArea.right
384+
(nextPopupRight > visibleArea.right || prevFlipRef.current.rl)
361385
) {
362386
let tmpNextOffsetX: number = nextOffsetX;
363387

@@ -372,20 +396,23 @@ export default function useAlign(
372396
getIntersectionVisibleArea(tmpNextOffsetX, nextOffsetY) >=
373397
originIntersectionVisibleArea
374398
) {
399+
prevFlipRef.current.rl = true;
375400
nextOffsetX = tmpNextOffsetX;
376401

377402
nextAlignInfo.points = [
378403
reversePoints(popupPoints, 1),
379404
reversePoints(targetPoints, 1),
380405
];
406+
} else {
407+
prevFlipRef.current.rl = false;
381408
}
382409
}
383410

384411
// Left to Right
385412
if (
386413
needAdjustX &&
387414
popupPoints[1] === 'r' &&
388-
nextPopupX < visibleArea.left
415+
(nextPopupX < visibleArea.left || prevFlipRef.current.lr)
389416
) {
390417
let tmpNextOffsetX: number = nextOffsetX;
391418

@@ -400,12 +427,15 @@ export default function useAlign(
400427
getIntersectionVisibleArea(tmpNextOffsetX, nextOffsetY) >=
401428
originIntersectionVisibleArea
402429
) {
430+
prevFlipRef.current.lr = true;
403431
nextOffsetX = tmpNextOffsetX;
404432

405433
nextAlignInfo.points = [
406434
reversePoints(popupPoints, 1),
407435
reversePoints(targetPoints, 1),
408436
];
437+
} else {
438+
prevFlipRef.current.lr = false;
409439
}
410440
}
411441

0 commit comments

Comments
 (0)