React - How can i close all other tooltips? - javascript

I currently have a tooltip component provided to me from work which all works great, so I have made a Tooltip Wrapper to fulfil extra requirements.
The issue is i want to close all other instances of the tooltip when i tap on another. I have already added a click event listener for window so any instances of the tooltip are closed whenever i click else ware. The issue i'm having is that this handleWindowClick event doesn't fire when i click on another tooltip and as a result i am able to open multiple tooltips at once. When the requirement is that, whenever a tooltip is opened, the other closes so there is only ever one tooltip open at once.
import { Tooltip } from "Tooltip";
import React, { Component } from "react";
export default class TooltipWrapper extends Component {
constructor() {
super();
this.handleWindowClick = this.handleWindowClick.bind(this);
this.toggleTooltip = this.toggleTooltip.bind(this);
this.onTooltipClosed = this.onTooltipClosed.bind(this);
this.state = {
show: false
};
}
componentDidMount() {
window.addEventListener('click', this.handleWindowClick);
}
componentWillUnmount() {
window.removeEventListener('click', this.handleWindowClick);
}
toggleTooltip() {
this.setState({
show: !this.state.show
});
}
handleWindowClick(event) {
this.setState({
show: false
});
}
onTooltipClosed() {
this.setState({
show: false
});
}
render() {
return (
<Tooltip
open={this.state.show}
tip={this.props.tip}
position="bottom"
closeButton="visible"
onClose={this.onTooltipClosed}
>
<div onClick={this.toggleTooltip}>{this.props.children}</div>
</Tooltip>
);
}
}

Maybe adding click listener to body may help you, please try this:
document.body.addEventListener('click', this.onClickBody);

Related

Bind Mousetrap Hotkey to certain DOM element - trigger only when div is active?

I want to bind some hotkeys to a div: Whenever the user clicks somewhere inside the div and then presses the S key I want to console.log something. However, I don't want this hotkey to be global and to be triggered each and every time the user presses S.
Here is what I've got so far:
import React from "react"
import Mousetrap from "mousetrap"
export default class Mouse extends React.Component {
constructor(props) {
super(props)
}
componentDidMount() {
let form = document.querySelector("form")
let m = new Mousetrap(form)
m.bind("s", () => {
console.log("s")
})
}
componentWillUnmount() {
// m.unbind("s", () => {
// console.log("s")
// })
}
render() {
return (
<form
style={{ width: "300px", height: "300px", backgroundColor: "pink" }}
>
<input type="text" />
</form>
)
}
}
The Mousetrap docs say that I can bind my mousetrap like so:
var form = document.querySelector('form');
var mousetrap = new Mousetrap(form);
mousetrap.bind('mod+s', _handleSave);
mousetrap.bind('mod+z', _handleUndo);
As you can see in my example above, that's what I've done. It does work in this sense: whenever I type S while I'm in the input of the form, the console.log is being triggered. However, I don't want to use a form and neither do I want my user to be inside an input: I just want my user to have clicked on the div. I cannot get this to work though. I would expect it to look something like this:
import React from "react"
import Mousetrap from "mousetrap"
export default class Mouse extends React.Component {
constructor(props) {
super(props)
}
componentDidMount() {
let form = document.querySelector(".trigger")
let m = new Mousetrap(form)
m.bind("s", () => {
console.log("s")
})
}
componentWillUnmount() {
// m.unbind("s", () => {
// console.log("s")
// })
}
render() {
return (
<div
className="trigger"
style={{ width: "300px", height: "300px", backgroundColor: "pink" }}
>
Click me!
</div>
)
}
}
However, this doesn't work. Nothing is being triggered.
Edit: Also, one thing I don't quite understand in the first example above is that I am binding Mousetrap to form. However, the s hotkey is only ever triggered when I am inside the input field of form, but never when I just click on the form but not the input.
The reason this happens is that the Mousetrap is checking if the element is focused. divs (or in fact any other block element like a form) can only be focused if they have a tabindex defined. Inputs can be focused without that.
But I believe you do not need to explicitly bind the Mousetrap to the div at all. All you need to do is to track the active state of your div and bind() or unbind() the trap accordingly.
Example:
class Mouse extends Component {
state = {
active: false,
}
constructor(props) {
super(props);
this.trap = new Mousetrap();
}
handleClick = () => {
this.setState(({active}) => {
if (!active) {
this.trap.bind('s', this.handleKeyPress);
} else {
this.trap.unbind('s');
}
return {active: !active}
})
}
handleKeyPress = () => {
console.log('User pressed S.')
}
componentWillUnmount() {
this.trap.reset();
}
render() {
const {active} = this.state;
return (
<div
className={cN('trigger', {active})}
onClick={this.handleClick}
>
Click me!
</div>
);
}
}
Demo:
I think I've found the solution. Giving the div a tabindex makes it selectable, and thus, the hot key will be registered. I am not sure why this is, whether it's strictly necessary or a bit of a hacky solution. So all I had to do is:
<div className="trigger" tabindex="0">
...if anyone has a better explanation that goes in some more depth, feel free to post still, as I won't select this as the final answer for now.

Navigation dropdown toggle doesn't show the menu after a scroll event has closed the menu

My current implementation works normally if you only click the dropdown on/off. However once you close the menu via scrolling, the menu doesn't show once you open the dropdown once again, or works every other time you open it.
I have this same CSSTransition component in my mobile navigation bar, and it works perfectly, so the only external factor here is the <Dropdown> component I have.
Does anyone know how I can fix this without any extraneous amount of work? Otherwise I suppose I'll have to ditch the dropdown component and implement my own show/hide system.
import React from 'react';
import { Link } from 'react-router-dom';
import { CSSTransition } from 'react-transition-group';
import Dropdown, {DropdownTrigger, DropdownContent} from 'react-simple-dropdown';
class Nav extends React.Component {
constructor() {
super();
this.state = { show: false, }
this.showDropdown = this.showDropdown.bind(this);
this.closeDropdown = this.closeDropdown.bind(this);
}
showDropdown() {
this.setState({ active: true }, () => {
document.addEventListener('click', this.closeDropdown);
window.addEventListener('scroll', this.closeDropdown);
});
}
closeDropdown() {
this.setState({ active: false }, () => {
document.removeEventListener('click', this.closeDropdown);
window.removeEventListener('scroll', this.closeDropdown);
});
}
render() {
return(
...
<Dropdown className="...">
<DropdownTrigger onClick={this.showDropdown} >
Dropdown {this.state.active
? <img /> //up arrow
: <img /> //down arrow
</DropdownTrigger>
<DropdownContent className="...">
<CSSTransition
in={this.state.active}
timeout={150}
unmountOnExit
>
<ul>
<li><Link ... ></Link></li>
<li><Link ... ></Link></li>
</ul>
</CSSTransition>
</DropdownContent>
</Dropdown>
);
}
}
Just to be clear, the dropdown icon I have works perfectly with the state. The only issue is the dropdown content. After scrolling to close the dropdown and clicking on the dropdown a second time, the icon changes, but the menu does not appear.
I found out that the <Dropdown> component had competing interests with <CSSTransitions>. They both handle hiding and showing the content, so eliminating the former solved the issue.
Essentially I replaced the <Dropdown> tags with similarly styled <div>'s, and handled any extra needed action in listeners (for clicking and scrolling outside of the element).
just to let you know I have never used react, but have you tried in the this.setState, instead of using { active: true/false } use { show: true/false } as { active: false } may be disabling it and after when you do click on it again it does { active: true } and maybe is just re-enabling it but not triggering it open until you click on it again ?
I hope this gives you an idea.

React - How do I make an element disappear after animation ends?

Background
I am trying to make an element disappear after the animation ends (I am using animate.css to create the animations).
The above 'copied' text uses animated fadeOut upon clicking the 'Copy to Journal Link'. Additionally, the above demo shows that it takes two clicks on the link to toggle the span containing the text 'copied' from displayed to not displayed.
According to the animate.css docs, one can also detect when an animation ends using:
const element = document.querySelector('.my-element')
element.classList.add('animated', 'bounceOutLeft')
element.addEventListener('animationend', function() { doSomething() })
My Problem
However, within the componentDidMount() tooltip is null when attempting to integrate what animate.css docs suggest.
What am I doing wrong? Is there a better way to handle this behavior?
ClipboardBtn.js
import React, { Component } from 'react'
import CopyToClipboard from 'react-copy-to-clipboard'
class ClipboardBtn extends Component {
constructor(props) {
super(props)
this.state = {
copied: false,
isShown: true,
}
}
componentDidMount() {
const tooltip = document.querySelector('#clipboard-tooltip')
tooltip.addEventListener('animationend', this.handleAnimationEnd)
}
handleAnimationEnd() {
this.setState({
isShown: false,
})
}
render() {
const { isShown, copied } = this.state
const { title, value } = this.props
return (
<span>
<CopyToClipboard onCopy={() => this.setState({ copied: !copied })} text={value}>
<span className="clipboard-btn">{title}</span>
</CopyToClipboard>
{this.state.copied ? (
<span
id="clipboard-tooltip"
className="animated fadeOut"
style={{
display: isShown ? 'inline' : 'none',
marginLeft: 15,
color: '#e0dbda',
}}
>
Copied!
</span>
) : null}
</span>
)
}
}
export default ClipboardBtn
Using query selectors in React is a big NO. You should NEVER do it. (not that that's the problem in this case)
But even though it's not the problem, it will fix your problem:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
https://reactjs.org/docs/refs-and-the-dom.html
componentDidMount gets called only once during the inital mount. I can see that in the inital component state, copied is false, hence #clipboard-tooltip never gets rendered. That is why tooltip is null.
Instead try this :
componentDidUpdate(prevProps, prevState) {
if(this.state.copied === true && prevState.copied === false) {
const tooltip = document.querySelector('#clipboard-tooltip')
tooltip.addEventListener('animationend', this.handleAnimationEnd)
}
if(this.state.copied === false && prevState.copied === true) {
const tooltip = document.querySelector('#clipboard-tooltip')
tooltip.removeEventListener('animationend', this.handleAnimationEnd)
}
}
componentDidUpdate gets called for every prop/state change and hence as soon as copied is set to true, the event handler is set inside componentDidUpdate. I have added a condition based on your requirement, so that it doesn't get executed everytime. Feel free to tweak it as needed.

React handling outside clicks to close custom button menu component

I have a button menu component I've created that acts as a simple "action" menu to use in table on a per row basis. I'm having an issue handling outside clicks to close the menu when it is visible. I currently have a listener that gets attached when the button is clicked and it works fine when only a single button-menu component is being rendered. However when I have multiple being rendered (like in a table), they all react to the same events - ie. the user clicks outside and they all open/close together at the same time. How can I make it so that they all respond individually?
Sample code of what I have below:
export default class MenuButton extends React.Component {
constructor(props) {
super(props);
this.node = React.createRef();
this.state = {
showMenu: false,
}
}
handleOutsideClick = (e) => {
// Ignore clicks on the component itself
if (this.node.current.contains(e.target)) {
return;
}
this.handleButtonClick();
}
handleButtonClick = () => {
if (!this.state.show) {
// Attach/remove event handler depending on state of component
document.addEventListener('click', this.handleOutsideClick, false);
} else {
document.removeEventListener('click', this.handleOutsideClick, false);
}
this.setState({
showMenu: !this.state.showMenu,
});
}
render() {
return (
<div>
<Button
text="Actions"
onClick={this.handleButtonClick}
ref={this.node}
menuTrigger
/>
<Menu anchor={this.node.current} visible={this.state.showMenu}>
<Menu.Group title={this.props.groupTitle}>
{this.props.children}
</Menu.Group>
</Menu>
</div>
)
}
}
So my problem was due to a typo in my code that I noticed. I have a showMenu state, but in my handleButtonClick I was checking this.state.show instead of this.state.showMenu.

mdbreact modal applies a class 'modal-open' to body tag even if the modal is not open yet

I am using mdbreact modal in my react application but facing some issue. I try to include the modal component in my page, but the modal is not open yet, inspite of that it is applying a class 'modal-open' to the body tag which stops body from scrolling.
My code is as follows:
import React, { Component } from 'react';
import { Modal, ModalBody, ModalHeader, ModalFooter } from 'mdbreact';
class GroupContainer extends Component {
constructor() {
super();
this.state = {
modal: false,
}
}
renderModal = () => {
this.setState({
modal: true,
})
}
closeModal = () => {
this.setstate({
modal: false,
})
}
render() {
return (
<div>
<Modal isOpen={this.state.modal} toggle={() => this.renderModal()} fullHeight position="bottom">
test
</Modal>
</div>
)
}
};
export default GroupContainer;
The state 'modal' is false still the modal-open class is getting applied. It must get applied only when the state is true. I will be making the state as true on click of an external button in order to show this modal. Any clue?
Each of the imports are MDB(name) ex MDBModal, MDBModalBody. Also for toggling you don't want two functions because your code will get confused. Example of a modal toggle
toggle = () => {
this.setState({
modal: !this.state.modal
})
}
//for multiple modals
//modal1
//modal2
//modal3
toggle = nr => () => {
let modalNumber = 'modal' + nr;
this.setState({
[modalNumber]: !this.state[modalNumber]
})
}

Categories