Skip to content

Commit

Permalink
fix: skip nonscrollable element to judge autoHide
Browse files Browse the repository at this point in the history
  • Loading branch information
YSMJ1994 committed Mar 21, 2024
1 parent 7ef82e3 commit 37f8819
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 27 deletions.
71 changes: 71 additions & 0 deletions demo/auto-hide-when-scroll-out.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
title: 滚动自动隐藏面板
order: 16
---

展示 `autoHideScrollOverflow` 的用法

```jsx
import Overlay from '@alifd/overlay';

const { Popup } = Overlay;

const style = {
width: 400,
height: 100,
padding: 10,
background: '#fff',
borderRadius: 2,
boxShadow: '2px 2px 20px rgba(0, 0, 0, 0.15)',
};

const FunctionalOverlay = (props) => (
<span {...props} style={style}>
Hello World From Popup!
</span>
);

const FunctionalButton = (props) => (
<button style={{ border: '4px solid' }} {...props}>
Open
</button>
);

ReactDOM.render(
<div>
<div className="scroll-box">
<div style={{ height: 50 }}></div>
<div>
<Popup overlay={<div className="my-popup">auto hide</div>} visible triggerType="click">
<button>trigger1</button>
</Popup>
<Popup
overlay={<div className="my-popup">not hide</div>}
visible
triggerType="click"
autoHideScrollOverflow={false}
>
<button style={{ marginLeft: 50 }}>trigger2</button>
</Popup>
</div>
<div style={{ height: 500 }}></div>
</div>
</div>,
mountNode
);
```

```css
.scroll-box {
height: 300px;
width: 400px;
border: 1px solid #000;
overflow: auto;
}
.my-popup {
background-color: cyan;
height: 150px;
text-align: center;
line-height: 150px;
}
```
8 changes: 4 additions & 4 deletions src/overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ const Overlay = React.forwardRef<HTMLDivElement, OverlayProps>((props, ref) => {
// - react17 中,如果弹窗 mousedown 阻止了 e.stopPropagation(), 那么 document 就不会监听到事件,因为事件冒泡到挂载节点 rootElement 就中断了。
// - https://reactjs.org/blog/2020/08/10/react-v17-rc.html#changes-to-event-delegation
useListener(
document as unknown as HTMLElement,
document,
'mousedown',
clickEvent,
false,
Expand All @@ -392,7 +392,7 @@ const Overlay = React.forwardRef<HTMLDivElement, OverlayProps>((props, ref) => {
}
};
useListener(
document as unknown as HTMLElement,
document,
'keydown',
keydownEvent,
false,
Expand All @@ -406,9 +406,9 @@ const Overlay = React.forwardRef<HTMLDivElement, OverlayProps>((props, ref) => {
updatePosition();
};
useListener(
overflowRef.current,
overflowRef.current?.map((t) => (t === document.documentElement ? document : t)),
'scroll',
scrollEvent as any,
scrollEvent,
false,
!!(visible && overlayRef.current && overflowRef.current?.length)
);
Expand Down
4 changes: 2 additions & 2 deletions src/placement.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CSSProperties } from 'react';
import { getViewTopLeft, getViewPort, getWidthHeight } from './utils';
import { getViewTopLeft, getViewPort, getWidthHeight, getRect } from './utils';

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

for (const node of scrollNode) {
const { top, left, width, height } = node.getBoundingClientRect();
const { top, left, width, height } = getRect(node);
if (
ttop + theight < top ||
ttop > top + height ||
Expand Down
92 changes: 71 additions & 21 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useEffect, useRef, useLayoutEffect, useCallback } from 'react';
import { findDOMNode } from 'react-dom';

type CanListenNode = Document | HTMLElement;

export function useListener(
nodeList: HTMLElement | HTMLElement[],
nodeList: CanListenNode | CanListenNode[],
eventName: string,
callback: EventListenerOrEventListenerObject,
useCapture: boolean,
Expand Down Expand Up @@ -32,7 +34,7 @@ export function useListener(
}

/**
* 将N个方法合并为一个链式调用的方法
* 将 N 个方法合并为一个链式调用的方法
* @return {Function} 合并后的方法
*
* @example
Expand Down Expand Up @@ -83,7 +85,7 @@ export function saveRef(ref: any) {
}

/**
* 获取 position != static ,用来计算相对位置的容器
* 获取 position != static,用来计算相对位置的容器
* @param container
* @returns
*/
Expand Down Expand Up @@ -117,23 +119,22 @@ export const getOverflowNodes = (targetNode: HTMLElement, container: HTMLElement
}

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

while (overflowNode && container.contains(overflowNode) && container !== overflowNode) {
overflowNodes.push(overflowNode);
if (overflowNode.parentElement) {
overflowNode = getViewPort(overflowNode.parentElement);
} else {
break;
}
overflowNode = getViewPortExcludeSelf(overflowNode);
}
if (isScrollableElement(container)) {
overflowNodes.push(container);
}
return overflowNodes;
};

/**
* 是否是webkit内核
* 是否是 webkit 内核
*/
function isWebKit(): boolean {
if (typeof CSS === 'undefined' || !CSS.supports) {
Expand Down Expand Up @@ -194,10 +195,38 @@ function getContainingBlock(element: HTMLElement): HTMLElement | null {
*/
const isContentClippedElement = (element: Element) => {
const overflow = getStyle(element, 'overflow');
// 测试环境overflow默认为 ''
return overflow && overflow !== 'visible';
// 测试环境 overflow 默认为 ''
return (overflow && overflow !== 'visible') || element === document.documentElement;
};

/**
* 判断元素是否是可滚动的元素,且滚动内容尺寸大于元素尺寸
*/
function isScrollableElement(element: Element) {
const overflow = getStyle(element, 'overflow');
// 这里兼容老的逻辑判断,忽略 hidden
if (element === document.documentElement || (overflow && overflow.match(/auto|scroll/))) {
const { clientWidth, clientHeight, scrollWidth, scrollHeight } = element;
// 仅当实际滚动高度大于元素尺寸时,才被视作是可滚动元素
return clientHeight !== scrollHeight || clientWidth !== scrollWidth;
}
return false;
}

export function getRect(target: HTMLElement) {
if (target === document.documentElement) {
const { clientWidth: width, clientHeight: height } = target;
return {
left: 0,
top: 0,
width,
height,
};
}
const { left, top, width, height } = target.getBoundingClientRect();
return { left, top, width, height };
}

/**
* 获取最近的裁剪内容区域的祖先节点
*/
Expand Down Expand Up @@ -229,6 +258,24 @@ function getOffsetParent(element: HTMLElement): HTMLElement | null {
return offsetParent as HTMLElement;
}

export function getViewPortExcludeSelf(target: HTMLElement) {
const fallbackViewportElement = document.documentElement;
if (!target) {
return fallbackViewportElement;
}
const parent = ['fixed', 'absolute'].includes(getStyle(target, 'position'))
? getOffsetParent(target) || getContainingBlock(target)
: target.parentElement;

if (!parent) {
return fallbackViewportElement;
}
if (isContentClippedElement(parent)) {
return parent;
}
return getViewPortExcludeSelf(parent);
}

/**
* 获取可视区域,用来计算弹窗应该相对哪个节点做上下左右的位置变化。
* @param container
Expand All @@ -241,25 +288,28 @@ export function getViewPort(container: HTMLElement): HTMLElement {
return fallbackViewportElement;
}

// 若 container 本身就是滚动容器,则直接返回
if (isContentClippedElement(container)) {
return container;
}

// 若 container 的 position 是 absolute 或 fixed,则有可能会脱离其最近的滚动容器,需要根据 offsetParent 和 containing block来综合判断
// 若 container 的 position 是 absolute 或 fixed,则有可能会脱离其最近的滚动容器,需要根据 offsetParent 和 containing block 来综合判断
if (['fixed', 'absolute'].includes(getStyle(container, 'position'))) {
if (isContentClippedElement(container)) {
return container;
}

// 先获取定位节点(若无则使用 containerBlock)
const offsetParent = getOffsetParent(container) || getContainingBlock(container);
// 拥有定位节点
if (offsetParent) {
// 从定位节点开始寻找父级滚动容器
return getViewPort(offsetParent);
} else {
// 无定位节点,也无containingBlock影响,则用 fallback元素
// 无定位节点,也无 containingBlock 影响,则用 fallback 元素
return fallbackViewportElement;
}
}

if (isContentClippedElement(container)) {
return container;
}

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

0 comments on commit 37f8819

Please sign in to comment.