Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Restore focus after a HTML dialog is shown modally
/**
* Updates the passed dialog to retain focus and restore it when the dialog is closed. Won't
* upgrade a dialog more than once. Supports IE11+ and is a no-op otherwise.
* @param {!HTMLDialogElement} dialog to upgrade
*/
var registerFocusRestoreDialog = (function() {
if (!window.WeakMap || !window.MutationObserver) {
return function() {};
}
var registered = new WeakMap();
// store previous focused node centrally
var previousFocus = null;
document.addEventListener('focusout', function(ev) {
previousFocus = ev.target;
}, true);
return function registerFocusRestoreDialog(dialog) {
if (dialog.localName !== 'dialog') {
throw new Error('Failed to upgrade focus on dialog: The element is not a dialog.');
}
if (registered.has(dialog)) { return; }
registered.set(dialog, null);
// replace showModal method directly, to save focus
var realShowModal = dialog.showModal;
dialog.showModal = function() {
var savedFocus = document.activeElement;
if (savedFocus === document || savedFocus === document.body) {
// some browsers read activeElement as body
savedFocus = previousFocus;
}
registered.set(dialog, savedFocus);
realShowModal.call(this);
};
// watch for 'open' change and clear saved
var mo = new MutationObserver(function() {
if (!dialog.hasAttribute('open')) {
registered.set(dialog, null);
} else {
// if open was cleared/set in the same frame, then the dialog will still be a modal (Y)
}
});
mo.observe(dialog, {attributes: true, attributeFilter: ['open']});
// on close, try to focus saved, if possible
dialog.addEventListener('close', function(ev) {
if (dialog.hasAttribute('open')) {
return; // in native, this fires the frame later
}
var savedFocus = registered.get(dialog);
if (document.contains(savedFocus)) {
var wasFocus = document.activeElement;
savedFocus.focus();
if (document.activeElement !== savedFocus) {
wasFocus.focus(); // restore focus, we couldn't focus saved
}
}
savedFocus = null;
});
// FIXME: If a modal dialog is readded to the page (either remove/add or .appendChild), it will
// be a non-modal. It will still have its 'close' handler called and try to focus on the saved
// element.
//
// These could basically be solved if 'close' yielded whether it was a modal or non-modal
// being closed. But it doesn't. It could also be solved by a permanent MutationObserver, as is
// done inside the polyfill.
}
}());
@sntran

This comment has been minimized.

Copy link

commented Feb 9, 2019

When testing this on Chrome version 71.0.3578.98 (Official Build) (64-bit), the focus is not transferred back to the saved element.

It looks like the MutationObserver clears the saved element before the close handler runs.

Here is an example: https://codepen.io/anon/pen/jdZdRE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.