feat: better keyboard control flow

Search input and nodes cannot be focused at same time.
Otherwise, moving cursor left/right through search key results in unexpected file nav.
This commit is contained in:
EnixCoda 2018-11-02 12:08:26 +08:00
parent 9682f5daf7
commit d05dc1b070
No known key found for this signature in database
GPG key ID: 6825847C88AA329A
5 changed files with 45 additions and 27 deletions

View file

@ -82,7 +82,7 @@ export default class FileExplorer extends React.Component {
}
render() {
const { stateText, visibleNodes, freeze, handleKeyDown, handleSearchKeyChange, onNodeClick, toggleShowSettings } = this.props
const { stateText, visibleNodes, freeze, handleKeyDown, handleSearchKeyChange, onNodeClick, toggleShowSettings, onFocusSearchBar } = this.props
return (
<div className={cx(`file-explorer`, { freeze })} tabIndex={-1} onKeyDown={handleKeyDown} onClick={ freeze ? toggleShowSettings : null}>
{
@ -90,7 +90,7 @@ export default class FileExplorer extends React.Component {
? <LoadingIndicator text={stateText} />
: visibleNodes && (
<React.Fragment>
<SearchBar onSearchKeyChange={handleSearchKeyChange} />
<SearchBar onSearchKeyChange={handleSearchKeyChange} onFocus={onFocusSearchBar} />
{this.renderFiles(visibleNodes, onNodeClick)}
</React.Fragment>
)

View file

@ -1,9 +1,10 @@
import React from 'react'
export default function SearchBar({ onSearchKeyChange }) {
export default function SearchBar({ onSearchKeyChange, onFocus }) {
return (
<div className={'search-input-wrapper'}>
<input
onFocus={onFocus}
tabIndex={0}
className="form-control search-input"
aria-label="search files"

View file

@ -87,26 +87,31 @@ const setStateText = dispatch => text => dispatch({
stateText: text,
})
const handleKeyDown = dispatch => ({ key }) => dispatch(({ visibleNodes: { nodes, focusedNode, expandedNodes, depths } }) => {
const handleKeyDown = dispatch => event => dispatch(({ visibleNodes: { nodes, focusedNode, expandedNodes, depths } }) => {
function handleVerticalMove(index) {
if (0 <= index && index < nodes.length) {
DOMHelper.focusFileExplorer()
dispatch(focusNode, nodes[index])
} else {
DOMHelper.focusSearchInput()
dispatch(focusNode, null)
}
}
const { key } = event
// prevent document body scrolling if the keypress results in Gitako action
let muteEvent = true
if (focusedNode) {
const focusedNodeIndex = nodes.indexOf(focusedNode)
switch (key) {
case 'ArrowUp':
// focus on previous node
if (focusedNodeIndex === 0) {
dispatch(focusNode, nodes[nodes.length - 1])
} else {
dispatch(focusNode, nodes[focusedNodeIndex - 1])
}
handleVerticalMove(focusedNodeIndex - 1)
break
case 'ArrowDown':
// focus on next node
if (focusedNodeIndex + 1 < nodes.length) {
dispatch(focusNode, nodes[focusedNodeIndex + 1])
} else {
dispatch(focusNode, nodes[0])
}
handleVerticalMove(focusedNodeIndex + 1)
break
case 'ArrowLeft':
@ -137,7 +142,6 @@ const handleKeyDown = dispatch => ({ key }) => dispatch(({ visibleNodes: { nodes
} else if (focusedNode.type === 'blob') {
DOMHelper.loadWithPJAX(focusedNode.url)
} else if (focusedNode.type === 'commit') {
// redirect to its parent folder
window.open(focusedNode.url)
}
break
@ -148,27 +152,39 @@ const handleKeyDown = dispatch => ({ key }) => dispatch(({ visibleNodes: { nodes
} else if (focusedNode.type === 'blob') {
DOMHelper.loadWithPJAX(focusedNode.url)
} else if (focusedNode.type === 'commit') {
// redirect to its parent folder
window.open(focusedNode.url)
}
break
default:
muteEvent = false
}
if (muteEvent) {
event.preventDefault()
}
} else {
// now search input is focused
if (nodes.length) {
switch (key) {
case 'ArrowDown':
DOMHelper.focusFileExplorer()
dispatch(focusNode, nodes[0])
break
case 'ArrowUp':
DOMHelper.focusFileExplorer()
dispatch(focusNode, nodes[nodes.length - 1])
break
default:
muteEvent = false
}
if (muteEvent) {
event.preventDefault()
}
}
}
})
const onFocusSearchBar = dispatch => () => dispatch(focusNode, null)
const handleSearchKeyChange = dispatch => {
let i = 0
return async event => {
@ -187,10 +203,7 @@ function shouldDelayExpand(node) {
const setExpand = dispatch => (node, expand) => {
visibleNodesGenerator.setExpand(node, expand)
const applyChanges = () => {
dispatch(focusNode, node)
tasksAfterRender.push(DOMHelper.focusSearchInput)
}
const applyChanges = () => dispatch(focusNode, node)
if (shouldDelayExpand(node)) {
dispatch(mountExpandingIndicator, node)
tasksAfterRender.push(() => setTimeout(applyChanges, 0))
@ -219,7 +232,6 @@ const focusNode = dispatch => (node, skipScroll) => dispatch(({ visibleNodes: {
// when focus a node not in viewport(by keyboard), scroll to it
const indexOfToBeFocusedNode = nodes.indexOf(node)
tasksAfterRender.push(() => DOMHelper.scrollToNodeElement(indexOfToBeFocusedNode))
tasksAfterRender.push(DOMHelper.focusSearchInput)
}
dispatch(updateVisibleNodes)
})
@ -261,6 +273,7 @@ export default {
execAfterRender,
setStateText,
handleKeyDown,
onFocusSearchBar,
handleSearchKeyChange,
setExpand,
toggleNodeExpansion,

View file

@ -73,7 +73,6 @@ const onPJAXEnd = dispatch => () => {
dispatch(({ metaData, copyFileButton, copySnippetButton }) => {
DOMHelper.unmountTopProgressBar()
DOMHelper.decorateGitHubPageContent({ copyFileButton, copySnippetButton })
DOMHelper.focusSearchInput()
const mergedMetaData = { ...metaData, ...URLHelper.parse() }
dispatch(setShouldShow, URLHelper.isInCodePage(mergedMetaData))
dispatch(setMetaData, mergedMetaData)
@ -94,7 +93,7 @@ const onKeyDown = dispatch => e => {
const toggleShowSideBar = dispatch => () => dispatch(({ shouldShow }) => dispatch(setShouldShow, !shouldShow))
const setShouldShow = dispatch => shouldShow => {
dispatch({ shouldShow }, shouldShow ? DOMHelper.focusSearchInput : null)
dispatch({ shouldShow }, shouldShow ? DOMHelper.focusFileExplorer : null)
DOMHelper.setBodyIndent(shouldShow)
}

View file

@ -40,7 +40,7 @@ function getCurrentBranch() {
/**
* add the logo element into DOM
*
*
*/
function insertLogoMountPoint() {
const logoSelector = '.gitako .gitako-logo'
@ -297,16 +297,21 @@ function attachCopySnippet() {
* focus to side bar, user will be able to manipulate it with keyboard
*/
function focusFileExplorer() {
const sideBarContentSelector = '.gitako .file-explorer'
const sideBarContentSelector = '.gitako-side-bar .file-explorer'
const sideBarElement = document.querySelector(sideBarContentSelector)
if (sideBarElement) {
sideBarElement.focus()
}
}
function focusSearchInput() {
function getSearchInput() {
const searchInputSelector = '.search-input'
const searchInputElement = document.querySelector(searchInputSelector)
return searchInputElement
}
function focusSearchInput() {
const searchInputElement = getSearchInput()
if (searchInputElement) {
if (document.activeElement !== searchInputElement) {
searchInputElement.focus()