Source: scripts/utilities/_focus.js

/**
 * @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)