Last active
April 28, 2020 17:22
-
-
Save Plotist/cfe6c186960d47186dc9ace5668c6db0 to your computer and use it in GitHub Desktop.
Nested (parallax) scrolling in Web with React HOC
This file contains hidden or 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
import React from "react"; | |
import "./withNestedScrolling"; | |
// On scrolling down comment-list-navigation will dissapear with slideOutUp animation | |
// On scrolling up comment-list-navigation will reappear with slideInDown animatio | |
const CommentsList = props => { | |
const { nestedScrollTargetHeight, scrollTargetRef, nestedScrollTargetRef } = props; | |
return ( | |
<div | |
className="comment-list-container" | |
ref={scrollTargetRef} | |
style={{ | |
overflowY: "auto" | |
}} | |
> | |
{/*Navigation we want to gracefully flip on scrolling up and down */} | |
<div | |
className="comment-list-navigation" | |
ref={nestedScrollTargetRef} | |
style={{ | |
position: "fixed", | |
display: "flex", | |
flexDirection: "column" | |
}} | |
> | |
<div> Browse </div> | |
<div> Articles </div> | |
<div> ... </div> | |
</div> | |
{/*List with all actual contents*/} | |
<div | |
className="comments-list" | |
style={{ | |
marginTop: `${nestedScrollTargetHeight}px`, | |
overflowY: "unset" | |
}} | |
> | |
{ this.props.comments.map(comment => <div> { comment } </div>) } | |
</div> | |
</div> | |
) | |
}; | |
export default withNestedScrolling(CommentsList, | |
"slideOutUp", | |
"slideInDown", | |
"flex" | |
) |
This file contains hidden or 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
import React, { useState, useRef, useEffect } from "react"; | |
import { animateCSS } from "../../helpers/animate"; | |
/** | |
* Provides nested scrolling behaviour for scrollable lists. | |
* | |
* $scrollTarget - node scrollTargetRef points to | |
* $nestedScrollTarget - node nestedScrollTargetRef points to | |
* | |
* 1. Hides $nestedScrollTarget with hideAnimation when $scrollTarget is being scrolled down | |
* unless $nestedScrollTarget.scrollTop is less or equal to $nestedScrollTarget's height. | |
* If hide animation already in progress $nestedScrollTarget will be immediately | |
* shown back with showAnimation as soon as hide animation finishes. | |
* 2. Shows $nestedScrollTarget with showAnimation when scrollTarget is being scrolled up. | |
* 3. On hiding and showing $nestedScrollTarget it will switch its display between | |
* 'none' and 'flex' accordingly. | |
* | |
* @param {component} scrollable component. It should provide isMobile prop in order to activate | |
* nested scrolling only on mobile view | |
* @param {object} hideAnimation: class name, | |
* showAnimation: class name, | |
* nestedScrollTargetDisplay: default $nestedScrollTarget display | |
* | |
* for available animation class names see https://daneden.github.io/animate.css/ | |
*/ | |
const withNestedScrolling = (ScrollableComponent, { | |
hideAnimation, | |
showAnimation, | |
nestedScrollTargetDisplay | |
}) => props => { | |
const scrollTargetRef = useRef(null), | |
nestedScrollTargetRef = useRef(null), | |
slideOutUpInProgress = useRef(false), | |
slideInDownInProgress = useRef(false), | |
handleScrollInProgress = useRef(false), | |
classificationCanBeHidden = useRef(false), | |
lastKnown = useRef(null), | |
isMobile = props.isMobile, | |
isMobileRef = useRef(props.isMobile); | |
useEffect(() => { | |
isMobileRef.current = isMobile; | |
}, [isMobile]); | |
const [nestedScrollTargetHeight, handleNestedScrollTargetHeight] = useState(0); | |
const nestedScrollTargetHeightRef = useRef(nestedScrollTargetHeight); | |
function onScrollDown() { | |
slideOutUpInProgress.current = true; | |
animateCSS(nestedScrollTargetRef.current, hideAnimation, () => { | |
// If at the end of hiding animation scrolling is already got to the top | |
// gracefully display it back instead of hiding component completely | |
if (classificationCanBeHidden.current) { | |
nestedScrollTargetRef.current.style.display = "none"; | |
} else { | |
onScrollUp() | |
} | |
slideOutUpInProgress.current = false; | |
}); | |
}; | |
function onScrollUp() { | |
slideInDownInProgress.current = true; | |
nestedScrollTargetRef.current.style.display = nestedScrollTargetDisplay; | |
animateCSS(nestedScrollTargetRef.current, showAnimation, () => { | |
slideInDownInProgress.current = false; | |
}); | |
} | |
function handleScrollY(scrollTop) { | |
if (scrollTop < nestedScrollTargetHeightRef.current) { | |
nestedScrollTargetRef.current.style.display = nestedScrollTargetDisplay; | |
lastKnown.current = scrollTop; | |
return null; | |
} | |
if (scrollTop > lastKnown.current) { | |
if( | |
!slideOutUpInProgress.current && | |
nestedScrollTargetRef.current.getBoundingClientRect().height !== 0 && | |
scrollTop > nestedScrollTargetHeightRef.current | |
) { | |
onScrollDown(); | |
} | |
} else { | |
if(!slideInDownInProgress.current && nestedScrollTargetRef.current.getBoundingClientRect().height === 0) { | |
onScrollUp() | |
} | |
} | |
lastKnown.current = scrollTop; | |
} | |
function handleEventsScroll (e) { | |
const scrollTop = e.target.scrollTop; | |
/* | |
Forbid hiding of the nested scrollable component when | |
scrolling already got up top while hiding animation is still in progress | |
*/ | |
classificationCanBeHidden.current = scrollTop >= nestedScrollTargetHeightRef.current; | |
if (!classificationCanBeHidden.current && !slideInDownInProgress.current && slideOutUpInProgress.current) { | |
onScrollUp(); | |
} | |
if (!handleScrollInProgress.current && !slideOutUpInProgress.current && !slideInDownInProgress.current) { | |
window.requestAnimationFrame(() => { | |
handleScrollY(scrollTop); | |
handleScrollInProgress.current = false; | |
}); | |
handleScrollInProgress.current = true; | |
} | |
} | |
useEffect(() => { | |
const currentScrollTarget = scrollTargetRef.current; | |
const height = nestedScrollTargetRef.current.getBoundingClientRect().height; | |
handleNestedScrollTargetHeight(height); | |
nestedScrollTargetHeightRef.current = height; | |
scrollTargetRef.current.addEventListener('scroll', handleEventsScroll); | |
return () => { | |
currentScrollTarget.removeEventListener('scroll', handleEventsScroll); | |
} | |
}, []); | |
useEffect(() => { | |
const height = nestedScrollTargetRef.current.getBoundingClientRect().height; | |
handleNestedScrollTargetHeight(nestedScrollTargetRef.current.getBoundingClientRect().height); | |
nestedScrollTargetHeightRef.current = height; | |
}, [isMobile]); | |
return ( | |
<ScrollableComponent | |
nestedScrollTargetHeight={nestedScrollTargetHeight} | |
scrollTargetRef={scrollTargetRef} | |
nestedScrollTargetRef={nestedScrollTargetRef} | |
{...props} | |
/> | |
) | |
}; | |
export default withNestedScrolling; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Great! That may help.