How can I detect a click outside of an element, which is initially hidden, in vanilla JS?

46 Views Asked by At

I know maybe it is a very common question, and I've searched, but I didn't find any scenarios like mine, or if I have, it's with frameworks and libraries I cannot use.

I have two elements: an anchor, and a section tag container:

<a clas="triggerMenu" href="#">Trigger</a>

<section class="menuToShow" style="display: none;">Section to show</section>

My goal is that, when I click the trigger anchor link, the section tag element, which is initially hidden, must appear. And just after that, if I detect a click outside my section tag element, this disappears.

I've tried this:

document.querySelector('.triggerMenu').addEventListener("click", (e)=>{
            e.preventDefault();
            this.showMenu();
        });


showMenu(){
        this.profileMenuWindow!.style.display = "flex";  
        detectClickOutside(this.profileMenuWindow!, this.hideProfileMenu.bind(this));
    }

hideProfileMenu(){
        this.profileMenuWindow!.style.display = "none";
    }

and, the detectClickOutside function which is an exportable function from another file:

export function detectClickOutside<T extends HTMLElement>(elementToDetectClickOutside: T, callback: () => void) {
    const handleClick = (event: MouseEvent) => {
        const target = event.target as Node;
        if (!elementToDetectClickOutside.contains(target)) {
            callback();
            document.removeEventListener('click', handleClick);
        }
    };

    document.addEventListener('click', handleClick);
}

Explanation: I set the listener to the trigger anchor element. When that element get clicked, the profileMenuWindows (the section tag), shows with a display flex. And just after that I call the function detectClickOutside who sets a listener for the entire document checking if elements clicked are outside the profileMenuWindows.

My problem is that the first click is detecting the trigger element as an element outside the menu. Tracking the code, I've detected that it executes fine, showing the menu a millisecond, but then closes it.

Is this approach the best way to open and close a menu triggered by another element? I can't use frameworks to do that (I know it would be easier but I'm on a Symfony project only with TypeScript)

1

There are 1 best solutions below

0
remedy_man On BEST ANSWER

The key is calling event.stopPropagation inside trigger element's event handler in order to prevent the click event from bubbling up to the document.

let triggerMenu = document.querySelector('.triggerMenu');
let profileMenuWindow = document.querySelector('.menuToShow');

triggerMenu.addEventListener('click', event => {
  event.preventDefault();
  // prevent the current event from bubbling up to the document
  event.stopPropagation();
 
  this.showMenu();
  this.detectClickOutside(profileMenuWindow, this.hideMenu)
});

function showMenu() {
  profileMenuWindow.style.display = 'flex';
}

function hideMenu() {
  profileMenuWindow.style.display = 'none';
}

// exported function
function detectClickOutside(elementToDetectClickOutside, callback) {
  const handleClick = event => {
    event.preventDefault();

    if (!elementToDetectClickOutside?.isEqualNode(event.target)) {
      callback();
      document.removeEventListener('click', handleClick);
    }
  };

  document.addEventListener('click', handleClick);
}
<a class="triggerMenu" href="#">Trigger</a>
<section class="menuToShow" style="display: none;">Section to show</section>