diff --git a/README.md b/README.md
index 86bb076..7f307ce 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,7 @@ Popup 是对 Overlay 的封装,children作为触发节点,弹出一个浮层
| onRequestClose | 弹层请求关闭时触发事件的回调函数 | Function | () => {} |
| target | 弹层定位的参照元素 | Function | ()=> document.body |
| points | 弹层相对于参照元素的定位 | [point, point] | ['tl', 'bl'] |
+| placement | 部分points的简写模式
**可选值**:
't'(上,对应 points: ['bc', 'tc'])
'r'(右,对应 points: ['lc', 'rc'])
'b'(下,对应 points: ['tc', 'bc'])
'l'(左,对应 points: ['rc', 'lc'])
'tl'(上左,对应 points: ['bl', 'tl'])
'tr'(上右,对应 points: ['br', 'tr'])
'bl'(下左,对应 points: ['tl', 'bl'])
'br'(下右,对应 points: ['tr', 'br'])
'lt'(左上,对应 points: ['rt', 'lt'])
'lb'(左下,对应 points: ['rb', 'lb'])
'rt'(右上,对应 points: ['lt', 'rt'])
'rb'(右下,对应 points: ['lb', 'rb']) | Enum | 'bl' | |
| offset | 弹层相对于trigger的定位的微调, 接收数组[hoz, ver], 表示弹层在 left / top 上的增量
e.g. [100, 100] 表示往右、下分布偏移100px | Array | [0, 0]|
| container | 渲染组件的容器,如果是函数需要返回 ref,如果是字符串则是该 DOM 的 id,也可以直接传入 DOM 节点 | any | - |
| hasMask | 是否显示遮罩 | Boolean | false |
diff --git a/package.json b/package.json
index 441681d..7e0d165 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
"@alifd/next": "1.x",
"@commitlint/cli": "^8.3.6",
"@iceworks/spec": "^1.0.0",
+ "@types/jest": "^26.0.24",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.3",
diff --git a/src/overlay.tsx b/src/overlay.tsx
index b9eee83..7f16522 100644
--- a/src/overlay.tsx
+++ b/src/overlay.tsx
@@ -25,6 +25,7 @@ import {
getFocusNodeList,
isSameObject,
useEvent,
+ getWidthHeight,
} from './utils';
import OverlayContext from './overlay-context';
@@ -257,7 +258,7 @@ const Overlay = React.forwardRef((props, ref) => {
if (!isSameObject(positionStyleRef.current, placements.style)) {
positionStyleRef.current = placements.style;
- setStyle(overlayNode, placements.style);
+ setStyle(overlayNode, { ...placements.style, visibility: '' });
typeof onPosition === 'function' && onPosition(placements);
}
});
@@ -268,7 +269,6 @@ const Overlay = React.forwardRef((props, ref) => {
const node = findDOMNode(nodeRef) as HTMLElement;
overlayRef.current = node;
callRef(ref, node);
-
if (node !== null && container) {
const containerNode = getRelativeContainer(getHTMLElement(container));
containerRef.current = containerNode;
@@ -285,13 +285,25 @@ const Overlay = React.forwardRef((props, ref) => {
overflowRef.current = getOverflowNodes(targetNode, containerNode);
// 1. 这里提前先设置好 position 属性,因为有的节点可能会因为设置了 position 属性导致宽度变小
- // 2. 提前设置 top/left -1000 先把弹窗藏起来,以免影响了 container 的高度计算
- setStyle(node, { position: fixed ? 'fixed' : 'absolute', top: -1000, left: -1000 });
+ // 2. 设置 visibility 先把弹窗藏起来,避免影响视图
+ // 3. 因为未知原因,原先 left&top 设置为 -1000的方式隐藏会导致获取到的overlay元素宽高不对
+ // https://drafts.csswg.org/css-position/#abspos-layout 未在此处找到相关解释,可能是浏览器优化,但使其有部分在可视区域内,就可以获取到渲染后正确的宽高, 然后使用visibility隐藏
+ const nodeRect = getWidthHeight(node);
+ setStyle(node, {
+ position: fixed ? 'fixed' : 'absolute',
+ // 这里 -nodeRect.width 是避免添加到容器内导致容器出现宽高变化, +1 是为了能确保有一部分在可视区域内
+ top: -nodeRect.height + 1,
+ left: -nodeRect.width + 1,
+ visibility: 'hidden',
+ });
const waitTime = 100;
- ro.current = new ResizeObserver(throttle(updatePosition.bind(this), waitTime));
+ const throttledUpdatePosition = throttle(updatePosition, waitTime);
+ ro.current = new ResizeObserver(throttledUpdatePosition);
ro.current.observe(containerNode);
ro.current.observe(node);
+ // fist call, 不依赖 ResizeObserver observe时的首次执行(测试环境不会执行),因为 throttle 原因也不会执行两次
+ throttledUpdatePosition();
forceUpdate({});
diff --git a/src/placement.ts b/src/placement.ts
index d9e8f5f..79b4733 100644
--- a/src/placement.ts
+++ b/src/placement.ts
@@ -136,6 +136,7 @@ function getXY(p: placementType | undefined, staticInfo: any) {
case 'r':
basex += plus * width;
break;
+ // no default
}
}
@@ -151,6 +152,7 @@ function getXY(p: placementType | undefined, staticInfo: any) {
case 'b':
basey += plus * height;
break;
+ // no default
}
}
@@ -193,6 +195,7 @@ function getXY(p: placementType | undefined, staticInfo: any) {
case 'r':
basex += placementOffset;
break;
+ // no default
}
}
@@ -229,58 +232,203 @@ function shouldResizePlacement(l: number, t: number, viewport: HTMLElement, stat
);
}
-function getNewPlacement(l: number, t: number, p: placementType, staticInfo: any) {
+function getNewPlacements(
+ l: number,
+ t: number,
+ p: placementType,
+ staticInfo: any
+): placementType[] {
const { overlayInfo, containerInfo } = staticInfo;
-
- let np = p.split('');
- if (np.length === 1) {
- np.push('');
+ const [direction, align = ''] = p.split('');
+
+ const topOut = t < 0;
+ const leftOut = l < 0;
+ const rightOut = l + overlayInfo.width > containerInfo.width;
+ const bottomOut = t + overlayInfo.height > containerInfo.height;
+ const forbiddenSet = new Set();
+ const forbid = (...ps: placementType[]) => ps.forEach((t) => forbiddenSet.add(t));
+ // 上方超出
+ if (topOut) {
+ forbid('t', 'tl', 'tr');
}
- // 区域不够
- if (t < 0) {
- // [上边 => 下边, 底部对齐 => 顶部对齐]
- np = [np[0].replace('t', 'b'), np[1].replace('b', 't')];
+ // 右侧超出
+ if (rightOut) {
+ forbid('r', 'rt', 'rb');
}
- // 区域不够
- if (l < 0) {
- // [左边 => 右边, 右对齐 => 左对齐]
- np = [np[0].replace('l', 'r'), np[1].replace('r', 'l')];
+
+ // 下方超出
+ if (bottomOut) {
+ forbid('b', 'bl', 'br');
}
- // 超出区域
- if (t + overlayInfo.height > containerInfo.height) {
- // [下边 => 上边, 顶部对齐 => 底部对齐]
- np = [np[0].replace('b', 't'), np[1].replace('t', 'b')];
+
+ // 左侧超出
+ if (leftOut) {
+ forbid('l', 'lt', 'lb');
}
- // 超出区域
- if (l + overlayInfo.width > containerInfo.width) {
- // [右边 => 左边, 左对齐 => 右对齐]
- np = [np[0].replace('r', 'l'), np[1].replace('l', 'r')];
+
+ const allPlacements = Object.keys(placementMap) as placementType[];
+ // 过滤出所有可用的
+ const canTryPlacements = allPlacements.filter((t) => !forbiddenSet.has(t));
+
+ // 无可用
+ if (!canTryPlacements.length) {
+ return null;
}
- return np.join('') as placementType;
+ // 排序规则: 同向 > 对向 > 其他方向; 同align > 其他align; 中间 > l = t > r = b; align规则仅在同侧比较时生效
+ // 参考: https://github.com/alibaba-fusion/overlay/issues/23
+
+ const reverseMap: Record = {
+ l: 'r',
+ r: 'l',
+ t: 'b',
+ b: 't',
+ '': '',
+ };
+ // direction权重, l=t > r=b
+ // 权重差值 4
+ const directionOrderWeights: Record = {
+ t: 10,
+ b: 6,
+ l: 10,
+ r: 6,
+ };
+ // 用户的 direction 最高
+ directionOrderWeights[direction] = 12;
+ // 用户 direction 的反转方向其次
+ directionOrderWeights[reverseMap[direction]] = 11;
+
+ // align排序权重: '' > l=t > r=b
+ // 此处取值 2, 1, 0 意为远小于 direction 权重值和其差值,使得在加权和比较时不影响 direction,达到"仅同向比较align的目的"
+ const alignOrderWeights: Record = {
+ '': 2,
+ l: 1,
+ r: 0,
+ t: 1,
+ b: 0,
+ };
+ // 用户的 align 权重最高
+ alignOrderWeights[align] = 3;
+
+ canTryPlacements.sort((after, before) => {
+ const [afterDirection, afterAlign = ''] = after.split('');
+ const [beforeDirection, beforeAlign = ''] = before.split('');
+ const afterDirectionWeight = directionOrderWeights[afterDirection];
+ const afterAlignWeight = alignOrderWeights[afterAlign];
+ const beforeDirectionWeight = directionOrderWeights[beforeDirection];
+ const beforeAlighWeight = alignOrderWeights[beforeAlign];
+ // direction都相同,比较align weight
+ if (afterDirection === beforeDirection) {
+ return afterAlignWeight > beforeAlighWeight ? -1 : 1;
+ }
+
+ // align 相同,比较 direction weight
+ if (afterAlign === beforeAlign) {
+ return afterDirectionWeight > beforeDirectionWeight ? -1 : 1;
+ }
+
+ // 都不同
+ // 与用户 direction相同情况优先最高
+ if ([afterDirection, beforeDirection].includes(direction)) {
+ return afterDirection === direction ? -1 : 1;
+ }
+
+ const reverseDirection = reverseMap[direction];
+ // 与用户 reverse direction 相同则优先级其次
+ if ([afterDirection, beforeDirection].includes(reverseDirection)) {
+ return afterDirection === reverseDirection ? -1 : 1;
+ }
+
+ // 与用户align相同,则优先级更高
+ if ([afterAlign, beforeAlign].includes(align)) {
+ return afterAlign === align ? -1 : 1;
+ }
+
+ // 没有特殊情况,进行加权和比较
+ return afterDirectionWeight + afterAlignWeight > beforeDirectionWeight + beforeAlighWeight
+ ? -1
+ : 1;
+ });
+
+ return canTryPlacements;
}
-function ajustLeftAndTop(l: number, t: number, staticInfo: any) {
+function getBackupPlacement(
+ l: number,
+ t: number,
+ p: placementType,
+ staticInfo: any
+): placementType | null {
const { overlayInfo, containerInfo } = staticInfo;
-
- if (t < 0) {
- t = 0;
+ const [direction, align] = p.split('');
+
+ const topOut = t < 0;
+ const leftOut = l < 0;
+ const rightOut = l + overlayInfo.width > containerInfo.width;
+ const bottomOut = t + overlayInfo.height > containerInfo.height;
+ const outNumber = [topOut, leftOut, rightOut, bottomOut].filter(Boolean).length;
+
+ if (outNumber === 3) {
+ // 三面超出,则使用未超出的方向
+ const maps: Array<{ direction: string; value: boolean }> = [
+ { direction: 't', value: topOut },
+ { direction: 'r', value: rightOut },
+ { direction: 'b', value: bottomOut },
+ { direction: 'l', value: leftOut },
+ ];
+ const backDirection = maps.find((t) => !t.value)?.direction;
+ // 若原来的方向跟调整后不一致,则使用调整后的
+ if (backDirection && backDirection !== direction) {
+ return backDirection as placementType;
+ }
}
- if (l < 0) {
- l = 0;
+ return null;
+}
+
+function autoAdjustPosition(
+ l: number,
+ t: number,
+ p: placementType,
+ staticInfo: any
+): { left: number; top: number; placement: placementType } | null {
+ let left = l;
+ let top = t;
+ const { viewport, container, containerInfo } = staticInfo;
+ const { left: cLeft, top: cTop, scrollLeft, scrollTop } = containerInfo;
+
+ // 此时left&top是相对于container的,若container不是 viewport,则需要调整为相对 viewport 的 left & top 再计算
+ if (viewport !== container) {
+ left += cLeft - scrollLeft;
+ top += cTop - scrollTop;
}
- if (t + overlayInfo.height > containerInfo.height) {
- t = containerInfo.height - overlayInfo.height;
+
+ // 根据位置超出情况,获取所有可以尝试的位置列表
+ const placements = getNewPlacements(left, top, p, staticInfo);
+ // 按顺序寻找能够容纳的位置
+ for (const placement of placements) {
+ const { left: nLeft, top: nTop } = getXY(placement, staticInfo);
+ if (!shouldResizePlacement(nLeft, nTop, viewport, staticInfo)) {
+ return {
+ left: nLeft,
+ top: nTop,
+ placement,
+ };
+ }
}
- if (l + overlayInfo.width > containerInfo.width) {
- l = containerInfo.width - overlayInfo.width;
+
+ const backupPlacement = getBackupPlacement(left, top, p, staticInfo);
+
+ if (backupPlacement) {
+ const { left: nLeft, top: nTop } = getXY(backupPlacement, staticInfo);
+ return {
+ left: nLeft,
+ top: nTop,
+ placement: backupPlacement,
+ };
}
- return {
- left: l,
- top: t,
- };
+ return null;
}
/**
@@ -354,6 +502,9 @@ export default function getPlacements(config: PlacementsConfig): PositionResult
scrollLeft: cscrollLeft,
} = container;
+ // 获取可视区域,来计算容器相对位置
+ const viewport = getViewPort(container);
+
const staticInfo = {
targetInfo: { width: twidth, height: theight, left: tleft, top: ttop },
containerInfo: {
@@ -364,54 +515,28 @@ export default function getPlacements(config: PlacementsConfig): PositionResult
scrollTop: cscrollTop,
scrollLeft: cscrollLeft,
},
+ overlay,
overlayInfo: { width: owidth, height: oheight },
points: opoints,
placementOffset,
offset,
container,
rtl,
+ viewport,
};
// step1: 根据 placement 计算位置
let { left, top, points } = getXY(placement, staticInfo);
- // 获取可视区域,来计算容器相对位置
- const viewport = getViewPort(container);
-
// step2: 根据 viewport(挂载容器不一定是可视区, eg: 挂载在父节点,但是弹窗超出父节点)重新计算位置. 根据可视区域优化位置
- // 位置动态优化思路见 https://github.com/alibaba-fusion/overlay/issues/2
+ // 位置动态优化思路见 https://github.com/alibaba-fusion/overlay/issues/23
if (autoAdjust && placement && shouldResizePlacement(left, top, viewport, staticInfo)) {
- const nplacement = getNewPlacement(left, top, placement, staticInfo);
- // step2: 空间不够,替换位置重新计算位置
- if (placement !== nplacement) {
- const { left: nleft, top: ntop } = getXY(nplacement, staticInfo);
-
- if (shouldResizePlacement(nleft, ntop, viewport, staticInfo)) {
- const nnplacement = getNewPlacement(nleft, ntop, nplacement, staticInfo);
- // step3: 空间依然不够,说明xy轴至少有一个方向是怎么更换位置都不够的。停止计算开始补偿逻辑
- if (nplacement !== nnplacement) {
- const { left: nnleft, top: nntop } = getXY(nnplacement, staticInfo);
-
- const { left: nnnleft, top: nnntop } = ajustLeftAndTop(nnleft, nntop, staticInfo);
-
- placement = nnplacement;
- left = nnnleft;
- top = nnntop;
- } else {
- placement = nplacement;
- left = nleft;
- top = ntop;
- }
- } else {
- placement = nplacement;
- left = nleft;
- top = ntop;
- }
+ const adjustResult = autoAdjustPosition(left, top, placement, staticInfo);
+ if (adjustResult) {
+ left = adjustResult.left;
+ top = adjustResult.top;
+ placement = adjustResult.placement;
}
-
- const { left: nleft, top: ntop } = ajustLeftAndTop(left, top, staticInfo);
- left = nleft;
- top = ntop;
}
const result: PositionResult = {
diff --git a/src/utils.ts b/src/utils.ts
index bcb7da3..a31b672 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -145,24 +145,134 @@ export const getOverflowNodes = (targetNode: HTMLElement, container: HTMLElement
return overflowNodes;
};
+/**
+ * 是否是webkit内核
+ */
+function isWebKit(): boolean {
+ if (typeof CSS === 'undefined' || !CSS.supports) {
+ return false;
+ }
+ return CSS.supports('-webkit-backdrop-filter', 'none');
+}
+
+/**
+ * 判断元素是否是会影响后代节点定位的 containing block
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
+ */
+function isContainingBlock(ele: Element) {
+ const webkit = isWebKit();
+ const css = getComputedStyle(ele);
+
+ return Boolean(
+ (css.transform && css.transform !== 'none') ||
+ (css.perspective && css.perspective !== 'none') ||
+ (css.containerType && css.containerType !== 'normal') ||
+ (!webkit && css.backdropFilter && css.backdropFilter !== 'none') ||
+ (!webkit && css.filter && css.filter !== 'none') ||
+ ['transform', 'perspective', 'filter'].some((value) =>
+ (css.willChange || '').includes(value)
+ ) ||
+ ['paint', 'layout', 'strict', 'content'].some((value) => (css.contain || '').includes(value))
+ );
+}
+
+/**
+ * 判断元素是否是 html 或 body 元素
+ */
+function isLastTraversableElement(ele: Element): boolean {
+ return ['html', 'body'].includes(ele.tagName.toLowerCase());
+}
+
+/**
+ * 获取 containing block
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
+ */
+function getContainingBlock(element: HTMLElement): HTMLElement | null {
+ let currentElement: HTMLElement | null = element.parentElement;
+
+ while (currentElement && !isLastTraversableElement(currentElement)) {
+ if (isContainingBlock(currentElement)) {
+ return currentElement;
+ } else {
+ currentElement = currentElement.parentElement;
+ }
+ }
+
+ return null;
+}
+
+/**
+ * 判断元素是否会裁剪内容区域
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/overflow
+ */
+const isContentClippedElement = (element: Element) => {
+ const overflow = getStyle(element, 'overflow');
+ // 测试环境overflow默认为 ''
+ return overflow && overflow !== 'visible';
+};
+
+/**
+ * 获取最近的裁剪内容区域的祖先节点
+ */
+function getContentClippedElement(element: HTMLElement) {
+ if (isContentClippedElement(element)) {
+ return element;
+ }
+ let parent = element.parentElement;
+ while (parent) {
+ if (isContentClippedElement(parent)) {
+ return parent;
+ }
+ parent = parent.parentElement;
+ }
+ return null;
+}
+
+/**
+ * 获取定位节点,忽略表格元素影响
+ * https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
+ * @param element
+ * @returns closest positioned ancestor element
+ */
+function getOffsetParent(element: HTMLElement): HTMLElement | null {
+ let { offsetParent } = element;
+ while (offsetParent && ['table', 'th', 'td'].includes(offsetParent.tagName.toLowerCase())) {
+ offsetParent = (offsetParent as HTMLElement).offsetParent;
+ }
+ return offsetParent as HTMLElement;
+}
+
/**
* 获取可视区域,用来计算弹窗应该相对哪个节点做上下左右的位置变化。
* @param container
* @returns
*/
export function getViewPort(container: HTMLElement) {
- let calcContainer: HTMLElement = container;
+ // 若 container 本身就是滚动容器,则直接返回
+ if (isContentClippedElement(container)) {
+ return container;
+ }
- while (calcContainer) {
- const overflow = getStyle(calcContainer, 'overflow');
- if (overflow?.match(/auto|scroll|hidden/)) {
- return calcContainer;
- }
+ const fallbackViewportElement = document.documentElement;
- calcContainer = calcContainer.parentNode as HTMLElement;
+ // 若 container 的 position 是 absolute 或 fixed,则有可能会脱离其最近的滚动容器,需要根据 offsetParent 和 containing block来综合判断
+ if (['fixed', 'absolute'].includes(getStyle(container, 'position'))) {
+ // 先获取定位节点(若无则使用 containerBlock)
+ const offsetParent = getOffsetParent(container) || getContainingBlock(container);
+ // 拥有定位节点
+ if (offsetParent) {
+ // 从定位节点开始寻找父级滚动容器
+ return getViewPort(offsetParent);
+ } else {
+ // 无定位节点,也无containingBlock影响,则用 fallback元素
+ return fallbackViewportElement;
+ }
}
- return document.documentElement;
+ if (container.parentElement) {
+ return getContentClippedElement(container.parentElement) || fallbackViewportElement;
+ }
+ return fallbackViewportElement;
}
export function getStyle(elt: Element, name: string) {
diff --git a/test/placement.test.jsx b/test/placement.test.jsx
index c996123..56001aa 100644
--- a/test/placement.test.jsx
+++ b/test/placement.test.jsx
@@ -1,6 +1,3 @@
-import React, { useRef, useState, useEffect } from 'react';
-import ReactTestUtils, { act } from 'react-dom/test-utils';
-
import getPlacements from '../src/placement';
import { getStyle, getViewPort } from '../src/utils';
@@ -22,11 +19,11 @@ const container = mockElement({
left: 0,
top: 0,
width: 1000,
- height: 200,
+ height: 300,
clientWidth: 1000,
- clientHeight: 200,
+ clientHeight: 300,
scrollWidth: 1000,
- scrollHeight: 200,
+ scrollHeight: 300,
scrollTop: 0,
scrollLeft: 0,
overflow: 'auto',
@@ -44,6 +41,34 @@ window.getComputedStyle = (node) => {
};
};
+function testBy({ target: t, container: c, placement: p, expect: e, adjustExpect: ae }) {
+ const { left: eLeft, top: eTop, placement: ePlacement } = e;
+ const config = {
+ target: mockElement({
+ ...target,
+ ...t,
+ }),
+ container: mockElement({ ...container, ...c }),
+ overlay,
+ placement: p,
+ autoAdjust: false,
+ };
+ const result = getPlacements(config);
+
+ expect(result.style.left).toBe(eLeft);
+ expect(result.style.top).toBe(eTop);
+
+ if (ae) {
+ const { left: aeLeft, top: aeTop, placement: aePlacement } = ae;
+ config.autoAdjust = true;
+ const result2 = getPlacements(config);
+
+ expect(result2.config.placement).toBe(aePlacement);
+ expect(result2.style.left).toBe(aeLeft);
+ expect(result2.style.top).toBe(aeTop);
+ }
+}
+
describe('utils', () => {
it('mock getStyle', () => {
expect(getStyle(container, 'overflow')).toBe('auto');
@@ -55,79 +80,840 @@ describe('utils', () => {
describe('placement', () => {
it('should support placement=bl', () => {
- const config = {
- target,
- container,
- overlay,
+ testBy({
placement: 'bl',
- autoAdjust: false,
- };
-
- const result = getPlacements(config);
-
- expect(result.style.left).toBe(target.left);
- expect(result.style.top).toBe(target.height + target.top);
+ expect: {
+ left: target.left,
+ top: target.height + target.top,
+ },
+ });
});
it('should support autoAdjust placement=tl -> bl', () => {
- const config = {
- target,
- container,
- overlay,
+ testBy({
placement: 'tl',
- autoAdjust: false,
- };
-
- const result = getPlacements(config);
-
- expect(result.style.left).toBe(target.left);
- expect(result.style.top).toBe(target.top - overlay.height);
-
- config.autoAdjust = true;
- const result2 = getPlacements(config);
-
- expect(result2.style.left).toBe(target.left);
- expect(result2.style.top).toBe(target.height + target.top);
+ expect: {
+ left: target.left,
+ top: target.top - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'bl',
+ left: target.left,
+ top: target.height + target.top,
+ },
+ });
});
- it('should support autoAdjust placement=tr -> bl', () => {
- const config = {
- target,
- container,
- overlay,
+ it('should support autoAdjust placement=tr -> bl', () => {
+ testBy({
placement: 'tr',
- autoAdjust: false,
- };
+ expect: {
+ left: target.left - overlay.width + target.width,
+ top: target.top - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'bl',
+ left: target.left,
+ top: target.height + target.top,
+ },
+ });
+ });
- const result = getPlacements(config);
+ it('should support autoAdjust make top/left > 0 placement=t -> b', () => {
+ testBy({
+ placement: 't',
+ expect: {
+ left: target.left + target.width / 2 - overlay.width / 2,
+ top: target.top - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'bl',
+ left: target.left,
+ top: target.height + target.top,
+ },
+ });
+ });
- expect(result.style.left).toBe(target.left - overlay.width + target.width);
- expect(result.style.top).toBe(target.top - overlay.height);
+ describe('should support autoAdjust when topOut', () => {
+ it('t -> b', () => {
+ testBy({
+ target: { left: 200, top: 0 },
+ placement: 't',
+ expect: {
+ left: 200 + target.width / 2 - overlay.width / 2,
+ top: 0 - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'b',
+ left: 200 + target.width / 2 - overlay.width / 2,
+ top: target.height,
+ },
+ });
+ });
+ it('tl -> bl, tr -> br', () => {
+ testBy({
+ target: { left: 200, top: 10 },
+ placement: 'tl',
+ expect: {
+ left: 200,
+ top: 10 - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'bl',
+ left: 200,
+ top: 10 + target.height,
+ },
+ });
+ testBy({
+ target: { left: 200, top: 10 },
+ placement: 'tr',
+ expect: {
+ left: 200 + target.width - overlay.width,
+ top: 10 - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'br',
+ left: 200 + target.width - overlay.width,
+ top: 10 + target.height,
+ },
+ });
+ });
+ it('l -> lt, r -> rt', () => {
+ testBy({
+ target: { left: 200, top: 10 },
+ placement: 'l',
+ expect: {
+ left: 200 - overlay.width,
+ top: 10 + target.height / 2 - overlay.height / 2,
+ },
+ adjustExpect: {
+ placement: 'lt',
+ left: 200 - overlay.width,
+ top: 10,
+ },
+ });
+ testBy({
+ target: { left: 200, top: 10 },
+ placement: 'r',
+ expect: {
+ left: 200 + target.width,
+ top: 10 + target.height / 2 - overlay.height / 2,
+ },
+ adjustExpect: {
+ placement: 'rt',
+ left: 200 + target.width,
+ top: 10,
+ },
+ });
+ });
+ it('lb -> lt, rb -> rt', () => {
+ testBy({
+ target: { left: 200, top: 10 },
+ placement: 'lb',
+ expect: {
+ left: 200 - overlay.width,
+ top: 10 + target.height - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'lt',
+ left: 200 - overlay.width,
+ top: 10,
+ },
+ });
+ testBy({
+ target: { left: 200, top: 10 },
+ placement: 'rb',
+ expect: {
+ left: 200 + target.width,
+ top: 10 + target.height - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'rt',
+ left: 200 + target.width,
+ top: 10,
+ },
+ });
+ });
+ });
+ describe('should support autoAdjust when rightOut', () => {
+ const targetLeft = container.width - target.width;
+ const targetTop = 120;
+ it('r -> l', () => {
+ testBy({
+ target: { left: targetLeft, top: targetTop },
+ placement: 'r',
+ expect: {
+ left: targetLeft + target.width,
+ top: targetTop + target.height / 2 - overlay.height / 2,
+ },
+ adjustExpect: {
+ placement: 'l',
+ left: targetLeft - overlay.width,
+ top: targetTop + target.height / 2 - overlay.height / 2,
+ },
+ });
+ });
- config.autoAdjust = true;
- const result2 = getPlacements(config);
+ it('rt -> lt, rb -> lb', () => {
+ testBy({
+ target: { left: targetLeft, top: targetTop },
+ placement: 'rt',
+ expect: {
+ left: targetLeft + target.width,
+ top: targetTop,
+ },
+ adjustExpect: {
+ placement: 'lt',
+ left: targetLeft - overlay.width,
+ top: targetTop,
+ },
+ });
+ testBy({
+ target: { left: targetLeft, top: targetTop },
+ placement: 'rb',
+ expect: {
+ left: targetLeft + target.width,
+ top: targetTop + target.height - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'lb',
+ left: targetLeft - overlay.width,
+ top: targetTop + target.height - overlay.height,
+ },
+ });
+ });
+
+ it('t -> tr, b -> br', () => {
+ testBy({
+ target: { left: targetLeft, top: targetTop },
+ placement: 't',
+ expect: {
+ left: targetLeft + target.width / 2 - overlay.width / 2,
+ top: targetTop - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'tr',
+ left: targetLeft + target.width - overlay.width,
+ top: targetTop - overlay.height,
+ },
+ });
+ testBy({
+ target: { left: targetLeft, top: targetTop },
+ placement: 'b',
+ expect: {
+ left: targetLeft + target.width / 2 - overlay.width / 2,
+ top: targetTop + target.height,
+ },
+ adjustExpect: {
+ placement: 'br',
+ left: targetLeft + target.width - overlay.width,
+ top: targetTop + target.height,
+ },
+ });
+ });
- expect(result2.style.left).toBe(target.left);
- expect(result2.style.top).toBe(target.height + target.top);
+ it('tl -> tr, bl -> br', () => {
+ testBy({
+ target: { left: targetLeft, top: targetTop },
+ placement: 'tl',
+ expect: {
+ left: targetLeft,
+ top: targetTop - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'tr',
+ left: targetLeft + target.width - overlay.width,
+ top: targetTop - overlay.height,
+ },
+ });
+ testBy({
+ target: { left: targetLeft, top: targetTop },
+ placement: 'bl',
+ expect: {
+ left: targetLeft,
+ top: targetTop + target.height,
+ },
+ adjustExpect: {
+ placement: 'br',
+ left: targetLeft + target.width - overlay.width,
+ top: targetTop + target.height,
+ },
+ });
+ });
});
- it('should support autoAdjust make top/left > 0 placement=t -> b', () => {
- const config = {
- target,
- container,
- overlay,
- placement: 't',
- autoAdjust: false,
- };
+ describe('should support autoAdjust when bottomOut', () => {
+ const tLeft = 220;
+ const tTop = container.height - target.height;
+ it('b -> t', () => {
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'b',
+ expect: {
+ left: tLeft + target.width / 2 - overlay.width / 2,
+ top: tTop + target.height,
+ },
+ adjustExpect: {
+ placement: 't',
+ left: tLeft + target.width / 2 - overlay.width / 2,
+ top: tTop - overlay.height,
+ },
+ });
+ });
+ it('bl -> tl, br -> tr', () => {
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'bl',
+ expect: {
+ left: tLeft,
+ top: tTop + target.height,
+ },
+ adjustExpect: {
+ placement: 'tl',
+ left: tLeft,
+ top: tTop - overlay.height,
+ },
+ });
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'bl',
+ expect: {
+ left: tLeft,
+ top: tTop + target.height,
+ },
+ adjustExpect: {
+ placement: 'tl',
+ left: tLeft,
+ top: tTop - overlay.height,
+ },
+ });
+ });
+ it('l -> lb, r -> rb', () => {
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'l',
+ expect: {
+ left: tLeft - overlay.width,
+ top: tTop + target.height / 2 - overlay.height / 2,
+ },
+ adjustExpect: {
+ placement: 'lb',
+ left: tLeft - overlay.width,
+ top: tTop + target.height - overlay.height,
+ },
+ });
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'r',
+ expect: {
+ left: tLeft + target.width,
+ top: tTop + target.height / 2 - overlay.height / 2,
+ },
+ adjustExpect: {
+ placement: 'rb',
+ left: tLeft + target.width,
+ top: tTop + target.height - overlay.height,
+ },
+ });
+ });
+ it('lt -> lb, rt -> rb', () => {});
+ });
+ describe('should support autoAdjust when leftOut', () => {
+ const tLeft = 0;
+ const tTop = 120;
+ it('l -> r', () => {
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'l',
+ expect: {
+ left: tLeft - overlay.width,
+ top: tTop + target.height / 2 - overlay.height / 2,
+ },
+ adjustExpect: {
+ placement: 'r',
+ left: tLeft + target.width,
+ top: tTop + target.height / 2 - overlay.height / 2,
+ },
+ });
+ });
+ it('lt -> rt, lb -> rb', () => {
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'lt',
+ expect: {
+ left: tLeft - overlay.width,
+ top: tTop,
+ },
+ adjustExpect: {
+ placement: 'rt',
+ left: tLeft + target.width,
+ top: tTop,
+ },
+ });
- const result = getPlacements(config);
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'lb',
+ expect: {
+ left: tLeft - overlay.width,
+ top: tTop + target.height - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'rb',
+ left: tLeft + target.width,
+ top: tTop + target.height - overlay.height,
+ },
+ });
+ });
+ it('t -> tl, b -> bl', () => {
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 't',
+ expect: {
+ left: tLeft + target.width / 2 - overlay.width / 2,
+ top: tTop - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'tl',
+ left: tLeft,
+ top: tTop - overlay.height,
+ },
+ });
- expect(result.style.left).toBe(target.left + target.width / 2 - overlay.width / 2);
- expect(result.style.top).toBe(target.top - overlay.height);
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'b',
+ expect: {
+ left: tLeft + target.width / 2 - overlay.width / 2,
+ top: tTop + target.height,
+ },
+ adjustExpect: {
+ placement: 'bl',
+ left: tLeft,
+ top: tTop + target.height,
+ },
+ });
+ });
+ it('tr -> tl, br -> bl', () => {
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'tr',
+ expect: {
+ left: tLeft + target.width - overlay.width,
+ top: tTop - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'tl',
+ left: tLeft,
+ top: tTop - overlay.height,
+ },
+ });
- config.autoAdjust = true;
- const result2 = getPlacements(config);
-
- expect(result2.style.left).toBe(0);
- expect(result2.style.top).toBe(target.height + target.top);
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'br',
+ expect: {
+ left: tLeft + target.width - overlay.width,
+ top: tTop + target.height,
+ },
+ adjustExpect: {
+ placement: 'bl',
+ left: tLeft,
+ top: tTop + target.height,
+ },
+ });
+ });
+ });
+ describe('should support autoAdjust when topOut & leftOut', () => {
+ const tLeft = 0;
+ const tTop = 0;
+ it('t -> bl, tr -> bl, tl -> bl', () => {
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 't',
+ expect: {
+ left: tLeft + target.width / 2 - overlay.width / 2,
+ top: tTop - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'bl',
+ left: tLeft,
+ top: tTop + target.height,
+ },
+ });
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'tr',
+ expect: {
+ left: tLeft + target.width - overlay.width,
+ top: tTop - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'bl',
+ left: tLeft,
+ top: tTop + target.height,
+ },
+ });
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'tl',
+ expect: {
+ left: tLeft,
+ top: tTop - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'bl',
+ left: tLeft,
+ top: tTop + target.height,
+ },
+ });
+ });
+ it('l -> rt, lb -> rt, lt -> rt', () => {
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'l',
+ expect: {
+ left: tLeft - overlay.width,
+ top: tTop + target.height / 2 - overlay.height / 2,
+ },
+ adjustExpect: {
+ placement: 'rt',
+ left: tLeft + target.width,
+ top: tTop,
+ },
+ });
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'lb',
+ expect: {
+ left: tLeft - overlay.width,
+ top: tTop + target.height - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'rt',
+ left: tLeft + target.width,
+ top: tTop,
+ },
+ });
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'lt',
+ expect: {
+ left: tLeft - overlay.width,
+ top: tTop,
+ },
+ adjustExpect: {
+ placement: 'rt',
+ left: tLeft + target.width,
+ top: tTop,
+ },
+ });
+ });
+ });
+ describe('should support autoAdjust when topOut & rightOut', () => {
+ const tLeft = container.width - target.width;
+ const tTop = 0;
+ it('t -> br, tl -> br, tr -> br', () => {
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 't',
+ expect: {
+ left: tLeft + target.width / 2 - overlay.width / 2,
+ top: tTop - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'br',
+ left: tLeft + target.width - overlay.width,
+ top: tTop + target.height,
+ },
+ });
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'tl',
+ expect: {
+ left: tLeft,
+ top: tTop - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'br',
+ left: tLeft + target.width - overlay.width,
+ top: tTop + target.height,
+ },
+ });
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'tr',
+ expect: {
+ left: tLeft + target.width - overlay.width,
+ top: tTop - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'br',
+ left: tLeft + target.width - overlay.width,
+ top: tTop + target.height,
+ },
+ });
+ });
+ it('r -> lt, rb -> lt, rt -> lt', () => {
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'r',
+ expect: {
+ left: tLeft + target.width,
+ top: tTop + target.height / 2 - overlay.height / 2,
+ },
+ adjustExpect: {
+ placement: 'lt',
+ left: tLeft - overlay.width,
+ top: tTop,
+ },
+ });
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'rb',
+ expect: {
+ left: tLeft + target.width,
+ top: tTop + target.height - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'lt',
+ left: tLeft - overlay.width,
+ top: tTop,
+ },
+ });
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'rt',
+ expect: {
+ left: tLeft + target.width,
+ top: tTop,
+ },
+ adjustExpect: {
+ placement: 'lt',
+ left: tLeft - overlay.width,
+ top: tTop,
+ },
+ });
+ });
+ });
+ describe('should support autoAdjust when rightOut & bottomOut', () => {
+ const tLeft = container.width - target.width;
+ const tTop = container.height - target.height;
+ it('r -> lb, rt -> lb, rb -> lb', () => {
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'r',
+ expect: {
+ left: tLeft + target.width,
+ top: tTop + target.height / 2 - overlay.height / 2,
+ },
+ adjustExpect: {
+ placement: 'lb',
+ left: tLeft - overlay.width,
+ top: tTop + target.height - overlay.height,
+ },
+ });
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'rt',
+ expect: {
+ left: tLeft + target.width,
+ top: tTop,
+ },
+ adjustExpect: {
+ placement: 'lb',
+ left: tLeft - overlay.width,
+ top: tTop + target.height - overlay.height,
+ },
+ });
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'rb',
+ expect: {
+ left: tLeft + target.width,
+ top: tTop + target.height - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'lb',
+ left: tLeft - overlay.width,
+ top: tTop + target.height - overlay.height,
+ },
+ });
+ });
+ it('b -> tr, bl -> tr, br -> tr', () => {
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'b',
+ expect: {
+ left: tLeft + target.width / 2 - overlay.width / 2,
+ top: tTop + target.height,
+ },
+ adjustExpect: {
+ placement: 'tr',
+ left: tLeft + target.width - overlay.width,
+ top: tTop - overlay.height,
+ },
+ });
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'bl',
+ expect: {
+ left: tLeft,
+ top: tTop + target.height,
+ },
+ adjustExpect: {
+ placement: 'tr',
+ left: tLeft + target.width - overlay.width,
+ top: tTop - overlay.height,
+ },
+ });
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'br',
+ expect: {
+ left: tLeft + target.width - overlay.width,
+ top: tTop + target.height,
+ },
+ adjustExpect: {
+ placement: 'tr',
+ left: tLeft + target.width - overlay.width,
+ top: tTop - overlay.height,
+ },
+ });
+ });
+ });
+ describe('should support autoAdjust when bottomOut & leftOut', () => {
+ const tLeft = 0;
+ const tTop = container.height - target.height;
+ it('l -> rb, lt -> rb, lb -> rb', () => {
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'l',
+ expect: {
+ left: tLeft - overlay.width,
+ top: tTop + target.height / 2 - overlay.height / 2,
+ },
+ adjustExpect: {
+ placement: 'rb',
+ left: tLeft + target.width,
+ top: tTop + target.height - overlay.height,
+ },
+ });
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'lt',
+ expect: {
+ left: tLeft - overlay.width,
+ top: tTop,
+ },
+ adjustExpect: {
+ placement: 'rb',
+ left: tLeft + target.width,
+ top: tTop + target.height - overlay.height,
+ },
+ });
+ testBy({
+ target: { left: tLeft, top: tTop },
+ placement: 'lb',
+ expect: {
+ left: tLeft - overlay.width,
+ top: tTop + target.height - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'rb',
+ left: tLeft + target.width,
+ top: tTop + target.height - overlay.height,
+ },
+ });
+ });
+ });
+ describe('should support autoAdjust when topOut & leftOut & rightOut', () => {
+ const tLeft = 0;
+ const tTop = 0;
+ const cWidth = target.width;
+ // 若 target 在容器内,则仅在 t 的情况下有可能 左上右 都超出
+ it('t -> b', () => {
+ testBy({
+ target: { left: tLeft, top: tTop },
+ container: { width: cWidth, clientWidth: cWidth, scrollWidth: cWidth },
+ placement: 't',
+ expect: {
+ left: tLeft + target.width / 2 - overlay.width / 2,
+ top: tTop - overlay.height,
+ },
+ adjustExpect: {
+ placement: 'b',
+ left: tLeft + target.width / 2 - overlay.width / 2,
+ top: tTop + target.height,
+ },
+ });
+ });
+ });
+ describe('should support autoAdjust when topOut & rightOut & bottomOut', () => {
+ const cHeight = target.height;
+ const tLeft = container.width - target.width;
+ const tTop = 0;
+ // 若 target 在容器内,则仅在 r 的情况下有可能 上右下 都超出
+ it('r -> l', () => {
+ testBy({
+ target: { left: tLeft, top: tTop },
+ container: { height: cHeight, clientHeight: cHeight, scrollHeight: cHeight },
+ placement: 'r',
+ expect: {
+ left: tLeft + target.width,
+ top: tTop + target.height / 2 - overlay.height / 2,
+ },
+ adjustExpect: {
+ placement: 'l',
+ left: tLeft - overlay.width,
+ top: tTop + target.height / 2 - overlay.height / 2,
+ },
+ });
+ });
+ });
+ describe('should support autoAdjust when rightOut & bottomOut & leftOut', () => {
+ const tLeft = 0;
+ const tTop = container.height - target.height;
+ const cWidth = target.width;
+ // 若 target 在容器内,则仅在 b 的情况下有可能 右下左 都超出
+ it('b -> t', () => {
+ testBy({
+ target: { left: tLeft, top: tTop },
+ container: { width: cWidth, clientWidth: cWidth, scrollWidth: cWidth },
+ placement: 'b',
+ expect: {
+ left: tLeft + target.width / 2 - overlay.width / 2,
+ top: tTop + target.height,
+ },
+ adjustExpect: {
+ placement: 't',
+ left: tLeft + target.width / 2 - overlay.width / 2,
+ top: tTop - overlay.height,
+ },
+ });
+ });
+ });
+ describe('should support autoAdjust when bottomOut & leftOut & topOut', () => {
+ const cHeight = target.height;
+ const tLeft = 0;
+ const tTop = 0;
+ // 若 target 在容器内,则仅在 l 的情况下有可能 上左下 都超出
+ it('l -> r', () => {
+ testBy({
+ target: { left: tLeft, top: tTop },
+ container: { height: cHeight, clientHeight: cHeight, scrollHeight: cHeight },
+ placement: 'l',
+ expect: {
+ left: tLeft - overlay.width,
+ top: tTop + target.height / 2 - overlay.height / 2,
+ },
+ adjustExpect: {
+ placement: 'r',
+ left: tLeft + target.width,
+ top: tTop + target.height / 2 - overlay.height / 2,
+ },
+ });
+ });
});
+ // 任意placement都不可能四面超出,不做测试
+ // describe('should do nothing when topOut & rightOut & bottomOut & leftOut', () => {});
});
diff --git a/test/utils.test.js b/test/utils.test.js
new file mode 100644
index 0000000..8db3e68
--- /dev/null
+++ b/test/utils.test.js
@@ -0,0 +1,246 @@
+import { getViewPort } from '../src/utils';
+
+function createElement(className, tagName = 'div') {
+ const el = document.createElement(tagName);
+ el.classList.add(className);
+ return el;
+}
+
+// polyfill offsetParent
+// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
+// https://github.com/jsdom/jsdom/issues/1261
+Object.defineProperty(HTMLElement.prototype, 'offsetParent', {
+ get() {
+ // display none will return null;
+ for (let el = this; el; el = el.parentNode) {
+ if (el.style?.display?.toLowerCase() === 'none') {
+ return null;
+ }
+ }
+
+ // fixed return null
+ if (this.style?.position?.toLowerCase() === 'fixed') {
+ return null;
+ }
+
+ // html body element return null
+ if (this.tagName.toLowerCase() in ['html', 'body']) {
+ return null;
+ }
+
+ // positioned element
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/position
+ if (this.style.position && this.style.position.toLowerCase() !== 'static') {
+ const isMatch =
+ this.style.position.toLowerCase() === 'sticky'
+ ? (el) => {
+ return el.style?.overflow && el.style.overflow.toLowerCase() !== 'visible';
+ }
+ : (el) => {
+ // containing block 情况这里只模拟transform类型
+ if (el.style?.transform && el.style.transform !== 'none') {
+ return true;
+ }
+ return el.style?.position && el.style.position.toLowerCase() !== 'static';
+ };
+ for (let el = this.parentNode; el; el = el.parentNode) {
+ if (isMatch(el)) {
+ return el;
+ }
+ }
+ return document.body;
+ }
+
+ return this.parentNode;
+ },
+});
+
+describe('utils', () => {
+ describe('getViewport', () => {
+ it('normal', () => {
+ const box = createElement('box');
+ const app = createElement('app');
+ app.appendChild(box);
+ document.body.appendChild(app);
+ expect(getViewPort(box)).toBe(document.documentElement);
+ });
+ it('when box is clipped', () => {
+ const box = createElement('box');
+ box.style.overflow = 'auto';
+ const app = createElement('app');
+ app.appendChild(box);
+ document.body.appendChild(app);
+ expect(getViewPort(box)).toBe(box);
+ });
+ it('when parent is clipped', () => {
+ const box = createElement('box');
+ const app = createElement('app');
+ app.style.overflow = 'auto';
+ app.appendChild(box);
+ document.body.appendChild(app);
+ expect(getViewPort(box)).toBe(app);
+ });
+ it('when ancestor element is clipped', () => {
+ const box = createElement('box');
+ const parent = createElement('parent');
+ const app = createElement('app');
+ app.style.overflow = 'auto';
+ parent.appendChild(box);
+ app.appendChild(parent);
+ document.body.appendChild(app);
+ expect(getViewPort(box)).toBe(app);
+ });
+ it('when box is absoluted', () => {
+ const box = createElement('box');
+ box.style.position = 'absolute';
+ const app = createElement('app');
+ app.appendChild(box);
+ document.body.appendChild(app);
+ expect(getViewPort(box)).toBe(document.documentElement);
+ });
+ it('when box is absoluted and parent is clipped', () => {
+ const box = createElement('box');
+ box.style.position = 'absolute';
+ const app = createElement('app');
+ app.style.overflow = 'auto';
+ app.appendChild(box);
+ document.body.appendChild(app);
+ expect(getViewPort(box)).toBe(document.documentElement);
+ });
+ it('when box is absoluted and parent is clipped&relative', () => {
+ const box = createElement('box');
+ box.style.position = 'absolute';
+ const app = createElement('app');
+ app.style.overflow = 'auto';
+ app.style.position = 'relative';
+ app.appendChild(box);
+ document.body.appendChild(app);
+ expect(getViewPort(box)).toBe(app);
+ });
+ it('when box is absoluted and parent is relative and ancestor is clipped', () => {
+ const box = createElement('box');
+ box.style.position = 'absolute';
+ const parent = createElement('parent');
+ parent.style.position = 'relative';
+ parent.appendChild(box);
+ const app = createElement('app');
+ app.style.overflow = 'auto';
+ app.appendChild(parent);
+ document.body.appendChild(app);
+ expect(getViewPort(box)).toBe(app);
+ });
+ it('when box is absoluted and parent is clipped&containingBlock', () => {
+ const box = createElement('box');
+ box.style.position = 'absolute';
+ const app = createElement('app');
+ app.style.overflow = 'auto';
+ app.style.transform = 'translate(1px)';
+ app.appendChild(box);
+ document.body.appendChild(app);
+ expect(getViewPort(box)).toBe(app);
+ });
+ it('when box is absoluted and parent is containingBlock and ancestor is clipped', () => {
+ const box = createElement('box');
+ box.style.position = 'absolute';
+ const parent = createElement('parent');
+ parent.style.transform = 'translate(1px)';
+ parent.appendChild(box);
+ const app = createElement('app');
+ app.style.overflow = 'auto';
+ app.appendChild(parent);
+ document.body.appendChild(app);
+ expect(getViewPort(box)).toBe(app);
+ });
+
+ it('when box is fixed', () => {
+ const box = createElement('box');
+ box.style.position = 'fixed';
+ const app = createElement('app');
+ app.appendChild(box);
+ document.body.appendChild(app);
+ expect(getViewPort(box)).toBe(document.documentElement);
+ });
+
+ it('when box is fixed and parent is relative', () => {
+ const box = createElement('box');
+ box.style.position = 'fixed';
+ const app = createElement('app');
+ app.style.position = 'relative';
+ app.appendChild(box);
+ document.body.appendChild(app);
+ expect(getViewPort(box)).toBe(document.documentElement);
+ });
+
+ it('when box is fixed and parent is clipped', () => {
+ const box = createElement('box');
+ box.style.position = 'fixed';
+ const app = createElement('app');
+ app.style.overflow = 'auto';
+ app.appendChild(box);
+ document.body.appendChild(app);
+ expect(getViewPort(box)).toBe(document.documentElement);
+ });
+ it('when box is fixed and parent is clipped&containingBlock', () => {
+ const box = createElement('box');
+ box.style.position = 'fixed';
+ const app = createElement('app');
+ app.style.transform = 'translate(1px)';
+ app.style.overflow = 'auto';
+ app.appendChild(box);
+ document.body.appendChild(app);
+ expect(getViewPort(box)).toBe(app);
+ });
+ it('when box is fixed and parent is containingBlock and ancestor is clipped', () => {
+ const box = createElement('box');
+ box.style.position = 'fixed';
+ const parent = createElement('parent');
+ parent.style.transform = 'translate(1px)';
+ parent.appendChild(box);
+ const app = createElement('app');
+ app.style.overflow = 'auto';
+ app.appendChild(parent);
+ document.body.appendChild(app);
+ expect(getViewPort(box)).toBe(app);
+ });
+ it('when box is sticky', () => {
+ const box = createElement('box');
+ box.style.position = 'sticky';
+ const app = createElement('app');
+ app.appendChild(box);
+ document.body.appendChild(app);
+ expect(getViewPort(box)).toBe(document.documentElement);
+ });
+ it('when box is sticky and parent is clipped', () => {
+ const box = createElement('box');
+ box.style.position = 'sticky';
+ const app = createElement('app');
+ app.style.overflow = 'auto';
+ app.appendChild(box);
+ document.body.appendChild(app);
+ expect(getViewPort(box)).toBe(app);
+ });
+ it('when box is sticky and ancestor is clipped', () => {
+ const box = createElement('box');
+ box.style.position = 'sticky';
+ const parent = createElement('parent');
+ parent.appendChild(box);
+ const app = createElement('app');
+ app.style.overflow = 'auto';
+ app.appendChild(parent);
+ document.body.appendChild(app);
+ expect(getViewPort(box)).toBe(app);
+ });
+ it('when box is sticky and parent is containingBlock and ancestor is clipped', () => {
+ const box = createElement('box');
+ box.style.position = 'sticky';
+ const parent = createElement('parent');
+ parent.style.transform = 'translate(1px)';
+ parent.appendChild(box);
+ const app = createElement('app');
+ app.style.overflow = 'auto';
+ app.appendChild(parent);
+ document.body.appendChild(app);
+ expect(getViewPort(box)).toBe(app);
+ });
+ });
+});