diff --git a/docs/demos/large-popup.md b/docs/demos/large-popup.md new file mode 100644 index 00000000..048c77a7 --- /dev/null +++ b/docs/demos/large-popup.md @@ -0,0 +1,8 @@ +--- +title: Large Popup +nav: + title: Demo + path: /demo +--- + + \ No newline at end of file diff --git a/docs/examples/large-popup.tsx b/docs/examples/large-popup.tsx new file mode 100644 index 00000000..8d402d32 --- /dev/null +++ b/docs/examples/large-popup.tsx @@ -0,0 +1,103 @@ +/* eslint no-console:0 */ +import Trigger from 'rc-trigger'; +import React from 'react'; +import '../../assets/index.less'; + +const builtinPlacements = { + top: { + points: ['bc', 'tc'], + overflow: { + shiftY: true, + adjustY: true, + }, + offset: [0, -10], + }, + bottom: { + points: ['tc', 'bc'], + overflow: { + shiftY: true, + adjustY: true, + }, + offset: [0, 10], + htmlRegion: 'scroll' as const, + }, +}; + +export default () => { + const containerRef = React.useRef(); + + React.useEffect(() => { + console.clear(); + containerRef.current.scrollTop = document.defaultView.innerHeight * 0.75; + }, []); + + return ( + +
+
+
+ + Popup 75vh +
+ } + popupStyle={{ boxShadow: '0 0 5px red' }} + popupVisible + popupPlacement="top" + builtinPlacements={builtinPlacements} + > + + Target + + +
+
+ + + {/*
*/} + + ); +}; diff --git a/src/hooks/useAlign.ts b/src/hooks/useAlign.ts index 448fc1eb..65630b7a 100644 --- a/src/hooks/useAlign.ts +++ b/src/hooks/useAlign.ts @@ -286,7 +286,7 @@ export default function useAlign( nextOffsetY, ); - // ================ Overflow ================= + // ========================== Overflow =========================== const targetAlignPointTL = getAlignPoint(targetRect, ['t', 'l']); const popupAlignPointTL = getAlignPoint(popupRect, ['t', 'l']); const targetAlignPointBR = getAlignPoint(targetRect, ['b', 'r']); @@ -302,10 +302,21 @@ export default function useAlign( return val >= 0; }; - // >>>>>>>>>> Top & Bottom - const nextPopupY = popupRect.y + nextOffsetY; - const nextPopupBottom = nextPopupY + popupHeight; + // Prepare position + let nextPopupY: number; + let nextPopupBottom: number; + let nextPopupX: number; + let nextPopupRight: number; + + function syncNextPopupPosition() { + nextPopupY = popupRect.y + nextOffsetY; + nextPopupBottom = nextPopupY + popupHeight; + nextPopupX = popupRect.x + nextOffsetX; + nextPopupRight = nextPopupX + popupWidth; + } + syncNextPopupPosition(); + // >>>>>>>>>> Top & Bottom const needAdjustY = supportAdjust(adjustY); const sameTB = popupPoints[0] === targetPoints[0]; @@ -367,9 +378,6 @@ export default function useAlign( } // >>>>>>>>>> Left & Right - const nextPopupX = popupRect.x + nextOffsetX; - const nextPopupRight = nextPopupX + popupWidth; - const needAdjustX = supportAdjust(adjustX); // >>>>> Flip @@ -431,7 +439,9 @@ export default function useAlign( } } - // >>>>> Shift + // ============================ Shift ============================ + syncNextPopupPosition(); + const numShiftX = shiftX === true ? 0 : shiftX; if (typeof numShiftX === 'number') { // Left @@ -476,6 +486,7 @@ export default function useAlign( } } + // ============================ Arrow ============================ // Arrow center align const popupLeft = popupRect.x + nextOffsetX; const popupRight = popupLeft + popupWidth; diff --git a/tests/flipShift.test.tsx b/tests/flipShift.test.tsx new file mode 100644 index 00000000..2b6aa632 --- /dev/null +++ b/tests/flipShift.test.tsx @@ -0,0 +1,150 @@ +import { act, cleanup, render } from '@testing-library/react'; +import { spyElementPrototypes } from 'rc-util/lib/test/domHook'; +import Trigger from '../src'; + +/* + *********** + ****************** * * + * Placement * * Popup * + * ********** * * * + * * Target * * * * + * ********** * *********** + * * + * * + ****************** + +When `placement` is `top`. It will find should flip to bottom: + + ****************** + * * + * ********** * + * * Target * * + * ********** * *********** top: 200 + * Placement * * * + * * * Popup * + ****************** * * + * * + *********** + +When `placement` is `bottom`. It will find should shift to show in viewport: + + ****************** + * * + * ********** * *********** top: 100 + * * Target * * * * + * ********** * * Popup * + * Placement * * * + * * * * + ****************** *********** + +*/ + +const builtinPlacements = { + top: { + points: ['bc', 'tc'], + overflow: { + adjustY: true, + shiftY: true, + }, + }, + bottom: { + points: ['tc', 'bc'], + overflow: { + adjustY: true, + shiftY: true, + }, + }, + left: { + points: ['cr', 'cl'], + overflow: { + adjustX: true, + shiftX: true, + }, + }, + right: { + points: ['cl', 'cr'], + overflow: { + adjustX: true, + shiftX: true, + }, + }, +}; + +describe('Trigger.Flip+Shift', () => { + beforeAll(() => { + // Viewport size + spyElementPrototypes(HTMLElement, { + clientWidth: { + get: () => 400, + }, + clientHeight: { + get: () => 400, + }, + }); + + // Popup size + spyElementPrototypes(HTMLDivElement, { + getBoundingClientRect() { + return { + x: 0, + y: 0, + width: 100, + height: 300, + }; + }, + }); + spyElementPrototypes(HTMLSpanElement, { + getBoundingClientRect() { + return { + x: 0, + y: 100, + width: 100, + height: 100, + }; + }, + }); + spyElementPrototypes(HTMLElement, { + offsetParent: { + get: () => document.body, + }, + }); + }); + + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + cleanup(); + jest.useRealTimers(); + }); + + it('both work', async () => { + render( + trigger} + > + + , + ); + + await act(async () => { + await Promise.resolve(); + }); + + console.log(document.body.innerHTML); + + expect( + document.querySelector('.rc-trigger-popup-placement-bottom'), + ).toBeTruthy(); + + expect( + document.querySelector('.rc-trigger-popup-placement-bottom'), + ).toHaveStyle({ + top: '100px', + }); + }); +});