A simple method to "trap" tabbing inside an element
const trapper = new FocusTrapper(element)
trapper.trap() // To trap focus
trapper.untrap() // To release
class FocusTrapper { | |
_element | |
_focusables | |
_lastFocused | |
_ignoreFocusing = false | |
_boundaries = [] | |
constructor(element) { | |
try { | |
if (!document.contains(element)) { | |
throw new Error() | |
} | |
} catch (e) { | |
throw new Error('Not a valid element!') | |
} | |
this._element = element | |
} | |
_getBoundaryDiv() { | |
const div = document.createElement('div') | |
div.setAttribute('tabindex', '0') | |
div.setAttribute('aria-hidden', 'true') | |
return div | |
} | |
_addBoundaries() { | |
this._boundaries = [] | |
this._boundaries.push(this._getBoundaryDiv()) | |
this._boundaries.push(this._getBoundaryDiv()) | |
this._element.parentNode.insertBefore(this._boundaries[0], this._element) | |
this._element.parentNode.insertBefore(this._boundaries[1], this._element.nextSibling) | |
} | |
_removeBoundaries() { | |
this._boundaries.forEach(e => e.remove()) | |
} | |
_findFocusables() { | |
if (!this._focusables) { | |
const selectors = [ | |
'a[href]:not([href="#"])', | |
'button', | |
'textarea', | |
'input', | |
'*[tabindex]:not([tabindex="-1"])', | |
].map(e => e += ':not([aria-hidden="true"]):not([disabled])') | |
this._focusables = this._element.querySelectorAll(selectors.join(', ')) | |
} | |
return this._focusables | |
} | |
_setFocus(position) { | |
const focusables = this._findFocusables() | |
const index = position === 'first' ? 0 : focusables.length - 1 | |
this._ignoreFocusing = true | |
focusables[index].focus() | |
this._ignoreFocusing = false | |
} | |
_focusTrap(event) { | |
event.stopImmediatePropagation() | |
if (this._ignoreFocusing) return | |
if (this._element.contains(event.target)) { | |
this._lastFocused = event.target | |
return | |
} | |
this._setFocus('first') | |
if (this._lastFocused === document.activeElement) { | |
this._setFocus('last') | |
} | |
this._lastFocused = document.activeElement | |
} | |
trap() { | |
this._addBoundaries() | |
document.addEventListener('focus', this._focusTrap.bind(this), true) | |
} | |
untrap() { | |
this._removeBoundaries() | |
this._focusables = null | |
document.removeEventListener('focus', this._focusTrap.bind(this), true) | |
} | |
} |