fix: switch to arrow-key-navigation library (#1607)
For left/right arrow key navigation, switch to a small library I made to handle this. Also make it load asynchronously, because why not.
This commit is contained in:
parent
e569c757d1
commit
bb85bcb32b
|
@ -48,6 +48,7 @@
|
|||
"@babel/preset-env": "^7.6.3",
|
||||
"@babel/runtime": "^7.6.3",
|
||||
"@webcomponents/custom-elements": "^1.3.0",
|
||||
"arrow-key-navigation": "^1.0.1",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
||||
"blurhash": "^1.1.3",
|
||||
|
|
|
@ -1,123 +1,24 @@
|
|||
// Makes it so the left and right arrows change focus, ala Tab/Shift+Tab. This is mostly designed
|
||||
// for KaiOS devices.
|
||||
|
||||
import { importArrowKeyNavigation } from '../../_utils/asyncModules'
|
||||
|
||||
let arrowKeyNav
|
||||
|
||||
export function leftRightFocusObservers (store) {
|
||||
if (!process.browser) {
|
||||
return
|
||||
}
|
||||
|
||||
function getDialogParent (element) {
|
||||
let parent = element.parentElement
|
||||
while (parent) {
|
||||
if (parent.classList.contains('modal-dialog')) {
|
||||
return parent
|
||||
}
|
||||
parent = parent.parentElement
|
||||
}
|
||||
}
|
||||
|
||||
function getFocusableElements (activeElement) {
|
||||
const query = `
|
||||
a,
|
||||
button,
|
||||
textarea,
|
||||
input[type=text],
|
||||
input[type=number],
|
||||
input[type=search],
|
||||
input[type=radio],
|
||||
input[type=checkbox],
|
||||
select,
|
||||
[tabindex="0"]
|
||||
`
|
||||
|
||||
// Respect focus trap inside of dialogs
|
||||
const dialogParent = getDialogParent(activeElement)
|
||||
const root = dialogParent || document
|
||||
|
||||
return Array.from(root.querySelectorAll(query))
|
||||
.filter(element => {
|
||||
if (element === activeElement) {
|
||||
return true
|
||||
}
|
||||
return !element.disabled &&
|
||||
element.getAttribute('tabindex') !== '-1' &&
|
||||
(element.offsetWidth > 0 || element.offsetHeight > 0)
|
||||
})
|
||||
}
|
||||
|
||||
function shouldIgnoreEvent (activeElement, key) {
|
||||
const isTextarea = activeElement.tagName === 'TEXTAREA'
|
||||
const isTextInput = activeElement.tagName === 'INPUT' &&
|
||||
['text', 'search', 'number', 'email', 'url'].includes(activeElement.getAttribute('type').toLowerCase())
|
||||
|
||||
if (!isTextarea && !isTextInput) {
|
||||
return false
|
||||
}
|
||||
|
||||
const { selectionStart, selectionEnd } = activeElement
|
||||
// if the cursor is at the start or end of the textarea and the user wants to navigate out of it,
|
||||
// then do so
|
||||
if (key === 'ArrowLeft' && selectionStart === selectionEnd && selectionStart === 0) {
|
||||
return false
|
||||
} else if (key === 'ArrowRight' && selectionStart === selectionEnd && selectionStart === activeElement.value.length) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function focusNextOrPrevious (event, key) {
|
||||
const { activeElement } = document
|
||||
if (shouldIgnoreEvent(activeElement, key)) {
|
||||
return
|
||||
}
|
||||
const focusable = getFocusableElements(activeElement)
|
||||
const index = focusable.indexOf(activeElement)
|
||||
let element
|
||||
if (key === 'ArrowLeft') {
|
||||
console.log('focus previous')
|
||||
element = focusable[index - 1] || focusable[0]
|
||||
} else { // ArrowRight
|
||||
console.log('focus next')
|
||||
element = focusable[index + 1] || focusable[focusable.length - 1]
|
||||
}
|
||||
element.focus()
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
function handleEnter (event) {
|
||||
const { activeElement } = document
|
||||
if (activeElement.tagName === 'INPUT' && ['checkbox', 'radio'].includes(activeElement.getAttribute('type'))) {
|
||||
// Explicitly override "enter" on an input and make it fire the checkbox/radio
|
||||
activeElement.click()
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
function keyListener (event) {
|
||||
if (event.altKey || event.metaKey || event.ctrlKey) {
|
||||
return // ignore e.g. Alt-Left and Ctrl-Right, which are used to switch browser tabs or navigate back/forward
|
||||
}
|
||||
const { key } = event
|
||||
switch (key) {
|
||||
case 'ArrowLeft':
|
||||
case 'ArrowRight': {
|
||||
focusNextOrPrevious(event, key)
|
||||
break
|
||||
}
|
||||
case 'Enter': {
|
||||
handleEnter(event)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
store.observe('leftRightChangesFocus', leftRightChangesFocus => {
|
||||
store.observe('leftRightChangesFocus', async leftRightChangesFocus => {
|
||||
if (leftRightChangesFocus) {
|
||||
window.addEventListener('keydown', keyListener)
|
||||
} else {
|
||||
window.removeEventListener('keydown', keyListener)
|
||||
if (!arrowKeyNav) {
|
||||
arrowKeyNav = await importArrowKeyNavigation()
|
||||
}
|
||||
arrowKeyNav.setFocusTrapTest(element => element.classList.contains('modal-dialog'))
|
||||
arrowKeyNav.register()
|
||||
} else if (arrowKeyNav) {
|
||||
arrowKeyNav.unregister()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -59,3 +59,7 @@ export const importVirtualListStore = () => import(
|
|||
export const importPageLifecycle = () => import(
|
||||
/* webpackChunkName: 'page-lifecycle' */ 'page-lifecycle/dist/lifecycle.mjs'
|
||||
).then(getDefault)
|
||||
|
||||
export const importArrowKeyNavigation = () => import(
|
||||
/* webpackChunkName: 'arrow-key-navigation' */ 'arrow-key-navigation'
|
||||
)
|
||||
|
|
|
@ -1225,6 +1225,11 @@ array-unique@^0.3.2:
|
|||
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
|
||||
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
|
||||
|
||||
arrow-key-navigation@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/arrow-key-navigation/-/arrow-key-navigation-1.0.1.tgz#557f5c0034791acc04091843718888e18c9cd61a"
|
||||
integrity sha512-/RZFi4p3MCr6Y2y2luNh8eP7CwlVsoq+F2oiNoE+jSHgRWUbc+fLaI2/8NWqDF6XZLu5GdTvXkN70KRpN0/5Hw==
|
||||
|
||||
asar@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/asar/-/asar-2.0.1.tgz#8518a1c62c238109c15a5f742213e83a09b9fd38"
|
||||
|
|
Loading…
Reference in a new issue