Skip to content

Commit 00000c1

Browse files
committed
1 parent c8418c0 commit 00000c1

File tree

4 files changed

+220
-0
lines changed

4 files changed

+220
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<!DOCTYPE html>
2+
3+
<script src="/common/get-host-info.sub.js"></script>
4+
5+
<p>Iframe 1</p>
6+
7+
<div style="width: 300px; height: 300px; overflow-y: scroll; outline: 1px red solid" id="scroller">
8+
<!-- Spacer to trigger scrolling -->
9+
<div style="height: 400px"></div>
10+
11+
<iframe id="iframe" width=250 height=300></iframe>
12+
</div>
13+
14+
<script>
15+
iframe.src = get_host_info().ORIGIN + "/intersection-observer/resources/scroll-margin-propagation-iframe-2.html";
16+
17+
window.addEventListener("message", event => {
18+
const data = event.data;
19+
20+
if (data.msgName === "setScrollTop") {
21+
if (data.target === "iframe1") {
22+
scroller.scrollTop = data.scrollTop;
23+
window.top.postMessage({ msgName: "scrollEnd", source: "iframe1" }, "*");
24+
} else
25+
iframe.contentWindow.postMessage(data, "*");
26+
}
27+
});
28+
</script>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<!DOCTYPE html>
2+
3+
<script src="/common/get-host-info.sub.js"></script>
4+
5+
<p>Iframe 2</p>
6+
7+
<div style="width: 200px; height: 200px; overflow-y: scroll; outline: 1px solid purple" id="scroller">
8+
<!-- Spacer to trigger scrolling -->
9+
<div style="height: 300px"></div>
10+
11+
<iframe id="iframe" width=150 height=200></iframe>
12+
</div>
13+
14+
<script>
15+
iframe.src = get_host_info().ORIGIN + "/intersection-observer/resources/scroll-margin-propagation-iframe-3.html";
16+
17+
window.addEventListener("message", event => {
18+
const data = event.data;
19+
20+
if (data.msgName === "setScrollTop" && data.target === "iframe2") {
21+
scroller.scrollTop = data.scrollTop;
22+
window.top.postMessage({ msgName: "scrollEnd", source: "iframe2" }, "*");
23+
}
24+
});
25+
</script>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!DOCTYPE html>
2+
3+
<p>Iframe 3</p>
4+
<div style="width: 100px; height: 100px; background: green" id="target">Target</div>
5+
6+
<script>
7+
const options = {
8+
root: null,
9+
scrollMargin: "50px"
10+
};
11+
12+
const observer = new IntersectionObserver(records => {
13+
window.top.postMessage({ msgName: "isIntersectingChanged", value: records[0].isIntersecting }, "*");
14+
}, options);
15+
16+
observer.observe(target);
17+
</script>
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<!DOCTYPE html>
2+
3+
<meta charset=utf-8>
4+
<meta name="viewport" content="width=device-width,initial-scale=1">
5+
6+
<title>Scroll margin propagation from descendant frame to top page</title>
7+
<link rel="author" title="Kiet Ho" href="mailto:[email protected]">
8+
<meta name="timeout" content="long">
9+
10+
<script src="/resources/testharness.js"></script>
11+
<script src="/resources/testharnessreport.js"></script>
12+
<script src="/common/get-host-info.sub.js"></script>
13+
<script src="./resources/intersection-observer-test-utils.js"></script>
14+
15+
<!--
16+
This tests that when
17+
(1) an implicit root intersection observer includes a scroll margin
18+
(2) the observer target is in a frame descendant of the top page
19+
20+
Then the scroll margin is applied up to, and excluding, the first cross-origin-domain
21+
frame in the chain from the target to the top page. Then, subsequent frames won't
22+
have scroll margin applied, even if any of subsequent frames are same-origin-domain.
23+
24+
This follows the discussion at [1] that says:
25+
> Implementation notes:
26+
> * [...]
27+
> * Should stop margins at a cross-origin iframe boundary for security
28+
29+
[1]: https://github.com/w3c/IntersectionObserver/issues/431#issuecomment-1542502858
30+
31+
The setup:
32+
* 3-level iframe nesting: top page -> iframe 1 -> iframe 2 -> iframe 3
33+
* Iframe 1 is cross-origin-domain with top page, iframe 2/3 are same-origin-domain
34+
* Top page and iframe 1/2 have a scroller, which consists of a spacer to trigger
35+
scrolling, and an iframe to the next level.
36+
* Iframe 3 has an implicit root intersection observer and the target.
37+
* The observer specifies a scroll margin, which should be applied to iframe 2,
38+
and not to iframe 1 and top page.
39+
40+
Communication between frames:
41+
* Iframe 3 sends a "isIntersectingChanged" to the top page when the target's
42+
isIntersecting changed.
43+
* Iframe 1, 2 accepts a "setScrollTop" message to set the scrollTop of its scroller.
44+
The message contains a destination, if the destination matches, it sets the scrollTop,
45+
otherwise it passes the message down the chain. After setting scrollTop, the iframe emits
46+
a "scrollEnd" message to the top frame.
47+
-->
48+
49+
<p>Top page</p>
50+
<div style="width: 400px; height: 400px; outline: 1px solid blue; overflow-y: scroll" id="scroller">
51+
<!-- Spacer to trigger scrolling -->
52+
<div style="height: 500px"></div>
53+
54+
<iframe width=350 height=400 id="iframe"></iframe>
55+
</div>
56+
57+
<script>
58+
iframe.src =
59+
get_host_info().HTTP_NOTSAMESITE_ORIGIN + "/intersection-observer/resources/scroll-margin-propagation-iframe-1.html";
60+
const iframeWindow = iframe.contentWindow;
61+
62+
// Set the scrollTop of the scroller in the frame specified by `target`:
63+
// "this" - top frame, "iframe1" - iframe 1, "iframe2" - iframe2
64+
// When setting scrollTop of remote frames, remote frame will send a "scrollEnd"
65+
// message to indicate the scroll has been set. Wait for this message before returning.
66+
async function setScrollTop(target, scrollTop) {
67+
if (target === "this") {
68+
scroller.scrollTop = scrollTop;
69+
} else {
70+
iframeWindow.postMessage({
71+
msgName: "setScrollTop",
72+
target: target,
73+
scrollTop: scrollTop
74+
}, "*");
75+
76+
await new Promise(resolve => {
77+
window.addEventListener("message", event => {
78+
if (event.data.msgName === "scrollEnd" && event.data.source === target)
79+
resolve();
80+
81+
}, { once: true })
82+
})
83+
}
84+
85+
// Wait for IntersectionObserver notifications to be generated.
86+
await new Promise(resolve => waitForNotification(null, resolve));
87+
await new Promise(resolve => waitForNotification(null, resolve));
88+
}
89+
90+
var grandchildFrameIsIntersecting = null;
91+
92+
promise_setup(() => {
93+
// Wait for the initial IntersectionObserver notification.
94+
// This indicates iframe 3 is fully ready for test.
95+
return new Promise(resolve => {
96+
window.addEventListener("message", event => {
97+
if (event.data.msgName === "isIntersectingChanged") {
98+
grandchildFrameIsIntersecting = event.data.value;
99+
100+
// Install a long-lasting event listener, since this listerner is one-shot
101+
window.addEventListener("message", event => {
102+
if (event.data.msgName === "isIntersectingChanged")
103+
grandchildFrameIsIntersecting = event.data.value;
104+
});
105+
106+
resolve();
107+
}
108+
}, { once: true });
109+
});
110+
});
111+
112+
promise_test(async t => {
113+
// Scroll everything to bottom, so target is fully visible
114+
await setScrollTop("this", 99999);
115+
await setScrollTop("iframe1", 99999);
116+
await setScrollTop("iframe2", 99999);
117+
assert_true(grandchildFrameIsIntersecting, "Target is fully visible and intersecting");
118+
119+
// Scroll iframe 2 up a bit so that target is not visible, but still intersecting
120+
// because of scroll margin.
121+
await setScrollTop("iframe2", 130);
122+
assert_true(grandchildFrameIsIntersecting, "Target is not visible, but in the scroll margin zone, so still intersects");
123+
124+
await setScrollTop("iframe2", 85);
125+
assert_false(grandchildFrameIsIntersecting, "Target is fully outside the visible and scroll margin zone");
126+
}, "Scroll margin is applied to iframe 2, because it's same-origin-domain with iframe 3");
127+
128+
promise_test(async t => {
129+
// Scroll everything to bottom, so target is fully visible
130+
await setScrollTop("this", 99999);
131+
await setScrollTop("iframe1", 99999);
132+
await setScrollTop("iframe2", 99999);
133+
assert_true(grandchildFrameIsIntersecting, "Target is fully visible");
134+
135+
await setScrollTop("iframe1", 180);
136+
assert_false(grandchildFrameIsIntersecting, "Target is not visible, in the scroll margin zone, but not intersecting because scroll margin doesn't apply to cross-origin-domain frames");
137+
}, "Scroll margin is not applied to iframe 1, because it's cross-origin-domain with iframe 3");
138+
139+
promise_test(async t => {
140+
// Scroll everything to bottom, so target is fully visible
141+
await setScrollTop("this", 99999);
142+
await setScrollTop("iframe1", 99999);
143+
await setScrollTop("iframe2", 99999);
144+
assert_true(grandchildFrameIsIntersecting, "Target is fully visible");
145+
146+
await setScrollTop("this", 235);
147+
assert_false(grandchildFrameIsIntersecting, "Target is not visible, in the scroll margin zone, but not intersecting because scroll margin doesn't apply to frames beyond cross-origin-domain frames");
148+
149+
}, "Scroll margin is not applied to top page, because scroll margin doesn't propagate past cross-origin-domain iframe 1");
150+
</script>

0 commit comments

Comments
 (0)