Close modal panel on blur (focus lost)


#1

My package has a modal panel to accept user input. Behaviour is typical: modal panel appears, dims the rest of Atom in the background, shows its contents (a descriptive line of text, a mini editor, an inline list of radio buttons).

this.rootElement = document.createElement('div');

this.message = document.createElement('div');
this.message.textContent = 'Descriptive text';
this.rootElement.appendChild(this.message);

this.miniEditor = new TextEditor({ mini: true });
this.rootElement.appendChild(this.miniEditor.element);

this.radioList = createElement('div');
this.radioList.innerHTML = `...`;
this.rootElement.appendChild(this.radioList);

this.modalPanel = atom.workspace.addModalPanel({
    item: this.rootElement,
    visible: false,
    autoFocus: true
});

Usually, you’d expect to be able to dismiss such a panel by clicking the dimmed background (as is the case with the command palette). However, I seem to be having a lot of trouble implementing this behaviour.

So far, I have tried adding an event listener to my mini editor. This works to the extent that the modal panel is closed whenever the mini editor’s input field loses focus. Unfortunately this also includes selecting any of the radio buttons, or clicking anywhere else in the modal panel.

this.miniEditor.element.addEventListener('blur', this.close.bind(this));

It made sense to me to simply shift the event listener to its parent div element, but this doesn’t seem to be registering at all.

this.rootElement.addEventListener('blur', () => {
    console.log('lost focus');
});

Any help figuring this bugger out would be appreciated :slight_smile:

ps: I’ve tried setting modal panel’s autoFocus true/false, makes no difference


#2

This is how atom-select-list does it, with a didLoseFocus() method that makes sure you haven’t focused something else relevant to the modal, then hides the modal if not.


#3

Something I forgot to add: I rewrote and administer the package shell-it, which spawns a modal based on atom-select-list but with additional input elements and behaviors. You might find it to be a useful thing to look over.


#4

Thanks for those suggestions, @DamnedScholar. I’m currently figuring out etch since that seems to be at the centre of the magic. Those two examples are definitely going to help!

P.S. My original thought of focusing the parent div (which was just missing a tabindex -1) wouldn’t have worked since focus on a child element removes focus from the parent anyway ¯_(ツ)_/¯


#5

I finished rewriting my panel’s view element using etch/jsx. As it turns out, neither had anything to do with the original issue, but learning about virtual DOMs was interesting nonetheless :slight_smile:

Anyway, on the off-chance anyone comes across the same issue, here’s how I solved it:

• a blur event listener on whichever item is focused by default when the modal panel is opened (in my case, the mini editor’s input field)

this.miniEditor.element.addEventListener('blur', this.onDidChangeFocus.bind(this));

• a handler method for that event that checks if the relatedTarget (the element focus is lost to) is contained in the modal panel’s root element (the object passed to the panel’s item parameter)

onDidChangeFocus(event) {
  if (this.rootElement.contains(event.relatedTarget)) {
    // refocus item with appropriate event listener
  } else {
    // close panel
  }
}

• a tabindex attribute on the modal panel’s root div to catch any click events that fall through from items that aren’t focusable by default, and therefore cannot qualify as related targets.

this.rootElement.setAttribute('tabindex', '-1');

This way, anything clicked in the modal panel will either receive focus (if it can), or fall-through to the parent container (rootElement), all of which will qualify as being contained in rootElement (even itself).


#6

Totally normal. I rewrote shell-it with etch not because I had to, but because the result would look so much cleaner and give me possible room for expansion in the future. It took longer than it would have if I had just added a new feature, but it pays off every time I look at the code.