Rework IntersectionObserver

1 - Always fire the callback on the next tick. This is probably the most
important change, as frameworks like React don't react well if the callback is
fired immediately (they expect to continue processing the page in its current
state, not in the mutated state from the callback)

2 - Always fire the callback for observed elements with a parent, whether or
not those intersect or are connected. From MDN, the callback is fired
"The first time the observer is initially asked to watch a target element."

3 - Add a mutation observer so that if a node is added to the root (or removed)
the callback is fired. This, I think, is the best we can currently do for
"intersection".
This commit is contained in:
Karl Seguin
2025-09-18 19:07:18 +08:00
parent dc85c6552a
commit 852c30b2e5
4 changed files with 468 additions and 259 deletions

View File

@@ -1,96 +1,163 @@
<!DOCTYPE html>
<body></body>
<script src="../testing.js"></script>
<script id=intersectionObserver>
// doesn't crash (I guess)
new IntersectionObserver(() => {}).observe(document.documentElement);
{
// never attached
let count = 0;
const div = document.createElement('div');
new IntersectionObserver((entries) => {
count += 1;
}).observe(div);
let count_a = 0;
const a1 = document.createElement('div');
new IntersectionObserver(entries => {count_a += 1;}).observe(a1);
testing.expectEqual(1, count_a);
testing.eventually(() => {
testing.expectEqual(0, count);
});
}
// This test is documenting current behavior, not correct behavior.
// Currently every time observe is called, the callback is called with all entries.
let count_b = 0;
let observer_b = new IntersectionObserver(entries => {count_b = entries.length;});
const b1 = document.createElement('div');
observer_b.observe(b1);
testing.expectEqual(1, count_b);
const b2 = document.createElement('div');
observer_b.observe(b2);
testing.expectEqual(2, count_b);
{
// not connected, but has parent
let count = 0;
const div1 = document.createElement('div');
const div2 = document.createElement('div');
new IntersectionObserver((entries) => {
console.log(entries[0]);
count += 1;
}).observe(div1);
div2.appendChild(div1);
testing.eventually(() => {
testing.expectEqual(1, count);
});
}
</script>
<script id=reobserve>
let count_bb = 0;
let observer_bb = new IntersectionObserver(entries => {count_bb = entries.length;});
const bb1 = document.createElement('div');
observer_bb.observe(bb1);
testing.expectEqual(1, count_bb)
observer_bb.observe(bb1);
testing.expectEqual(1, count_bb) // Still 1, not 2
{
let count = 0;
let observer = new IntersectionObserver(entries => {
count += entries.length;
});
const div1 = document.createElement('div');
document.body.appendChild(div1);
// cannot fire synchronously, must be on the next tick
testing.expectEqual(0, count);
observer.observe(div1);
testing.expectEqual(0, count);
observer.observe(div1);
observer.observe(div1);
testing.expectEqual(0, count);
testing.eventually(() => {
testing.expectEqual(1, count);
});
}
</script>
<script id=unobserve>
let count_c = 0;
let observer_c = new IntersectionObserver(entries => { count_c = entries.length;});
const c1 = document.createElement('div');
observer_c.observe(c1);
testing.expectEqual(1, count_c);
observer_c.unobserve(c1);
const c2 = document.createElement('div');
observer_c.observe(c2);
testing.expectEqual(1, count_c);
</script>
{
let count = 0;
let observer = new IntersectionObserver(entries => {
count += entries.length;
});
<script id=takeRecords>
let observer_e = new IntersectionObserver(entries => {});
let e1 = document.createElement('div');
observer_e.observe(e1);
const e2 = document.createElement('div');
observer_e.observe(e2);
testing.expectEqual(2, observer_e.takeRecords().length);
const div1 = document.createElement('div');
document.body.appendChild(div1);
testing.expectEqual(0, count);
observer.observe(div1);
testing.expectEqual(0, count);
observer.observe(div1);
observer.observe(div1);
testing.expectEqual(0, count);
observer.unobserve(div1);
testing.eventually(() => {
testing.expectEqual(0, count);
});
}
</script>
<script id=disconnect>
let observer_d = new IntersectionObserver(entries => {});
let d1 = document.createElement('div');
observer_d.observe(d1);
observer_d.disconnect();
testing.expectEqual(0, observer_d.takeRecords().length);
{
let count = 0;
let observer = new IntersectionObserver(entries => {
count += entries.length;
});
const div1 = document.createElement('div');
document.body.appendChild(div1);
// cannot fire synchronously, must be on the next tick
testing.expectEqual(0, count);
observer.observe(div1);
testing.expectEqual(0, count);
observer.observe(div1);
observer.observe(div1);
testing.expectEqual(0, count);
observer.disconnect();
testing.eventually(() => {
testing.expectEqual(0, count);
});
}
</script>
<script id=entry>
let entry;
let div1 = document.createElement('div');
document.body.appendChild(div1);
new IntersectionObserver(entries => { entry = entries[0]; }).observe(div1);
{
let entry = null;
let observer = new IntersectionObserver(entries => {
entry = entries[0];
});
testing.expectEqual(0, entry.boundingClientRect.x);
testing.expectEqual(1, entry.intersectionRatio);
testing.expectEqual(0, entry.intersectionRect.x);
testing.expectEqual(0, entry.intersectionRect.y);
testing.expectEqual(1, entry.intersectionRect.width);
testing.expectEqual(1, entry.intersectionRect.height);
testing.expectEqual(true, entry.isIntersecting);
testing.expectEqual(0, entry.rootBounds.x);
testing.expectEqual(0, entry.rootBounds.y);
testing.expectEqual(1, entry.rootBounds.width);
testing.expectEqual(1, entry.rootBounds.height);
testing.expectEqual('[object HTMLDivElement]', entry.target.toString());
let div1 = document.createElement('div');
document.body.appendChild(div1);
new IntersectionObserver(entries => { entry = entries[0]; }).observe(div1);
testing.eventually(() => {
testing.expectEqual(0, entry.boundingClientRect.x);
testing.expectEqual(1, entry.intersectionRatio);
testing.expectEqual(0, entry.intersectionRect.x);
testing.expectEqual(0, entry.intersectionRect.y);
testing.expectEqual(1, entry.intersectionRect.width);
testing.expectEqual(1, entry.intersectionRect.height);
testing.expectEqual(true, entry.isIntersecting);
testing.expectEqual(0, entry.rootBounds.x);
testing.expectEqual(0, entry.rootBounds.y);
testing.expectEqual(1, entry.rootBounds.width);
testing.expectEqual(1, entry.rootBounds.height);
testing.expectEqual('[object HTMLDivElement]', entry.target.toString());
});
}
</script>
<script id=options>
const new_root = document.createElement('span');
document.body.appendChild(new_root);
<script id=timing>
{
const capture = [];
const observer = new IntersectionObserver(() => {
capture.push('callback');
});
let new_entry;
const new_observer = new IntersectionObserver(
(entries) => { new_entry = entries[0]; },
{root: new_root, rootMargin: '0px 0px 0px 0px', threshold: [0]}
);
const div = document.createElement('div');
capture.push('pre-append');
document.body.appendChild(div);
capture.push('post-append');
new_observer.observe(document.createElement('div'));
testing.expectEqual(1, new_entry.rootBounds.x);
capture.push('pre-observe');
observer.observe(div);
capture.push('post-observe');
testing.eventually(() => {
testing.expectEqual([
'pre-append',
'post-append',
'pre-observe',
'post-observe',
'callback',
], capture)
});
}
</script>