diff --git a/docs/examples/inside.tsx b/docs/examples/inside.tsx index 030420f1..268bcd43 100644 --- a/docs/examples/inside.tsx +++ b/docs/examples/inside.tsx @@ -70,9 +70,11 @@ export const builtinPlacements = { }, }; -const popupPlacement = 'leftBottom'; +const popupPlacement = 'top'; export default () => { + const [popupHeight, setPopupHeight] = React.useState(60); + const containerRef = React.useRef(); React.useEffect(() => { @@ -81,59 +83,70 @@ export default () => { }, []); return ( -
+ <> +
+ +
- + + Popup +
+ } + popupVisible + getPopupContainer={() => containerRef.current} + popupPlacement={popupPlacement} + builtinPlacements={builtinPlacements} + > + - Popup -
- } - popupVisible - getPopupContainer={() => containerRef.current} - popupPlacement={popupPlacement} - builtinPlacements={builtinPlacements} - > - - Target - - + Target + + + - + ); }; diff --git a/src/hooks/useAlign.ts b/src/hooks/useAlign.ts index b096e587..fa65ef8e 100644 --- a/src/hooks/useAlign.ts +++ b/src/hooks/useAlign.ts @@ -113,6 +113,24 @@ export default function useAlign( return collectScroller(popupEle); }, [popupEle]); + // ========================= Flip ========================== + // We will memo flip info. + // If size change to make flip, it will memo the flip info and use it in next align. + const prevFlipRef = React.useRef<{ + tb?: boolean; + bt?: boolean; + lr?: boolean; + rl?: boolean; + }>({}); + + const resetFlipCache = () => { + prevFlipRef.current = {}; + }; + + if (!open) { + resetFlipCache(); + } + // ========================= Align ========================= const onAlign = useEvent(() => { if (popupEle && target && open) { @@ -295,7 +313,7 @@ export default function useAlign( if ( needAdjustY && popupPoints[0] === 't' && - nextPopupBottom > visibleArea.bottom + (nextPopupBottom > visibleArea.bottom || prevFlipRef.current.bt) ) { let tmpNextOffsetY: number = nextOffsetY; @@ -310,12 +328,15 @@ export default function useAlign( getIntersectionVisibleArea(nextOffsetX, tmpNextOffsetY) >= originIntersectionVisibleArea ) { + prevFlipRef.current.bt = true; nextOffsetY = tmpNextOffsetY; nextAlignInfo.points = [ reversePoints(popupPoints, 0), reversePoints(targetPoints, 0), ]; + } else { + prevFlipRef.current.bt = false; } } @@ -323,7 +344,7 @@ export default function useAlign( if ( needAdjustY && popupPoints[0] === 'b' && - nextPopupY < visibleArea.top + (nextPopupY < visibleArea.top || prevFlipRef.current.tb) ) { let tmpNextOffsetY: number = nextOffsetY; @@ -338,12 +359,15 @@ export default function useAlign( getIntersectionVisibleArea(nextOffsetX, tmpNextOffsetY) >= originIntersectionVisibleArea ) { + prevFlipRef.current.tb = true; nextOffsetY = tmpNextOffsetY; nextAlignInfo.points = [ reversePoints(popupPoints, 0), reversePoints(targetPoints, 0), ]; + } else { + prevFlipRef.current.tb = false; } } @@ -357,7 +381,7 @@ export default function useAlign( if ( needAdjustX && popupPoints[1] === 'l' && - nextPopupRight > visibleArea.right + (nextPopupRight > visibleArea.right || prevFlipRef.current.rl) ) { let tmpNextOffsetX: number = nextOffsetX; @@ -372,12 +396,15 @@ export default function useAlign( getIntersectionVisibleArea(tmpNextOffsetX, nextOffsetY) >= originIntersectionVisibleArea ) { + prevFlipRef.current.rl = true; nextOffsetX = tmpNextOffsetX; nextAlignInfo.points = [ reversePoints(popupPoints, 1), reversePoints(targetPoints, 1), ]; + } else { + prevFlipRef.current.rl = false; } } @@ -385,7 +412,7 @@ export default function useAlign( if ( needAdjustX && popupPoints[1] === 'r' && - nextPopupX < visibleArea.left + (nextPopupX < visibleArea.left || prevFlipRef.current.lr) ) { let tmpNextOffsetX: number = nextOffsetX; @@ -400,12 +427,15 @@ export default function useAlign( getIntersectionVisibleArea(tmpNextOffsetX, nextOffsetY) >= originIntersectionVisibleArea ) { + prevFlipRef.current.lr = true; nextOffsetX = tmpNextOffsetX; nextAlignInfo.points = [ reversePoints(popupPoints, 1), reversePoints(targetPoints, 1), ]; + } else { + prevFlipRef.current.lr = false; } } diff --git a/tests/flip.test.tsx b/tests/flip.test.tsx index 8ffe4dc3..b01103a8 100644 --- a/tests/flip.test.tsx +++ b/tests/flip.test.tsx @@ -1,8 +1,16 @@ import { act, cleanup, render } from '@testing-library/react'; +import { _rs } from 'rc-resize-observer'; import { spyElementPrototypes } from 'rc-util/lib/test/domHook'; +import type { TriggerProps } from '../src'; import Trigger from '../src'; import { getVisibleArea } from '../src/util'; +const flush = async () => { + await act(async () => { + await Promise.resolve(); + }); +}; + const builtinPlacements = { top: { points: ['bc', 'tc'], @@ -139,9 +147,7 @@ describe('Trigger.Align', () => { , ); - await act(async () => { - await Promise.resolve(); - }); + await flush(); expect(document.querySelector(className)).toBeTruthy(); }); @@ -188,9 +194,7 @@ describe('Trigger.Align', () => { , ); - await act(async () => { - await Promise.resolve(); - }); + await flush(); expect(document.querySelector(className)).toBeTruthy(); }); @@ -257,9 +261,7 @@ describe('Trigger.Align', () => { , ); - await act(async () => { - await Promise.resolve(); - }); + await flush(); // Flip expect( @@ -420,12 +422,134 @@ describe('Trigger.Align', () => { , ); - await act(async () => { - await Promise.resolve(); - }); + await flush(); expect( document.querySelector('.rc-trigger-popup-placement-bottom'), ).toBeTruthy(); }); + + // https://github.com/ant-design/ant-design/issues/41728 + describe('save prev flip position', () => { + const flipList: { + name: string; + placement: string; + x?: number; + y?: number; + className: string; + + // Move target position should back to origin placement which is visible + backX?: number; + backY?: number; + backClassName: string; + }[] = [ + { + name: 'top2bottom', + placement: 'top', + y: 20, + className: '.rc-trigger-popup-placement-bottom', + backY: 95, + backClassName: '.rc-trigger-popup-placement-top', + }, + { + name: 'bottom2top', + placement: 'bottom', + y: 80, + className: '.rc-trigger-popup-placement-top', + backY: 5, + backClassName: '.rc-trigger-popup-placement-bottom', + }, + { + name: 'left2right', + placement: 'left', + x: 20, + className: '.rc-trigger-popup-placement-right', + backX: 95, + backClassName: '.rc-trigger-popup-placement-left', + }, + { + name: 'right2left', + placement: 'right', + x: 80, + className: '.rc-trigger-popup-placement-left', + backX: 5, + backClassName: '.rc-trigger-popup-placement-right', + }, + ]; + + flipList.forEach( + ({ + name, + placement, + x = 0, + y = 0, + backX = 0, + backY = 0, + className, + backClassName, + }) => { + it(name, async () => { + spanRect.x = x; + spanRect.y = y; + popupRect.width = 30; + popupRect.height = 30; + + const onPopupAlign = jest.fn(); + + const Demo = ({ popupPlacement }: Partial) => ( + trigger} + onPopupAlign={onPopupAlign} + > + + + ); + + render(); + + await flush(); + + expect(document.querySelector(className)).toBeTruthy(); + expect(onPopupAlign).toHaveBeenCalled(); + onPopupAlign.mockReset(); + + // Change size to small than target position + popupRect.width = 10; + popupRect.height = 10; + + act(() => { + _rs([ + { + target: document.querySelector('.rc-trigger-popup'), + } as ResizeObserverEntry, + ]); + }); + await flush(); + + expect(document.querySelector(className)).toBeTruthy(); + expect(onPopupAlign).toHaveBeenCalled(); + onPopupAlign.mockReset(); + + // Change target position to back of origin placement + spanRect.x = backX; + spanRect.y = backY; + + act(() => { + _rs([ + { + target: document.querySelector('.target'), + } as ResizeObserverEntry, + ]); + }); + await flush(); + + expect(document.querySelector(backClassName)).toBeTruthy(); + expect(onPopupAlign).toHaveBeenCalled(); + }); + }, + ); + }); });