Skip to content

Commit 3042aa5

Browse files
committed
Handle zero-area and display: none cases
Attempts to satsify the spec: https://www.w3.org/TR/intersection-observer/#update-intersection-observations-algo isIntersecting, non-zero area, and display:nonen are all related, so fixing in one swoop. Fixes: #93 #73 Related issues: w3c/IntersectionObserver#69 w3c/IntersectionObserver#222
1 parent b754bab commit 3042aa5

11 files changed

+669
-533
lines changed

src/index.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1111

1212
import { SpanielIntersectionObserver, generateEntry } from './intersection-observer';
1313

14-
import { entrySatisfiesRatio } from './utils';
15-
1614
import { SpanielTrackedElement, DOMMargin, IntersectionObserverClass } from './interfaces';
1715

1816
export { Watcher, WatcherConfig } from './watcher';
@@ -46,13 +44,13 @@ export function queryElement(el: Element, callback: (bcr: ClientRect, frame: Fra
4644
}
4745

4846
export function elementSatisfiesRatio(
49-
el: Element,
47+
el: HTMLElement,
5048
ratio: number = 0,
5149
callback: (result: Boolean) => void,
5250
rootMargin: DOMMargin = { top: 0, bottom: 0, left: 0, right: 0 }
5351
) {
5452
queryElement(el, (bcr: ClientRect, frame: Frame) => {
5553
let entry = generateEntry(frame, bcr, el, rootMargin);
56-
callback(entrySatisfiesRatio(entry, ratio));
54+
callback(entry.isIntersecting && entry.intersectionRatio >= ratio);
5755
});
5856
}

src/interfaces.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ distributed under the License is distributed on an "AS IS" BASIS,
99
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1010
*/
1111

12-
export interface SpanielTrackedElement extends Element {
12+
export interface SpanielTrackedElement extends HTMLElement {
1313
__spanielId: string;
1414
}
1515

src/intersection-observer.ts

+36-8
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ distributed under the License is distributed on an "AS IS" BASIS,
99
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1010
*/
1111

12-
import { entrySatisfiesRatio } from './utils';
12+
import { calculateIsIntersecting } from './utils';
1313

1414
import { Frame, ElementScheduler, generateToken } from './metal/index';
1515

@@ -63,7 +63,7 @@ export class SpanielIntersectionObserver implements IntersectionObserver {
6363
public thresholds: number[];
6464
private records: { [index: string]: EntryEvent };
6565

66-
observe(target: Element) {
66+
observe(target: HTMLElement) {
6767
let trackedTarget = target as SpanielTrackedElement;
6868

6969
let id = (trackedTarget.__spanielId = trackedTarget.__spanielId || generateToken());
@@ -77,7 +77,7 @@ export class SpanielIntersectionObserver implements IntersectionObserver {
7777
);
7878
return id;
7979
}
80-
private onTick(frame: Frame, id: string, bcr: DOMRectReadOnly, el: Element) {
80+
private onTick(frame: Frame, id: string, bcr: DOMRectReadOnly, el: SpanielTrackedElement) {
8181
let { numSatisfiedThresholds, entry } = this.generateEntryEvent(frame, bcr, el);
8282
let record: EntryEvent =
8383
this.records[id] ||
@@ -86,8 +86,12 @@ export class SpanielIntersectionObserver implements IntersectionObserver {
8686
numSatisfiedThresholds: 0
8787
});
8888

89-
if (numSatisfiedThresholds !== record.numSatisfiedThresholds) {
89+
if (
90+
numSatisfiedThresholds !== record.numSatisfiedThresholds ||
91+
entry.isIntersecting !== record.entry.isIntersecting
92+
) {
9093
record.numSatisfiedThresholds = numSatisfiedThresholds;
94+
record.entry = entry;
9195
this.scheduler.scheduleWork(() => {
9296
this.callback([entry]);
9397
});
@@ -104,13 +108,13 @@ export class SpanielIntersectionObserver implements IntersectionObserver {
104108
takeRecords(): IntersectionObserverEntry[] {
105109
return [];
106110
}
107-
private generateEntryEvent(frame: Frame, bcr: DOMRectReadOnly, el: Element): EntryEvent {
111+
private generateEntryEvent(frame: Frame, bcr: DOMRectReadOnly, el: HTMLElement): EntryEvent {
108112
let count: number = 0;
109113
let entry = generateEntry(frame, bcr, el, this.rootMarginObj);
110114

111115
for (let i = 0; i < this.thresholds.length; i++) {
112116
let threshold = this.thresholds[i];
113-
if (entrySatisfiesRatio(entry, threshold)) {
117+
if (entry.intersectionRatio >= threshold) {
114118
count++;
115119
}
116120
}
@@ -149,7 +153,7 @@ function addRatio(entryInit: SpanielIntersectionObserverEntryInit): Intersection
149153
intersectionRect,
150154
target,
151155
intersectionRatio,
152-
isIntersecting: null
156+
isIntersecting: calculateIsIntersecting({ intersectionRect })
153157
};
154158
}
155159

@@ -179,12 +183,36 @@ export class IntersectionObserverEntry implements IntersectionObserverEntryInit
179183
};
180184
*/
181185

186+
function emptyRect(): ClientRect | DOMRect {
187+
return {
188+
bottom: 0,
189+
height: 0,
190+
left: 0,
191+
right: 0,
192+
top: 0,
193+
width: 0,
194+
x: 0,
195+
y: 0
196+
};
197+
}
198+
182199
export function generateEntry(
183200
frame: Frame,
184201
bcr: DOMRectReadOnly,
185-
el: Element,
202+
el: HTMLElement,
186203
rootMargin: DOMMargin
187204
): IntersectionObserverEntry {
205+
if (el.style.display === 'none') {
206+
return {
207+
boundingClientRect: emptyRect(),
208+
intersectionRatio: 0,
209+
intersectionRect: emptyRect(),
210+
isIntersecting: false,
211+
rootBounds: emptyRect(),
212+
target: el,
213+
time: frame.timestamp
214+
};
215+
}
188216
let { bottom, right } = bcr;
189217
let rootBounds: ClientRect = {
190218
left: frame.x - rootMargin.left,

src/native-spaniel-observer.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ distributed under the License is distributed on an "AS IS" BASIS,
99
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1010
*/
1111

12-
import { entrySatisfiesRatio } from './utils';
12+
import { calculateIsIntersecting } from './utils';
1313

1414
import {
1515
IntersectionObserverInit,
@@ -187,9 +187,10 @@ export class SpanielObserver implements SpanielObserverInterface {
187187
let hasTimeThreshold = !!state.threshold.time;
188188
let spanielEntry: SpanielObserverEntry = this.generateSpanielEntry(entry, state);
189189

190-
const ratioSatisfied = entrySatisfiesRatio(entry, state.threshold.ratio);
190+
const ratioSatisfied = entry.intersectionRatio >= state.threshold.ratio;
191+
const isIntersecting = calculateIsIntersecting(entry);
191192

192-
if (ratioSatisfied && !state.lastSatisfied) {
193+
if (ratioSatisfied && !state.lastSatisfied && isIntersecting) {
193194
spanielEntry.entering = true;
194195
if (hasTimeThreshold) {
195196
state.lastVisible = time;

src/spaniel-observer.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ distributed under the License is distributed on an "AS IS" BASIS,
99
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1010
*/
1111

12-
import { entrySatisfiesRatio } from './utils';
12+
import { calculateIsIntersecting } from './utils';
1313

1414
import { SpanielIntersectionObserver } from './intersection-observer';
1515

@@ -192,9 +192,10 @@ export class SpanielObserver implements SpanielObserverInterface {
192192
let hasTimeThreshold = !!state.threshold.time;
193193
let spanielEntry: SpanielObserverEntry = this.generateSpanielEntry(entry, state);
194194

195-
const ratioSatisfied = entrySatisfiesRatio(entry, state.threshold.ratio);
195+
const ratioSatisfied = entry.intersectionRatio >= state.threshold.ratio;
196+
const isIntersecting = calculateIsIntersecting(entry);
196197

197-
if (ratioSatisfied && !state.lastSatisfied) {
198+
if (ratioSatisfied && !state.lastSatisfied && isIntersecting) {
198199
spanielEntry.entering = true;
199200
if (hasTimeThreshold) {
200201
state.lastVisible = time;

src/utils.ts

+2-15
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,3 @@
1-
export function entrySatisfiesRatio(entry: IntersectionObserverEntry, threshold: number) {
2-
let { boundingClientRect, intersectionRatio } = entry;
3-
4-
// Edge case where item has no actual area
5-
if (boundingClientRect.width === 0 || boundingClientRect.height === 0) {
6-
let { boundingClientRect, intersectionRect } = entry;
7-
return (
8-
boundingClientRect.left === intersectionRect.left &&
9-
boundingClientRect.top === intersectionRect.top &&
10-
intersectionRect.width >= 0 &&
11-
intersectionRect.height >= 0
12-
);
13-
} else {
14-
return intersectionRatio > threshold || (intersectionRatio === 1 && threshold === 1);
15-
}
1+
export function calculateIsIntersecting({ intersectionRect }: { intersectionRect: ClientRect }) {
2+
return intersectionRect.width > 0 || intersectionRect.height > 0;
163
}

test/headless/context.js

+13-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
Copyright 2017 LinkedIn Corp. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
 You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
3-
3+
44
Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an "AS IS" BASIS, 
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
55
*/
66

@@ -13,19 +13,21 @@ const MAC_WINDOW_BAR_HEIGHT = 22; // See https://github.com/segmentio/nightmare/
1313

1414
export default class Context {
1515
constructor() {
16-
this._nightmare = Nightmare({ show: false }),
17-
this._nightmare.viewport(400, 400 + MAC_WINDOW_BAR_HEIGHT);
16+
(this._nightmare = Nightmare({ show: false })), this._nightmare.viewport(400, 400 + MAC_WINDOW_BAR_HEIGHT);
1817
this._events = [];
1918
this._results = [];
2019
this._assertions = [];
21-
this._execution = this._root = this._nightmare.goto('http://localhost:3000/').wait(10).evaluate(function() {
22-
window.STATE = {};
23-
window.createDiv = function(id) {
24-
var div = document.createElement('div');
25-
div.id = id;
26-
document.body.appendChild(div);
27-
}
28-
});
20+
this._execution = this._root = this._nightmare
21+
.goto('http://localhost:3000/')
22+
.wait(10)
23+
.evaluate(function() {
24+
window.STATE = {};
25+
window.createDiv = function(id) {
26+
var div = document.createElement('div');
27+
div.id = id;
28+
document.body.appendChild(div);
29+
};
30+
});
2931
}
3032
close() {
3133
return this._root.end();

test/headless/run.js

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ server.stdout.on('data', data => {
1616
'--require',
1717
'@babel/register',
1818
'test/headless/specs/**/*.js',
19+
'test/headless/specs/*.js',
1920
'--exit',
2021
'--timeout',
2122
'5000'

0 commit comments

Comments
 (0)