From 355cd8cecbcaa1cb3471b535c5c177c4cba4bdc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Thu, 16 Mar 2023 22:15:46 +0800 Subject: [PATCH] fix: Only flip if space is enough (#343) * chore: improve check logic only flip when has more space * test: add test case --- docs/examples/container.tsx | 2 +- src/hooks/useAlign.ts | 103 ++++++++++++++----- tests/flip.test.tsx | 190 ++++++++++++++++++++++++++++++++++++ 3 files changed, 270 insertions(+), 25 deletions(-) create mode 100644 tests/flip.test.tsx diff --git a/docs/examples/container.tsx b/docs/examples/container.tsx index d559409f..c1a17166 100644 --- a/docs/examples/container.tsx +++ b/docs/examples/container.tsx @@ -55,7 +55,7 @@ const builtinPlacements = { }, }; -const popupPlacement = 'bottom'; +const popupPlacement = 'right'; export default () => { console.log('Demo Render!'); diff --git a/src/hooks/useAlign.ts b/src/hooks/useAlign.ts index 3f79bb95..c745841f 100644 --- a/src/hooks/useAlign.ts +++ b/src/hooks/useAlign.ts @@ -267,6 +267,25 @@ export default function useAlign( let nextOffsetX = targetAlignPoint.x - popupAlignPoint.x + popupOffsetX; let nextOffsetY = targetAlignPoint.y - popupAlignPoint.y + popupOffsetY; + // ============== Intersection =============== + // Get area by position. Used for check if flip area is better + function getIntersectionVisibleArea(x: number, y: number) { + const r = x + popupWidth; + const b = y + popupHeight; + + const visibleX = Math.max(x, visibleArea.left); + const visibleY = Math.max(y, visibleArea.top); + const visibleR = Math.min(r, visibleArea.right); + const visibleB = Math.min(b, visibleArea.bottom); + + return (visibleR - visibleX) * (visibleB - visibleY); + } + + const originIntersectionVisibleArea = getIntersectionVisibleArea( + nextOffsetX, + nextOffsetY, + ); + // ================ Overflow ================= const targetAlignPointTL = getAlignPoint(targetRect, ['t', 'l']); const popupAlignPointTL = getAlignPoint(popupRect, ['t', 'l']); @@ -297,17 +316,26 @@ export default function useAlign( popupPoints[0] === 't' && nextPopupBottom > visibleArea.bottom ) { + let tmpNextOffsetY: number; + if (sameTB) { - nextOffsetY -= popupHeight - targetHeight; + tmpNextOffsetY -= popupHeight - targetHeight; } else { - nextOffsetY = + tmpNextOffsetY = targetAlignPointTL.y - popupAlignPointBR.y - popupOffsetY; } - nextAlignInfo.points = [ - reversePoints(popupPoints, 0), - reversePoints(targetPoints, 0), - ]; + if ( + getIntersectionVisibleArea(nextOffsetX, tmpNextOffsetY) > + originIntersectionVisibleArea + ) { + nextOffsetY = tmpNextOffsetY; + + nextAlignInfo.points = [ + reversePoints(popupPoints, 0), + reversePoints(targetPoints, 0), + ]; + } } // Top to Bottom @@ -316,17 +344,26 @@ export default function useAlign( popupPoints[0] === 'b' && nextPopupY < visibleArea.top ) { + let tmpNextOffsetY: number; + if (sameTB) { - nextOffsetY += popupHeight - targetHeight; + tmpNextOffsetY += popupHeight - targetHeight; } else { - nextOffsetY = + tmpNextOffsetY = targetAlignPointBR.y - popupAlignPointTL.y - popupOffsetY; } - nextAlignInfo.points = [ - reversePoints(popupPoints, 0), - reversePoints(targetPoints, 0), - ]; + if ( + getIntersectionVisibleArea(nextOffsetX, tmpNextOffsetY) > + originIntersectionVisibleArea + ) { + nextOffsetY = tmpNextOffsetY; + + nextAlignInfo.points = [ + reversePoints(popupPoints, 0), + reversePoints(targetPoints, 0), + ]; + } } // >>>>>>>>>> Left & Right @@ -344,17 +381,26 @@ export default function useAlign( popupPoints[1] === 'l' && nextPopupRight > visibleArea.right ) { + let tmpNextOffsetX: number; + if (sameLR) { - nextOffsetX -= popupWidth - targetWidth; + tmpNextOffsetX -= popupWidth - targetWidth; } else { - nextOffsetX = + tmpNextOffsetX = targetAlignPointTL.x - popupAlignPointBR.x - popupOffsetX; } - nextAlignInfo.points = [ - reversePoints(popupPoints, 1), - reversePoints(targetPoints, 1), - ]; + if ( + getIntersectionVisibleArea(tmpNextOffsetX, nextOffsetY) > + originIntersectionVisibleArea + ) { + nextOffsetX = tmpNextOffsetX; + + nextAlignInfo.points = [ + reversePoints(popupPoints, 1), + reversePoints(targetPoints, 1), + ]; + } } // Left to Right @@ -363,17 +409,26 @@ export default function useAlign( popupPoints[1] === 'r' && nextPopupX < visibleArea.left ) { + let tmpNextOffsetX: number; + if (sameLR) { - nextOffsetX += popupWidth - targetWidth; + tmpNextOffsetX += popupWidth - targetWidth; } else { - nextOffsetX = + tmpNextOffsetX = targetAlignPointBR.x - popupAlignPointTL.x - popupOffsetX; } - nextAlignInfo.points = [ - reversePoints(popupPoints, 1), - reversePoints(targetPoints, 1), - ]; + if ( + getIntersectionVisibleArea(tmpNextOffsetX, nextOffsetY) > + originIntersectionVisibleArea + ) { + nextOffsetX = tmpNextOffsetX; + + nextAlignInfo.points = [ + reversePoints(popupPoints, 1), + reversePoints(targetPoints, 1), + ]; + } } // >>>>> Shift diff --git a/tests/flip.test.tsx b/tests/flip.test.tsx new file mode 100644 index 00000000..95287e78 --- /dev/null +++ b/tests/flip.test.tsx @@ -0,0 +1,190 @@ +import { act, cleanup, render } from '@testing-library/react'; +import { spyElementPrototypes } from 'rc-util/lib/test/domHook'; +import Trigger from '../src'; + +const builtinPlacements = { + top: { + points: ['bc', 'tc'], + overflow: { + adjustX: true, + adjustY: true, + }, + }, + bottom: { + points: ['tc', 'bc'], + overflow: { + adjustX: true, + adjustY: true, + }, + }, + left: { + points: ['cr', 'cl'], + overflow: { + adjustX: true, + adjustY: true, + }, + }, + right: { + points: ['cl', 'cr'], + overflow: { + adjustX: true, + adjustY: true, + }, + }, +}; + +describe('Trigger.Align', () => { + let spanRect = { + x: 0, + y: 0, + width: 1, + height: 1, + }; + + beforeAll(() => { + spyElementPrototypes(HTMLElement, { + clientWidth: { + get: () => 100, + }, + clientHeight: { + get: () => 100, + }, + }); + + spyElementPrototypes(HTMLDivElement, { + getBoundingClientRect() { + return { + x: 0, + y: 0, + width: 100, + height: 100, + }; + }, + }); + + spyElementPrototypes(HTMLSpanElement, { + getBoundingClientRect() { + return spanRect; + }, + }); + + spyElementPrototypes(HTMLElement, { + offsetParent: { + get: () => document.body, + }, + }); + }); + + beforeEach(() => { + spanRect = { + x: 0, + y: 0, + width: 1, + height: 1, + }; + jest.useFakeTimers(); + }); + + afterEach(() => { + cleanup(); + jest.useRealTimers(); + }); + + describe('not flip if cant', () => { + const list = [ + { + placement: 'right', + x: 10, + className: '.rc-trigger-popup-placement-right', + }, + { + placement: 'left', + x: 90, + className: '.rc-trigger-popup-placement-left', + }, + { + placement: 'top', + y: 90, + className: '.rc-trigger-popup-placement-top', + }, + { + placement: 'bottom', + y: 10, + className: '.rc-trigger-popup-placement-bottom', + }, + ]; + + list.forEach(({ placement, x = 0, y = 0, className }) => { + it(placement, async () => { + spanRect.x = x; + spanRect.y = y; + + render( + trigger} + > + + , + ); + + await act(async () => { + await Promise.resolve(); + }); + + expect(document.querySelector(className)).toBeTruthy(); + }); + }); + }); + + describe('flip if can', () => { + const list = [ + { + placement: 'right', + x: 90, + className: '.rc-trigger-popup-placement-left', + }, + { + placement: 'left', + x: 10, + className: '.rc-trigger-popup-placement-right', + }, + { + placement: 'top', + y: 10, + className: '.rc-trigger-popup-placement-bottom', + }, + { + placement: 'bottom', + y: 90, + className: '.rc-trigger-popup-placement-top', + }, + ]; + + list.forEach(({ placement, x = 0, y = 0, className }) => { + it(placement, async () => { + spanRect.x = x; + spanRect.y = y; + + render( + trigger} + > + + , + ); + + await act(async () => { + await Promise.resolve(); + }); + + expect(document.querySelector(className)).toBeTruthy(); + }); + }); + }); +});