From 2d170aac4bafd6171dc6b4b31f47a21b58f651d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8F=B5=E4=B9=8B?= Date: Mon, 27 Nov 2023 11:00:07 +0800 Subject: [PATCH] fix: back adjust x&y while overlay is oversized --- .eslintignore | 2 ++ demo/autoAdjust.md | 2 ++ src/overlay.tsx | 15 +++----- src/placement.ts | 89 ++++++++++++++++++++++++++++++++++++++++++++++ src/utils.ts | 35 ++++++------------ 5 files changed, 109 insertions(+), 34 deletions(-) diff --git a/.eslintignore b/.eslintignore index f6ee039..021da93 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,7 @@ # 忽略目录 build/ +es/ +lib/ node_modules/ **/*-min.js **/*.min.js diff --git a/demo/autoAdjust.md b/demo/autoAdjust.md index 8d4070b..ed40006 100644 --- a/demo/autoAdjust.md +++ b/demo/autoAdjust.md @@ -5,6 +5,8 @@ order: 7 能够根据空间大小自动更换 placement +> 若调整后位置始终不符合预期,可能是渲染过程中overlay内容宽度发生了变化导致计算错误,可以尝试固定overlay内容宽度来解决 + ```jsx import { useState } from 'react'; import Overlay from '@alifd/overlay'; diff --git a/src/overlay.tsx b/src/overlay.tsx index 7f16522..821747c 100644 --- a/src/overlay.tsx +++ b/src/overlay.tsx @@ -254,11 +254,12 @@ const Overlay = React.forwardRef((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); } }); @@ -284,17 +285,11 @@ const Overlay = React.forwardRef((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; diff --git a/src/placement.ts b/src/placement.ts index 79b4733..fc07eb9 100644 --- a/src/placement.ts +++ b/src/placement.ts @@ -354,6 +354,10 @@ function getNewPlacements( return canTryPlacements; } +/** + * 任意预设位置都无法完全容纳overlay,则走兜底逻辑,原则是哪边空间大用哪边 + * fixme: 在overlay尺寸宽高超过滚动容器宽高情况没有考虑,先走adjustXY逻辑 + */ function getBackupPlacement( l: number, t: number, @@ -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, @@ -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, diff --git a/src/utils.ts b/src/utils.ts index a31b672..9ac9b29 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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; }; @@ -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; @@ -270,7 +257,7 @@ export function getViewPort(container: HTMLElement) { } if (container.parentElement) { - return getContentClippedElement(container.parentElement) || fallbackViewportElement; + return getViewPort(container.parentElement) || fallbackViewportElement; } return fallbackViewportElement; }