<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
Related
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;
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.
So i have a directive it will run a function after it reached a certain threshold from the top of the viewport but i cannot seem to remove the event listener :/.
Here is the directive:
const scrollHandler = (el, binding) => {
const yOffset = el.getBoundingClientRect().y;
const isScrolled = document.body.scrollTop > (yOffset - 20) || document.documentElement.scrollTop > (yOffset - 20);
binding.value(isScrolled, el);
};
const GlobalDirectives = {
install(Vue) {
Vue.directive('scroll-watch', {
inserted(el, binding) {
window.addEventListener('scroll', () => scrollHandler(el, binding));
},
unbind(el, binding) {
window.removeEventListener('scroll', () => scrollHandler(el, binding), true);
console.log('is unbind');
},
});
},
};
and in the component:
<template>
<section v-scroll-watch="doSomething">
// html
</section>
</template>
<script>
export default {
methods: {
doSomething(val, el) {
console.log(val, el);
},
},
};
</script>
even when i navigate to another route somehow i can see that unbind is working but it still calls the event listener. can anyone tell me what im doing wrong?
[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.
I'm working in Vue.js and would like to conditionally display a back to top button when the user scrolls past a certain point. What am I doing wrong (no JQuery)?
In my template:
<div class="scroll">
<span class="scroll_button">Top</span>
</div>
In my mounted() function
const toTop = document.getElementsByClassName('scroll').addEventListener('scroll', function() {
if (window.scrollY > 0) {
this.classList.add('shown')
}
});
toTop();
data () {
return {
scrolled: false
};
},
methods: {
handleScroll () {
this.scrolled = window.scrollY > 0;
}
},
created () {
window.addEventListener('scroll', this.handleScroll);
}