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,
+ });
+ });
});