I am building a slider component in React JS, and it's working, however I am facing a bug which I believe to be a conflict between onMouseDown and onMouseLeave events.
I have the div range-container, which receives the events, inside it I have another div and within this last one I have two spans, they are the thumbs of my slider.
This is what is happening:
As seen in this gif the thumbs don't respect the limits of the line as they should. The data on the left are the variable move, responsable to determine whether the X can be changed or not and the position of the mouse.
This is how it should work:
onMouseDown sets move to true and allows the thumb to move;
onMouseUp sets move as false and blocks the movements;
onMouseMove changes the value of value and makes the thumb moves;
onMouseLeave sets move to false and also blocks the movements.
I realize that onMouseLeave is only triggered when the cursor leaves the element and its children, because of that I can't just leave the div, I need to leave the thumb as well, but I don't know how to limit it by the limits of the line.
Here is my component:
import React, { Fragment } from 'react'
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome";
import './Filter.css'
const Filter = props => {
let [value, setValue] = React.useState(190)
let [move, setMove] = React.useState(false)
const handleChange = e => {
var x = e.clientX;
if (move === true) {
setValue(x)
}
};
const moveOn = e => {
var x = e.clientX;
setValue(x)
setMove(true)
}
const moveOff = () => {
setMove(false)
}
let moveText = 'false'
move === true ? moveText = 'true' : moveText = 'false'
return (
<Fragment>
<div>{moveText}</div>
<div>{value}</div>
<div className="filter-container d-flex justify-content-between">
<div className="range-container"
onMouseMove={(e) => handleChange(e)}
onMouseDown={(e) => moveOn(e)}
onMouseUp={() => moveOff()}
onMouseLeave={() => moveOff()}
>
<div className="range"
>
<span className="rounded-circle"
style={{
width:'15px',
height: '15px',
backgroundColor: 'red',
marginTop: '-6px',
left: value - 7 + 'px'
}}
></span>
<span className="rounded-circle"
style={{
width:'10px',
height: '10px',
backgroundColor: 'black',
marginTop: '-4px',
marginLeft: '195px'
}}
></span>
</div>
</div>
</div>
</Fragment>
)
}
export default Filter
CSS:
.range-container {
width: 200px;
height: 15px;
cursor: pointer;
}
.range {
width: 100%;
height: 2px;
background-color: black;
margin-bottom: 50px;
}
.range span {
width: 10px;
height: 10px;
position: absolute;
}
How can I solve this?
It seems like I have more success when using mouseout instead of mouseleave. The mouseleave function never get called then the click is held. You can check this code here : https://codesandbox.io/s/inspiring-edison-63s0h?file=/src/App.js
I couldn't rely on the events to reach what I aimed, so I had to change a little the perspective.
What I did was to store the initial position of the thumb in a variable and only change value if it is higher than the initial value, otherwise value receives the value of off_x.
const off_x = 190 // initial position
let [value, setValue] = React.useState(off_x)
let [move, setMove] = React.useState(false)
const handleChange = e => {
var x = e.clientX;
if (move === true) {
value > off_x ? setValue(x) : setValue(off_x)
}
}
Now it works properly.
Related
I want to change the source of image onscroll in reactjs. Like if scrollY is greater than 100 change the image source and if it is greater than 200 change it another source.
i tried to do it but could not. any ideas?
import React, { useEffect, useState, useRef } from 'react';
import './Video.css';
import { useInView } from 'react-intersection-observer';
function Video() {
const videoSrc1 = "https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/6341303c29c5340961dc9ae6_Mco-1-transcode.mp4";
const videoSrc2 = "https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/63413ff244f1dc616b7148a0_Mco-transcode.mp4";
const videoSrc3 = "https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/63455a67996ba248148c4e31_add-options%20(3)-transcode.mp4";
const img1 = 'https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/63455a67996ba248148c4e31_add-options%20(3)-poster-00001.jpg';
const img2 = 'https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/63413ff244f1dc616b7148a0_Mco-poster-00001.jpg';
const img3 = 'https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/63455a67996ba248148c4e31_add-options%20(3)-poster-00001.jpg';
const [scrollPosition, setScrollPosition] = useState(0);
const handleScroll = () => {
const position = window.pageYOffset;
setScrollPosition(position);
};
useEffect(() => {
window.addEventListener('scroll', handleScroll, { passive: true })
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
{
if (scrollPosition>=316){
// this.src={videoSrc2}
}
}
console.log("position;", scrollPosition)
return (
<div className='container'>
<video loop autoPlay muted className='video'>
<source src={videoSrc1} type="video/webm" />
</video>
</div>
)
}
export default Video
You can do a combination of Vanilla JS methods and React methods to achieve this.
Your best best bet is to use the useEffect hook and add an event listener to the Window DOM object based on where on the page the scroll is position.
First you need a function that executes every time the DOM re-renders (scrolling does this)
start by using the useEffect hook
useEffect(() => {}, [])
Next you want a function that executes specifically when you scroll the page
you can add an event handler to the window DOM element
window.addEventListener('scroll',() => {})
Then you want to track the where you are on the page (how far up or how far down)
You can use the window's scrollTop property to return how far up or down you are on the page relative to the top of the page
document.documentElement.scrollTop
Now comes the logic part, you said you want to change the image's src based on how far up or down you've scrolled on the page
This is where, useState, boolean flags and the ternary operator come into play
You can write a useState hook to store the Y position of the scroll, and the useEffect and scroll event listener will keep updating it to the current position
const [scrollPosition, getScrollPositon] = useState(document.documentElement.scrollTop)
finally nest the hook function into the window 'scroll' function and nest that in the useEffect hook
const [scrollPosition, getScrollPositon] = useState(document.documentElement.scrollTop)
useEffect(() => {
window.addEventListener('scroll',() => {
getScrollPositon(document.documentElement.scrollTop);
})
}, [])
AND finally write the logic in your .jsx code to say 'when we are x number of pixel below the top of the screen...change the image source'
const App = () => {
return (
<div className='app'>
<img src={scrollPosition < 1000 ? 'http://imagelinkA.com' : 'http://imagelinkB.com'}>
</div>
);
}
Now you put it all together...
// App.js/jsx
import { useState, useEffect } from 'react';
const App = () => {
// initial scroll positon on page load
const [scrollPosition, getScrollPositon] = useState(document.documentElement.scrollTop)
// hook and event handlers to keep track of and update scroll
useEffect(() => {
window.addEventListener('scroll',() => {
getScrollPositon(document.documentElement.scrollTop);
})
}, [])
// your .jsx code with appropriate boolean flags and use of the ternary operator
return (
<div className='app'>
<img src={scrollPosition < 1000 ? 'http://imagelinkA.com' : 'http://imagelinkB.com'}>
</div>
);
}
Hope I was able to help!
Setting the scroll position will trigger needless rerenders, instead you only want to trigger a rerender when the data source will change.
To select the proper data source, putting the list of data sources in a list is a good way to do this. Then you can properly determine the index of data source to show with something like this:
// Y_OFFSET_DIFFERENCE is the value that determines when the next image should be shown.
const index =
Math.floor(position / Y_OFFSET_DIFFERENCE) % dataSources.length;
You can see how this is properly calculated:
If position = 0 and Y_OFFSET_DIFFERENCE = 100 then 0/100 = 0 and 0 % 2 is 0. 0 is the index of the first element of your list.
If position = 100 and Y_OFFSET_DIFFERENCE = 100 then 100/100 = 1 and 1 % 2 is 1. 1 is the index of the second element in your list.
If position = 150 and Y_OFFSET_DIFFERENCE = 100 then 150/100 = 1.5 and Math.floor(1.5) = 1 and 1 % 2 is 1. 1 is the index of the second element in your list.
If position = 200 and Y_OFFSET_DIFFERENCE = 100 then 200/100 = 2 and 2 % 2 is 0. 0 is the index of the first element in your list.
And it'll continue like this forever.
Here is the full code.
import { useState, useEffect } from "react";
const dataSources = [
"https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/63455a67996ba248148c4e31_add-options%20(3)-poster-00001.jpg",
"https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/63413ff244f1dc616b7148a0_Mco-poster-00001.jpg"
];
const DEFAULT_DATA_SOURCE = dataSources[0];
const Y_OFFSET_DIFFERENCE = 100;
export default function App() {
const [dataSource, setDataSource] = useState(DEFAULT_DATA_SOURCE);
useEffect(() => {
const handleScroll = () => {
const position = window.pageYOffset;
const index =
Math.floor(position / Y_OFFSET_DIFFERENCE) % dataSources.length;
const selectedSource = dataSources[index];
if (selectedSource === dataSource) {
return;
}
setDataSource(selectedSource);
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [dataSource]);
return (
<div
style={{ height: 2000, backgroundImage: "linear-gradient(blue, green)" }}
>
<div
style={{
position: "sticky",
top: 10,
left: 10,
display: "flex",
justifyContent: "center",
flexDirection: "column",
alignItems: "center"
}}
>
<p style={{ color: "white", textAlign: "center" }}>{dataSource}</p>
<img
src={dataSource}
alt="currently selected source"
width={100}
height={100}
/>
</div>
</div>
);
}
codesandbox demo
I am trying to create an effect similar to the one found here: https://gruev.space/about. When you hover over certain words, an image appears and follows your curser. My implementation will be a bit different though, as i don't need to see the characters under the image when it appears. I'm simply attempting to make an image pop up and follow my cursor when I hover over certain words in a paragraph. I'm trying to create a react component that can hold a paragraph with certain highlighted words that are "hoverable".
Below is what I have tried so far. I found the "pointer-events: none" property to be critically important so that the onMouseEnter and onMouseLeave events don't get triggered over and over when the image showes up and then subsiquently removes itself.
What I need help with is figuring out how to embed these "hoverable" words into a paragraph.
import React from "react";
import "../../index.css";
import codingJoke from "../../images/codingJoke.png";
import "./raisedTextContainer.css";
export default class RaisedTextContainer extends React.Component {
constructor(props) {
super(props);
this.state = { mouseOver: false };
this.showImageWhenHover = this.showImageWhenHover.bind(this);
this._onMouseMove = this._onMouseMove.bind(this);
}
componentDidMount() {
this.imageUpdateInterval = setInterval(this.showImageWhenHover, 10);
}
componentWillUnmount() {
clearInterval(this.imageUpdateInterval);
}
showImageWhenHover(e) {
console.log(this.state);
if (this.state.mouseOver) {
let x = this.state.mouseX;
let y = this.state.mouseY;
const image = document.querySelector(".hoverimage");
image.style.left = x - 300 + "px";
image.style.top = y - 150 + image.style.height / 2 + "px";
console.log(image.style.left, image.style.top);
//this.forceUpdate();
}
}
_onMouseMove(e) {
this.setState({ mouseX: e.clientX, mouseY: e.clientY });
}
render() {
return (
<div className="grid-item rounded raised">
<div
style={{ "z-index": 2 }}
className="photo-hover"
onMouseMove={this._onMouseMove}
onMouseEnter={() => {
this.setState({ mouseOver: true });
const image = document.querySelector(".hoverimage");
image.style.display = "block";
}}
onMouseLeave={() => {
this.setState({ mouseOver: false });
const image = document.querySelector(".hoverimage");
image.style.display = "none";
}}
>
<h3>test Image hover thing</h3>
</div>
<div class="hoverimage">
<img src={codingJoke} />
</div>
<p>{this.props.paragraph}</p>
</div>
);
}
}
.hoverimage {
position: absolute;
display: none;
pointer-events: none;
}
I'm working on adding a class to the DOM when the scroll position reaches a certain number. If the number is 150 or above, a class gets added. If the number is less than 150, the class gets removed. Currently, when I scroll down, all is well and the class gets added. However, when you scroll back up, if the number reaches a certain point, the scrollTop number bounces from one number to another repetitively. This is my code:
The Functionality
const useVisibilityHook = threshold => {
const [visible, setVisible] = useState(false);
useEffect(() => {
const getPos = () => {
const currentPos = window.pageYOffset;
console.log(currentPos, threshold);
setVisible(currentPos > threshold);
}
window.addEventListener('scroll', getPos);
return () => {
window.removeEventListener('scroll', getPos);
};
}, []);
return visible;
};
const Banner = () => {
const isVisible = useVisibilityHook(150);
return (
<div className={`violator ${isVisible ? 'hide' : ''}`}>
What to update
className={`violator ${!visible ? 'hide' : ''}`}
I'm not sure the best way to describe what I'm seeing, but basically. The currentPos variable will flip between a number like 124 and 292 respectively. The numbers certainly change as you slowly scroll the page back up, but you get the idea. This causes the class to toggle, which is no good.
This is what I see over and over again
You are adding and removing scroll event listener on each render (whenever you set the state). You should lock the dependencies of the useEffect block, so it will only be called once.
I've create a custom hook useVisibilityHook that should do what I assume you want (I've used 300 as threshold to make the change point clearer):
const { useState, useEffect } = React;
const useVisibilityHook = threshold => {
const [visible, setVisible] = useState(false);
useEffect(() => {
const getPos = () => {
const currentPos = window.pageYOffset;
setVisible(currentPos > threshold);
}
window.addEventListener('scroll', getPos);
return () => {
window.removeEventListener('scroll', getPos);
};
getPos();
}, []);
return visible;
};
const App = () => {
const isVisible = useVisibilityHook(300);
return (
<div className={`app ${isVisible ? 'show' : ''}`} />
);
};
ReactDOM.render(
<App />,
root
);
.app {
height: 5000px;
background: blue;
visibility: hidden;
}
.show {
visibility: visible;
}
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
I'm new here and I have a question that I couldn't find answer to..
I am currently working on a website using ReactJS, and I want to have a button that fills itself whenever the user clicks on it. The button should have a total of 5 stages to it.
I am not asking for you to code it for me, but simply help me finding the best approach to this thing.
So what exactly am I asking? As you can see in this
It's a boxed element that whenever the user clicks on it (it can click on the whole element), the progress fills and it becomes something like this
So the first line is now marked. When the user presses on it again, the 2nd bar fills
Important - there will be text inside these bars that fills.
What have I done so far? I have been thinking of having 5 different images for every time the user presses on the element, but I was wondering if there might be a better approach to it (Like having the DIV background the image, and have sub-divs that fills up whenever the user presses... )
I hope I made myself clear, and thank you all for your time!
Here is a working example. You don't need an image for all of the different states. It is far more flexible to do this dynamically with HTML.
The key to this is keeping track of the number of times the button has been clicked. In this example it uses currentState to keep track of how many times it has been clicked.
const defaultStyle = {
width: 100,
padding: 5,
};
class MultiLevelButton extends React.Component {
constructor(props) {
super(props);
this.state = {
currentState: 0,
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
const { currentState } = this.state;
if (currentState < this.props.states.length) {
this.setState({
currentState: currentState + 1,
});
}
}
reset() {
this.setState({
currentState: 0,
});
}
render() {
const { currentState } = this.state;
const { states } = this.props;
return (
<button onClick={this.handleClick} style={{border: 'none', outline: 'none'}}>
{
states
.map((s, i) => {
const stateNumber = states.length - i;
const overrides = stateNumber <= currentState ? { backgroundColor: '#000', color: '#fff' } : {};
const style = {
...defaultStyle,
backgroundColor: s,
...overrides,
};
return <div style={{...style}}>{stateNumber}</div>
})
}
</button>
)
}
}
const buttonRef = React.createRef();
ReactDOM.render(
<div>
<MultiLevelButton ref={buttonRef} states={['#bbb', '#ccc', '#ddd', '#eee', '#fff']} />
<MultiLevelButton states={['#fcc', '#cfc', '#ccf']} />
<div>
<button onClick={() => buttonRef.current.reset()}>Reset</button>
</div>
</div>,
document.getElementById('app')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app" />
As you are using react. use create step/level as component and you will pass props of styles. you can map that component 5 times or n times depending upon requirement. The view you have shown no need images use css to achieve it.
change props of component when user click on it.
You can use state in order to keep the click count and change the button background on the basis of click count.
const colors = ["#dfddc7", "#f58b54", "#a34a28", "#211717"]; //color array
class App extends Component {
constructor(props) {
super(props);
this.state = {
index : 0
}
}
handleChange() { // function will be called whenever we click on button
let {index} = this.state;
if (index >= 5) {
return; // you don't want to change color after count 5
}
index++;
console.log(index);
this.setState({index})
}
render() {
const {index} = this.state;
return (
<div className="App">
<button style = {{background:colors[index]}} //we are setting dynamic color from array on the basis of index
onClick = {this.handleChange.bind(this)}> click to change images
</button>
</div>
);
}
}
You can place <div></div> inside a button with different background color instead of images.
In the following example, I hold the number of clicks in the state. By comparing this value with the index of the step, you can see if it needs to be green or transparent
const numberOfSteps = 5;
class App extends React.Component {
state = {
numberOfClicks: 0
};
handleClick = e => {
this.setState({
numberOfClicks: (this.state.numberOfClicks + 1) % (numberOfSteps + 1)
}); // Use module (%) to reset the counter after 5 clicks
};
render() {
const { numberOfClicks } = this.state;
const steps = Array(numberOfSteps)
.fill()
.map((v, i) => i)
.map((i, index) => {
const color = numberOfClicks >= index + 1 ? "limegreen" : "transparent";
return (
<div
style={{
backgroundColor: color,
height: "30px",
border: ".5px solid gray"
}}
>
Bar {index + 1}
</div>
);
});
return (
<button
className="button"
style={{ height: "200px", width: "200px", backgroundColor: "lightgreen" }}
onClick={this.handleClick}
>
{steps}
</button>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root">
<!-- This element's contents will be replaced with your component. -->
</div>
The key concept is state. When you click your button you should set some state, and when you render the button you should render it based on the state.
Here's a simple example where I render a button which contains 5 div elements that are filled (by setting backgroundColor style property) based on the state.
class Example extends Component {
constructor(props) {
super(props);
this.state = {
buttonState: 0,
};
}
onClick = () => {
let buttonState = this.state.buttonState;
buttonState++;
if(buttonState > 4) buttonState = 0; // wrap around from 4 - 0
this.setState({buttonState: buttonState});
}
// render a button element with some text, and a background color based on whether filled is true/false
renderButtonElement(elementText, filled) {
const backgroundStyle = filled ? {backgroundColor: 'green'} : {backgroundColor: 'white'};
const textStyle = {color: 'grey'};
return(
<div style={[backgroundStyle, textStyle]}>
<div>{elementText}</div>
</div>
);
}
render() {
return(
<button
onClick={this.onClick}
>
{/* make a temporary array of 5 elements, map over them and render an element for each one */}
{Array(5).fill().map((_, i) => this.renderButtonElement(i + 1, i <= this.state.buttonState))}
</button>
);
}
}
I want the div element to get the class of "showtext" when you scroll 100 pixels or less above the element. When you're 100 pixels or more above it, it has the class of "hidden".
I am trying to use a ref to access the div element, and use a method called showText to check and see when we scroll to 100 pixels or less above that div element, i'm using scrollTop for this.
Then i use componentDidMount to add a window event listener of scroll, and call my showText method.
I am new to this, so I am sure there is mistakes here and probably bad code. But any help is appreciated!
import React, {Component} from 'react';
class SlideIn extends Component{
state={
showTexts: false,
}
showText=()=>{
const node= this.showTextRef;
if(node.scollTop<=100)
this.setState({
showTexts: true
})
}
componentDidMount(){
window.addEventListener('scroll', this.showText() )
}
render(){
const intro= document.querySelector('.intro')
return(
<div classname={this.state.showTexts ? 'showText' : 'hidden'} ref={node =>this.showTextRef = node}>
{window.addEventListener('scroll', this.showText)}
<h1>You did it!</h1>
</div>
)
}
}
export default SlideIn
I have tried using this.showText in my window scroll event, and as you see above this.showText(), neither have worked. I tried to use the current property on my div ref in my showText method, and it threw a error saying the scrollTop could not define the property of null.
Again I am new to this and have never added a window event listener this way, nor have I ever used scrollTop.
Thanks for any help!
When you attach an event listener you have to pass a function as a parameter. You are calling the function directly when you add the event listener.
In essence, you need to change:
componentDidMount(){
window.addEventListener('scroll', this.showText() )
}
to:
componentDidMount(){
window.addEventListener('scroll', this.showText)
}
In your scroll listener you should check the scroll position of the window(which is the element where you are performing the scroll):
showText = () => {
if (window.scrollY <= 100) {
this.setState({
showTexts: true
});
}
}
Also, you are attaching the event listener in the render method. The render method should only contain logic to render the elements.
Pass function as parameter like
window.addEventListener('scroll', this.showText)
and remove it from return.
Then you just need to do only this in function
if(window.scrollY<=100)
this.setState({
showTexts: true
})
use your div position here
You need to use getBoundingCLientRect() to get scroll position.
window.addEventListener("scroll", this.showText); you need to pass this.showText instead of calling it.
classname has speeling mistake.
showText = () => {
const node = this.showTextRef;
const {
y = 0
} = (node && node.getBoundingClientRect()) || {};
this.setState({
showTexts: y <= 100
});
};
componentDidMount() {
window.addEventListener("scroll", this.showText);
}
render() {
const intro = document.querySelector(".intro");
return (
<div
className={this.state.showTexts ? "showText" : "hidden"}
ref={node => (this.showTextRef = node)}
>
<h1>You did it!</h1>
</div>
);
}
condesandbox of working example: https://codesandbox.io/s/intelligent-shannon-1p6sp
I've put together a working sample for you to reference, here's the link: https://codesandbox.io/embed/summer-forest-cksfh
There are few things to point out here in your code:
componentDidMount(){
window.addEventListener('scroll', this.showText() )
}
Just like mgracia has mentioned, using this.showText() means you're directly calling the function. The right way is just to use this.showText.
In showText function, the idea is you have to get how far user has scrolled from the top position of document. As it was called using:
const top = window.pageYOffset || document.documentElement.scrollTop;
now it's safe to check for your logic and set state according to the value you want, here I have put it like this:
this.setState({
showTexts: top <= 100
})
In your componentDidMount, you have to call showText once to trigger the first time page loading, otherwise when you reload the page it won't trigger the function.
Hope this help
Full code:
class SlideIn extends Component {
state = {
showTexts: false,
}
showText = () => {
// get how many px we've scrolled
const top = window.pageYOffset || document.documentElement.scrollTop;
this.setState({
showTexts: top <= 100
})
}
componentDidMount() {
window.addEventListener('scroll', this.showText)
this.showText();
}
render() {
return (
<div className={`box ${this.state.showTexts ? 'visible' : 'hidden'}`}
ref={node => this.showTextRef = node}>
<h1>You did it!</h1>
</div>
)
}
}
.App {
font-family: sans-serif;
text-align: center;
height: 2500px;
}
.box {
width: 100px;
height: 100px;
background: blue;
position: fixed;
left: 10px;
top: 10px;
z-index: 10;
}
.visible {
display: block;
}
.hidden {
display: none;
}