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) | |
| } | |
| } |