Skip to content

Commit b36cd13

Browse files
committed
fix: skip nonscrollable element to judge autoHide
1 parent 7ef82e3 commit b36cd13

File tree

4 files changed

+148
-27
lines changed

4 files changed

+148
-27
lines changed

demo/auto-hide-when-scroll-out.md

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
title: 滚动自动隐藏面板
3+
order: 16
4+
---
5+
6+
展示 `autoHideScrollOverflow` 的用法
7+
8+
```jsx
9+
import Overlay from '@alifd/overlay';
10+
11+
const { Popup } = Overlay;
12+
13+
const style = {
14+
width: 400,
15+
height: 100,
16+
padding: 10,
17+
background: '#fff',
18+
borderRadius: 2,
19+
boxShadow: '2px 2px 20px rgba(0, 0, 0, 0.15)',
20+
};
21+
22+
const FunctionalOverlay = (props) => (
23+
<span {...props} style={style}>
24+
Hello World From Popup!
25+
</span>
26+
);
27+
28+
const FunctionalButton = (props) => (
29+
<button style={{ border: '4px solid' }} {...props}>
30+
Open
31+
</button>
32+
);
33+
34+
ReactDOM.render(
35+
<div>
36+
<div className="scroll-box">
37+
<div style={{ height: 50 }}></div>
38+
<div>
39+
<Popup overlay={<div className="my-popup">auto hide</div>} visible triggerType="click">
40+
<button>trigger1</button>
41+
</Popup>
42+
<Popup
43+
overlay={<div className="my-popup">not hide</div>}
44+
visible
45+
triggerType="click"
46+
autoHideScrollOverflow={false}
47+
>
48+
<button style={{ marginLeft: 50 }}>trigger2</button>
49+
</Popup>
50+
</div>
51+
<div style={{ height: 500 }}></div>
52+
</div>
53+
</div>,
54+
mountNode
55+
);
56+
```
57+
58+
```css
59+
.scroll-box {
60+
height: 300px;
61+
width: 400px;
62+
border: 1px solid #000;
63+
overflow: auto;
64+
}
65+
.my-popup {
66+
background-color: cyan;
67+
height: 150px;
68+
text-align: center;
69+
line-height: 150px;
70+
}
71+
```

src/overlay.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ const Overlay = React.forwardRef<HTMLDivElement, OverlayProps>((props, ref) => {
374374
// - react17 中,如果弹窗 mousedown 阻止了 e.stopPropagation(), 那么 document 就不会监听到事件,因为事件冒泡到挂载节点 rootElement 就中断了。
375375
// - https://reactjs.org/blog/2020/08/10/react-v17-rc.html#changes-to-event-delegation
376376
useListener(
377-
document as unknown as HTMLElement,
377+
document,
378378
'mousedown',
379379
clickEvent,
380380
false,
@@ -392,7 +392,7 @@ const Overlay = React.forwardRef<HTMLDivElement, OverlayProps>((props, ref) => {
392392
}
393393
};
394394
useListener(
395-
document as unknown as HTMLElement,
395+
document,
396396
'keydown',
397397
keydownEvent,
398398
false,
@@ -406,9 +406,9 @@ const Overlay = React.forwardRef<HTMLDivElement, OverlayProps>((props, ref) => {
406406
updatePosition();
407407
};
408408
useListener(
409-
overflowRef.current,
409+
overflowRef.current?.map((t) => (t === document.documentElement ? document : t)),
410410
'scroll',
411-
scrollEvent as any,
411+
scrollEvent,
412412
false,
413413
!!(visible && overlayRef.current && overflowRef.current?.length)
414414
);

src/placement.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { CSSProperties } from 'react';
2-
import { getViewTopLeft, getViewPort, getWidthHeight } from './utils';
2+
import { getViewTopLeft, getViewPort, getWidthHeight, getRect } from './utils';
33

44
type point = 'tl' | 'tc' | 'tr' | 'cl' | 'cc' | 'cr' | 'bl' | 'bc' | 'br';
55
export type pointsType = [point, point];
@@ -658,7 +658,7 @@ export default function getPlacements(config: PlacementsConfig): PositionResult
658658
// result.style.top = 0;
659659

660660
for (const node of scrollNode) {
661-
const { top, left, width, height } = node.getBoundingClientRect();
661+
const { top, left, width, height } = getRect(node);
662662
if (
663663
ttop + theight < top ||
664664
ttop > top + height ||

src/utils.ts

+71-21
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { useEffect, useRef, useLayoutEffect, useCallback } from 'react';
22
import { findDOMNode } from 'react-dom';
33

4+
type CanListenNode = Document | HTMLElement;
5+
46
export function useListener(
5-
nodeList: HTMLElement | HTMLElement[],
7+
nodeList: CanListenNode | CanListenNode[],
68
eventName: string,
79
callback: EventListenerOrEventListenerObject,
810
useCapture: boolean,
@@ -32,7 +34,7 @@ export function useListener(
3234
}
3335

3436
/**
35-
* 将N个方法合并为一个链式调用的方法
37+
* 将 N 个方法合并为一个链式调用的方法
3638
* @return {Function} 合并后的方法
3739
*
3840
* @example
@@ -83,7 +85,7 @@ export function saveRef(ref: any) {
8385
}
8486

8587
/**
86-
* 获取 position != static ,用来计算相对位置的容器
88+
* 获取 position != static,用来计算相对位置的容器
8789
* @param container
8890
* @returns
8991
*/
@@ -117,23 +119,22 @@ export const getOverflowNodes = (targetNode: HTMLElement, container: HTMLElement
117119
}
118120

119121
const overflowNodes: HTMLElement[] = [];
120-
// 使用getViewPort方式获取滚动节点,考虑元素可能会跳出最近的滚动容器的情况(绝对定位,containingBlock等原因
122+
// 使用 getViewPort 方式获取滚动节点,考虑元素可能会跳出最近的滚动容器的情况(绝对定位,containingBlock 等原因
121123
// 原先的只获取了可滚动的滚动容器(滚动高度超出容器高度),改成只要具有滚动属性即可,因为后面可能会发生内容变化导致其变得可滚动了
122-
let overflowNode = getViewPort(targetNode.parentElement);
124+
let overflowNode = getViewPortExcludeSelf(targetNode);
123125

124126
while (overflowNode && container.contains(overflowNode) && container !== overflowNode) {
125127
overflowNodes.push(overflowNode);
126-
if (overflowNode.parentElement) {
127-
overflowNode = getViewPort(overflowNode.parentElement);
128-
} else {
129-
break;
130-
}
128+
overflowNode = getViewPortExcludeSelf(overflowNode);
129+
}
130+
if (isScrollableElement(container)) {
131+
overflowNodes.push(container);
131132
}
132133
return overflowNodes;
133134
};
134135

135136
/**
136-
* 是否是webkit内核
137+
* 是否是 webkit 内核
137138
*/
138139
function isWebKit(): boolean {
139140
if (typeof CSS === 'undefined' || !CSS.supports) {
@@ -194,10 +195,38 @@ function getContainingBlock(element: HTMLElement): HTMLElement | null {
194195
*/
195196
const isContentClippedElement = (element: Element) => {
196197
const overflow = getStyle(element, 'overflow');
197-
// 测试环境overflow默认为 ''
198-
return overflow && overflow !== 'visible';
198+
// 测试环境 overflow 默认为 ''
199+
return (overflow && overflow !== 'visible') || element === document.documentElement;
199200
};
200201

202+
/**
203+
* 判断元素是否是可滚动的元素,且滚动内容尺寸大于元素尺寸
204+
*/
205+
function isScrollableElement(element: Element) {
206+
const overflow = getStyle(element, 'overflow');
207+
// 这里兼容老的逻辑判断,忽略 hidden
208+
if (element === document.documentElement || (overflow && overflow.match(/auto|scroll/))) {
209+
const { clientWidth, clientHeight, scrollWidth, scrollHeight } = element;
210+
// 仅当实际滚动高度大于元素尺寸时,才被视作是可滚动元素
211+
return clientHeight !== scrollHeight || clientWidth !== scrollWidth;
212+
}
213+
return false;
214+
}
215+
216+
export function getRect(target: HTMLElement) {
217+
if (target === document.documentElement) {
218+
const { clientWidth: width, clientHeight: height } = target;
219+
return {
220+
left: 0,
221+
top: 0,
222+
width,
223+
height,
224+
};
225+
}
226+
const { left, top, width, height } = target.getBoundingClientRect();
227+
return { left, top, width, height };
228+
}
229+
201230
/**
202231
* 获取最近的裁剪内容区域的祖先节点
203232
*/
@@ -229,6 +258,24 @@ function getOffsetParent(element: HTMLElement): HTMLElement | null {
229258
return offsetParent as HTMLElement;
230259
}
231260

261+
export function getViewPortExcludeSelf(target: HTMLElement) {
262+
const fallbackViewportElement = document.documentElement;
263+
if (!target) {
264+
return fallbackViewportElement;
265+
}
266+
const parent = ['fixed', 'absolute'].includes(getStyle(target, 'position'))
267+
? getOffsetParent(target) || getContainingBlock(target)
268+
: target.parentElement;
269+
270+
if (!parent) {
271+
return fallbackViewportElement;
272+
}
273+
if (isContentClippedElement(parent)) {
274+
return parent;
275+
}
276+
return getViewPortExcludeSelf(parent);
277+
}
278+
232279
/**
233280
* 获取可视区域,用来计算弹窗应该相对哪个节点做上下左右的位置变化。
234281
* @param container
@@ -241,25 +288,28 @@ export function getViewPort(container: HTMLElement): HTMLElement {
241288
return fallbackViewportElement;
242289
}
243290

244-
// 若 container 本身就是滚动容器,则直接返回
245-
if (isContentClippedElement(container)) {
246-
return container;
247-
}
248-
249-
// 若 container 的 position 是 absolute 或 fixed,则有可能会脱离其最近的滚动容器,需要根据 offsetParent 和 containing block来综合判断
291+
// 若 container 的 position 是 absolute 或 fixed,则有可能会脱离其最近的滚动容器,需要根据 offsetParent 和 containing block 来综合判断
250292
if (['fixed', 'absolute'].includes(getStyle(container, 'position'))) {
293+
if (isContentClippedElement(container)) {
294+
return container;
295+
}
296+
251297
// 先获取定位节点(若无则使用 containerBlock)
252298
const offsetParent = getOffsetParent(container) || getContainingBlock(container);
253299
// 拥有定位节点
254300
if (offsetParent) {
255301
// 从定位节点开始寻找父级滚动容器
256302
return getViewPort(offsetParent);
257303
} else {
258-
// 无定位节点,也无containingBlock影响,则用 fallback元素
304+
// 无定位节点,也无 containingBlock 影响,则用 fallback 元素
259305
return fallbackViewportElement;
260306
}
261307
}
262308

309+
if (isContentClippedElement(container)) {
310+
return container;
311+
}
312+
263313
if (container.parentElement) {
264314
return getViewPort(container.parentElement) || fallbackViewportElement;
265315
}
@@ -335,7 +385,7 @@ export function debounce(func: Function, wait: number) {
335385
*/
336386
export function getViewTopLeft(node: HTMLElement) {
337387
/**
338-
* document.body 向下滚动后 scrollTop 一直为0,同时 top=-xx 为负数,相当于本身是没有滚动条的,这个逻辑是正确的。
388+
* document.body 向下滚动后 scrollTop 一直为 0,同时 top=-xx 为负数,相当于本身是没有滚动条的,这个逻辑是正确的。
339389
* document.documentElement 向下滚动后 scrollTop/top 都在变化,会影响计算逻辑,所以这里写死 0
340390
*/
341391
if (node === document.documentElement) {

0 commit comments

Comments
 (0)