Handling Scroll in Reactjs - javascript

I'm building a React app, and one of the main features will be a full-screen title page. When the user scrolls on the title page, the page automatically scrolls down and the header bar sticks to the top. I do not want the body overflow hidden here, because as soon as I go to put the overflow back in, the scrollbar will appear and the whole page jolts left about 5px.
I've seen this feature a lot; it's pretty common for single-page web designs. However, I can't seem to get it right. I've gotten to a decent point where the app will scroll for you, but it always bugs out when I try to scroll in the middle of the automatic scroll. This is the closest I've gotten to a solution:
import React, {Component} from 'react';
import Scroll from 'react-scroll';
class Header extends Component {
constructor() {
super();
this.state = {
scrolling: false,
inLogo: false,
sticky: "",
lastScrollPos: 0
}
/* Must bind to access 'this' */
this.handleScroll = this.handleScroll.bind(this);
this.isScrollingDown = this.isScrollingDown.bind(this);
this.scroller = Scroll.animateScroll;
}
componentDidMount() {
window.addEventListener("scroll", this.handleScroll);
Scroll.Events.scrollEvent.register('end', function(to, element) {
this.state.scrolling = false;
if(!this.state.inLogo) {
this.setState({sticky: "sticky"});
}
}.bind(this));
if(window.pageYOffset < 150) {
this.state.inLogo = true;
this.scroller.scrollToTop();
} else {
this.state.inLogo = false;
this.scroller.scrollTo(window.innerHeight);
}
}
componentDidUpdate() {
if(this.state.scrolling) {
if(this.state.inLogo) {
this.scroller.scrollToTop();
} else {
this.scroller.scrollTo(window.innerHeight);
}
}
}
handleScroll(e) {
console.log(this.state.scrolling);
if(this.state.scrolling) {
return;
}
var scrollDown = this.isScrollingDown();
var inLogo = this.isInLogo();
if(inLogo) {
if(scrollDown) {
console.log("Scrolling down in logo");
this.setState({
scrolling: true,
inLogo: false
});
}
if(!scrollDown) {
console.log("Scrolling up in logo");
this.setState({
scrolling: true,
inLogo: true,
sticky: ""
});
}
}
}
isScrollingDown() {
var scrollingDown = window.pageYOffset > this.state.lastScrollPos;
this.state.lastScrollPos = window.pageYOffset;
return scrollingDown;
}
isInLogo() {
return window.pageYOffset > 150 && window.pageYOffset < window.innerHeight;
}
render() {
return (
<div id="app-header" className={"header "+this.state.sticky}>
<div className="header-filler"></div>
<button>Contact Us</button>
<button>First</button>
</div>
);
}
}
The problem here is that when you scroll in the middle of an automatic scroll, the "end" scroll event in react-scroll is fired. So, the callback for the "end" scroll even sets this.state.scrolling to false. Now, since that's false, the scroll is handled and I setState() again, potentially making the header bar sticky.
Bottom line: the ideal solution is to disable the scroll in componentDidUpdate when it begins scrolling, and enable it again in the "end" event handler. The problem with this solution is this event handler is called when the user interrupts an automatic scroll.
Lastly, I've found some methods on stackoverflow to disable/enable scrolling, but I took them out because they didn't seem to help the "end" event handling issue. It just re-enables the scrolling anyway when the handler is fired.
Please ask questions, I'm trying really hard to explain this clearly.

Found the solution. I had to basically "continue" the scroll in my 'end' event handler if it is called before the scroll is actually finished. Here is the updated code:
import React, { Component } from 'react';
import Scroll from 'react-scroll';
class Header extends Component {
constructor() {
super();
//scroll direction: (1) - down, (2) - up
this.state = {
scrolling: false,
inLogo: false,
sticky: "",
lastScrollPos: 0
}
/* Must bind to access 'this' */
this.handleScroll = this.handleScroll.bind(this);
this.scroller = Scroll.animateScroll;
this.isScrollingDown = this.isScrollingDown.bind(this);
}
componentDidMount() {
window.addEventListener("scroll", this.handleScroll);
Scroll.Events.scrollEvent.register('end', function() {
if(window.pageYOffset == 0) {
console.log("End: "+window.pageYOffset);
this.setState({sticky: "", scrolling: false});
}
else if(window.pageYOffset == window.innerHeight) {
console.log("End: "+window.pageYOffset);
this.setState({sticky: "sticky", scrolling: false});
} else {
if(this.state.inLogo) {
this.scroller.scrollToTop();
} else {
this.scroller.scrollTo(window.innerHeight);
}
}
}.bind(this));
if(window.pageYOffset < 150) {
this.state.inLogo = true;
this.scroller.scrollTo(0);
} else {
this.state.inLogo = false;
this.scroller.scrollTo(window.innerHeight);
}
}
componentDidUpdate() {
if(this.state.scrolling) {
if(this.state.inLogo) {
this.scroller.scrollToTop();
} else {
this.scroller.scrollTo(window.innerHeight);
}
}
}
handleScroll(e) {
console.log(this.state.scrolling);
if(this.state.scrolling) {
return;
}
var scrollDown = this.isScrollingDown();
var inLogo = this.isInLogo();
if(inLogo) {
if(scrollDown) {
console.log("Scrolling down in logo");
this.setState({
scrolling: true,
inLogo: false
});
}
if(!scrollDown) {
console.log("Scrolling up in logo");
this.setState({
scrolling: true,
inLogo: true,
sticky: ""
});
}
}
}
isScrollingDown() {
var scrollingDown = window.pageYOffset > this.state.lastScrollPos;
this.state.lastScrollPos = window.pageYOffset;
return scrollingDown;
}
isInLogo() {
return window.pageYOffset > 150 && window.pageYOffset < window.innerHeight - 5;
}

Related

Change position fixed of div if scroll

I tried to fix a div after scroll on vuejs; My code like this :
...
async created() {
window.addEventListener('scroll', this.calendarScroll);
}
methods: {
calendarScroll() {
console.log('Scroll');
const dateHeader = document.querySelector('.dates-header');
if (dateHeader) {
if (window.scrollTop() >= 200) {
dateHeader.style.top = 0;
dateHeader.style.position = 'fixed';
}
}
},
}
the error is : Calendar.vue?404a:681 Uncaught TypeError: window.scrollTop is not a function
You can add scrollTo to the Element prototype before you mount:
Element.prototype.scrollTo = () => {}
All Elements created after you add this code will have the scrollTo method.
For more detail, you can check this: https://github.com/vuejs/vue-test-utils/issues/319#issuecomment-354667621

Side navigation menu responsive class to hide on window resize

Hey stackoverflow community!
I have a small issue regarding this JS logic to add a class name to my body tag to hide the side navigation menu upon window resizing. Let me try to explain the issue as clearly as I can. Currently I'm using a UI template by creativetime called Argon. The page when loaded on a full scaled width looks like this:
When I resize the window after the page has loaded it looks like this:
But when I refresh the page, the side navigation is no longer there as how it should be, which is like this:
After refreshing the page, resizing the window thereafter makes the side navigation to hide as it should. The sidenav just doesn't go hidden on first page load for some reason.
The JS for this is as follows:
var Layout = (function() {
function pinSidenav() {
$('.sidenav-toggler').addClass('active');
$('.sidenav-toggler').data('action', 'sidenav-unpin');
$('body').removeClass('g-sidenav-hidden').addClass('g-sidenav-show g-sidenav-pinned');
$('body').append('<div class="backdrop d-xl-none" data-action="sidenav-unpin" data-target='+$('#sidenav-main').data('target')+' />');
// Store the sidenav state in a cookie session
Cookies.set('sidenav-state', 'pinned');
}
function unpinSidenav() {
$('.sidenav-toggler').removeClass('active');
$('.sidenav-toggler').data('action', 'sidenav-pin');
$('body').removeClass('g-sidenav-pinned').addClass('g-sidenav-hidden');
$('body').find('.backdrop').remove();
// Store the sidenav state in a cookie session
Cookies.set('sidenav-state', 'unpinned');
}
// Set sidenav state from cookie
var $sidenavState = Cookies.get('sidenav-state') ? Cookies.get('sidenav-state') : 'pinned';
if($(window).width() > 1200) {
if($sidenavState == 'pinned') {
pinSidenav()
}
if(Cookies.get('sidenav-state') == 'unpinned') {
unpinSidenav()
}
$(window).resize(function() {
if( $('body').hasClass('g-sidenav-show') && !$('body').hasClass('g-sidenav-pinned')) {
$('body').removeClass('g-sidenav-show').addClass('g-sidenav-hidden');
}
})
}
if($(window).width() < 1200){
$('body').removeClass('g-sidenav-hide').addClass('g-sidenav-hidden');
$('body').removeClass('g-sidenav-show');
$(window).resize(function() {
if( $('body').hasClass('g-sidenav-show') && !$('body').hasClass('g-sidenav-pinned')) {
$('body').removeClass('g-sidenav-show').addClass('g-sidenav-hidden');
}
})
}
$("body").on("click", "[data-action]", function(e) {
e.preventDefault();
var $this = $(this);
var action = $this.data('action');
var target = $this.data('target');
// Manage actions
switch (action) {
case 'search-show':
target = $this.data('target');
$('body').removeClass('g-navbar-search-show').addClass('g-navbar-search-showing');
setTimeout(function() {
$('body').removeClass('g-navbar-search-showing').addClass('g-navbar-search-show');
}, 150);
setTimeout(function() {
$('body').addClass('g-navbar-search-shown');
}, 300)
break;
case 'search-close':
target = $this.data('target');
$('body').removeClass('g-navbar-search-shown');
setTimeout(function() {
$('body').removeClass('g-navbar-search-show').addClass('g-navbar-search-hiding');
}, 150);
setTimeout(function() {
$('body').removeClass('g-navbar-search-hiding').addClass('g-navbar-search-hidden');
}, 300);
setTimeout(function() {
$('body').removeClass('g-navbar-search-hidden');
}, 500);
break;
}
})
// Add sidenav modifier classes on mouse events
$('.sidenav').on('mouseenter', function() {
if(! $('body').hasClass('g-sidenav-pinned')) {
$('body').removeClass('g-sidenav-hide').removeClass('g-sidenav-hidden').addClass('g-sidenav-show');
}
})
$('.sidenav').on('mouseleave', function() {
if(! $('body').hasClass('g-sidenav-pinned')) {
$('body').removeClass('g-sidenav-show').addClass('g-sidenav-hide');
setTimeout(function() {
$('body').removeClass('g-sidenav-hide').addClass('g-sidenav-hidden');
}, 300);
}
})
// Make the body full screen size if it has not enough content inside
$(window).on('load resize', function() {
if($('body').height() < 800) {
$('body').css('min-height', '100vh');
$('#footer-main').addClass('footer-auto-bottom')
}
})
})();
Working JS Fiddle: https://jsfiddle.net/Vaulient/kthw39gs/6/

How do I detect whether a scrollbar exists?

I need some code which continuously detects for a scrollbar. If a scrollbar is found, add an additional 50px to an iframe's height, then re-run the code again so eventually there will be no scrollbar. My current code, however, doesn't work. How would I do this in HTML/CSS/JS?
Page Code:
<script>
iframeheight()
function iframeheight() {
alert('running');
var vs = window.innerWidth > document.documentElement.clientWidth;
if vs > 0 {
document.getElementById('maincode').style.height = currentheight + "50px";
}
else {}
setTimeout(iframeheight, 1);
}
</script>
<iframe id="maincode" class="maincode" src="index2.html" frameBorder="0" border="0"
onclick="iframeheight()"></iframe>
This is my solution
The key is to use clientHeight and scrollHeight on the document.documentElement
If the page has a scrollbar, the scrollHeight will be bigger than the clientHeight
Also I did not use setTimeout or setInterval (polling) but created an eventListener for the resize event and a MutationObserver on the full document. This may or may not suit your needs. Be aware that MutationObserver may have a significant impact on performance, depending on the situation. See the link in my code for further information about that.
"use strict";
{
const hasScrollbar = (el) => {
return el.scrollHeight > el.clientHeight
}
const checkScrollbar = () => {
scrollbar = hasScrollbar(document.documentElement)
viewUpdate()
}
let scrollbar;
const viewUpdate = () => {
if (scrollbar) {
document.documentElement.style.backgroundColor = 'gold';
} else {
document.documentElement.style.backgroundColor = 'silver';
}
}
window.addEventListener('resize', checkScrollbar)
// be aware that MutationObserver may have performance issues
// See https://stackoverflow.com/a/39332340/476951
const observer = new MutationObserver(checkScrollbar);
const config = { attributes: true, childList: true, subtree: true };
observer.observe(document.documentElement, config);
// For demonstration purpose
const addContent = () => {
document.body.appendChild(document.createElement('p'));
}
document.getElementById('btn-add').addEventListener('click', addContent)
const removeContent = () => {
document.body.removeChild(document.body.querySelector('p'));
}
document.getElementById('btn-remove').addEventListener('click', removeContent)
}
p {
height: 100px;
background-color: red;
}
p:nth-child(2n) {
background-color: green;
}
<button id="btn-add">Add</button>
<button id="btn-remove">Remove</button>

How to scroll to top in antd steps. react

I am using Steps component of antd.
I want whenever a user goes to the next step or to the previous step the page should scroll to top.
I use window.scrollTo(0, 0) and window.top=0; but it does not work.
can anyone help how can I scroll to top.
previousStep = () => {
window.scrollTo(0, 0);
window.scrollTop = 0;
const { currentStep } = this.state;
this.setState({ currentStep: currentStep - 1 });
};
onstructor(props) {
super(props)
this.myRef = React.createRef() // Create a ref object
}
componentDidMount() {
this.myRef.current.scrollTo(0, 0);
}
render() {
return <div ref={this.myRef}></div>
} // attach the ref property to a dom element

How to render a self-destructing paragraph at mouse position in React?

I am trying to get a paragraph to appear at the location of the mouse coordinates, but self-destruct after 1 second.
$(function(){
var fadeDelay = 1000;
var fadeDuration = 1000;
$(document).click(function(e){
var div = $('<div class="image-wrapper">')
.css({
"left": e.pageX + 'px',
"top": e.pageY + 'px'
})
.append($('<img src="" alt="myimage" />'))
.appendTo(document.body);
setTimeout(function() {
div.addClass('fade-out');
setTimeout(function() { div.remove(); }, fadeDuration);
}, fadeDelay);
});
});
The code above is from a fiddle which represents the effect that I am looking for; however, it uses jQuery - while I am working with React.
I tried following this linear process:
1 - In the state, toggle a boolean with mouse clicks
playerAttack = () => {
this.setState({ hasPlayerAttacked: true })
}
2 - In a function, when the boolean is true, return a paragraph and set the boolean back to false
renderDamageDealtParagraph = () => {
if (this.state.hasPlayerAttacked) {
return <p>{this.state.playerAttack}</p>;
this.setState({ hasPlayerAttacked: false });
}
};
However, with this approach there were too many fallacies; main one being that upon resetting the boolean back to false, the rendered paragraph immediately disappears (instead of after a timeout of 1000ms).
What is the best wait to implement something like the linked fiddle, in ReactJS using vanilla JS?
Thanks in advance to whoever might be able to help.
You can basically do something like this:
Have state to track the mouse position x and y, and two booleans isShown and shouldHide to coordinate the disappering div
On click, show the div by setting isShown to true and immediately setTimeout to start hiding it in the future by adding a class by flipping the shouldHide to true
Once the class is added, the element will fade and will trigger the transitionend event at which point you can remove the div entirely by flipping the isShown to false and shouldHide to false boolean
Sample Implementation (Sorry for the shitty code, been a while since I React-ed)
JS Fiddle
class SelfDestructDemo extends React.Component {
constructor(props) {
super(props);
this.state = {
x: 0,
y: 0,
isShown: false,
shouldHide: false
};
this.handleClick = this.handleClick.bind(this);
this.reset = this.reset.bind(this);
}
reset() {
this.setState({
x: 0,
y: 0,
isShown: false,
shouldHide: false
});
}
handleClick(event) {
if (this.state.isShown) {
return;
}
const { clientX, clientY } = event;
this.setState({
x: clientX,
y: clientY,
isShown: true,
shouldHide: false
});
setTimeout(() => {
this.setState({ shouldHide: true });
}, 1000);
}
render() {
const p = this.state.isShown && (
<div
onTransitionEnd={this.reset}
className={`${this.state.shouldHide ? "should-hide" : ""} modal`}
style={{ top: this.state.y, left: this.state.x }}
>
Self destructing....
</div>
);
return (
<div className="container" onClick={this.handleClick}>
{p}
</div>
);
}
}
ReactDOM.render(<SelfDestructDemo />, document.querySelector("#app"));

Categories

Resources