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); + }); + }); +});