Created
November 25, 2019 04:37
-
-
Save cancer/35012bc7f5b3c188565758774b63176d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* This content is licensed according to the W3C Software License at | |
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document | |
*/ | |
var aria = aria || {}; | |
aria.Utils = aria.Utils || {}; | |
(function () { | |
/* | |
* When util functions move focus around, set this true so the focus listener | |
* can ignore the events. | |
*/ | |
aria.Utils.IgnoreUtilFocusChanges = false; | |
aria.Utils.dialogOpenClass = 'has-dialog'; | |
/** | |
* @desc Set focus on descendant nodes until the first focusable element is | |
* found. | |
* @param element | |
* DOM node for which to find the first focusable descendant. | |
* @returns | |
* true if a focusable element is found and focus is set. | |
*/ | |
aria.Utils.focusFirstDescendant = function (element) { | |
for (var i = 0; i < element.childNodes.length; i++) { | |
var child = element.childNodes[i]; | |
if (aria.Utils.attemptFocus(child) || | |
aria.Utils.focusFirstDescendant(child)) { | |
return true; | |
} | |
} | |
return false; | |
}; // end focusFirstDescendant | |
/** | |
* @desc Find the last descendant node that is focusable. | |
* @param element | |
* DOM node for which to find the last focusable descendant. | |
* @returns | |
* true if a focusable element is found and focus is set. | |
*/ | |
aria.Utils.focusLastDescendant = function (element) { | |
for (var i = element.childNodes.length - 1; i >= 0; i--) { | |
var child = element.childNodes[i]; | |
if (aria.Utils.attemptFocus(child) || | |
aria.Utils.focusLastDescendant(child)) { | |
return true; | |
} | |
} | |
return false; | |
}; // end focusLastDescendant | |
/** | |
* @desc Set Attempt to set focus on the current node. | |
* @param element | |
* The node to attempt to focus on. | |
* @returns | |
* true if element is focused. | |
*/ | |
aria.Utils.attemptFocus = function (element) { | |
if (!aria.Utils.isFocusable(element)) { | |
return false; | |
} | |
aria.Utils.IgnoreUtilFocusChanges = true; | |
try { | |
element.focus(); | |
} | |
catch (e) { | |
} | |
aria.Utils.IgnoreUtilFocusChanges = false; | |
return (document.activeElement === element); | |
}; // end attemptFocus | |
/* Modals can open modals. Keep track of them with this array. */ | |
aria.OpenDialogList = aria.OpenDialogList || new Array(0); | |
/** | |
* @returns the last opened dialog (the current dialog) | |
*/ | |
aria.getCurrentDialog = function () { | |
if (aria.OpenDialogList && aria.OpenDialogList.length) { | |
return aria.OpenDialogList[aria.OpenDialogList.length - 1]; | |
} | |
}; | |
aria.closeCurrentDialog = function () { | |
var currentDialog = aria.getCurrentDialog(); | |
if (currentDialog) { | |
currentDialog.close(); | |
return true; | |
} | |
return false; | |
}; | |
aria.handleEscape = function (event) { | |
var key = event.which || event.keyCode; | |
if (key === aria.KeyCode.ESC && aria.closeCurrentDialog()) { | |
event.stopPropagation(); | |
} | |
}; | |
document.addEventListener('keyup', aria.handleEscape); | |
/** | |
* @constructor | |
* @desc Dialog object providing modal focus management. | |
* | |
* Assumptions: The element serving as the dialog container is present in the | |
* DOM and hidden. The dialog container has role='dialog'. | |
* | |
* @param dialogId | |
* The ID of the element serving as the dialog container. | |
* @param focusAfterClosed | |
* Either the DOM node or the ID of the DOM node to focus when the | |
* dialog closes. | |
* @param focusFirst | |
* Optional parameter containing either the DOM node or the ID of the | |
* DOM node to focus when the dialog opens. If not specified, the | |
* first focusable element in the dialog will receive focus. | |
*/ | |
aria.Dialog = function (dialogId, focusAfterClosed, focusFirst) { | |
this.dialogNode = document.getElementById(dialogId); | |
if (this.dialogNode === null) { | |
throw new Error('No element found with id="' + dialogId + '".'); | |
} | |
var validRoles = ['dialog', 'alertdialog']; | |
var isDialog = (this.dialogNode.getAttribute('role') || '') | |
.trim() | |
.split(/\s+/g) | |
.some(function (token) { | |
return validRoles.some(function (role) { | |
return token === role; | |
}); | |
}); | |
if (!isDialog) { | |
throw new Error( | |
'Dialog() requires a DOM element with ARIA role of dialog or alertdialog.'); | |
} | |
// Wrap in an individual backdrop element if one doesn't exist | |
// Native <dialog> elements use the ::backdrop pseudo-element, which | |
// works similarly. | |
var backdropClass = 'dialog-backdrop'; | |
if (this.dialogNode.parentNode.classList.contains(backdropClass)) { | |
this.backdropNode = this.dialogNode.parentNode; | |
} | |
else { | |
this.backdropNode = document.createElement('div'); | |
this.backdropNode.className = backdropClass; | |
this.dialogNode.parentNode.insertBefore(this.backdropNode, this.dialogNode); | |
this.backdropNode.appendChild(this.dialogNode); | |
} | |
this.backdropNode.classList.add('active'); | |
// Disable scroll on the body element | |
document.body.classList.add(aria.Utils.dialogOpenClass); | |
if (typeof focusAfterClosed === 'string') { | |
this.focusAfterClosed = document.getElementById(focusAfterClosed); | |
} | |
else if (typeof focusAfterClosed === 'object') { | |
this.focusAfterClosed = focusAfterClosed; | |
} | |
else { | |
throw new Error( | |
'the focusAfterClosed parameter is required for the aria.Dialog constructor.'); | |
} | |
if (typeof focusFirst === 'string') { | |
this.focusFirst = document.getElementById(focusFirst); | |
} | |
else if (typeof focusFirst === 'object') { | |
this.focusFirst = focusFirst; | |
} | |
else { | |
this.focusFirst = null; | |
} | |
// Bracket the dialog node with two invisible, focusable nodes. | |
// While this dialog is open, we use these to make sure that focus never | |
// leaves the document even if dialogNode is the first or last node. | |
var preDiv = document.createElement('div'); | |
this.preNode = this.dialogNode.parentNode.insertBefore(preDiv, | |
this.dialogNode); | |
this.preNode.tabIndex = 0; | |
var postDiv = document.createElement('div'); | |
this.postNode = this.dialogNode.parentNode.insertBefore(postDiv, | |
this.dialogNode.nextSibling); | |
this.postNode.tabIndex = 0; | |
// If this modal is opening on top of one that is already open, | |
// get rid of the document focus listener of the open dialog. | |
if (aria.OpenDialogList.length > 0) { | |
aria.getCurrentDialog().removeListeners(); | |
} | |
this.addListeners(); | |
aria.OpenDialogList.push(this); | |
this.clearDialog(); | |
this.dialogNode.className = 'default_dialog'; // make visible | |
if (this.focusFirst) { | |
this.focusFirst.focus(); | |
} | |
else { | |
aria.Utils.focusFirstDescendant(this.dialogNode); | |
} | |
this.lastFocus = document.activeElement; | |
}; // end Dialog constructor | |
aria.Dialog.prototype.clearDialog = function () { | |
Array.prototype.map.call( | |
this.dialogNode.querySelectorAll('input'), | |
function (input) { | |
input.value = ''; | |
} | |
); | |
}; | |
/** | |
* @desc | |
* Hides the current top dialog, | |
* removes listeners of the top dialog, | |
* restore listeners of a parent dialog if one was open under the one that just closed, | |
* and sets focus on the element specified for focusAfterClosed. | |
*/ | |
aria.Dialog.prototype.close = function () { | |
aria.OpenDialogList.pop(); | |
this.removeListeners(); | |
aria.Utils.remove(this.preNode); | |
aria.Utils.remove(this.postNode); | |
this.dialogNode.className = 'hidden'; | |
this.backdropNode.classList.remove('active'); | |
this.focusAfterClosed.focus(); | |
// If a dialog was open underneath this one, restore its listeners. | |
if (aria.OpenDialogList.length > 0) { | |
aria.getCurrentDialog().addListeners(); | |
} | |
else { | |
document.body.classList.remove(aria.Utils.dialogOpenClass); | |
} | |
}; // end close | |
/** | |
* @desc | |
* Hides the current dialog and replaces it with another. | |
* | |
* @param newDialogId | |
* ID of the dialog that will replace the currently open top dialog. | |
* @param newFocusAfterClosed | |
* Optional ID or DOM node specifying where to place focus when the new dialog closes. | |
* If not specified, focus will be placed on the element specified by the dialog being replaced. | |
* @param newFocusFirst | |
* Optional ID or DOM node specifying where to place focus in the new dialog when it opens. | |
* If not specified, the first focusable element will receive focus. | |
*/ | |
aria.Dialog.prototype.replace = function (newDialogId, newFocusAfterClosed, | |
newFocusFirst) { | |
var closedDialog = aria.getCurrentDialog(); | |
aria.OpenDialogList.pop(); | |
this.removeListeners(); | |
aria.Utils.remove(this.preNode); | |
aria.Utils.remove(this.postNode); | |
this.dialogNode.className = 'hidden'; | |
this.backdropNode.classList.remove('active'); | |
var focusAfterClosed = newFocusAfterClosed || this.focusAfterClosed; | |
var dialog = new aria.Dialog(newDialogId, focusAfterClosed, newFocusFirst); | |
}; // end replace | |
aria.Dialog.prototype.addListeners = function () { | |
document.addEventListener('focus', this.trapFocus, true); | |
}; // end addListeners | |
aria.Dialog.prototype.removeListeners = function () { | |
document.removeEventListener('focus', this.trapFocus, true); | |
}; // end removeListeners | |
aria.Dialog.prototype.trapFocus = function (event) { | |
if (aria.Utils.IgnoreUtilFocusChanges) { | |
return; | |
} | |
var currentDialog = aria.getCurrentDialog(); | |
if (currentDialog.dialogNode.contains(event.target)) { | |
currentDialog.lastFocus = event.target; | |
} | |
else { | |
aria.Utils.focusFirstDescendant(currentDialog.dialogNode); | |
if (currentDialog.lastFocus == document.activeElement) { | |
aria.Utils.focusLastDescendant(currentDialog.dialogNode); | |
} | |
currentDialog.lastFocus = document.activeElement; | |
} | |
}; // end trapFocus | |
window.openDialog = function (dialogId, focusAfterClosed, focusFirst) { | |
var dialog = new aria.Dialog(dialogId, focusAfterClosed, focusFirst); | |
}; | |
window.closeDialog = function (closeButton) { | |
var topDialog = aria.getCurrentDialog(); | |
if (topDialog.dialogNode.contains(closeButton)) { | |
topDialog.close(); | |
} | |
}; // end closeDialog | |
window.replaceDialog = function (newDialogId, newFocusAfterClosed, | |
newFocusFirst) { | |
var topDialog = aria.getCurrentDialog(); | |
if (topDialog.dialogNode.contains(document.activeElement)) { | |
topDialog.replace(newDialogId, newFocusAfterClosed, newFocusFirst); | |
} | |
}; // end replaceDialog | |
}()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment