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/
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'));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With