Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React stop other events on that element

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'));
like image 537
Martin Dawson Avatar asked Feb 17 '17 15:02

Martin Dawson


2 Answers

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>
like image 174
Martin Dawson Avatar answered Sep 28 '22 17:09

Martin Dawson


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'));
like image 31
Felipe T. Avatar answered Sep 28 '22 16:09

Felipe T.