Skip to content

Instantly share code, notes, and snippets.

@Plotist
Last active April 28, 2020 17:22
Show Gist options
  • Save Plotist/cfe6c186960d47186dc9ace5668c6db0 to your computer and use it in GitHub Desktop.
Save Plotist/cfe6c186960d47186dc9ace5668c6db0 to your computer and use it in GitHub Desktop.
Nested (parallax) scrolling in Web with React HOC
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"
)
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;
@VitaliyPtitsyn
Copy link

Great! That may help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment