/**
* @file
* Focus helpers.
*/
/* global UCASUtilities */
/**
* Focus utility.
* @namespace focus
* @memberof UCASUtilities
* @param {object} _u - UCASUtilities object.
*/
(function (_u) {
/**
* Reports lost focus.
* @function lost
* @memberof! UCASUtilities.focus
* @param {string} identifier - an optional arbitrary identifier.
*/
function lost (identifier) {
// Double-check that we don't have focus as we don't want to change it unnecessarily.
if (!document.hasFocus() || document.activeElement === document.body) {
/**
* @event focusLost
* @memberof! UCASUtilities.focus
*/
var event = document.createEvent('Event')
event.initEvent('focusLost', true, true)
event.focusLostIdentifier = identifier
document.dispatchEvent(event)
}
}
/**
* Sets new focus.
* @function focus
* @memberof! UCASUtilities.focus
* @param {node} el - the element to set focus on.
*/
function focus (el) {
if (isFocusable(el)) {
el.focus()
} else {
// OK, so we now need to focus on something generic.
// First, try locating #main-content.
var mainContent = document.getElementById('main-content')
if (isFocusable(mainContent)) {
mainContent.focus()
} else {
// If it doesn't exist, set the focus on the first h1.
var h1 = document.querySelector('h1')
if (h1) {
// Make it focusable.
h1.setAttribute('tabIndex', 0)
h1.focus()
} else {
// Warn developers immediately rather than use UCASUtilities.log.
console.warn("DF Tried to set focus but couldn't find an appropriate element.")
}
}
}
}
/**
* Determines if an element is focusable.
* An element is considered focusable if it is of a certain type or has appropriate tabIndex, is visible and not disabled.
* @function isFocusable
* @memberof! UCASUtilities.focus
* @param {node} el - the element to check focusability of.
* @returns {boolean} - true if focusable.
*/
function isFocusable (el) {
// Focusable elements are:
// a, button, input, textarea, select, summary,[tabindex]:not([tabindex="-1"])
return (
// Element exists.
el &&
// Element is visible to the user.
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
el.offsetParent !== null &&
// Element is not itself hidden or disabled.
(!el.hidden && !el.disabled) &&
// Element can receive focus.
(el.getAttribute('tabIndex') || ['A', 'BUTTON', 'INPUT', 'TEXTAREA', 'SELECT', 'SUMMARY'].indexOf(el.tagName) !== -1)
)
}
/**
* Lists all focusable elements within an element or the document.
* This uses the same logic as isFocusable but also
* excludes links within the closed meganav.
* @function listFocusableElements
* @memberof! UCASUtilities.focus
* @param {Node} [context=document] - DOM element
* @returns {Array} - an array of focusable nodes.
*/
function listFocusableElements (context) {
context = context || document
var list = Array.prototype.slice.call(context.querySelectorAll('a:not([disabled]):not([hidden]),button:not([disabled]):not([hidden]),input:not([disabled]):not([hidden]),textarea:not([disabled]):not([hidden]),select:not([disabled]):not([hidden]),summary:not([disabled]):not([hidden]),:not([disabled]):not([hidden])[tabindex]'))
// Filter to make sure that elements that are not visible to the user are not included.
// Includes a specific selector for links within the closed meganav.
return list.filter(function (el) { return el.offsetParent !== null && el.closest('[data-global-link-state="closed"] .link-panel') === null })
}
/**
* Moves focus to next focusable element.
* @function next
* @memberof! UCASUtilities.focus
* @param {node} [el=activeElement] - the focusable element we want to set focus in relation to.
* @param {node} [context=document] - DOM element
*/
function next (el, context) {
el = el || document.activeElement
context = context || document
var focusableElements = listFocusableElements(context)
var index = focusableElements.indexOf(el)
var nextIndex = index !== focusableElements.length - 1 ? index + 1 : 0
focusableElements[nextIndex].focus()
}
/**
* Moves focus to previous focusable element.
* @function prev
* @memberof! UCASUtilities.focus
* @param {node} [el=activeElement] - the focusable element we want to set focus in relation to.
* @param {node} [context=document] - DOM element
*/
function prev (el, context) {
el = el || document.activeElement
context = context || document
var focusableElements = listFocusableElements(context)
var index = focusableElements.indexOf(el)
var prevIndex = index > 0 ? index - 1 : focusableElements.length - 1
focusableElements[prevIndex].focus()
}
_u.focus = {
focus: focus,
isFocusable: isFocusable,
listFocusableElements: listFocusableElements,
lost: lost,
next: next,
prev: prev
}
})(UCASUtilities)