Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions src/interaction-types/frontend/elementScrolling.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ InteractRunner.addInteractionConfig( {
const animation = interaction.createTimelineInstance( 0, {} )
const trigger = interaction.getCurrentTrigger()

const scrollHandler = isFirstCall => {
const update = isFirstCall => {
const rect = trigger.getBoundingClientRect()
const viewportHeight = window.innerHeight

Expand All @@ -20,19 +20,38 @@ InteractRunner.addInteractionConfig( {
// Negative offset means start counting later and finish sooner, contracting the bounds.
const elementScroll = viewportHeight - rect.top + offset
const totalScroll = viewportHeight + rect.height + ( offset * 2 )
const scrolled = elementScroll / totalScroll
const scrolled = totalScroll ? elementScroll / totalScroll : 0

// Clamp between 0 and 1
const clampedScrolled = Math.max( 0, Math.min( 1, scrolled ) )
animation.seekPercentage( clampedScrolled, isFirstCall === true ? 0 : smoothness )
}

window.addEventListener( 'scroll', scrollHandler )
scrollHandler( true )
// Coalesce scroll events into a single rAF so we only seek once per
// frame. Firing anime.js on every scroll event janks slower Android
// devices.
let rafId = null
const scrollHandler = () => {
if ( rafId !== null ) {
return
}
rafId = window.requestAnimationFrame( () => {
rafId = null
update( false )
} )
}

// Passive so the listener never blocks scrolling on mobile.
window.addEventListener( 'scroll', scrollHandler, { passive: true } )
update( true )

return () => {
if ( rafId !== null ) {
window.cancelAnimationFrame( rafId )
rafId = null
}
animation.destroy()
window.removeEventListener( 'scroll', scrollHandler )
window.removeEventListener( 'scroll', scrollHandler, { passive: true } )
}
},
},
Expand Down
26 changes: 22 additions & 4 deletions src/interaction-types/frontend/enterViewport.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,18 @@ InteractRunner.addInteractionConfig( {
// Normalize the threshold to be between 0 and the maximum possible threshold
// for the current trigger. This ensures the interaction always works.
const rect = trigger.getBoundingClientRect()
const visibleHeight = Math.min( window.innerHeight, rect.height )
const maxThreshold = ( visibleHeight / rect.height ) - 0.01
const threshold = parseFloat( interaction.getOption( 'threshold', 0.3 ) )
const normalizedThreshold = Math.min( Math.max( threshold, 0 ), maxThreshold )
// Guard against a zero (or not-yet-laid-out) height. On slower
// devices the element may not have its final height when we
// initialize, which would make maxThreshold NaN and throw when
// constructing the IntersectionObserver.
let normalizedThreshold = Math.max( threshold, 0 )
if ( rect.height > 0 ) {
const visibleHeight = Math.min( window.innerHeight, rect.height )
const maxThreshold = ( visibleHeight / rect.height ) - 0.01
normalizedThreshold = Math.min( normalizedThreshold, maxThreshold )
}
normalizedThreshold = Math.max( normalizedThreshold, 0 )

// Use Intersection Observer to detect when the target enters the viewport
const callback = entries => {
Expand All @@ -30,7 +38,17 @@ InteractRunner.addInteractionConfig( {
} )
}

const io = new IntersectionObserver( callback, { threshold: normalizedThreshold } ) // eslint-disable-line compat/compat
// Fire slightly before the element scrolls into view on Android only.
// Android Chrome batches/defers IntersectionObserver callbacks during
// momentum (fling) scrolling, so without this lead time reveals
// "pop in" late compared to iOS Safari.
const isAndroid = /Android/i.test( navigator.userAgent )
const observerOptions = { threshold: normalizedThreshold }
if ( isAndroid ) {
observerOptions.rootMargin = '0px 0px 15% 0px'
}

const io = new IntersectionObserver( callback, observerOptions ) // eslint-disable-line compat/compat
io.observe( trigger )

return () => {
Expand Down
34 changes: 28 additions & 6 deletions src/interaction-types/frontend/pageScrolling.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,41 @@ InteractRunner.addInteractionConfig( {
const smoothness = interaction.getOption( 'smoothness', 200 )
const animation = interaction.createTimelineInstance( 0, {} )

const scrollHandler = isFirstCall => {
const update = isFirstCall => {
const winScroll = document.body.scrollTop || document.documentElement.scrollTop
const height = document.documentElement.scrollHeight - document.documentElement.clientHeight
const scrolled = winScroll / height
animation.seekPercentage( scrolled, isFirstCall === true ? 0 : smoothness )
const scrolled = height ? winScroll / height : 0

// Clamp between 0 and 1 (iOS overscroll can produce negative values).
const clampedScrolled = Math.max( 0, Math.min( 1, scrolled ) )
animation.seekPercentage( clampedScrolled, isFirstCall === true ? 0 : smoothness )
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Coalesce scroll events into a single rAF so we only seek once per
// frame. Firing anime.js on every scroll event janks slower Android
// devices.
let rafId = null
const scrollHandler = () => {
if ( rafId !== null ) {
return
}
rafId = window.requestAnimationFrame( () => {
rafId = null
update( false )
} )
}

window.addEventListener( 'scroll', scrollHandler )
scrollHandler( true )
// Passive so the listener never blocks scrolling on mobile.
window.addEventListener( 'scroll', scrollHandler, { passive: true } )
update( true )

return () => {
if ( rafId !== null ) {
window.cancelAnimationFrame( rafId )
rafId = null
}
animation.destroy()
window.removeEventListener( 'scroll', scrollHandler )
window.removeEventListener( 'scroll', scrollHandler, { passive: true } )
}
},
},
Expand Down
Loading