mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
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:
parent
9682f5daf7
commit
d05dc1b070
5 changed files with 45 additions and 27 deletions
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue