From 8d6895176828b80e18881ffed0bed6e682e3ee81 Mon Sep 17 00:00:00 2001 From: Ryota Kunisada Date: Thu, 30 Jan 2020 11:49:49 +0900 Subject: [PATCH] fix: compute iframe parents --- integration/__snapshots__/iframe.test.js.snap | 5 ++ .../iframe_outside_viewport.test.js.snap | 11 ++++ integration/iframe.test.js | 2 +- integration/iframe_outside_viewport.html | 12 ++++ integration/iframe_outside_viewport.test.js | 21 +++++++ src/index.ts | 56 ++++++++++++++----- 6 files changed, 93 insertions(+), 14 deletions(-) create mode 100644 integration/__snapshots__/iframe_outside_viewport.test.js.snap create mode 100644 integration/iframe_outside_viewport.html create mode 100644 integration/iframe_outside_viewport.test.js diff --git a/integration/__snapshots__/iframe.test.js.snap b/integration/__snapshots__/iframe.test.js.snap index fde59f66..62d712ca 100644 --- a/integration/__snapshots__/iframe.test.js.snap +++ b/integration/__snapshots__/iframe.test.js.snap @@ -7,5 +7,10 @@ Array [ "left": 0, "top": 116, }, + Object { + "el": "html", + "left": 0, + "top": 0, + }, ] `; diff --git a/integration/__snapshots__/iframe_outside_viewport.test.js.snap b/integration/__snapshots__/iframe_outside_viewport.test.js.snap new file mode 100644 index 00000000..3c43d630 --- /dev/null +++ b/integration/__snapshots__/iframe_outside_viewport.test.js.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`target in iframe is outside viewport should scroll top window 1`] = ` +Array [ + Object { + "el": "html", + "left": 0, + "top": 366, + }, +] +`; diff --git a/integration/iframe.test.js b/integration/iframe.test.js index 70a9e63a..c51bf6d1 100644 --- a/integration/iframe.test.js +++ b/integration/iframe.test.js @@ -14,7 +14,7 @@ describe('scrollable element is "overflow: visible" but hidden by iframe', () => }) .map(window.mapActions) }) - expect(actual).toHaveLength(1) + expect(actual).toHaveLength(2) expect(actual[0]).toMatchObject({ el: 'html' }) expect(actual).toMatchSnapshot() }) diff --git a/integration/iframe_outside_viewport.html b/integration/iframe_outside_viewport.html new file mode 100644 index 00000000..e5e6ca09 --- /dev/null +++ b/integration/iframe_outside_viewport.html @@ -0,0 +1,12 @@ + + + + + + +
+ + diff --git a/integration/iframe_outside_viewport.test.js b/integration/iframe_outside_viewport.test.js new file mode 100644 index 00000000..b0ed15fb --- /dev/null +++ b/integration/iframe_outside_viewport.test.js @@ -0,0 +1,21 @@ +beforeAll(async () => { + await page.goto('http://localhost:3000/integration/iframe_outside_viewport') +}) + +describe('target in iframe is outside viewport', () => { + test('should scroll top window', async () => { + expect.assertions(3) + const actual = await page.evaluate(() => { + const iframe = document.querySelector('iframe') + const target = iframe.contentDocument.querySelector('.target') + return window + .computeScrollIntoView(target, { + scrollMode: 'always', + }) + .map(window.mapActions) + }) + expect(actual).toHaveLength(1) + expect(actual[0]).toMatchObject({ el: 'html' }) + expect(actual).toMatchSnapshot() + }) +}) diff --git a/src/index.ts b/src/index.ts index dbd208ee..51c24985 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,10 +49,14 @@ interface CustomScrollAction { } // @TODO better shadowdom test, 11 = document fragment -function isElement(el: any) { +function isElement(el: any): el is Element { return el != null && typeof el === 'object' && el.nodeType === 1 } +function isDocument(el: any): el is Document { + return el != null && typeof el === 'object' && el.nodeType === 9 +} + function canOverflow( overflow: string | null, skipOverflowHiddenElements?: boolean @@ -236,6 +240,34 @@ function alignNearest( return 0 } +/** + * Get rect considering iframe + */ +function getRect(target: Element) { + const clientRect = target.getBoundingClientRect() + const rect = { + height: clientRect.height, + width: clientRect.width, + top: clientRect.top, + left: clientRect.left, + bottom: clientRect.bottom, + right: clientRect.right, + } + const doc = target.ownerDocument + let childWindow = doc?.defaultView + + while (childWindow && window.top !== childWindow) { + const frameRect = childWindow.frameElement.getBoundingClientRect() + rect.top += frameRect.top + rect.bottom += frameRect.top + rect.left += frameRect.left + rect.right += frameRect.left + childWindow = childWindow.parent + } + + return rect +} + export default (target: Element, options: Options): CustomScrollAction[] => { const { scrollMode, @@ -260,9 +292,14 @@ export default (target: Element, options: Options): CustomScrollAction[] => { // Collect all the scrolling boxes, as defined in the spec: https://drafts.csswg.org/cssom-view/#scrolling-box const frames: Element[] = [] let cursor = target - while (isElement(cursor) && checkBoundary(cursor)) { - // Move cursor to parent - cursor = cursor.parentNode as Element + while ((isElement(cursor) || isDocument(cursor)) && checkBoundary(cursor)) { + if (isDocument(cursor)) { + const document = cursor + cursor = document.defaultView?.frameElement as Element + } else { + // Move cursor to parent + cursor = cursor.parentNode as Element + } // Stop when we reach the viewport if (cursor === scrollingElement) { @@ -308,7 +345,7 @@ export default (target: Element, options: Options): CustomScrollAction[] => { right: targetRight, bottom: targetBottom, left: targetLeft, - } = target.getBoundingClientRect() + } = getRect(target) // These values mutate as we loop through and generate scroll coordinates let targetBlock: number = @@ -332,14 +369,7 @@ export default (target: Element, options: Options): CustomScrollAction[] => { // @TODO add a shouldScroll hook here that allows userland code to take control - const { - height, - width, - top, - right, - bottom, - left, - } = frame.getBoundingClientRect() + const { height, width, top, right, bottom, left } = getRect(frame) // If the element is already visible we can end it here // @TODO targetBlock and targetInline should be taken into account to be compliant with https://github.com/w3c/csswg-drafts/pull/1805/files#diff-3c17f0e43c20f8ecf89419d49e7ef5e0R1333