Hide a popup on clicking outside the popup area

Not sure how I’d do this with Stimulus:

var box = document.querySelector(".box");

// Detect all clicks on the document
document.addEventListener("click", function(event) {
	// If user clicks inside the element, do nothing
	if (event.target.closest(".box")) return;

	// If user clicks outside the element, hide it!
	box.classList.add("js-is-hidden");
});

Any suggestions would be appreciated.

Thanks! :slight_smile:

I have something like this where this.bodyElement is just the body tag and this.closeSidebar() a function that removed classes (in my case it’s a sidebar overlay instead of a modal but does the same thing.

_canClose() {
  const outsideClickListener = event => {
    if (!event.target.closest('.form-sidebar')) {
      this.closeSidebar();
      removeClickListener();
    }
  };

  const removeClickListener = () => {
    this.bodyElement.removeEventListener('click', outsideClickListener);
  };

  this.bodyElement.addEventListener('click', outsideClickListener);
}

I was thinking along similar lines, but was just wondering if there was something achievable with the data-action/targers :grimacing:

Yep, you can use a data-action="click@window->controller-name#hide", like here:

3 Likes

Awesome! :sunglasses:

Thanks for the replies all :smile:

@pasacallaliberte the only issue seems to be that I can’t click an element inside the dropdown (I have an input) without triggering the hide - any suggestions?

e.g. code is here: https://codepen.io/askalot/pen/PQmEXL - but I am unable to get it to work as the static target = [‘menu’,‘button’] line is throwing an error in CodePen.

@arunan you might be able to do something like this:

if (this.element.contains(event.target) == false) {
  this.droptownTarget.classList.add("hidden");
}
3 Likes

Thanks! Your suggestion was useful for me.

When i tried to use few dropdowns, clicks on their buttons prevented hiding of opened popups. So, I’ve done some improvements:

import { Controller } from 'stimulus';

export default class extends Controller {
    static targets = [ 'button', 'popup' ]

    toggle () {
        // removed event.stopPropagation() to continue the bubbling for others dropdowns
        event.preventDefault();

        if (this.buttonTarget.getAttribute('aria-expanded') == "false") {
            this.show();
        } else {
            this.hide(null);
        }
    }

    show() {
        this.buttonTarget.setAttribute('aria-expanded', 'true');
        this.buttonTarget.classList.add('is-active');
        this.popupTarget.classList.remove('is-hidden');
    }

    hide(event) {
        // changed your solution with crispinheneise's recommendation and added additional check:
        if (event && (this.popupTarget.contains(event.target) || this.buttonTarget.contains(event.target))) {
            event.preventDefault();
            return;
        }

        this.buttonTarget.setAttribute('aria-expanded', 'false');
        this.buttonTarget.classList.remove('is-active');
        this.popupTarget.classList.add('is-hidden');
    }
}
2 Likes