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:
Nolan Lawson 2019-10-28 08:16:51 -07:00 committed by GitHub
parent e569c757d1
commit bb85bcb32b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 22 additions and 111 deletions

View File

@ -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",

View File

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

View File

@ -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'
)

View File

@ -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"