-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'trunk' into stepper-tracking/support-loading
- Loading branch information
Showing
33 changed files
with
573 additions
and
237 deletions.
There are no files selected for viewing
34 changes: 0 additions & 34 deletions
34
client/a8c-for-agencies/sections/agency-tier/early-access-banner/index.tsx
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
/** | ||
* Tracks scroll depth for a container element. | ||
*/ | ||
export default class ScrollTracker { | ||
private container: HTMLElement | null = null; | ||
private maxScrollDepth: number = 0; | ||
|
||
private handleScroll = (): void => { | ||
if ( ! this.container ) { | ||
return; | ||
} | ||
|
||
const scrollTop = this.container.scrollTop ?? 0; | ||
const scrollHeight = this.container.scrollHeight ?? 0; | ||
const clientHeight = this.container.clientHeight ?? 0; | ||
|
||
const denominator = scrollHeight - clientHeight; | ||
const scrollDepth = denominator <= 0 ? 0 : scrollTop / denominator; | ||
|
||
this.maxScrollDepth = calcAndClampMaxValue( scrollDepth, this.maxScrollDepth ); | ||
}; | ||
|
||
/** | ||
* Sets the container element to track scrolling on. | ||
* Removes scroll listener from previous container if it exists. | ||
* @param container - The HTML element to track scrolling on, or null to stop tracking | ||
*/ | ||
public setContainer( container: HTMLElement | null ): void { | ||
this.cleanup(); | ||
this.resetMaxScrollDepth(); | ||
this.container = container; | ||
if ( container ) { | ||
container.addEventListener( 'scroll', this.handleScroll ); | ||
} | ||
} | ||
|
||
/** | ||
* Gets the maximum scroll depth reached as a decimal between 0 and 1. | ||
* @returns A number between 0 and 1 representing the maximum scroll depth | ||
*/ | ||
public getMaxScrollDepth(): number { | ||
return this.maxScrollDepth; | ||
} | ||
|
||
/** | ||
* Gets the maximum scroll depth reached as a percentage between 0 and 100. | ||
* @returns A rounded number between 0 and 100 representing the maximum scroll depth percentage | ||
*/ | ||
public getMaxScrollDepthAsPercentage(): number { | ||
return Math.round( this.maxScrollDepth * 100 ); | ||
} | ||
|
||
/** | ||
* Resets the maximum scroll depth back to 0. | ||
*/ | ||
public resetMaxScrollDepth = (): void => { | ||
this.maxScrollDepth = 0; | ||
}; | ||
|
||
/** | ||
* Removes scroll event listener from container. | ||
* Should be called when tracking is no longer needed. | ||
*/ | ||
public cleanup(): void { | ||
if ( this.container ) { | ||
this.container.removeEventListener( 'scroll', this.handleScroll ); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Calculates the maximum value between two numbers and clamps the result between 0 and 1. | ||
* @param valueA - First number to compare | ||
* @param valueB - Second number to compare | ||
* @returns A number between 0 and 1 representing the maximum value between valueA and valueB | ||
*/ | ||
function calcAndClampMaxValue( valueA: number, valueB: number ): number { | ||
return Math.min( 1, Math.max( 0, valueA, valueB ) ); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
/** | ||
* @jest-environment jsdom | ||
*/ | ||
import ScrollTracker from '../scroll-tracker'; | ||
|
||
describe( 'ScrollTracker', () => { | ||
let scrollTracker; | ||
let container; | ||
|
||
beforeEach( () => { | ||
scrollTracker = new ScrollTracker(); | ||
container = document.createElement( 'div' ); | ||
// Setup scrollable container | ||
Object.defineProperties( container, { | ||
scrollTop: { value: 0, configurable: true }, | ||
scrollHeight: { value: 1000, configurable: true }, | ||
clientHeight: { value: 500, configurable: true }, | ||
} ); | ||
} ); | ||
|
||
afterEach( () => { | ||
scrollTracker.cleanup(); | ||
} ); | ||
|
||
describe( 'getMaxScrollDepth()', () => { | ||
it( 'should return 0 when no scrolling has occurred', () => { | ||
expect( scrollTracker.getMaxScrollDepth() ).toBe( 0 ); | ||
} ); | ||
|
||
it( 'should return correct depth when scrolled', () => { | ||
scrollTracker.setContainer( container ); | ||
Object.defineProperty( container, 'scrollTop', { value: 250 } ); | ||
container.dispatchEvent( new Event( 'scroll' ) ); | ||
expect( scrollTracker.getMaxScrollDepth() ).toBe( 0.5 ); | ||
} ); | ||
|
||
it( 'should return the highest depth reached', () => { | ||
scrollTracker.setContainer( container ); | ||
|
||
Object.defineProperty( container, 'scrollTop', { value: 375 } ); | ||
container.dispatchEvent( new Event( 'scroll' ) ); | ||
|
||
Object.defineProperty( container, 'scrollTop', { value: 250 } ); | ||
container.dispatchEvent( new Event( 'scroll' ) ); | ||
|
||
expect( scrollTracker.getMaxScrollDepth() ).toBe( 0.75 ); | ||
} ); | ||
|
||
it( 'should reset when container changes', () => { | ||
scrollTracker.setContainer( container ); | ||
Object.defineProperty( container, 'scrollTop', { value: 250 } ); | ||
container.dispatchEvent( new Event( 'scroll' ) ); | ||
|
||
const newContainer = document.createElement( 'div' ); | ||
Object.defineProperties( newContainer, { | ||
scrollTop: { value: 0, configurable: true }, | ||
scrollHeight: { value: 1000, configurable: true }, | ||
clientHeight: { value: 500, configurable: true }, | ||
} ); | ||
scrollTracker.setContainer( newContainer ); | ||
|
||
expect( scrollTracker.getMaxScrollDepth() ).toBe( 0 ); | ||
} ); | ||
} ); | ||
|
||
describe( 'getMaxScrollDepthAsPercentage()', () => { | ||
it( 'should return 0 when no scrolling has occurred', () => { | ||
expect( scrollTracker.getMaxScrollDepthAsPercentage() ).toBe( 0 ); | ||
} ); | ||
|
||
it( 'should return the correct percentage when scrolled', () => { | ||
scrollTracker.setContainer( container ); | ||
Object.defineProperty( container, 'scrollTop', { value: 250 } ); | ||
container.dispatchEvent( new Event( 'scroll' ) ); | ||
expect( scrollTracker.getMaxScrollDepthAsPercentage() ).toBe( 50 ); | ||
} ); | ||
|
||
it( 'should return the highest percentage reached', () => { | ||
scrollTracker.setContainer( container ); | ||
|
||
Object.defineProperty( container, 'scrollTop', { value: 375 } ); | ||
container.dispatchEvent( new Event( 'scroll' ) ); | ||
|
||
Object.defineProperty( container, 'scrollTop', { value: 250 } ); | ||
container.dispatchEvent( new Event( 'scroll' ) ); | ||
|
||
expect( scrollTracker.getMaxScrollDepthAsPercentage() ).toBe( 75 ); | ||
} ); | ||
} ); | ||
|
||
describe( 'setContainer()', () => { | ||
it( 'should track scroll events on the new container', () => { | ||
scrollTracker.setContainer( container ); | ||
Object.defineProperty( container, 'scrollTop', { value: 250 } ); | ||
container.dispatchEvent( new Event( 'scroll' ) ); | ||
expect( scrollTracker.getMaxScrollDepthAsPercentage() ).toBe( 50 ); | ||
} ); | ||
|
||
it( 'should stop tracking previous container when setting new one', () => { | ||
const oldContainer = document.createElement( 'div' ); | ||
Object.defineProperties( oldContainer, { | ||
scrollTop: { value: 0, configurable: true }, | ||
scrollHeight: { value: 1000, configurable: true }, | ||
clientHeight: { value: 500, configurable: true }, | ||
} ); | ||
|
||
scrollTracker.setContainer( oldContainer ); | ||
Object.defineProperty( oldContainer, 'scrollTop', { value: 250 } ); | ||
oldContainer.dispatchEvent( new Event( 'scroll' ) ); | ||
|
||
scrollTracker.setContainer( container ); | ||
Object.defineProperty( oldContainer, 'scrollTop', { value: 375 } ); | ||
oldContainer.dispatchEvent( new Event( 'scroll' ) ); | ||
|
||
expect( scrollTracker.getMaxScrollDepthAsPercentage() ).toBe( 0 ); | ||
} ); | ||
} ); | ||
|
||
describe( 'cleanup()', () => { | ||
it( 'should stop tracking scroll events', () => { | ||
scrollTracker.setContainer( container ); | ||
scrollTracker.cleanup(); | ||
|
||
Object.defineProperty( container, 'scrollTop', { value: 250 } ); | ||
container.dispatchEvent( new Event( 'scroll' ) ); | ||
|
||
expect( scrollTracker.getMaxScrollDepthAsPercentage() ).toBe( 0 ); | ||
} ); | ||
} ); | ||
} ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.