How to prevent tooltip going out of screen reactjs - javascript

Here a small demo. There are a few block; hovering on each block appears a tooltip(orange rect). It doesn't work correctly.
Tooltip should be displayed from left or right side. To get sizes of tooltip need to display it. Coords to display tooltip can be calculated only after tooltip is displayed
Codesandbox https://codesandbox.io/s/react-ref-65jj6?file=/src/index.js:88-231
const { useState, useEffect, useCallback } = React;
function App() {
return (
<div>
<HoveredBlock index={1} />
<HoveredBlock index={2} blockStyle={{ marginLeft: "5%" }} />
<HoveredBlock index={3} blockStyle={{ marginLeft: "50%" }} />
</div>
);
}
function calcCoords(blockRect, hoverRect) {
const docWidth = document.documentElement.clientWidth;
const isLeft = blockRect.right + hoverRect.width > docWidth;
const coords = {};
if (!isLeft) {
coords.x = blockRect.right;
coords.y = blockRect.top;
coords.type = "right";
} else {
coords.x = blockRect.left - 5 - hoverRect.width;
coords.y = blockRect.top;
coords.type = "left";
}
return coords;
}
function HoveredBlock({ index, blockStyle }) {
const [blockRect, setBlockRect] = useState();
const [hoverRect, setHoverRect] = useState();
const [showHover, setShowHover] = useState(false);
const [coords, setCoords] = useState();
const blockRef = useCallback((node) => {
if (node) {
setBlockRect(node.getBoundingClientRect());
}
}, []);
const hoverRef = useCallback(
(node) => {
if (showHover && node) {
setHoverRect(node.getBoundingClientRect());
}
},
[showHover]
);
useEffect(() => {
if (showHover && hoverRect) {
const coords = calcCoords(blockRect, hoverRect);
setCoords(coords);
}
}, [hoverRect]);
const isHidden = !showHover || !coords ? 'hidden' : '';
return (
<div>
<div
ref={blockRef}
className="block"
style={blockStyle}
onMouseEnter={() => setShowHover(true)}
onMouseLeave={() => setShowHover(false)}
>
{index}
</div>
<div
ref={hoverRef}
className={'hover-block' + isHidden}
style={{
left: coords && coords.x,
top: coords && coords.y
}}
/>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
rootElement
);
.block {
width: 100px;
height: 100px;
background-color: aquamarine;
margin-left: 82%;
}
.hover-block {
position: fixed;
width: 100px;
height: 100px;
background-color: coral;
}
.hidden {
display: none;
}
<script src="https://unpkg.com/react#17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>

I solved it. I changed the way how to hide element: visibility:hidded instead of display:none
export default function HoveredBlock({ blockStyle }) {
const [blockRect, setBlockRect] = useState();
const [hoverRect, setHoverRect] = useState();
const [showHover, setShowHover] = useState(false);
const [coords, setCoords] = useState();
const blockRef = useCallback((node) => {
if (node) {
setBlockRect(node.getBoundingClientRect());
}
}, []);
const hoverRef = useCallback((node) => {
if (node) {
setHoverRect(node.getBoundingClientRect());
}
}, []);
useEffect(() => {
if (showHover) {
console.log({ blockRect, hoverRect });
const coords = calcCoords(blockRect, hoverRect);
setCoords(coords);
}
}, [showHover, blockRect, hoverRect]);
return (
<>
<div
ref={blockRef}
className="block"
style={blockStyle}
onMouseEnter={() => setShowHover(true)}
onMouseLeave={() => setShowHover(false)}
/>
<div
ref={hoverRef}
className={cx("hover-block", {
hidden: !showHover || !coords
})}
style={{
left: coords && coords.x,
top: coords && coords.y
}}
></div>
</>
);
}
.block {
width: 100px;
height: 100px;
background-color: aquamarine;
margin-left: 20%;
}
.hover-block {
position: fixed;
width: 100px;
height: 100px;
background-color: coral;
}
.hidden {
visibility: hidden;
}

Related

Unable to see the updated state -React with refs

I am unable to view the updated state with mouse moves when using setState() post mounting. What is wrong with the below approach? The state is not updated post mounting the RefDemo Component .
The requirementis to have a slider with 3 thumbs which should all start from 0 and can be dragged individually to different positions.
import React, { Component } from "react";
import "./Slider.css";
class RefsDemo extends Component {
constructor(props) {
super(props);
this.slider1 = React.createRef();
this.slider2 = React.createRef();
this.slider3 = React.createRef();
this.value1 = React.createRef();
this.value2 = React.createRef();
this.value3 = React.createRef();
this.bar = React.createRef();
this.state = {
lastOffset1: 0,
posCurrentX1: 0,
lastOffset2: 0,
posCurrentX2: 0,
lastOffset3: 0,
posCurrentX3: 0,
name: ""
};
this.range = 100;
//console.log(this.state);
this.onmouseDown1 = this.onmouseDown1.bind(this);
this.onmouseDown2 = this.onmouseDown2.bind(this);
this.onmouseDown3 = this.onmouseDown3.bind(this);
}
componentDidMount() {
//console.log(this.slider1);
//var {lastOffset1, posCurrentX1, lastOffset2, posCurrentX2, lastOffset3, posCurrentX3} = this.state
this.barLength = getComputedStyle(this.bar.current).width;
this.sliderLength = getComputedStyle(this.slider1.current).width;
this.travelLength = Math.round(
this.barLength.substring(0, this.barLength.length - 2)
);
console.log(this.state);
this.slider1.current.addEventListener("mousedown", this.onmouseDown1);
this.slider2.current.addEventListener("mousedown", this.onmouseDown2);
this.slider3.current.addEventListener("mousedown", this.onmouseDown3);
}
onType = (event) => {
this.setState({
...this.state,
name: event.target.value
});
};
onmouseDown1 = (event) => {
event.preventDefault();
const posInitialX = event.clientX;
this.slider1.current.addEventListener("mouseup", (event) => {
this.slider1.current.onMouseMove = null;
this.slider1.current.onMouseUp = null;
//this.lastOffset1 = this.posCurrentX1;
this.setState({
...this.state,
lastOffset1: this.state.posCurrentX1
});
});
this.slider1.current.addEventListener("mousemove", (event) => {
event.preventDefault();
//this.posCurrentX1 = this.lastOffset1 + event.clientX - posInitialX;
this.setState({
...this.state,
posCurrentX1: this.state.lastOffset1 + event.clientX - posInitialX
});
//console.log(window.innerWidth);
// console.log(document.body.style.width);
if (
this.state.posCurrentX1 <= this.travelLength &&
this.state.posCurrentX1 >= 0
) {
// console.log('posCurrentX: ', posCurrentX, 'barLength: ', travelLength);
this.slider1.current.style.left = `${this.state.posCurrentX1}px`;
this.value1.current.innerHTML = Math.round(
(this.range / this.travelLength) * this.state.posCurrentX1
);
}
});
};
onmouseDown2 = (event) => {
event.preventDefault();
const posInitialX = event.clientX;
document.addEventListener("mouseup", (event) => {
this.slider2.current.onMouseMove = null;
this.slider2.current.onMouseUp = null;
//this.lastOffset2 = this.posCurrentX2;
this.setState({
...this.state,
lastOffset2: this.state.posCurrentX2
});
});
document.addEventListener("mousemove", (event) => {
event.preventDefault();
//this.posCurrentX2 = this.lastOffset2 + event.clientX - posInitialX;
this.setState({
...this.state,
posCurrentX2: this.state.lastOffset2 + event.clientX - posInitialX
});
// console.log(window.innerWidth);
// console.log(document.body.style.width);
if (
this.state.posCurrentX2 <= this.travelLength &&
this.state.posCurrentX2 >= 0
) {
// console.log('posCurrentX: ', posCurrentX, 'barLength: ', travelLength);
this.slider2.current.style.left = `${this.state.posCurrentX2}px`;
this.value2.current.innerHTML = Math.round(
(this.range / this.travelLength) * this.state.posCurrentX2
);
}
});
};
onmouseDown3 = (event) => {
event.preventDefault();
const posInitialX = event.clientX;
document.addEventListener("mouseup", (event) => {
this.slider3.current.onMouseMove = null;
this.slider3.current.onMouseUp = null;
//this.lastOffset3 = this.posCurrentX3;
this.setState({
...this.state,
lastOffset3: this.state.posCurrentX3
});
});
document.addEventListener("mousemove", (event) => {
event.preventDefault();
//this.posCurrentX3 = this.lastOffset3 + event.clientX - posInitialX;
this.setState({
...this.state,
posCurrentX3: this.state.lastOffset3 + event.clientX - posInitialX
});
// console.log(window.innerWidth);
// console.log(document.body.style.width);
if (
this.state.posCurrentX3 <= this.travelLength &&
this.state.posCurrentX3 >= 0
) {
// console.log('posCurrentX: ', posCurrentX, 'barLength: ', travelLength);
this.slider3.current.style.left = `${this.state.posCurrentX3}px`;
this.value3.current.innerHTML = Math.round(
(this.range / this.travelLength) * this.state.posCurrentX3
);
}
});
};
componentDidUpdate() {
this.slider1.current.addEventListener("mousedown", this.onmouseDown1);
this.slider2.current.addEventListener("mousedown", this.onmouseDown2);
this.slider3.current.addEventListener("mousedown", this.onmouseDown3);
console.log(this.state);
}
// componentWillUnmount() {
// document.removeEventListener("mousedown", this.onmousedown1);
// document.removeEventListener("mousedown", this.onmousedown2);
// document.removeEventListener("mousedown", this.onmousedown3);
// }
render() {
return (
<>
<h3>Hello All</h3>
<input type="text" name={this.state.name} onChange={this.onType} />
<p>{this.state.name}</p>
<div className="box">
<div className="container">
<div className="bar" id="bar" ref={this.bar}>
<div
className="slider"
id="slider-1"
ref={this.slider1}
onMouseDown={this.onmouseDown1}
></div>
<div
className="slider"
id="slider-2"
ref={this.slider2}
onMouseDown={this.onmouseDown2}
></div>
<div
className="slider"
id="slider-3"
ref={this.slider3}
onMouseDown={this.onmouseDown3}
></div>
</div>
</div>
<div className="values">
<div className="value" id="value-1">
3
</div>
<div className="value" id="value-2">
4
</div>
<div className="value" id="value-3">
5
</div>
</div>
</div>
</>
);
}
}
export default RefsDemo;
Slider.css
.box {
width: 60%;
display: flex;
margin: 20px auto;
}
.container {
flex: 1 1 auto;
background-color: #eee;
padding: 50px 30px;
border-radius: 6px;
margin-right: 20px;
}
.bar {
height: 6px;
border-radius: 100px;
background-color: #aaa;
position: relative;
}
.slider {
position: absolute;
top: 50%;
left: 0;
height: 30px;
width: 30px;
border-radius: 50%;
background-color: #fff;
transform: translate(-50%, -50%);
box-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
cursor: pointer;
}
#slider-1 {
background-color: #6cc639;
}
#slider-2 {
background-color: #3e4cda;
}
#slider-3 {
background-color: #d33f43;
}
.values {
width: 60px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
font-family: Roboto, Helvetica, sans-serif;
font-size: 24px;
}
#value-1 {
color: #6cc639;
}
#value-2 {
color: #3e4cda;
}
#value-3 {
color: #d33f43;
}
The three pointers are stuck at same place and I am unable to record mouse movements while dragging the pointers one by one.
I have tried adding the listeners as below in Slider.js file.However it still doesn't work.

framer.motion animation is instant instead of animating

I have several boxes that I want to animate through,
Here's a simple app example (Also a codesandbox here)
Each "box" should fade in and fade out, however, in this example, the animation happens isntantly.
const Box = styled.div`
width: 100px;
height: 100px;
background: green;
`;
const Test = ({ isActive }) => {
return (
<motion.div
animate={isActive ? { opacity: 1 } : { opacity: 0 }}
transition={{ duration: 3 }}
>
<Box>hello world</Box>
</motion.div>
);
};
export default function App() {
const [currentIndex, setCurrentIndex] = useState(0);
const boxes = [
{
component: ({ isActive }) => <Test isActive={isActive} />
},
{
component: ({ isActive }) => <Test isActive={isActive} />
}
];
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<div onClick={() => setCurrentIndex(currentIndex === 0 ? 1 : 0)}>
{boxes.map((box, index) => {
const isActive = index === currentIndex;
return <box.component isActive={isActive} />;
})}
</div>
</div>
);
I have never used framer.motion before, but looking at their documentation, I think you can use variants, to achieve what you need. https://www.framer.com/api/motion/examples/
I've slightly refactored your code, to get it working:
import "./styles.css";
import { motion } from "framer-motion";
import styled from "styled-components";
import { useEffect, useState } from "react";
const Box = styled.div`
width: 100px;
height: 100px;
background: green;
`;
const variants = {
open: { opacity: 1 },
closed: { opacity: 0 }
};
const Test = ({ index, currentIndex }) => {
return (
<motion.div
animate={index === currentIndex ? "open" : "closed"}
variants={variants}
transition={{ duration: 3 }}
>
<Box>hello world</Box>
</motion.div>
);
};
export default function App() {
const [currentIndex, setCurrentIndex] = useState(0);
const boxes = ["a", "b"];
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>{currentIndex}</h2>
<div onClick={() => setCurrentIndex(currentIndex === 0 ? 1 : 0)}>
{boxes.map((box, i) => {
return <Test index={i} currentIndex={currentIndex} />;
})}
</div>
</div>
);
}
the currentIndex is passed as props to the child Test components, they check themselves whether their index matches the currentIndex and update their animations accordingly.
Edited codesandbox here: https://codesandbox.io/s/suspicious-austin-tymvx
In framer motion, you have useCycle properties. Here is an example.
Code in example:
import * as React from "react";
import { render } from "react-dom";
import { Frame, useCycle } from "framer";
import "./styles.css";
export function MyComponent() {
const [animate, cycle] = useCycle(
{ scale: 1.5, rotate: 0, opacity: 1 },
{ scale: 1.0, rotate: 90, opacity: 0 }
);
return (
<Frame
animate={animate}
onTap={() => cycle()}
size={150}
radius={30}
background={"#fff"}
/>
);
}
const rootElement = document.getElementById("root");
render(<MyComponent />, rootElement);
and some simple css:
body {
margin: 0;
padding: 0;
}
#root {
font-family: sans-serif;
text-align: center;
width: 100vw;
height: 100vh;
display: flex;
place-content: center;
place-items: center;
background: rgba(0, 85, 255, 1);
margin: 0;
padding: 0;
perspective: 1000px;
}
I don't recommend you to use this type of construction: animate={index === currentIndex ? "open" : "closed"} , because you might have some lagging/breaking animation.
Try always to search examples/elements of MotionAPI lib. You will have less code lines and mostly "clean" code with no useless variables.

How to use transition on className change in React?

on click button, I add or remove className 'a' to div. It becomes 50px width but without transition
const Navbar = ({ size }) => {
const MobileNavigation = styled.nav`
div {
width: 100px;
height: 100px;
background-color: blue;
transition: width 1s;
&.a {
width: 50px;
}
}
`;
const [active, setActive] = useState(0);
if (size.width < 500 || (size.width > size.height && size.width < 800)) {
return (
<MobileNavigation>
<button
onClick={() => {
if (active) {
setActive(false);
} else {
setActive(true);
}
}}
>
button
</button>
<div className={active ? 'a' : ''}></div>
</MobileNavigation>
}
);
export default withSize()(Navbar);
How do I add class to this element with transition? Thanks!
your styled component needs to be moved outside of NavBar. Every time NavBar rerenders you are creating a brand new MobileNavigation component and that new component has no idea that it was supposed to transition from a previous width
const MobileNavigation = styled.nav`
div {
width: 100px;
height: 100px;
background-color: blue;
transition: width 1s;
&.a {
width: 50px;
}
}
`;
const Navbar = ({ size }) => {
const [active, setActive] = useState(false);
if (size.width < 500 || (size.width > size.height && size.width < 800)) {
return (
<MobileNavigation>
<button
onClick={() => {
if (active) {
setActive(prevState => !prevState);
} else {
setActive(prevState => !prevState);
}
}}
>
button
</button>
<div className={active ? "a" : ""} />
</MobileNavigation>
);
}
return null;
};
export default withSize()(Navbar);

Clickable ticker in React

I have literally tried for a few hours to replicate a clickable ticker, much like they have at the very top of this site: https://www.thebay.com/
I'm confused about what triggers useEffect and long story short, I can't come up with a solution that keeps the ticker moving AND also gives the option of clicking forward/backwards via arrows. Clicking the arrow should not permanently pause the ticker.
function Ticker() {
const [tickerDisplay, setTickerDisplay] = useState('Free In-store Pickup')
const [tickerIndex, setTickerIndex] = useState(0)
const [arrowClicked, setArrowClicked] = useState(false)
const notices = [
'Easy Returns within 30 Days of Purchase',
'Free Shipping on $99+ Orders',
'Free In-store Pickup',
]
const handleClick = (side) => {
setArrowClicked(true)
switch (side) {
case 'left':
setTickerIndex(
tickerIndex === 0 ? notices.length - 1 : tickerIndex - 1
)
break
case 'right':
setTickerIndex(
tickerIndex === notices.length - 1 ? 0 : tickerIndex + 1
)
break
default:
console.log('something went wrong')
break
}
}
useEffect(() => {
if (arrowClicked) {
setTickerDisplay(notices[tickerIndex])
setTickerIndex(
tickerIndex === notices.length - 1 ? 0 : tickerIndex + 1
)
setArrowClicked(false)
return
}
setTimeout(() => {
setTickerDisplay(notices[tickerIndex])
setTickerIndex(
tickerIndex === notices.length - 1 ? 0 : tickerIndex + 1
)
console.log('This will run every 6 seconds!')
}, 6000)
}, [tickerIndex, notices, tickerDisplay, arrowClicked])
return (
<IconContext.Provider value={{ className: 'ticker-icons-provider' }}>
<div className='ticker'>
<FaChevronLeft onClick={() => handleClick('left')} />
<div className='ticker_msg-wrapper'>{tickerDisplay}</div>
<FaChevronRight onClick={() => handleClick('right')} />
</div>
</IconContext.Provider>
)
}
export default Ticker
What is the best way to code this component?
This is not a work of art and probably some things could've been done better.
Hope that suits you.
const { useRef, useState, useEffect } = React;
const getItems = () => Promise.resolve(['All of our questions are now open', 'Answers extended: 72 hours after questions open', 'Post a question or get an answer', 'Free badges on 20k points'])
const Ticker = ({onPrevious, onNext, items, currentIndex}) => {
const ref = useRef(null);
const [size, setSize] = useState({
width: 0,
widthPx: '0px',
height: 0,
heightPx: '0px'
})
useEffect(() => {
if(ref && ref.current) {
const {width, height} = ref.current.getBoundingClientRect();
setSize({
width,
widthPx: `${width}px`,
height,
height: `${height}px`
})
}
}, [ref]);
const calculateStyleForItem = (index) => {
return {
width: size.width,
transform: `translateX(${0}px)`
}
}
const calculateStyleForContainer = () => {
return {
width: `${size.width * (items.length + 1)}px`,
transform: `translateX(${-currentIndex * size.width + 2 * size.width}px)`
}
}
return <div ref={ref} className="ticker">
<div style={{width: size.widthPx, height: size.heightPx}} className="ticker__foreground">
<div onClick={onPrevious} className="arrow">{'<'}</div>
<div onClick={onNext} className="arrow">{'>'}</div>
</div>
<div>
<div style={calculateStyleForContainer()} className="ticker__values">
{items.map((value, index) => <div key={index} style={calculateStyleForItem(index)}className="ticker__value">{value}</div>)}
</div>
</div>
</div>
}
const App = () => {
const [items, setItems] = useState([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [clicked, setClicked] = useState(false);
useEffect(() => {
let isUnmounted = false;
getItems()
.then(items => {
if(isUnmounted) {
return
}
setItems(items);
})
return () => {
isUnmounted = true;
}
}, [])
useEffect(() => {
if(!items.length) {
return () => {
}
}
let handle = null;
const loop = () => {
if(!clicked) {
onNext(null);
}
setClicked(false);
handle = setTimeout(loop, 2000);
}
handle = setTimeout(loop, 2000);
return () => {
clearTimeout(handle);
}
}, [items, clicked])
const onPrevious = () => {
setClicked(true);
setCurrentIndex(index => (index - 1) > -1 ? index - 1 : items.length - 1)
}
const onNext = (programmatically) => {
if(programmatically) {
setClicked(programmatically);
}
setCurrentIndex(index => (index + 1) % items.length)
}
return <div>
{items.length ? <Ticker onPrevious={onPrevious} onNext={onNext} currentIndex={currentIndex} items={items}/> : 'Loading'}
</div>
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
html, body {
box-sizing: border-box;
margin: 0;
}
.ticker {
display: flex;
justify-content: center;
align-items: center;
background: black;
font-size: 1rem;
color: white;
font-weight: bold;
padding: 1rem;
overflow: hidden;
}
.ticker__foreground {
position: absolute;
z-index: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.ticker__values {
transition: all .3s ease-in;
}
.ticker__value {
text-align: center;
display: inline-block;
vertical-align: middle;
float: none;
}
.arrow {
font-size: 1.5rem;
cursor: pointer;
padding: 1rem;
}
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone#6/babel.min.js"></script>
<div id="root"></div>

show average rating in read mode and just total or half value in write mode

I am trying to create star rating where the functionality has to be following:
In read mode, the stars are shown as per average (should support 100%
i.e 5 or 96% i.e 4.6) in write mode, the user can only rate 1, 1.5, 2,
2.5 etc not 2.6
The read mode is working as expected but is having problem with write mode.
The problem in write mode is I cannot update the rating with non-decimal value from 1 to 5 and also half value like 1.5, 2.5, 3.5 etc. On hovering how do i decide if my mouse pointer is in the full star or half of star? Can anyone look at this, please?
I have created a sandbox for showing the demo
Here it is
https://codesandbox.io/s/9l6kmnw7vw
The code is as follow
UPDATED CODE
// #flow
import React from "react";
import styled, { css } from "styled-components";
const StyledIcon = styled.i`
display: inline-block;
width: 12px;
overflow: hidden;
direction: ${props => props.direction && props.direction};
${props => props.css && css(...props.css)};
`;
const StyledRating = styled.div`
unicode-bidi: bidi-override;
font-size: 25px;
height: 25px;
width: 125px;
margin: 0 auto;
position: relative;
padding: 0;
text-shadow: 0px 1px 0 #a2a2a2;
color: grey;
`;
const TopStyledRating = styled.div`
padding: 0;
position: absolute;
z-index: 1;
display: block;
top: 0;
left: 0;
overflow: hidden;
${props => props.css && css(...props.css)};
width: ${props => props.width && props.width};
`;
const BottomStyledRating = styled.div`
padding: 0;
display: block;
z-index: 0;
`;
class Rating extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
rating: this.props.rating || null,
// eslint-disable-next-line
temp_rating: null
};
}
handleMouseover(rating) {
console.log("rating", rating);
this.setState(prev => ({
rating,
// eslint-disable-next-line
temp_rating: prev.rating
}));
}
handleMouseout() {
this.setState(prev => ({
rating: prev.temp_rating
}));
}
rate(rating) {
this.setState({
rating,
// eslint-disable-next-line
temp_rating: rating
});
}
calculateWidth = value => {
const { total } = this.props;
const { rating } = this.state;
return Math.floor((rating / total) * 100).toFixed(2) + "%";
};
render() {
const { disabled, isReadonly } = this.props;
const { rating } = this.state;
const topStars = [];
const bottomStars = [];
const writableStars = [];
console.log("rating", rating);
// eslint-disable-next-line
if (isReadonly) {
for (let i = 0; i < 5; i++) {
topStars.push(<span>★</span>);
}
for (let i = 0; i < 5; i++) {
bottomStars.push(<span>★</span>);
}
} else {
// eslint-disable-next-line
for (let i = 0; i < 10; i++) {
let klass = "star_border";
if (rating >= i && rating !== null) {
klass = "star";
}
writableStars.push(
<StyledIcon
direction={i % 2 === 0 ? "ltr" : "rtl"}
className="material-icons"
css={this.props.css}
onMouseOver={() => !disabled && this.handleMouseover(i)}
onFocus={() => !disabled && this.handleMouseover(i)}
onClick={() => !disabled && this.rate(i)}
onMouseOut={() => !disabled && this.handleMouseout()}
onBlur={() => !disabled && this.handleMouseout()}
>
{klass}
</StyledIcon>
);
}
}
return (
<React.Fragment>
{isReadonly ? (
<StyledRating>
<TopStyledRating
css={this.props.css}
width={this.calculateWidth(this.props.rating)}
>
{topStars}
</TopStyledRating>
<BottomStyledRating>{bottomStars}</BottomStyledRating>
</StyledRating>
) : (
<React.Fragment>
{rating}
{writableStars}
</React.Fragment>
)}
</React.Fragment>
);
}
}
Rating.defaultProps = {
css: "",
disabled: false
};
export default Rating;
Now the writable stars is separately done to show the stars status when hovering and clicking but when I am supplying rating as 5 it is filling the third stars instead of 5th.
I think your current problem seems to be with where your mouse event is set, as you are handling it on the individual stars, they disappear, and trigger a mouseout event, causing this constant switch in visibility.
I would rather set the detection of the rating on the outer div, and then track where the mouse is in relation to the div, and set the width of the writable stars according to that.
I tried to make a sample from scratch, that shows how you could handle the changes from the outer div. I am sure the formula I used can be simplified still, but okay, this was just to demonstrate how it can work.
const { Component } = React;
const getRating = x => (parseInt(x / 20) * 20 + (x % 20 >= 13 ? 20 : x % 20 >= 7 ? 10 : 0));
class Rating extends Component {
constructor() {
super();
this.state = {
appliedRating: '86%'
};
this.setParentElement = this.setParentElement.bind( this );
this.handleMouseOver = this.handleMouseOver.bind( this );
this.applyRating = this.applyRating.bind( this );
this.reset = this.reset.bind( this );
this.stopReset = this.stopReset.bind( this );
}
stopReset() {
clearTimeout( this.resetTimeout );
}
setParentElement(e) {
this.parentElement = e;
}
handleMouseOver(e) {
this.stopReset();
if (e.currentTarget !== this.parentElement) {
return;
}
const targetRating = getRating(e.clientX - this.parentElement.offsetLeft);
if (this.state.setRating !== targetRating) {
this.setState({
setRating: targetRating
});
}
}
applyRating(e) {
this.setState({
currentRating: this.state.setRating
});
}
reset(e) {
this.resetTimeout = setTimeout(() => this.setState( { setRating: null } ), 50 );
}
renderStars( width, ...classes ) {
return (
<div
onMouseEnter={this.stopReset}
className={ ['flex-rating', ...classes].join(' ')}
style={{width}}>
<span onMouseEnter={this.stopReset} className="star">★</span>
<span onMouseEnter={this.stopReset} className="star">★</span>
<span onMouseEnter={this.stopReset} className="star">★</span>
<span onMouseEnter={this.stopReset} className="star">★</span>
<span onMouseEnter={this.stopReset} className="star">★</span>
</div>
);
}
renderFixed() {
return this.renderStars('100%', 'fixed');
}
renderReadOnlyRating() {
const { appliedRating } = this.state;
return this.renderStars( appliedRating, 'readonly' );
}
renderWriteRating() {
let { setRating, currentRating } = this.state;
if (setRating === 0) {
setRating = '0%';
}
if (currentRating === undefined) {
currentRating = '100%';
}
return this.renderStars( setRating || currentRating, 'writable' );
}
render() {
return (
<div>
<div
ref={ this.setParentElement }
className="rating"
onMouseMove={ this.handleMouseOver }
onMouseOut={ this.reset }
onClick={ this.applyRating }>
{ this.renderFixed() }
{ this.renderReadOnlyRating() }
{ this.renderWriteRating() }
</div>
<div>Current rating: { ( ( this.state.currentRating || 0 ) / 20) }</div>
</div>
);
}
}
ReactDOM.render( <Rating />, document.getElementById('container') );
body { margin: 50px; }
.rating {
font-family: 'Courier new';
font-size: 16px;
position: relative;
display: inline-block;
width: 100px;
height: 25px;
align-items: flex-start;
justify-content: center;
align-content: center;
background-color: white;
}
.flex-rating {
position: absolute;
top: 0;
left: 0;
display: flex;
height: 100%;
overflow: hidden;
cursor: pointer;
}
.fixed {
color: black;
font-size: 1.1em;
font-weight: bold;
}
.readonly {
color: silver;
font-weight: bold;
}
.writable {
color: blue;
background-color: rgba(100, 100, 100, .5);
}
.star {
text-align: center;
width: 20px;
max-width: 20px;
min-width: 20px;
}
<script id="react" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.2/react.js"></script>
<script id="react-dom" src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/15.6.2/react-dom.js"></script>
<div id="container"></div>

Categories