Skip to content

Instantly share code, notes, and snippets.

@jakewhiteley
Last active February 4, 2021 09:58
Show Gist options
  • Save jakewhiteley/9a6068f2180467ece5894efd98dd97ed to your computer and use it in GitHub Desktop.
Save jakewhiteley/9a6068f2180467ece5894efd98dd97ed to your computer and use it in GitHub Desktop.
A class for allowing mobile focus on elements during hashchange
import E from './E'
class FocusManager {
/**
* Call this method to boot up.
*/
static init() {
if (document.location.hash) {
FocusManager.focus(document.getElementById(window.location.hash.replace('#', '')), true)
}
E.on('hashchange', window, () => {
FocusManager.focus(document.getElementById(window.location.hash.replace('#', '')), true)
})
}
/**
* Whether an element is natively focusable.
* @param {HTMLElement|node} element
* @return {boolean}
*/
static isFocusable(element) {
if (!Element.prototype.matches) {
Element.prototype.matches = Element.prototype.msMatchesSelector
}
return element.matches('input:not([disabled]), a, button, textarea, select, iframe, object, [tabindex])')
}
/**
* Traps focus in the element - looping tab order when the first/last element is reached.
* @param {HTMLElement} element
*/
static focusTrapElement(element) {
const parent = element.parentNode
const pre = document.createElement('div')
pre.setAttribute('data-focus-trap', 'pre')
pre.setAttribute('tabindex', '0')
const post = document.createElement('div')
post.setAttribute('data-focus-trap', 'post')
post.setAttribute('tabindex', '0')
E.on('focus', post, () => this.focusFirstChild(element))
E.on('focus', pre, () => this.focusLastChild(element))
parent.insertBefore(pre, element)
parent.insertBefore(post, element.nextSibling)
}
/**
* Sets focus on the first focusable child of the supplied element.
* @param {HTMLElement|node} element
* @return {boolean}
*/
static focusFirstChild(element) {
for (let i = 0; i < element.children.length; i++) {
const child = element.children[i]
if (FocusManager.focus(child) || FocusManager.focusFirstChild(child)) {
return true
}
}
return false
}
/**
* Sets focus on the last focusable child of the supplied element.
* @param {HTMLElement|node} element
* @return {boolean}
*/
static focusLastChild(element) {
for (let i = element.children.length - 1; i >= 0; i--) {
const child = element.children[i]
if (FocusManager.focus(child) || FocusManager.focusLastChild(child)) {
return true
}
}
return false
}
/**
* Focus on the element.
* @param {HTMLElement} element
* @param {boolean} force
*/
static focus(element, force = false) {
if (element === undefined || element === null) {
return false
}
if (force === true && FocusManager.isFocusable(element) === false) {
element.setAttribute('tabindex', '-1')
E.on('blur', element, FocusManager.removeTabIndex)
E.on('focusout', element, FocusManager.removeTabIndex)
}
try {
element.focus()
} catch (e) {}
return document.activeElement === element
}
/**
* Clean up after focus is lost.
*/
static removeTabIndex() {
this.removeAttribute('tabindex')
E.off('blur', this, FocusManager.removeTabIndex)
E.off('focusout', this, FocusManager.removeTabIndex)
}
}
export default FocusManager
@jakewhiteley
Copy link
Author

E is https://www.npmjs.com/package/@unseenco/e.

  • Improved isFocusable
  • Added focusTrapElement, focusFirstChild, focusLastChild methods
  • focus now has a second param - force. Setting it to true will focus on anything, while omitting will only set focus on natively focusable elements
  • focus now returns a bool to indicate if focus was set or not

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