Skip to content

Commit

Permalink
Add support for progress events
Browse files Browse the repository at this point in the history
  • Loading branch information
rdmurphy committed Mar 21, 2022
1 parent 8a8c419 commit 1f61d39
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 5 deletions.
6 changes: 5 additions & 1 deletion examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
document.addEventListener('scroll-scene-exit', ({ detail }) => {
detail.element.classList.remove('active');
});

document.addEventListener('scroll-scene-progress', ({ detail }) => {
detail.element.textContent = `Progress: ${detail.progress}`;
});
</script>
<style>
body {
Expand Down Expand Up @@ -55,7 +59,7 @@
<body>
<scroll-scene>Scene 1</scroll-scene>
<scroll-scene offset="0.7">Scene 2</scroll-scene>
<scroll-scene>Scene 3</scroll-scene>
<scroll-scene progress>Scene 3</scroll-scene>

<div class="midpoint"></div>

Expand Down
76 changes: 73 additions & 3 deletions src/scroll-scene-element.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
const offsetObservers = new Map<number, IntersectionObserver>();
type ProgressCommands = { on: () => void; off: () => void };
const progressListeners = new WeakMap<ScrollSceneElement, ProgressCommands>();

let previousScrollDepth = 0;
let isScrollingDown = false;

Expand All @@ -13,11 +16,26 @@ function createOffsetObserver(offset: number) {
const element = entry.target as ScrollSceneElement;
const bounds = entry.boundingClientRect;
const offset = element.offset;
const progress = element.progress;
const isIntersecting = entry.isIntersecting;

if (progress) {
let commands = progressListeners.get(element);

const event = entry.isIntersecting ? 'enter' : 'exit';
if (!commands) {
commands = observeProgress(element);
progressListeners.set(element, commands);
}

if (isIntersecting) {
commands.on();
} else {
commands.off();
}
}

element.dispatchEvent(
new CustomEvent(`scroll-scene-${event}`, {
new CustomEvent(`scroll-scene-${isIntersecting ? 'enter' : 'exit'}`, {
bubbles: true,
detail: {
bounds,
Expand All @@ -35,6 +53,46 @@ function createOffsetObserver(offset: number) {
);
}

function observeProgress(element: ScrollSceneElement): ProgressCommands {
/**
* Called on each scroll event.
*/
function scroll() {
const bounds = element.getBoundingClientRect();
const offset = element.offset;
const top = bounds.top;
const bottom = bounds.bottom;
// ensure progress is never less than 0 or greater than 1
const progress = Math.max(
0,
Math.min((window.innerHeight * offset - top) / (bottom - top), 1),
);

element.dispatchEvent(
new CustomEvent('scroll-scene-progress', {
bubbles: true,
detail: {
bounds,
element,
progress,
offset,
},
}),
);
}

return {
on() {
// initial hit
scroll();
window.addEventListener('scroll', scroll, false);
},
off() {
window.removeEventListener('scroll', scroll, false);
},
};
}

class ScrollSceneElement extends HTMLElement {
connectedCallback() {
this._connectToObserver(this.offset);
Expand All @@ -56,7 +114,7 @@ class ScrollSceneElement extends HTMLElement {
}

static get observedAttributes() {
return ['offset'];
return ['offset', 'progress'];
}

get offset() {
Expand All @@ -67,6 +125,18 @@ class ScrollSceneElement extends HTMLElement {
this.setAttribute('offset', value.toString());
}

get progress() {
return this.hasAttribute('progress');
}

set progress(value: boolean) {
if (value) {
this.setAttribute('progress', '');
} else {
this.removeAttribute('progress');
}
}

private _connectToObserver(offset: number) {
let observer = offsetObservers.get(offset);

Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/offset.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Integration</title>
<title>Offset</title>
<script src="/dist/index.modern.js" type="module"></script>
<style>
body {
Expand Down
29 changes: 29 additions & 0 deletions tests/fixtures/progress.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Progress</title>
<script src="/dist/index.modern.js" type="module"></script>
<style>
body {
margin-bottom: 100vh;
margin-top: 100vh;
}

scroll-scene {
display: block;
height: 500px;
width: 100%;
}

scroll-scene + scroll-scene {
margin-top: 30px;
}
</style>
</head>
<body>
<scroll-scene progress>Scene 1</scroll-scene>
</body>
</html>
20 changes: 20 additions & 0 deletions tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,26 @@ export function scrollToTopOfElement(locator: Locator) {
return scrollTopOfElementToOffset(locator, 0.5);
}

export function scrollWindowToElementOffsetDepth(
locator: Locator,
offset: number,
depth: number,
) {
return locator.evaluate(
(element, [offset, depth]) => {
const bounds = element.getBoundingClientRect();
window.scrollTo(
0,
bounds.top +
bounds.height * depth +
window.scrollY -
window.innerHeight * (1 - offset),
);
},
[offset, depth],
);
}

export function scrollAboveElement(locator: Locator) {
return locator.evaluate((element) => {
window.scrollTo(
Expand Down
80 changes: 80 additions & 0 deletions tests/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
scrollTopOfElementToOffset,
scrollToTop,
scrollToTopOfElement,
scrollWindowToElementOffsetDepth,
} from './helpers';

// types
Expand Down Expand Up @@ -195,5 +196,84 @@ test.describe('scroll-scene', () => {
scrollTopOfElementToOffset(locator, 0.7),
]);
});

test('elements may opt-in to progress events', async ({ page }) => {
await page.goto('/tests/fixtures/progress.html');

const locator = page.locator('scroll-scene');
const offset = 0.5;
const depth = 0.75;

// confirm progress events begin once the offset is reached
await Promise.all([
locator.evaluate(
(element) =>
new Promise((resolve) =>
element.addEventListener(
'scroll-scene-progress',
function progress(event) {
if ((event as CustomEvent).detail.progress === 0) {
element.removeEventListener(
'scroll-scene-progress',
progress,
);
resolve(true);
}
},
),
),
),
scrollWindowToElementOffsetDepth(locator, offset, 0),
]);

// make sure progress events continue as you scroll
await Promise.all([
locator.evaluate(
(element, depth) =>
new Promise((resolve) =>
element.addEventListener(
'scroll-scene-progress',
function progress(event) {
const detail = (event as CustomEvent).detail;

if (detail.progress >= depth) {
element.removeEventListener(
'scroll-scene-progress',
progress,
);
resolve(true);
}
},
),
),
depth,
),
scrollWindowToElementOffsetDepth(locator, offset, 0.75),
]);

// make sure it maxes out at 1
await Promise.all([
locator.evaluate(
(element) =>
new Promise((resolve) =>
element.addEventListener(
'scroll-scene-progress',
function progress(event) {
const detail = (event as CustomEvent).detail;

if (detail.progress === 1) {
element.removeEventListener(
'scroll-scene-progress',
progress,
);
resolve(true);
}
},
),
),
),
scrollBelowElement(locator),
]);
});
});
});

0 comments on commit 1f61d39

Please sign in to comment.