Sticky header with hide and show on scroll - javascript

I have a navbar with two part (navbarTop and navbarBottom); navbarTop will shown when the scrollY is bigger than 110 and the navbarbottom will shown when the user scroll up, but my problem is that each component will be re-called after each scroll, even scrolling down, or when the navbottom already exist and the user scroll up and it's still keep calling the component.
here is the code:
const Navbar: FC = () => {
const [stickyHeader, setStickyHeader] = useState(false);
const [stickyMenu, setStickyMenu] = useState(false);
console.log('navbar')
const [y, setY] = useState(null);
const handleNavigation = useCallback(
(e) => {
const window = e.currentTarget;
if (!stickyMenu) {
if (y > window.scrollY) {
setStickyMenu(true)
// console.log("scrolling up");
}
}
if (!stickyHeader) {
if (window.scrollY >= 110) {
setStickyHeader(true);
}
}
if (stickyHeader) {
if (window.scrollY <= 110) {
setStickyHeader(false);
}
}
if (stickyMenu) {
if (y < window.scrollY) {
setStickyMenu(false)
// console.log("scrolling down");
}
}
setY(window.scrollY);
},
[y, stickyMenu,stickyHeader]
);
useEffect(() => {
window.addEventListener("scroll", handleNavigation);
return () => {
window.removeEventListener("scroll", handleNavigation);
};
}, [handleNavigation, stickyMenu]);
return (
<>
<div className={s.root}>
{stickyHeader === false ? (
<div className='hidden xl:block md:w-auto'>
<NavbarTop sticky={false}/>
<NavbarBottom
sticky={false}/>
</div>
) : (
<div className='absolute hidden xl:block md:w-auto'>
<NavbarTop sticky={true}/>
{stickyMenu &&
<NavbarBottom sticky={true}/>
}
</div>
)}
<SmallNav/>
</div>
</>
)
}
export default Navbar

In fact, I did not quite understand your question. You said that each component is re-called after each scroll. Of course it will be re-called, right?
Whenever you scroll, you change the stickyHeader or stickyMenu. React listens for the state change and re-renders the component. This is how React works.
I see that you're using tailwindcss.
You can use a useMemo to listen to the state and then return the appropriate class based on the different state states. This way you avoid having to create DOM nodes over and over again. I don't know if this would help you or not. I hope this will help you.
const Navbar: FC = () => {
const [stickyHeader, setStickyHeader] = useState(false);
const [stickyMenu, setStickyMenu] = useState(false);
// console.log('navbar');
const [y, setY] = useState(null);
const handleNavigation = useCallback(
(e) => {
const window = e.currentTarget;
if (!stickyMenu) {
if (y > window.scrollY) {
setStickyMenu(true);
// console.log("scrolling up");
}
}
if (!stickyHeader) {
if (window.scrollY >= 110) {
setStickyHeader(true);
}
}
if (stickyHeader) {
if (window.scrollY <= 110) {
setStickyHeader(false);
}
}
if (stickyMenu) {
if (y < window.scrollY) {
setStickyMenu(false);
// console.log("scrolling down");
}
}
setY(window.scrollY);
},
[y, stickyMenu, stickyHeader],
);
useEffect(() => {
window.addEventListener('scroll', handleNavigation);
return () => {
window.removeEventListener('scroll', handleNavigation);
};
}, [handleNavigation, stickyMenu]);
const stickyClassName = useMemo(() => {
if (stickyHeader) {
return 'sticky top-0 xl:block md:w-auto';
}
return 'xl:block md:w-auto';
}, [stickyHeader]);
return (
<div className="test" style={{ height: 2000 }}>
<div className={stickyClassName}>
<NavbarTop sticky={stickyHeader} />
{stickyMenu && <NavbarBottom sticky={stickyHeader} />}
</div>
<div>SmallNav</div>
</div>
);
};
export default Navbar;

Related

How to deact scroll and show component when user scrolled to top in react native

When the user scroll to the top and then scrolls event returns a higher number like - 10 or 20 or 2000 or 300 based on the content and when the user scrolled to the button then it will return 0
const [scrolledRecord, setScrolledRecord] = useState(false);
const scrolledEvent = scroll => {
if (scroll?.contentOffset?.y > 0) {
setScrolledRecord(true);
} else {
setScrolledRecord(false);
}
};
return {scrolledRecord ? <ChatHeader item={userData} /> : null}
In the about I have implemented the logic I think i am not correct
const useOnTop = () => {
const [onTop, setOnTop] = useState(false);
useEffect(() => {
if (window.scrollY === 0) {
setOnTop(true);
} else {
setOnTop(false);
}
}, [window.scrollY]);
return onTop;
}
and use it as follows
const MyComponent = (props) => {
const isOnTop = useOnTop();
<>
isOnTop && <MyOtherComponent />
</>
}

my countdown timer is not working fine everytime in react

This is a game that I am building and in this game I have created some levels. But when the game ends for the first time the second time my countdown timer is not getting decremented.
this is my App.js component:
import "./App.css";
import React, { useState, useEffect, useCallback } from "react";
import SingleCard from "./components/singleCard/SingleCard";
import Timer from "./components/timer/Timer";
import Modal from "./components/modal/Modal";
import soundOn from "./images/soundon.png";
import soundOff from "./images/soundoff.png";
import { Helmet } from "react-helmet";
const cardImages = [
{ src: "/img/img-1.png", matched: false },
{ src: "/img/img-2.png", matched: false },
{ src: "/img/img-3.png", matched: false },
{ src: "/img/img-4.png", matched: false },
{ src: "/img/img-5.png", matched: false },
{ src: "/img/img-6.png", matched: false },
];
function App({ background, wrongAns, correctAns, deadSound, winSound }) {
const [cards, setCards] = useState([]);
const [turns, setTurns] = useState(0);
const [choiceOne, setChoiceOne] = useState(null);
const [choiceTwo, setChoiceTwo] = useState(null);
const [disabled, setDisabled] = useState(false);
const [isgameEnd, setGameEnd] = useState(false);
const [timerStart, setTimerStart] = useState(false);
const [playSound, setPlaySound] = useState(false);
const [count, setCount] = useState(0);
const [IsPlaying, setIsPlaying] = useState(true);
const [isModalOpen, setModalIsOpen] = useState(false);
const [restartTimer, setRestartTimer] = useState(false);
const [isMute, setMute] = useState(false);
const [loading, setLoading] = useState(true);
function handleMute(state = false) {
background.muted = state;
wrongAns.muted = state;
correctAns.muted = state;
deadSound.muted = state;
winSound.muted = state;
setMute(state);
}
let timer;
// function that will decide the condition for opening the modal
const toggleModal = () => {
setModalIsOpen(true);
};
// function that will execute when we click a button in the modal
const handlePlaySound = () => {
setPlaySound(false);
};
// function that will execute when game is set to background in android
function AudioBgOnPause() {
if (playSound === true) {
background.pause();
setIsPlaying(false);
}
}
// functiona that will execute when game is again resumed
function AudioBgOnResume() {
if (IsPlaying === false) {
setIsPlaying(true);
}
}
// creating there global reference so that we can call these functions in the index file
window.AudioBgOnPause = AudioBgOnPause;
window.AudioBgOnResume = AudioBgOnResume;
// check if playSound is off or on
if (playSound === false) {
background.pause();
} else if (playSound === true && IsPlaying === true) {
background.play();
}
// Play Again
const playAgain = () => {
// setCards([]);
shuffleCards();
setTurns(0);
setChoiceOne(null);
setChoiceTwo(null);
setDisabled(false);
setGameEnd(false);
setTimerStart(false);
setPlaySound(false);
setCount(0);
setIsPlaying(true);
setModalIsOpen(false);
setRestartTimer(true);
setMute(false);
};
const restartGame = () => {
playAgain();
};
// check if isGameEnd is true i.e. the game is ended
// losing condition
useEffect(() => {
if (turns < 6 && isgameEnd === true) {
setDisabled(true);
setTimerStart(false);
clearInterval(timer);
if (playSound === true) {
deadSound.play();
}
setPlaySound(false);
setTimeout(() => {
toggleModal();
}, 2000);
}
}, [turns, isgameEnd]);
// winning situation
useEffect(() => {
if (
(turns === 6 && isgameEnd === false) ||
(turns === 6 && isgameEnd === true)
) {
// clearInterval(timer);
// setDisabled(true);
setRestartTimer(true);
setTimerStart(false);
if (playSound === true) {
winSound.play();
}
setPlaySound(playSound);
shuffleCards();
// setTimeout(() => {
// toggleModal();
// }, 2000);
}
}, [turns, isgameEnd]);
// shuffle Cards
const shuffleCards = () => {
const shuffleCards = [...cardImages, ...cardImages]
.sort(() => Math.random() - 0.5)
.map((card) => ({ ...card, id: Math.random() }));
setCards(shuffleCards);
setTurns(0);
};
// console.log("cards array", cards);
// handle a choice
const handleChoice = (card) => {
setTimerStart(true);
// background.play();
background.loop = true;
// checking if the counter is one only then set sound to true when the card is flipped for first time
count === 1 ? setPlaySound(true) : setPlaySound(playSound);
// after that increment the counter so that the upper condition should not hold anymore
setCount(count + 1);
choiceOne ? setChoiceTwo(card) : setChoiceOne(card);
};
// compare 2 selected cards
useEffect(() => {
if (choiceOne && choiceTwo) {
setDisabled(true);
if (choiceOne.src === choiceTwo.src) {
setCards((prevCards) => {
return prevCards.map((card) => {
if (card.src === choiceOne.src) {
return { ...card, matched: true };
} else {
return card;
}
});
});
if (playSound === true) {
correctAns.play();
}
setTurns((prevTurns) => prevTurns + 1);
resetTurn();
} else {
if (playSound === true) {
wrongAns.play();
}
setTimeout(() => resetTurn(), 500);
}
}
}, [choiceOne, choiceTwo]);
// start a new game automatically
// set counter to one when the component first mounts so that sound starts to play on first click only
useEffect(() => {
shuffleCards();
setCount(count + 1);
}, []);
// reset choices
const resetTurn = () => {
setChoiceOne(null);
setChoiceTwo(null);
setDisabled(false);
};
// console.log("restart App", restartTimer);
// timer callback
const onGameEnd = useCallback(() => {
setGameEnd(!isgameEnd);
}, [isgameEnd]);
useEffect(() => {
setTimeout(() => {
setLoading(false);
}, 4000);
}, []);
return (
<>
<Helmet>
<meta charSet="utf-8" />
<title>Match Maker</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
</Helmet>
<div className="App">
{loading && (
<div className="loader-container">
<div className="spinner"></div>
</div>
)}
<>
{/* <img
className="logo"
src="https://cheetay.pk/static/images/newLandingPage/logo.svg"
alt="card back"
/> */}
<div
style={
loading ? { visibility: "hidden" } : { visibility: "inherit" }
}
>
<div className="soundBtn">
{!isMute === true ? (
<div
className="soundIcon"
style={{
cursor: "pointer",
khtmlUserSelect: "none",
MozUserSelect: "none",
OUserSelect: "none",
userSelect: "none",
}}
onClick={() => handleMute(!isMute)}
>
<img src={soundOn} alt="soundOff" />
</div>
) : (
<div
className="soundIcon"
style={{
cursor: "pointer",
khtmlUserSelect: "none",
MozUserSelect: "none",
OUserSelect: "none",
userSelect: "none",
}}
onClick={() => handleMute(!isMute)}
>
<img src={soundOff} alt="soundOff" />
</div>
)}
</div>
<div className="card-grid">
{cards.map((card) => (
<SingleCard
key={card.id}
card={card}
handleChoice={handleChoice}
flipped={
card === choiceOne || card === choiceTwo || card.matched
}
disabled={disabled}
isModalOpen={isModalOpen}
/>
))}
</div>
<div className="TimerAndTurnsInfo">
<Timer
timerStart={timerStart}
timer={timer}
onGameEnd={onGameEnd}
restartTimer={restartTimer}
/>
<p>matched {turns}</p>
</div>
</div>
</>
</div>
{isModalOpen && (
<Modal handlePlaySound={handlePlaySound} restartGame={restartGame} />
)}
</>
);
}
export default App;
and this is my timer component:
import React, { useEffect, useState } from "react";
import "./Timer.css";
const Child = ({ timerStart, timer, onGameEnd, restartTimer }) => {
const [seconds, setSeconds] = useState(40);
// let time = 40;
useEffect(() => {
if (restartTimer === true) {
setSeconds(40);
}
}, [seconds, restartTimer]);
// console.log("restart Timer", restartTimer);
useEffect(() => {
if (timerStart === true) {
timer = setInterval(() => {
if (seconds > 0) {
setSeconds(seconds - 1);
}
if (seconds === 0) {
onGameEnd(true);
clearInterval(timer);
}
}, 1000);
}
return () => clearInterval(timer);
});
return (
<p className="time">
Time{" "}
<span className="span1">
<span>{seconds}s</span>
</span>
</p>
);
};
const Timer = React.memo(({ timerStart, timer, onGameEnd, restartTimer }) => (
<Child
timerStart={timerStart}
timer={timer}
onGameEnd={onGameEnd}
restartTimer={restartTimer}
/>
));
export default Timer;
my timer gets re initialized when restartTimer state is set to true.
I think your problem is here
useEffect(() => {
if (restartTimer === true) {
setSeconds(40);
}
}, [seconds, restartTimer]);
whenever the seconds reduce and restartTimer === true (can't find where you set it to false after useState) you reset it to 40.
try removing seconds from the dependency

Why the button stays always hidden?

<div class="app__land-bottom" v-if="isVisible">
<a href="#projects">
<img ref="arrowRef" id="arrow" src="./../assets/down.png" alt srcset />
</a>
</div>
In Vue3 setup isn't working, but on Vue2 is working the following solution for not displaying a button based on scrolling.
VUE3
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
let isVisible = ref(false);
onMounted(() => {
window.addEventListener("scroll", () => {
hideArrow();
});
});
onUnmounted(() => {
window.removeEventListener("scroll", () => {
hideArrow();
});
});
const hideArrow = () => {
const currentScroll = window.pageYOffset;
if (currentScroll > 100) {
isVisible = false;
}
else if (currentScroll < 100) {
isVisible = true;
}
}
</script>
VUE2
<script>
export default {
created() {
window.addEventListener('scroll', this.hideArrow)
},
data() {
return {
isVisible: false,
}
},
methods: {
hideArrow() {
const currentScroll = window.pageYOffset;
if (currentScroll > 100) {
this.isVisible = false;
} else if (currentScroll < 100) {
this.isVisible = true;
}
},
},
}
</script>
In vue2 the solution is working but on Vue3, not. Any suggestions?
I don't understand exactly where the problem is? It would be helpful an answer.
when mutating your ref: isVisible you need to type: isVisible.value instead of isVisible
your hideArrow function should be:
const hideArrow = () => {
const currentScroll = window.pageYOffset;
if (currentScroll > 100) {
isVisible.value = false;
}
else if (currentScroll < 100) {
isVisible.value = true;
}
}
for more details: check Ref docs in Vue

selected element move up and down, but scroll behavior is not working with

I make a list of div tags for listing filename.
and After selecting a div, then I can change focus up and down using Arrow keys
Because I have a long file list, I add overflow: scroll to the container
but Scroll does not move along with my focus(so active div disappear from the viewport),
How can I make scroll behavior move down along with active div?
I create an example in codesandbox
import "./styles.css";
import { useEffect, useState } from "react";
export default function App() {
const [selectedItem, setSelectedItem] = useState(0);
useEffect(() => {
const keyPress = (e) => {
if (e.key === "ArrowLeft") {
setSelectedItem((prev) => Number(prev) - 1);
}
if (e.key === "ArrowRight") {
setSelectedItem((prev) => Number(prev) + 1);
}
};
window.addEventListener("keydown", keyPress);
return () => {
window.removeEventListener("keydown", keyPress);
};
}, [selectedItem]);
const onClickDiv = (e) => {
setSelectedItem(e.target.id);
};
const renderList = () => {
let items = [];
console.log(selectedItem);
for (let i = 0; i < 60; i++) {
items.push(
<div
key={i}
className={`item ${Number(selectedItem) === i ? "active" : ""}`}
id={i}
onClick={onClickDiv}
>
Item{i}.png
</div>
);
}
return items;
};
return (
<div className="App">
<div className="list-container">{renderList()}</div>
</div>
);
}
.list-container {
height: 300px;
overflow: scroll;
}
.active {
background-color: orangered;
}
------------------ EDIT -----------------------
I finally complete this example, I sincerely thank you guys for answering my question.
Here is code sandbox final code
Here's my take on it.
I am using refs as well along with scrollIntoView.This way we don't have to scroll by a fixed amount and also we only are scrolling when we are at the end of the viewport.
Here's the demo
I am storing refs of each element.
ref={(ref) => {
elementRefs.current = { ...elementRefs.current, [i]: ref };
}}
And then we will use scrollIntoView when focus changes.
const prevItem = elementRefs.current[selectedItem - 1];
prevItem && prevItem.scrollIntoView({ block: "end" });
Notice the {block:"end"} argument here. It makes sure we only scroll if the element is not in the viewport.
You can learn more about scrollIntoView here.
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
import { useEffect, useState, useRef } from "react";
export default function App() {
const [selectedItem, setSelectedItem] = useState(0);
const elementRefs = useRef({});
useEffect(() => {
const keyPress = (e) => {
if (e.key === "ArrowLeft") {
setSelectedItem((prev) => Number(prev) - 1);
const prevItem = elementRefs.current[selectedItem - 1];
prevItem && prevItem.scrollIntoView({ block: "end" });
}
if (e.key === "ArrowRight") {
console.log(elementRefs.current[selectedItem]);
// if (selectedItem < elementRefs.current.length)
const nextItem = elementRefs.current[selectedItem + 1];
nextItem && nextItem.scrollIntoView({ block: "end" });
setSelectedItem((prev) => Number(prev) + 1);
}
};
window.addEventListener("keydown", keyPress);
return () => {
window.removeEventListener("keydown", keyPress);
};
}, [selectedItem]);
const onClickDiv = (e) => {
setSelectedItem(e.target.id);
};
const renderList = () => {
let items = [];
console.log(selectedItem);
for (let i = 0; i < 60; i++) {
items.push(
<div
key={i}
className={`item ${Number(selectedItem) === i ? "active" : ""}`}
id={i}
onClick={onClickDiv}
ref={(ref) => {
elementRefs.current = { ...elementRefs.current, [i]: ref };
}}
>
Item{i}.png
</div>
);
}
return items;
};
return (
<div className="App">
<div className="list-container">{renderList()}</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("container"));
One way to approach this problem is to use ref in React.
First define a ref using
const scrollRef = useRef(null);
Then, assign it to your scrolling element like this,
<div className="list-container" ref={scrollRef}>
What this does is, gives you a reference to the html element inside your code.
ref.current is the HTML Div element now.
Now, you can use scrollBy method on HTML element to scroll up or down.
Like this,
useEffect(() => {
const keyPress = (e) => {
if (e.key === "ArrowLeft") {
setSelectedItem((prev) => Number(prev) - 1);
scrollRef.current.scrollBy(0, -18); // <-- Scrolls the div 18px to the top
}
if (e.key === "ArrowRight") {
setSelectedItem((prev) => Number(prev) + 1);
scrollRef.current.scrollBy(0, 18); // <-- Scrolls the div 18px to the bottom
}
};
window.addEventListener("keydown", keyPress);
return () => {
window.removeEventListener("keydown", keyPress);
};
}, [selectedItem]);
I have given 18 because I know the height of my list item.
I have updated your Sandbox. Check it out.

how to detect scrollPosition with fireEvent.scroll? - React-testing-library

[For question purpose, I make dummy example to make it easier to understand]
Let's say I have this homepage component that will toggle color when reach 1/3 of page height.
// Homepage.js
const Home = (props) => {
const [toggle, setToggle] = useState(false)
useEffect(() => {
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [toggle])
const handleScroll = () => {
const scrollPosition = document.documentElement.scrollTop;
const oneThirdPageHeight = (1 / 3) * document.documentElement.offsetHeight;
if (scrollPosition > onethirdPageHeight) {
return setToggle(true)
}
return setToggle(false)
}
return <div style={{ height: '1500px', color: toggle ? 'blue' : 'red'}}>hello</div>
}
and for the test file
// Homepage.test.js
test('should toggle color when scrolled', () => {
const { getByText } = render(<Homepage />);
const DivEl = getByText('hello');
expect(DivEl).toHaveStyle('color: red');
fireEvent.scroll(window, { target: { scrollY: 800 } });
expect(DivEl).toHaveStyle('color: blue'); // --> failing
});
It seems like I can't make the DOM scrolled by doing fireEvent.scroll(window, { target: { scrollY: 800 } }). Where did I miss? Please help, thank you in advance.

Categories

Resources