Skip to content

Commit

Permalink
fix: back adjust x&y while overlay is oversized
Browse files Browse the repository at this point in the history
  • Loading branch information
YSMJ1994 committed Nov 27, 2023
1 parent f7c3e5d commit 2d170aa
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 34 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# 忽略目录
build/
es/
lib/
node_modules/
**/*-min.js
**/*.min.js
Expand Down
2 changes: 2 additions & 0 deletions demo/autoAdjust.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ order: 7

能够根据空间大小自动更换 placement

> 若调整后位置始终不符合预期,可能是渲染过程中overlay内容宽度发生了变化导致计算错误,可以尝试固定overlay内容宽度来解决
```jsx
import { useState } from 'react';
import Overlay from '@alifd/overlay';
Expand Down
15 changes: 5 additions & 10 deletions src/overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,12 @@ const Overlay = React.forwardRef<HTMLDivElement, OverlayProps>((props, ref) => {
beforePosition,
autoAdjust,
rtl,
autoHideScrollOverflow: others.autoHideScrollOverflow,
});

if (!isSameObject(positionStyleRef.current, placements.style)) {
positionStyleRef.current = placements.style;
setStyle(overlayNode, { ...placements.style, visibility: '' });
setStyle(overlayNode, placements.style);
typeof onPosition === 'function' && onPosition(placements);
}
});
Expand All @@ -284,17 +285,11 @@ const Overlay = React.forwardRef<HTMLDivElement, OverlayProps>((props, ref) => {

overflowRef.current = getOverflowNodes(targetNode, containerNode);

// 1. 这里提前先设置好 position 属性,因为有的节点可能会因为设置了 position 属性导致宽度变小
// 2. 设置 visibility 先把弹窗藏起来,避免影响视图
// 3. 因为未知原因,原先 left&top 设置为 -1000的方式隐藏会导致获取到的overlay元素宽高不对
// https://drafts.csswg.org/css-position/#abspos-layout 未在此处找到相关解释,可能是浏览器优化,但使其有部分在可视区域内,就可以获取到渲染后正确的宽高, 然后使用visibility隐藏
const nodeRect = getWidthHeight(node);
// fixme: 在followTrigger且空间受限且overlay自动宽度情况下,overlay宽度会跟随left设定自动撑满containing block最右侧,这里建议手动设定overlay宽度或拥有固定内容宽度的overlay来解决,这里暂时使用原来的-1000位置的方案隐藏overlay并不影响容器宽高
setStyle(node, {
position: fixed ? 'fixed' : 'absolute',
// 这里 -nodeRect.width 是避免添加到容器内导致容器出现宽高变化, +1 是为了能确保有一部分在可视区域内
top: -nodeRect.height + 1,
left: -nodeRect.width + 1,
visibility: 'hidden',
top: -1000,
left: -1000,
});

const waitTime = 100;
Expand Down
89 changes: 89 additions & 0 deletions src/placement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,10 @@ function getNewPlacements(
return canTryPlacements;
}

/**
* 任意预设位置都无法完全容纳overlay,则走兜底逻辑,原则是哪边空间大用哪边
* fixme: 在overlay尺寸宽高超过滚动容器宽高情况没有考虑,先走adjustXY逻辑
*/
function getBackupPlacement(
l: number,
t: number,
Expand Down Expand Up @@ -386,6 +390,84 @@ function getBackupPlacement(
return null;
}

/**
* 基于xy的兜底调整
* @param left overlay距离定位节点左侧距离
* @param top overlay距离定位节点上方距离
* @param placement 位置
* @param staticInfo 其它信息
*/
function adjustXY(
left: number,
top: number,
placement: placementType,
staticInfo: any
): { left: number; top: number; placement: placementType } | null {
const { viewport, container, containerInfo, overlayInfo, rtl } = staticInfo;
if (!shouldResizePlacement(left, top, viewport, staticInfo)) {
// 无需调整
return null;
}
// 仍然需要调整
let x = left;
let y = top;
let xAdjust = 0;
let yAdjust = 0;
// 调整为基于 viewport 的xy
if (viewport !== container) {
const { left: cLeft, top: cTop, scrollLeft, scrollTop } = containerInfo;
xAdjust = cLeft - scrollLeft;
yAdjust = cTop - scrollTop;
x += xAdjust;
y += yAdjust;
}
const { width: oWidth, height: oHeight } = overlayInfo;
const { scrollWidth: vWidth, scrollHeight: vHeight } = viewport;
const leftOut = x < 0;
const topOut = y < 0;
const rightOut = x + oWidth > vWidth;
const bottomOut = y + oHeight > vHeight;

if (oWidth > vWidth || oHeight > vHeight) {
// overlay 比 可视区域还要大,方案有:
// 1. 根据rtl模式,强制对齐习惯侧边缘,忽略另一侧超出
// 2. 强制调整overlay宽高,并设置overflow
// 第二种会影响用户布局,先采用第一种办法吧

if (oWidth > vWidth) {
if (rtl) {
x = vWidth - oWidth;
} else {
x = 0;
}
}
if (oHeight > vHeight) {
y = 0;
}
} else {
// viewport可以容纳 overlay
// 则哪边超出,哪边重置为边缘位置
if (leftOut) {
x = 0;
}
if (topOut) {
y = 0;
}
if (rightOut) {
x = vWidth - oWidth;
}
if (bottomOut) {
y = vHeight - oHeight;
}
}

return {
left: x - xAdjust,
top: y - yAdjust,
placement,
};
}

function autoAdjustPosition(
l: number,
t: number,
Expand Down Expand Up @@ -539,6 +621,13 @@ export default function getPlacements(config: PlacementsConfig): PositionResult
}
}

const adjustXYResult = adjustXY(left, top, placement, staticInfo);
if (adjustXYResult) {
left = adjustXYResult.left;
top = adjustXYResult.top;
placement = adjustXYResult.placement;
}

const result: PositionResult = {
config: {
placement,
Expand Down
35 changes: 11 additions & 24 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,31 +117,18 @@ export const getOverflowNodes = (targetNode: HTMLElement, container: HTMLElement
}

const overflowNodes: HTMLElement[] = [];

let calcContainer: HTMLElement = targetNode;

while (true) {
// 忽略 body/documentElement, 不算额外滚动元素
if (
!calcContainer ||
calcContainer === container ||
calcContainer === document.body ||
calcContainer === document.documentElement
) {
// 使用getViewPort方式获取滚动节点,考虑元素可能会跳出最近的滚动容器的情况(绝对定位,containingBlock等原因)
// 原先的只获取了可滚动的滚动容器(滚动高度超出容器高度),改成只要具有滚动属性即可,因为后面可能会发生内容变化导致其变得可滚动了
let overflowNode = getViewPort(targetNode.parentElement);

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

const overflow = getStyle(calcContainer, 'overflow');
if (overflow && overflow.match(/auto|scroll/)) {
const { clientWidth, clientHeight, scrollWidth, scrollHeight } = calcContainer;
if (clientHeight !== scrollHeight || clientWidth !== scrollWidth) {
overflowNodes.push(calcContainer);
}
}

calcContainer = calcContainer.parentNode as HTMLElement;
}

return overflowNodes;
};

Expand Down Expand Up @@ -247,7 +234,7 @@ function getOffsetParent(element: HTMLElement): HTMLElement | null {
* @param container
* @returns
*/
export function getViewPort(container: HTMLElement) {
export function getViewPort(container: HTMLElement): HTMLElement {
// 若 container 本身就是滚动容器,则直接返回
if (isContentClippedElement(container)) {
return container;
Expand All @@ -270,7 +257,7 @@ export function getViewPort(container: HTMLElement) {
}

if (container.parentElement) {
return getContentClippedElement(container.parentElement) || fallbackViewportElement;
return getViewPort(container.parentElement) || fallbackViewportElement;
}
return fallbackViewportElement;
}
Expand Down

0 comments on commit 2d170aa

Please sign in to comment.