diff --git a/docs/demos/static-scroll.md b/docs/demos/static-scroll.md new file mode 100644 index 00000000..14a1846a --- /dev/null +++ b/docs/demos/static-scroll.md @@ -0,0 +1,8 @@ +--- +title: Static Scroll +nav: + title: Demo + path: /demo +--- + + diff --git a/docs/examples/inside.tsx b/docs/examples/inside.tsx index 671b2b4d..030420f1 100644 --- a/docs/examples/inside.tsx +++ b/docs/examples/inside.tsx @@ -3,7 +3,7 @@ import React from 'react'; import '../../assets/index.less'; import Trigger from '../../src'; -const builtinPlacements = { +export const builtinPlacements = { top: { points: ['bc', 'tc'], overflow: { diff --git a/docs/examples/static-scroll.tsx b/docs/examples/static-scroll.tsx new file mode 100644 index 00000000..1f0e6e2b --- /dev/null +++ b/docs/examples/static-scroll.tsx @@ -0,0 +1,64 @@ +/* eslint no-console:0 */ +import Trigger from 'rc-trigger'; +import React from 'react'; +import '../../assets/index.less'; +import { builtinPlacements } from './inside'; + +export default () => { + return ( + +
+ + Popup +
+ } + popupStyle={{ boxShadow: '0 0 5px red' }} + popupVisible + builtinPlacements={builtinPlacements} + popupPlacement="top" + stretch="minWidth" + getPopupContainer={(e) => e.parentElement!} + > + + Target + + + {new Array(20).fill(null).map((_, index) => ( +

+ Placeholder Line {index} +

+ ))} + +
+ ); +}; diff --git a/src/hooks/useAlign.ts b/src/hooks/useAlign.ts index 089bc5e2..2c182f24 100644 --- a/src/hooks/useAlign.ts +++ b/src/hooks/useAlign.ts @@ -9,16 +9,12 @@ import type { AlignPointTopBottom, AlignType, } from '../interface'; -import { collectScroller, getWin } from '../util'; +import { collectScroller, getVisibleArea, getWin, toNum } from '../util'; type Rect = Record<'x' | 'y' | 'width' | 'height', number>; type Points = [topBottom: AlignPointTopBottom, leftRight: AlignPointLeftRight]; -function toNum(num: number) { - return Number.isNaN(num) ? 1 : num; -} - function splitPoints(points: string = ''): Points { return [points[0] as any, points[1] as any]; } @@ -174,7 +170,7 @@ export default function useAlign( const targetWidth = targetRect.width; // Get bounding of visible area - const visibleArea = + let visibleArea = placementInfo.htmlRegion === 'scroll' ? // Scroll region should take scrollLeft & scrollTop into account { @@ -190,36 +186,7 @@ export default function useAlign( bottom: clientHeight, }; - (scrollerList || []).forEach((ele) => { - if (ele instanceof HTMLBodyElement) { - return; - } - - const eleRect = ele.getBoundingClientRect(); - const { - offsetHeight: eleOutHeight, - clientHeight: eleInnerHeight, - offsetWidth: eleOutWidth, - clientWidth: eleInnerWidth, - } = ele; - - const scaleX = toNum( - Math.round((eleRect.width / eleOutWidth) * 1000) / 1000, - ); - const scaleY = toNum( - Math.round((eleRect.height / eleOutHeight) * 1000) / 1000, - ); - - const eleScrollWidth = (eleOutWidth - eleInnerWidth) * scaleX; - const eleScrollHeight = (eleOutHeight - eleInnerHeight) * scaleY; - const eleRight = eleRect.x + eleRect.width - eleScrollWidth; - const eleBottom = eleRect.y + eleRect.height - eleScrollHeight; - - visibleArea.left = Math.max(visibleArea.left, eleRect.left); - visibleArea.top = Math.max(visibleArea.top, eleRect.top); - visibleArea.right = Math.min(visibleArea.right, eleRight); - visibleArea.bottom = Math.min(visibleArea.bottom, eleBottom); - }); + visibleArea = getVisibleArea(visibleArea, scrollerList); // Reset back popupElement.style.left = originLeft; diff --git a/src/util.ts b/src/util.ts index 32971786..8c46e7fd 100644 --- a/src/util.ts +++ b/src/util.ts @@ -69,6 +69,11 @@ export function getWin(ele: HTMLElement) { return ele.ownerDocument.defaultView; } +/** + * Get all the scrollable parent elements of the element + * @param ele The element to be detected + * @param areaOnly Only return the parent which will cut visible area + */ export function collectScroller(ele: HTMLElement) { const scrollerList: HTMLElement[] = []; let current = ele?.parentElement; @@ -86,3 +91,60 @@ export function collectScroller(ele: HTMLElement) { return scrollerList; } + +export function toNum(num: number) { + return Number.isNaN(num) ? 1 : num; +} + +export interface VisibleArea { + left: number; + top: number; + right: number; + bottom: number; +} + +export function getVisibleArea( + initArea: VisibleArea, + scrollerList?: HTMLElement[], +) { + const visibleArea = { ...initArea }; + + (scrollerList || []).forEach((ele) => { + if (ele instanceof HTMLBodyElement) { + return; + } + + // Skip if static position which will not affect visible area + const { position } = getWin(ele).getComputedStyle(ele); + if (position === 'static') { + return; + } + + const eleRect = ele.getBoundingClientRect(); + const { + offsetHeight: eleOutHeight, + clientHeight: eleInnerHeight, + offsetWidth: eleOutWidth, + clientWidth: eleInnerWidth, + } = ele; + + const scaleX = toNum( + Math.round((eleRect.width / eleOutWidth) * 1000) / 1000, + ); + const scaleY = toNum( + Math.round((eleRect.height / eleOutHeight) * 1000) / 1000, + ); + + const eleScrollWidth = (eleOutWidth - eleInnerWidth) * scaleX; + const eleScrollHeight = (eleOutHeight - eleInnerHeight) * scaleY; + const eleRight = eleRect.x + eleRect.width - eleScrollWidth; + const eleBottom = eleRect.y + eleRect.height - eleScrollHeight; + + visibleArea.left = Math.max(visibleArea.left, eleRect.x); + visibleArea.top = Math.max(visibleArea.top, eleRect.y); + visibleArea.right = Math.min(visibleArea.right, eleRight); + visibleArea.bottom = Math.min(visibleArea.bottom, eleBottom); + }); + + return visibleArea; +} diff --git a/tests/flip.test.tsx b/tests/flip.test.tsx index 441698c8..265923ef 100644 --- a/tests/flip.test.tsx +++ b/tests/flip.test.tsx @@ -1,6 +1,7 @@ import { act, cleanup, render } from '@testing-library/react'; import { spyElementPrototypes } from 'rc-util/lib/test/domHook'; import Trigger from '../src'; +import { getVisibleArea } from '../src/util'; const builtinPlacements = { top: { @@ -270,4 +271,92 @@ describe('Trigger.Align', () => { top: `0px`, }); }); + + // Static parent should not affect popup position + // https://github.com/ant-design/ant-design/issues/41644 + it('static parent should not affect popup position', async () => { + /* + + ******************** + * * + * ************** * + * * Affect * * + * * ********** * * + * * * Not * * * + * * ********** * * + * * + * ************** * + * * + ******************** + + */ + const initArea = { + left: 0, + right: 500, + top: 0, + bottom: 500, + }; + + // Affected area + const affectEle = document.createElement('div'); + document.body.appendChild(affectEle); + + affectEle.style.position = 'absolute'; + Object.defineProperties(affectEle, { + offsetHeight: { + get: () => 300, + }, + offsetWidth: { + get: () => 300, + }, + clientHeight: { + get: () => 300, + }, + clientWidth: { + get: () => 300, + }, + }); + affectEle.getBoundingClientRect = () => + ({ + x: 100, + y: 100, + width: 300, + height: 300, + } as any); + + // Skip area + const skipEle = document.createElement('div'); + document.body.appendChild(skipEle); + + skipEle.style.position = 'static'; + Object.defineProperties(skipEle, { + offsetHeight: { + get: () => 100, + }, + offsetWidth: { + get: () => 100, + }, + clientHeight: { + get: () => 100, + }, + clientWidth: { + get: () => 100, + }, + }); + skipEle.getBoundingClientRect = () => + ({ + x: 200, + y: 200, + width: 100, + height: 100, + } as any); + + const visibleArea = getVisibleArea(initArea, [affectEle, skipEle]); + expect(visibleArea).toEqual({ + left: 100, + right: 400, + top: 100, + bottom: 400, + }); + }); });