I have a volume element that shows the volume bar when the user hovers over it. This all works great in desktop. However to get the same functionality on mobile, the user has clicks on the volume element which also toggles the mute click event.
I am wanting to stop that mute event when the user clicks (i.e taps) on that element on mobile.
I don't want to modify the Mute or VolumeBar classes to fix this because these are generic classes in my library that the developer uses.
https://jsfiddle.net/jwm6k66c/2145/
Actual: The mute click event gets fired and the volume bar opens.
Expected: The mute click event doesn't gets fired and the volume bar opens.
Open the console -> go into mobile view (CTRL + SHIFT + M on chrome) -> click the volume button and observe the console logs.
What I have tried:
Using volumeControlOnClick to stop propogation when the volume bar's height is 0 (i.e not visible), this does not cancel the onClick though.
What I want:
To cancel the mute click event if the user clicks on the volume icon for the first time in mobile. Instead it should only show the volume bar.
const volumeControlOnClick = (e) => {
const volumeBarContainer =
e.currentTarget.nextElementSibling.querySelector('.jp-volume-bar-container');
/* Stop propogation is for mobiles to stop
triggering mute when showing volume bar */
if (volumeBarContainer.clientHeight === 0) {
e.stopPropagation();
console.log("stop propogation")
}
};
class Volume extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<div className="jp-volume-container">
<Mute onTouchStart={volumeControlOnClick}><i className="fa fa-volume-up" /></Mute>
<div className="jp-volume-controls">
<div className="jp-volume-bar-container">
<VolumeBar />
</div>
</div>
</div>
);
}
};
class Mute extends React.Component {
render() {
return (
<button className="jp-mute" onClick={() => console.log("mute toggled")} onTouchStart={this.props.onTouchStart}>
{this.props.children}
</button>
);
}
};
class VolumeBar extends React.Component {
render() {
return (
<div className="jp-volume-bar" onClick={() => console.log("bar moved")}>
{this.props.children}
</div>
);
}
};
React.render(<Volume />, document.getElementById('container'));
Here's what I ended up with. It works because onTouchStart is always calld before onClick if it's a touch event and if not's then the custom logic gets called anyway. It also fires before the hover has happened. This preserves the :hover event. e.preventDefault() did not.
let isVolumeBarVisible;
const onTouchStartMute = e => (
isVolumeBarVisible = e.currentTarget.nextElementSibling
.querySelector('.jp-volume-bar-container').clientHeight > 0
);
const onClickMute = () => () => {
if (isVolumeBarVisible !== false) {
// Do custom mute logic
}
isVolumeBarVisible = undefined;
};
<Mute
aria-haspopup onTouchStart={onTouchStartMute}
onClick={onClickMute}
>
<i className="fa">{/* Icon set in css*/}</i>
</Mute>
What you can do is use a flag to indicate you were in a touch event before being in your mouse event, as long as you are using bubble phase. So attach a listener to your container element like this:
let isTouch = false;
const handleContainerClick = () => isTouch = false;
const handleMuteClick = () => {
if (isTouch == false) {
console.log("mute toggled");
}
};
const volumeControlOnClick = () => {
isTouch = true;
};
class Volume extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<div className="jp-volume-container" onClick={handleContainerClick}>
<Mute onTouchStart={volumeControlOnClick} onClick={handleMuteClick}><i className="fa fa-volume-up" /></Mute>
<div className="jp-volume-controls">
<div className="jp-volume-bar-container">
<VolumeBar />
</div>
</div>
</div>
);
}
};
class Mute extends React.Component {
render() {
return (
<button className="jp-mute" onTouchStart={this.props.onTouchStart} onClick={this.props.onClick}>
{this.props.children}
</button>
);
}
};
class VolumeBar extends React.Component {
render() {
return (
<div className="jp-volume-bar" onClick={() => console.log("bar moved")}>
{this.props.children}
</div>
);
}
};
render(<Volume />, document.getElementById('container'));
If you are not using bubble phase so you can register a timeout of 100ms with the same logic above, where after 100ms you make your flag variable false again. Just add to your touchStart handler:
setTimeout(() => {isTouch = false}, 100);
EDIT: Even though touch events are supposed to be passive by default in Chrome 56, you call preventDefault() from a touchEnd event to prevent the click handler from firing. So, if you can't modify the click handler of your Mute class in any way, but you can add a touchEnd event, than you could do:
const handleTouchEnd = (e) => e.preventDefault();
const volumeControlOnClick = () => console.log("volumeControlOnClick");
class Volume extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<div className="jp-volume-container">
<Mute onTouchStart={volumeControlOnClick} onTouchEnd={handleTouchEnd}><i className="fa fa-volume-up" /></Mute>
<div className="jp-volume-controls">
<div className="jp-volume-bar-container">
<VolumeBar />
</div>
</div>
</div>
);
}
};
class Mute extends React.Component {
render() {
return (
<button className="jp-mute" onTouchStart={this.props.onTouchStart} onTouchEnd={this.props.onTouchEnd} onClick={() => console.log("mute toggled")}>
{this.props.children}
</button>
);
}
};
class VolumeBar extends React.Component {
render() {
return (
<div className="jp-volume-bar" onClick={() => console.log("bar moved")}>
{this.props.children}
</div>
);
}
};
render(<Volume />, document.getElementById('container'));
Try this
render() {
return (
<div className="jp-volume-container" onClick={handleContainerClick}>
<Mute onTouchStart={volumeControlOnClick} onClick={handleMuteClick}><i className="fa fa-volume-up" /></Mute>
<div className="jp-volume-controls">
<div className="jp-volume-bar-container">
<VolumeBar />
</div>
</div>
</div>
);
}
};
class Mute extends React.Component {
render() {
return (
<button className="jp-mute" onTouchStart={this.props.onTouchStart} onClick={this.props.onClick}>
{this.props.children}
</button>
);
}
};
class VolumeBar extends React.Component {
render() {
return (
<div className="jp-volume-bar" onClick={() => console.log("bar moved")}>
{this.props.children}
</div>
);
}
};
render(<Volume />, document.getElementById('container'));
Related
It is my understanding that refs are not defined outside the react lifecycle (source). The problem I am trying to solve is to capture a key press at the document level (i.e. trigger the event no matter what element is in focus), and then interact with a react ref. Below is a simplified example of what I am trying to do:
export default class Console extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
text: "",
};
}
print(output: string) {
this.setState({
text: this.state.text + output + "\n"
})
}
toggleVisible()
{
this.setState({visible: !this.state.visible});
}
render() {
const footer_style = {
display: this.state.visible ? "inline" : "none",
};
return (
<footer id="console-footer" className="footer container-fluid fixed-bottom" style={footer_style}>
<div className="row">
<textarea id="console" className="form-control" rows={5} readOnly={true}>{this.state.text}</textarea>
</div>
</footer>
);
}
}
class App extends React.Component {
private console: Console;
constructor() {
super({});
this.console = React.createRef();
}
keyDown = (e) =>
{
this.console.current.toggleVisible(); // <-- this is undefined
}
componentDidMount(){
document.addEventListener("keydown", this.keyDown);
},
componentWillUnmount() {
document.removeEventListener("keydown", this.keyDown);
},
render() {
return (
<div className="App" onKeyDown={this.keyDown}> // <-- this only works when this element is in focus
// other that has access to this.console that will call console.print(...)
<Console ref={this.console} />
</div>
);
}
}
My question is: is there a way to have this sort of document level key press within the lifesycle of react so that the ref is not undefined inside the event handler keyDown? I've seen a lot of solutions that involve setting the tabIndex and hacking to make sure the proper element has focus at the right time, but these do not seem like robust solutions to me.
I'm just learning React so maybe this is a design limitation of React or I am not designing my components properly. But this sort of functionality seems quite basice to me, having the ability to pass components from one to the other and call methods on eachother.
You're calling the onKeyDown callback twice, once on document and once on App.
Events bubble up the tree.
When the textarea is not in focus, only document.onkeydown is called.
When it is in focus, both document.onkeydown and App's div.onkeydown are called, effectively cancelling the effect(toggling state off and back on).
Here's a working example: https://codesandbox.io/s/icy-hooks-8zuy7?file=/src/App.js
import React from "react";
class Console extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
text: ""
};
}
print(output: string) {
this.setState({
text: this.state.text + output + "\n"
});
}
toggleVisible() {
this.setState({ visible: !this.state.visible });
}
render() {
const footer_style = {
display: this.state.visible ? "inline" : "none"
};
return (
<footer
id="console-footer"
className="footer container-fluid fixed-bottom"
style={footer_style}
>
<div className="row">
<textarea id="console" className="form-control" rows={5} readOnly>
{this.state.text}
</textarea>
</div>
</footer>
);
}
}
export default class App extends React.Component {
constructor(props) {
super(props);
this.console = React.createRef();
}
keyDown = (e) => {
this.console.current.toggleVisible(); // <-- this is undefined
};
componentDidMount() {
document.addEventListener("keydown", this.keyDown);
}
componentWillUnmount() {
document.removeEventListener("keydown", this.keyDown);
}
render() {
return (
<div className="App" style={{ backgroundColor: "blueviolet" }}>
enter key to toggle console
<Console ref={this.console} />
</div>
);
}
}
Also, I recommend using react's hooks:
export default App = () => {
const console = React.createRef();
const keyDown = (e) => {
console.current.toggleVisible(); // <-- this is undefined
};
React.useEffect(() => {
// bind onComponentDidMount
document.addEventListener("keydown", keyDown);
// unbind onComponentDidUnmount
return () => document.removeEventListener("keydown", keyDown);
});
return (
<div className="App" style={{ backgroundColor: "blueviolet" }}>
press key to toggle console
<Console ref={console} />
</div>
);
};
I have a React <App> component with inside this structure:
{/*
INSIDE <APP>
<BreadCrumb>
<Chip/>
<Chip/>
<Chip/>
...
<BreadCrumb/>
<CardContainer>
<Card/> // just a clickable image
<Card/>
<Card/>
...
<Button/>
<CardContainer/>
*/}
I need a click on <Card> to activate a <Button> function, and this function should change the state of <App> as "activate" I mean that when I click on a <Card> the <Button> becomes clickable.
I have some problems to understand how pass function parents to children and set the state of a parent from inside a child.
this is my App component
class App extends React.Component {
constructor(props){
super(props)
this.state = {
activeIndex: 1
}
}
submitChoice() {
this.setState({activeIndex : this.state.activeIndex ++});
}
}
render() {
return (
<Button onClick = {this.submitChoice})/>
}
and this is the Button
class Button extends React.Component {
render() {
return(
<button
onClick = {() => this.props.onClick()}
className="button">
Continua
</button>
);
}
}
when i click on the button i receve this error
TypeError: Cannot read property 'activeIndex' of undefined
Use a property of the "Card" component to pass your callback function:
const Card = ({ onClick, id }) => {
const triggerClick = () => {
onClick(id);
};
return (
<div onClick={triggerClick}>Click the card</div>
);
};
const App = () => {
const cardClicked = id => {
console.log(`Card with id ${id} was clicked`);
//Modify App state here
};
return (
<CardContainer>
<Card onClick={cardClicked} id="card-1"/>
<Card onClick={cardClicked} id="card-2"/>
</CardContainer>
);
}
I have a header component which manages the state for my navigation component.
The navigation successfully toggles if the user clicks on the hamburger icon however, if the user clicks or taps anywhere outside of the navigation I need the navigation to close.
How can I achieve this?
Here is my code:
export default class Header extends React.Component {
constructor() {
super();
this.state = {
mobileOpenNav: false
};
bindAll([
'openMobileNav',
'openContactModal'
],this);
}
openMobileNav() {
this.props.contactModalToggle(false);
this.setState({
mobileOpenNav: !this.state.mobileOpenNav
})
}
openContactModal() {
this.props.contactModalToggle();
this.setState({
mobileOpenNav: !this.state.mobileOpenNav
});
}
render() {
const {nav, contactModalToggle, location, logos} = this.props;
const {mobileOpenNav} = this.state;
return (
<div className="header-wrap">
<div className="header">
<Logo location={location} logoUrls={logos} />
<Navigation
location={location}
nav={nav}
contactModalToggle={this.openContactModal}
mobileOpen={mobileOpenNav}
mobileToggle={this.openMobileNav}
/>
<div className="hamburger" onClick={this.openMobileNav}><img src={HamburgerIcon} /></div>
</div>
</div>
)
}
}
The following solution should work for you.
componentDidMount() {
document.addEventListener('click', this.handleClickOutside.bind(this), true);
}
componentWillUnmount() {
document.removeEventListner('click', this.handleClickOutside.bind(this), true);
}
handleClickOutside(e) {
const domNode = ReactDOM.findDOMNode(this);
if(!domNode || !domNode.contains(event.target)) {
this.setState({
mobileOpenNav: false
});
}
}
use react-onclickoutside module https://github.com/Pomax/react-onclickoutside
import onClickOutside from "react-onclickoutside"
import Navigation from "pathToNvaigation"
const ContentWrapper = onClickOutside(Navigation)
and use
<ContentWrapper
location={location}
nav={nav}
contactModalToggle={this.openContactModal}
mobileOpen={mobileOpenNav}
mobileToggle={this.openMobileNav}
/>
This question already has answers here:
Prevent onClick event by clicking on a child div
(2 answers)
How to call stopPropagation in reactjs?
(2 answers)
Closed 5 years ago.
Can someone help explain how to stop event propagation on a click? I've read many other posts, but I still can't figure it out.
I have a grid of 40x50 boxes, and when I click one I'd like to see that box's id. Currently, when I click it bubbles up and returns Board as what's been clicked. So I need to stop the propagation, right? Where/how do I do that? I've tried passing i.stopPropagation(); in the handleClick() method, but it tells me that i.stopPropagation(); isn't a function.
function Square(props) {
return (
<div className="square" id={props.id} onClick={props.onClick}/>
);
}
class Board extends Component {
rowsByColumns(rows, columns) {
let arr=[];
let k=0;
let m=0
for (let i=0;i<rows;i++) {
arr.push([])
for (let j=0;j<columns;j++) {
arr[i].push(<Square key={"square"+m++} id={"square"+m} living={false} onClick={() => this.props.onClick(this)}/>)
}
}
let newArr = arr.map(row => <Row key={"row"+k++}>{row}</Row>);
return (newArr);
}
render() {
return (
<div id="board">
{this.rowsByColumns(40,50)}
</div>
);
}
}
class App extends Component {
constructor() {
super();
this.state = {
alive: ["square1"],
};
this.handleClick = this.handleClick.bind(this);
}
handleClick(i) {
console.log(i);
this.setState({
alive: this.state.alive.concat([i])
})
}
render() {
return (
<div className="App container-fluid">
<main className="row justify-content-center">
<Board alive={this.state.alive} onClick={i => this.handleClick()}/>
</div>
</main>
</div>
);
}
}
Your click handler receives an event object. Use stopPropagation on it:
handleClick(e) {
e.stopPropagation();
}
then in your onClick:
onClick={this.handleClick}
Live example — the child stops every other click it sees:
class Parent extends React.Component {
constructor(...args) {
super(...args);
this.handleClick = () => {
console.log("Parent got the click");
};
}
render() {
return <div onClick={this.handleClick}>
Click here to see parent handle it.
<Child />
</div>;
}
}
class Child extends React.Component {
constructor(...args) {
super(...args);
this.stopClick = true;
this.handleClick = e => {
if (this.stopClick) {
e.stopPropagation();
console.log("Child got the click and stopped it");
} else {
console.log("Child got the click and didn't stop it");
}
this.stopClick = !this.stopClick;
};
}
render() {
return <div onClick={this.handleClick}>I'm the child</div>;
}
}
ReactDOM.render(
<Parent />,
document.getElementById("root")
);
<div id="root"></div><script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
I am building a menubar using React JS. On clicking a menu item, respective megamenu should open. On clicking outside of the megamenu, it should close itself. I have made upto this by toggling the state of the megamenu. But I also want to close the megamenu, when the menu-item is clicked second-time(i.e. toggle close and open of megamenu by clicking menu-item). I am stuck here, on clicking the menu-item second time, the state is not toggling back.
class Menubar extends React.Component {
constructor() {
super();
this.state = {
clicked: false
};
this.handleClick = this.handleClick.bind(this);
this.handleOutsideClick = this.handleOutsideClick.bind(this);
}
componentWillMount() {
document.addEventListener('click', this.handleOutsideClick, false);
}
componentWillUnmount(){
document.removeEventListener('click', this.handleOutsideClick, false);
}
handleClick() {
this.setState({clicked: !this.state.clicked});
}
handleOutsideClick(){
if (this.refs.megaMenu.contains(event.target)) {
} else {
this.setState({
clicked: false
});
}
}
render() {
return (
<div className="container">
<div className="menu-bar">
{/* Menu*/}
<div className="menu-bar-item">
<a className="menu-bar-link" href="#" onClick={this.handleClick}>Points</a>
<div className={"mega-menu"+" "+this.state.clicked} ref="megaMenu">
<div className="mega-menu-content">
<p>Points Menu</p>
</div>
</div>
</div>
</div>
</div>
);
}
}
ReactDOM.render(
<Menubar />,
document.getElementById('example')
);
Codepen Demo
You need to move ref="megaMenu" so it includes button as well, otherwise when you click on button handleOutsideClick is also triggered and you flip this.state.clicked twice. Also, you forgot to pass event in handleOutsideClick handler.
class Menubar extends React.Component {
constructor() {
super();
this.state = {
clicked: false
};
this.handleClick = this.handleClick.bind(this);
this.handleOutsideClick = this.handleOutsideClick.bind(this);
}
componentWillMount() {
document.addEventListener('click', this.handleOutsideClick, false);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleOutsideClick, false);
}
handleClick() {
this.setState({clicked: !this.state.clicked});
}
handleOutsideClick(event) {
if (!this.menu.contains(event.target)) {
this.setState({
clicked: false
});
}
}
render() {
return (
<div className="container">
<div className="menu-bar">
{/* Menu*/}
<div className="menu-bar-item" ref={el => this.menu = el}>
<a className="menu-bar-link" href="#" onClick={this.handleClick}>Points</a>
<div className={"mega-menu" + " " + this.state.clicked}>
<div className="mega-menu-content">
<p>Points Menu</p>
</div>
</div>
</div>
</div>
</div>
);
}
}
ReactDOM.render(
<Menubar />,
document.getElementById('example')
);
I've fixed your Codepen Demo
Also, consider using callback refs like ref={el => this.menu = el}
It's a better way to do it.