Skip to content

Instantly share code, notes, and snippets.

@samthor
Last active December 1, 2023 14:39
Show Gist options
  • Save samthor/babe9fad4a65625b301ba482dad284d1 to your computer and use it in GitHub Desktop.
Save samthor/babe9fad4a65625b301ba482dad284d1 to your computer and use it in GitHub Desktop.
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
Copy link

sntran 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

@Leland
Copy link

Leland commented Dec 1, 2023

Why localName and not nodeName?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment