127 lines
2.7 KiB
JavaScript
127 lines
2.7 KiB
JavaScript
|
|
import { getCurrentScreen } from '../helpers/current-screen'
|
||
|
|
import { isTouchDevice } from '../helpers/is-touch-device'
|
||
|
|
|
||
|
|
// Maybe try to check focus with :has(:focus) instead of document.activeElement.
|
||
|
|
|
||
|
|
const tabbables = [
|
||
|
|
'button:enabled:not([readonly])',
|
||
|
|
'select:enabled:not([readonly])',
|
||
|
|
'textarea:enabled:not([readonly])',
|
||
|
|
|
||
|
|
// Exclude hidden inputs as they are not part of the tab order
|
||
|
|
// and can cause focus to escape the modal when they are the last element
|
||
|
|
'input:enabled:not([readonly]):not([type="hidden"])',
|
||
|
|
|
||
|
|
'a[href]',
|
||
|
|
'area[href]',
|
||
|
|
|
||
|
|
'iframe',
|
||
|
|
'object',
|
||
|
|
'embed',
|
||
|
|
|
||
|
|
'[tabindex]',
|
||
|
|
'[contenteditable]',
|
||
|
|
'[autofocus]',
|
||
|
|
]
|
||
|
|
|
||
|
|
let lockedElement = null
|
||
|
|
|
||
|
|
const handleKeydown = (e) => {
|
||
|
|
if (!lockedElement) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
let focusableEls = [...lockedElement.querySelectorAll(tabbables.join(','))]
|
||
|
|
|
||
|
|
if (
|
||
|
|
lockedElement.querySelector('[data-device="mobile"]') &&
|
||
|
|
getCurrentScreen() !== 'mobile'
|
||
|
|
) {
|
||
|
|
focusableEls = focusableEls.filter(
|
||
|
|
(el) => !el.closest('[data-device="mobile"]')
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
const firstFocusableEl = focusableEls[0]
|
||
|
|
const lastFocusableEl = focusableEls[focusableEls.length - 1]
|
||
|
|
|
||
|
|
if (e.key !== 'Tab' && e.keyCode !== 9) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
let isElFocusable = focusableEls.includes(document.activeElement)
|
||
|
|
|
||
|
|
// Firefox will make scrollable elements focusable, even if those are not
|
||
|
|
// tabbable usually.
|
||
|
|
if (
|
||
|
|
lockedElement.contains(document.activeElement) &&
|
||
|
|
document.activeElement.scrollHeight >
|
||
|
|
document.activeElement.clientHeight
|
||
|
|
) {
|
||
|
|
isElFocusable = true
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!isElFocusable) {
|
||
|
|
firstFocusableEl.focus()
|
||
|
|
e.preventDefault()
|
||
|
|
}
|
||
|
|
|
||
|
|
if (e.shiftKey) {
|
||
|
|
if (document.activeElement === firstFocusableEl) {
|
||
|
|
lastFocusableEl.focus()
|
||
|
|
e.preventDefault()
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
if (document.activeElement === lastFocusableEl) {
|
||
|
|
firstFocusableEl.focus()
|
||
|
|
e.preventDefault()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const focusLockOn = (element, settings = {}) => {
|
||
|
|
settings = {
|
||
|
|
focusOnMount: true,
|
||
|
|
...settings,
|
||
|
|
}
|
||
|
|
|
||
|
|
if (lockedElement && lockedElement !== element) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
const focusableEls = element.querySelectorAll(tabbables.join(','))
|
||
|
|
|
||
|
|
if (focusableEls.length === 0) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
lockedElement = element
|
||
|
|
document.addEventListener('keydown', handleKeydown)
|
||
|
|
|
||
|
|
if (settings.focusOnMount && !isTouchDevice()) {
|
||
|
|
setTimeout(() => {
|
||
|
|
focusableEls[0].focus()
|
||
|
|
}, 200)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const focusLockOff = (element) => {
|
||
|
|
element.removeEventListener('keydown', handleKeydown)
|
||
|
|
lockedElement = null
|
||
|
|
}
|
||
|
|
|
||
|
|
export const focusLockManager = () => {
|
||
|
|
if (window.ctFrontend && window.ctFrontend.focusLockManager) {
|
||
|
|
return window.ctFrontend.focusLockManager
|
||
|
|
}
|
||
|
|
|
||
|
|
window.ctFrontend = window.ctFrontend || {}
|
||
|
|
|
||
|
|
window.ctFrontend.focusLockManager = {
|
||
|
|
focusLockOn,
|
||
|
|
focusLockOff,
|
||
|
|
}
|
||
|
|
|
||
|
|
return window.ctFrontend.focusLockManager
|
||
|
|
}
|