Last active
February 4, 2021 09:58
-
-
Save jakewhiteley/9a6068f2180467ece5894efd98dd97ed to your computer and use it in GitHub Desktop.
A class for allowing mobile focus on elements during hashchange
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
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
E
is https://www.npmjs.com/package/@unseenco/e.isFocusable
focusTrapElement
,focusFirstChild
,focusLastChild
methodsfocus
now has a second param -force
. Setting it totrue
will focus on anything, while omitting will only set focus on natively focusable elementsfocus
now returns a bool to indicate if focus was set or not