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/preset-env": "^7.6.3",
|
||||||
"@babel/runtime": "^7.6.3",
|
"@babel/runtime": "^7.6.3",
|
||||||
"@webcomponents/custom-elements": "^1.3.0",
|
"@webcomponents/custom-elements": "^1.3.0",
|
||||||
|
"arrow-key-navigation": "^1.0.1",
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.0.6",
|
||||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
||||||
"blurhash": "^1.1.3",
|
"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
|
// Makes it so the left and right arrows change focus, ala Tab/Shift+Tab. This is mostly designed
|
||||||
// for KaiOS devices.
|
// for KaiOS devices.
|
||||||
|
|
||||||
|
import { importArrowKeyNavigation } from '../../_utils/asyncModules'
|
||||||
|
|
||||||
|
let arrowKeyNav
|
||||||
|
|
||||||
export function leftRightFocusObservers (store) {
|
export function leftRightFocusObservers (store) {
|
||||||
if (!process.browser) {
|
if (!process.browser) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDialogParent (element) {
|
store.observe('leftRightChangesFocus', async leftRightChangesFocus => {
|
||||||
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 => {
|
|
||||||
if (leftRightChangesFocus) {
|
if (leftRightChangesFocus) {
|
||||||
window.addEventListener('keydown', keyListener)
|
if (!arrowKeyNav) {
|
||||||
} else {
|
arrowKeyNav = await importArrowKeyNavigation()
|
||||||
window.removeEventListener('keydown', keyListener)
|
}
|
||||||
|
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(
|
export const importPageLifecycle = () => import(
|
||||||
/* webpackChunkName: 'page-lifecycle' */ 'page-lifecycle/dist/lifecycle.mjs'
|
/* webpackChunkName: 'page-lifecycle' */ 'page-lifecycle/dist/lifecycle.mjs'
|
||||||
).then(getDefault)
|
).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"
|
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
|
||||||
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
|
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:
|
asar@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/asar/-/asar-2.0.1.tgz#8518a1c62c238109c15a5f742213e83a09b9fd38"
|
resolved "https://registry.yarnpkg.com/asar/-/asar-2.0.1.tgz#8518a1c62c238109c15a5f742213e83a09b9fd38"
|
||||||
|
|
Loading…
Reference in a new issue