Introduction

It has been way too many times when I implemented (with greater or lesser success) automated way for closing dropdowns when clicking outside of them or by pressing the Escape key. This utility is a way for me to stop repeating myself and have it working always the way it should.

Usage

Let's assume you have an element on the page that acts as a popup, modal or dropdown of some kind. Those elements usually go away once the user clicks outside of them or presses the Escape key.

<div id="popup">
  Lorem ipsum...
</div>

That is where Defocuser comes in. First you need to import it as a dependency. That can be done either directly using a <script> tag:

<script src="https://unpkg.com/defocuser"></script>

or (if you're using some kind of build system like Webpack or Rollup) as an NPM dependency:

npm install --save-dev defocuser

and then import it in your source like this:

import Defocuser from 'defocuser'

Once you have decided which way you want to include it create an instance of Defocuser and add the root element of your dialog/popup/dropdown, select the phase the callback should be called and provide the actual callback itself:

const popup = document.querySelector('#popup')
const defocuser = new Defocuser()
defocuser.addElement(popup, 'bubbling', () => { popup.remove() })

Please note the callback calls remove() on the popup effectively removing it from the DOM tree. Defocuser interally uses MutationObserver and once it detects that the added node is removed it is also removed from the internal state of defocuser.

It works this way because Defocuser is ready for stacked elements! This means that if you have a modal that has a set of dropdowns that maybe have some other popups Defocuser keeps tracl of all those elements and closes only the recent one! Just like you would expect!

Special case

Sometimes there is more than one element that you would like to treat as part of your dialog. Let's examine the following case:

<span id="input">My input</span>
<ul id="options">
  <li>Option 1</li>
  <li>Option 2</li>
  <li>Option 3</li>
</ul>

This if structure is dynamic and the element that actiones the dropdown is static you can set it to be also taken into account when tracking user interactions:

const dropdown = document.querySelector('#options')
const secondary = document.querySelector('#input')
defocuser.setSecondaryElement(dropdown, secondary)

This way when defocuser checks if the click happened outside of your dropdown it will also take the secondary element into account as though it would be inside of your dropdown.

It is especially usefull when your input steers some part of your dropdown (like selecting start and end date of a period of time).

Remarks

The Defocuser uses MutationObserver to detect when the element has been removed from the DOM. So you should physically remove the element to clean things up.