From d05dc1b070b1168965632857a18b88af3b04ef85 Mon Sep 17 00:00:00 2001 From: EnixCoda Date: Fri, 2 Nov 2018 12:08:26 +0800 Subject: [PATCH] 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. --- src/components/FileExplorer.js | 4 +-- src/components/SearchBar.js | 3 +- src/driver/core/FileExplorer.js | 51 +++++++++++++++++++++------------ src/driver/core/SideBar.js | 3 +- src/utils/DOMHelper.js | 11 +++++-- 5 files changed, 45 insertions(+), 27 deletions(-) diff --git a/src/components/FileExplorer.js b/src/components/FileExplorer.js index 3dd07c6..4d7a860 100644 --- a/src/components/FileExplorer.js +++ b/src/components/FileExplorer.js @@ -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 (
{ @@ -90,7 +90,7 @@ export default class FileExplorer extends React.Component { ? : visibleNodes && ( - + {this.renderFiles(visibleNodes, onNodeClick)} ) diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js index 9b8508e..50547a4 100644 --- a/src/components/SearchBar.js +++ b/src/components/SearchBar.js @@ -1,9 +1,10 @@ import React from 'react' -export default function SearchBar({ onSearchKeyChange }) { +export default function SearchBar({ onSearchKeyChange, onFocus }) { return (
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, diff --git a/src/driver/core/SideBar.js b/src/driver/core/SideBar.js index f5cbf9d..cdb00e2 100644 --- a/src/driver/core/SideBar.js +++ b/src/driver/core/SideBar.js @@ -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) } diff --git a/src/utils/DOMHelper.js b/src/utils/DOMHelper.js index dddca68..139b326 100644 --- a/src/utils/DOMHelper.js +++ b/src/utils/DOMHelper.js @@ -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()