Creating a tooltip on hover in React - javascript

I made a tooltip which appears when I hover on an element, and shows the full name of the product, productName.
<div
className="product-select-info"
onMouseEnter={e => productNameHandleHover(e)}
onMouseLeave={productNameHandleNoHover}
>
{productName}
<div
className="tooltip"
style={{
display: isTooltipShown ? "block" : "none",
top: mouseLocation.y,
left: mouseLocation.x,
}}
>
{productName}
</div>
</div>
And here are my handlers:
const productNameHandleHover = (event: any): void => {
setmouseLocation({
x: event.pageX,
y: event.pageY,
});
setisTooltipShown(true);
};
const productNameHandleNoHover = (): void => {
setisTooltipShown(false);
};
My problem is, I want to only show the tooltip after like 0.5 seconds. Currently, the tooltip appears as soon as the mouse goes over the div. How do I achieve this? I tried using setTimeout but I was just running into issues with that.

It is good to use css transitions as ritaj has mentioned in the comments.
But if you absolutely want javascript implementation, Whenever you are hovering over your element, set a class variable to be true.
const productNameHandleHover = (event: any): void => {
this.hovering = true;
...
}
and set it false whenever it is not.
const productNameHandleNoHover = (): void => {
this.hovering = false;
setisTooltipShown(false);
};
And when you actually set your tooltip check if your class variable is set or not.
const productNameHandleHover = (event: any): void => {
this.hovering = true;
setmouseLocation({
x: event.pageX,
y: event.pageY,
});
setTimeout(() => {
if (this.hovering) {
setisTooltipShown(true);
}
}, 500)
};
Here is a codesandbox that does what you need.
But you can already see the amount of effort you have to put in. So coming back to the original point. Using css transitions is a better option

Related

Angular variables

I've got a line of text right now, and when that line of text is being overflown something gets set to true which let's me load in a tooltip! Code below:
Template
<div>
<p #tooltip [tooltip]="/* ShowToolTipSomeHow? ? name : null */" delay="300">{{name}}</p>
</div>
This is where it should check if it should show the tooltip or not. As you can see it should somehow detect if the tooltip should be shown or not, I have no idea how and that's my question right now.
Component
#ViewChildren('tooltip') private tooltips!: QueryList<ElementRef>;
ngAfterViewInit(): void {
this.tooltips.changes.subscribe((tts: QueryList<ElementRef>) => {
tts.forEach((tooltip, index) => {
this.checkTooltipTruncated(tooltip);
});
});
}
private checkTooltipTruncated(tooltip: ElementRef) {
// Checks if the text has overflown
const truncated = this.isTextTruncated(tooltip);
if (truncated) {
// Change the ShowToolTipSomehow? value?
}
}
In the component it somehow changes some value that the tooltip can detect so that it can update itself to hide the tooltip. The additional problem is that it's not 1 tooltip to change, but infinite tooltips (so basically 1 or more).
My question is, how would I do this because I'm pretty stuck.
Create an array that holds boolean values and using the tooltip elements index set the value in the array.
ts:
#ViewChildren('tooltip') private tooltips!: QueryList<ElementRef>;
tooltipsVisible: boolean[];
ngAfterViewInit(): void {
if (!this.tooltipsVisible) {
this.tooltipsVisible = new Array(this.tooltips.length).fill(false);
}
this.tooltips.changes.subscribe((tts: QueryList<ElementRef>) => {
tts.forEach((tooltip, index) => {
this.checkTooltipTruncated(tooltip, index);
});
});
}
private checkTooltipTruncated(tooltip: ElementRef, index) {
// Checks if the text has overflown
const truncated = this.isTextTruncated(tooltip);
if (truncated) {
this.tooltipsVisible[index] = true;
// Change the ShowToolTipSomehow? value?
}
}
html:
<div>
<p #tooltip [tooltip]="tooltipsVisible[i] ? ...." delay="300">{{name}}</p>
</div>
You should create your p elements using *ngFor you can have the index available in your template...

Callback to be used when the element (div) is visible and not visible in Angular?

I need to find a way to get two callbacks, which are trigged when the div/img is visible on the screen and when it is not longer visible. What Can I use on Angular?
<div (onVisibleOnScreen)="doSomething1()" (onDisappearOnScreen)="doSomething2()">
You can use behavior subject to control both state and visibility of the DIV element.
Then invoke a function depending on his state.
Check this working example:
https://stackblitz.com/edit/angular-ivy-4ws3hq?file=src/app/app.component.ts
You can use a HostListener on the window scroll event. At the view initialization and each time the user scroll, you compute if your div/img is on screen with a ViewChild and the window.
#HostListener("window:scroll", [])
onWindowScroll() {
this.onScroll();
}
#ViewChild("myElement") myElement: ElementRef<HTMLDivElement>;
public onScroll(): void {
const windowYVisibility = {
min: window.pageYOffset,
max: window.pageYOffset + window.innerHeight
};
const myElementYVisibility = {
min: this.myElement.nativeElement.offsetTop,
max:
this.myElement.nativeElement.offsetTop +
this.myElement.nativeElement.offsetHeight
};
const isElementVisible =
(windowYVisibility.max > myElementYVisibility.min &&
windowYVisibility.min < myElementYVisibility.min) ||
(windowYVisibility.min < myElementYVisibility.max &&
windowYVisibility.max > myElementYVisibility.max);
}
Here is a working stackblitz

Keeping inline style changes when dispatch is fired?

I'm having a problem with inline style changes that are reset when my dispatch is finished, because the state is being re-rendered, despite the other functionality of my component is working (you can still see that the counter is not stopping).
Here's a demonstration of what I mean.
You can see that the orange bar of the left box vanishes when the orange bar of the right bar finishes (the animation ends). Essentially what I'm doing here is changing the width property in inline styles.
import React, { useEffect, useRef } from "react";
import { useDispatch, connect } from "react-redux";
import { addProfessionExperience } from "../../actions/index";
import "./Professions.sass";
const timers = [];
const progressWidths = [];
const mapStateToProps = (state, ownProps) => {
const filterID = +ownProps.match.params.filter;
const { professions, professionExperience } = state;
return {
professions: professions.find(item => item.id === filterID),
professionExperience: professionExperience
};
};
const produceResource = (dispatch, profession, sub, subRef) => {
if(timers[sub.id]) return;
/*
* Begin the progress bar animation/width-change.
*/
Object.assign(subRef.current[sub.id].style, {
width: "100%",
transitionDuration: `${sub.duration}s`
});
/*
* Updates the progress text with the remaining time left until done.
*/
let timeLeft = sub.duration;
const timeLeftCountdown = _ => {
timeLeft--;
timeLeft > 0 ? setTimeout(timeLeftCountdown, 1000) : timeLeft = sub.duration;
subRef.current[sub.id].parentElement.setAttribute("data-duration", timeLeft + "s");
}
setTimeout(timeLeftCountdown, 1000);
/*
* Dispatch the added experience from profession ID and sub-profession level.
* We do not allow duplicate timers, only one can be run at a time.
*/
const timer = setTimeout(() => {
Object.assign(subRef.current[sub.id].style, {
width: "0%",
transitionDuration: "0.2s"
});
dispatch(addProfessionExperience({ id: profession.id, level: sub.level }));
delete timers[sub.id];
}, sub.duration * 1000);
timers[sub.id] = timer;
};
const isSubUnlocked = (professionMaxExperience, subLevel, professionExperience) => {
if(professionExperience <= 0 && subLevel > 1) return false;
return professionExperience >= getExperienceThreshold(professionMaxExperience, subLevel);
};
const getExperienceThreshold = (professionMaxExperience, subLevel) => (((subLevel - 1) * 1) * (professionMaxExperience / 10) * subLevel);
const ConnectedList = ({ professions, professionExperience }) => {
const currentExperience = professionExperience.find(item => item.profession === professions.id);
const subRef = useRef([]);
const dispatch = useDispatch();
useEffect(() => {
subRef.current = subRef.current.slice(0, professions.subProfessions.length);
}, [professions.subProfessions]);
return (
<div>
<div className="list">
<ul>
{professions.subProfessions.map(el => {
const unlocked = isSubUnlocked(
professions.maxExperience,
el.level,
(currentExperience ? currentExperience.amount : 0)
);
const remainingExperience = getExperienceThreshold(professions.maxExperience, el.level) - (currentExperience ? currentExperience.amount : 0);
return (
<li
key={Math.random()}
style={{ "opacity": unlocked ? "1" : "0.5" }}
>
<div className="sprite">
<img alt="" src={`/images/professions/${el.image}.png`} />
</div>
<div className="caption">{el.name}</div>
<div
className="progress-bar"
data-duration={unlocked ? `${el.duration}s` : `${remainingExperience} XP to Unlock`}
data-identifier={el.id}
>
<span ref={r => subRef.current[el.id] = r} ></span>
</div>
<div className="footer">
<button
className="btn"
onClick={() => unlocked ? produceResource(dispatch, professions, el, subRef) : false}
>
{unlocked ?
`Click` :
<i className="fa fa-lock"></i>
}
</button>
</div>
</li>
);
})}
</ul>
</div>
</div>
);
};
const List = connect(mapStateToProps)(ConnectedList);
export default List;
How can I make it so the orange bars persist on their own and not disappears when another one finishes?
One problem is that you're using Math.random() to generate your keys. Keys are what the virtual DOM uses to determine whether an element is the "same" as the one on a previous render. By using a random key, you're telling the virtual DOM that you want to spit out a brand new DOM element instead of reusing the prior one, which means the new one won't retain any of the side effects you placed on the original element. Read up on React's reconciliation for more info on this.
Try to use keys that logically represent the thing you're rendering. In the case of your code, el.id looks like it may be a unique identifier for the subprofession you're rendering. Use that for the key instead of Math.random().
Additionally, refs are going to make reasoning about your code really difficult. Rather than using refs to manipulate your DOM, use state manipulation and prop passing, and let React re-render your elements with the new attributes.

How can I make this element class change on scroll in React?

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;
}

Drag and Drop implemented using Rxjs not working

Trying to create a drag n drop implementation from an Rxjs course example, but its not working correctly. Some time the box is dragged back to original position some times it just get stuck. Here is the plunkr
https://plnkr.co/edit/9Nqx5qiLVwsOV7zU6Diw?p=preview
the js code:
var $drag = $('#drag');
var $document = $(document);
var $dropAreas = $('.drop-area');
var beginDrag$ = Rx.Observable.fromEvent($drag, 'mousedown');
var endDrag$ = Rx.Observable.fromEvent($document, 'mouseup');
var mouseMove$ = Rx.Observable.fromEvent($document, 'mousemove');
var currentOverArea$ = Rx.Observable.merge(
Rx.Observable.fromEvent($dropAreas, 'mouseover').map(e => $(e.target)),
Rx.Observable.fromEvent($dropAreas, 'mouseout').map(e => null)
);
var drops$ = beginDrag$
.do(e => {
e.preventDefault();
$drag.addClass('dragging');
})
.mergeMap(startEvent => {
return mouseMove$
.takeUntil(endDrag$)
.do(moveEvent => moveDrag(startEvent, moveEvent))
.last()
.withLatestFrom(currentOverArea$, (_, $area) => $area);
})
.do(() => {
$drag.removeClass('dragging')
.animate({top: 0, left: 0}, 250);
})
.subscribe( $dropArea => {
$dropAreas.removeClass('dropped');
if($dropArea) $dropArea.addClass('dropped');
});
function moveDrag(startEvent, moveEvent) {
$drag.css(
{left: moveEvent.clientX - startEvent.offsetX,
top: moveEvent.clientY - startEvent.offsetY}
);
}
If I remove the withLatestFrom operator, then dragging of div always work fine, but without this I cannot get the drop feature implemented.
Problem one: Some time the box is dragged back to original position some times it just get stuck.
Answer: you should replace order of chain, ".do" before ".withLatestFrom" like this:
const drops$ = beginDrag$
.do( e => {
e.preventDefault();
$drag.addClass('dragging');
})
.mergeMap(startEvent => {
return mouseMove$
.takeUntil(endDrag$)
.do(mouseEvent => {
moveDrag(startEvent, mouseEvent);
})
.last()
.do((x) => {
console.log("hey from last event",x);
$drag.removeClass('dragging')
.stop()
.animate({ top:0, left: 0}, 250);
}
)
.withLatestFrom(currentOverArea$, (_, $area) => {
console.log('area',$area);
return $area;
});
Problem two: drop and drag outside not working correctly.
Answer: because of mouse event causing by "pointer-events" is not clearly.
In Css File, at:
.dragable .dragging {
background: #555;
pointer-events: none;
}
This is not Enough, the "mouseout" (or "mouseleave") still working, so when you drag box and drop. it happening the same time event "mouseover" and "mouseout". So the drag area never change color.
What to do ?:
make it better by clear every mouse event from the target element. In this case, it is div#drag.dragable.dragging. Add only this to CSS and problem is solve.
div#drag.dragable.dragging {
pointer-events: none;
}
(Holly shit, it take me 8 hours to resolve this. Readmore or see Repo at: Repository
)

Categories