Made kb controls scroll friendly

Reviewed By: antonk52

Differential Revision: D41838167

fbshipit-source-id: fb32941ed750fa22797586bab8da39880131eac9
This commit is contained in:
Luke De Feo
2022-12-12 07:28:37 -08:00
committed by Facebook GitHub Bot
parent 2692476647
commit 0e51914e9e

View File

@@ -8,8 +8,16 @@
*/ */
import {Id, UINode} from '../types'; import {Id, UINode} from '../types';
import React, {Ref, useEffect, useState} from 'react'; import React, {
Ref,
RefObject,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { import {
Atom,
HighlightManager, HighlightManager,
HighlightProvider, HighlightProvider,
styled, styled,
@@ -37,11 +45,25 @@ export function Tree2({
const expandedNodes = useValue(instance.uiState.expandedNodes); const expandedNodes = useValue(instance.uiState.expandedNodes);
const searchTerm = useValue(instance.uiState.searchTerm); const searchTerm = useValue(instance.uiState.searchTerm);
const treeNodes = toTreeList(nodes, rootId, expandedNodes); const {treeNodes, refs} = useMemo(() => {
const treeNodes = toTreeList(nodes, rootId, expandedNodes);
const refs = treeNodes.map(() => React.createRef<HTMLLIElement>()); const refs: React.RefObject<HTMLLIElement>[] = treeNodes.map(() =>
React.createRef<HTMLLIElement>(),
);
useKeyboardShortcuts(treeNodes, selectedNode, onSelectNode); return {treeNodes, refs};
}, [expandedNodes, nodes, rootId]);
const isUsingKBToScroll = useRef(false);
useKeyboardShortcuts(
treeNodes,
refs,
selectedNode,
onSelectNode,
isUsingKBToScroll,
);
useEffect(() => { useEffect(() => {
if (selectedNode) { if (selectedNode) {
@@ -64,6 +86,7 @@ export function Tree2({
{treeNodes.map((treeNode, index) => ( {treeNodes.map((treeNode, index) => (
<TreeItemContainer <TreeItemContainer
innerRef={refs[index]} innerRef={refs[index]}
isUsingKBToScroll={isUsingKBToScroll}
key={treeNode.id} key={treeNode.id}
treeNode={treeNode} treeNode={treeNode}
selectedNode={selectedNode} selectedNode={selectedNode}
@@ -82,11 +105,13 @@ export type TreeNode = UINode & {
function TreeItemContainer({ function TreeItemContainer({
innerRef, innerRef,
isUsingKBToScroll,
treeNode, treeNode,
selectedNode, selectedNode,
onSelectNode, onSelectNode,
}: { }: {
innerRef: Ref<any>; innerRef: Ref<any>;
isUsingKBToScroll: RefObject<boolean>;
treeNode: TreeNode; treeNode: TreeNode;
selectedNode?: Id; selectedNode?: Id;
hoveredNode?: Id; hoveredNode?: Id;
@@ -100,7 +125,9 @@ function TreeItemContainer({
isSelected={treeNode.id === selectedNode} isSelected={treeNode.id === selectedNode}
isHovered={isHovered} isHovered={isHovered}
onMouseEnter={() => { onMouseEnter={() => {
instance.uiState.hoveredNodes.set([treeNode.id]); if (isUsingKBToScroll.current === false) {
instance.uiState.hoveredNodes.set([treeNode.id]);
}
}} }}
onClick={() => { onClick={() => {
onSelectNode(treeNode.id); onSelectNode(treeNode.id);
@@ -238,14 +265,24 @@ function toTreeList(
function useKeyboardShortcuts( function useKeyboardShortcuts(
treeNodes: TreeNode[], treeNodes: TreeNode[],
refs: React.RefObject<HTMLLIElement>[],
selectedNode: Id | undefined, selectedNode: Id | undefined,
onSelectNode: (id?: Id) => void, onSelectNode: (id?: Id) => void,
isUsingKBToScroll: React.MutableRefObject<boolean>,
) { ) {
const instance = usePlugin(plugin); const instance = usePlugin(plugin);
useEffect(() => { useEffect(() => {
const listener = (event: KeyboardEvent) => { const listener = (event: KeyboardEvent) => {
switch (event.key) { switch (event.key) {
case 'Enter': {
const hoveredNode = head(instance.uiState.hoveredNodes.get());
if (hoveredNode != null) {
onSelectNode(hoveredNode);
}
break;
}
case 'ArrowRight': { case 'ArrowRight': {
event.preventDefault(); event.preventDefault();
@@ -267,37 +304,18 @@ function useKeyboardShortcuts(
break; break;
} }
case 'ArrowDown': { case 'ArrowDown':
case 'ArrowUp':
event.preventDefault(); event.preventDefault();
moveHoveredNodeUpOrDown(
const curIdx = treeNodes.findIndex( event.key,
(item) => item.id === head(instance.uiState.hoveredNodes.get()), treeNodes,
refs,
instance.uiState.hoveredNodes,
isUsingKBToScroll,
); );
if (curIdx != -1) {
const nextIdx = curIdx + 1;
if (nextIdx < treeNodes.length) {
const nextNode = treeNodes[nextIdx];
instance.uiState.hoveredNodes.set([nextNode.id]);
}
}
break;
}
case 'ArrowUp': {
event.preventDefault();
const curIdx = treeNodes.findIndex(
(item) => item.id === head(instance.uiState.hoveredNodes.get()),
);
if (curIdx != -1) {
const prevIdx = curIdx - 1;
if (prevIdx >= 0) {
const prevNode = treeNodes[prevIdx];
instance.uiState.hoveredNodes.set([prevNode.id]);
}
}
break; break;
}
} }
}; };
window.addEventListener('keydown', listener); window.addEventListener('keydown', listener);
@@ -305,10 +323,48 @@ function useKeyboardShortcuts(
window.removeEventListener('keydown', listener); window.removeEventListener('keydown', listener);
}; };
}, [ }, [
refs,
instance.uiState.expandedNodes, instance.uiState.expandedNodes,
treeNodes, treeNodes,
onSelectNode, onSelectNode,
selectedNode, selectedNode,
instance.uiState.hoveredNodes, instance.uiState.hoveredNodes,
isUsingKBToScroll,
]); ]);
} }
export type UpOrDown = 'ArrowDown' | 'ArrowUp';
function moveHoveredNodeUpOrDown(
direction: UpOrDown,
treeNodes: TreeNode[],
refs: React.RefObject<HTMLLIElement>[],
hoveredNodes: Atom<Id[]>,
isUsingKBToScroll: React.MutableRefObject<boolean>,
) {
const curIdx = treeNodes.findIndex(
(item) => item.id === head(hoveredNodes.get()),
);
if (curIdx != -1) {
const increment = direction === 'ArrowDown' ? 1 : -1;
const newIdx = curIdx + increment;
if (newIdx >= 0 && newIdx < treeNodes.length) {
const newNode = treeNodes[newIdx];
hoveredNodes.set([newNode.id]);
const newNodeDomRef = refs[newIdx].current;
/**
* The reason for this grossness is that when scrolling to an element via keyboard, it will move a new dom node
* under the cursor which will trigger the onmouseenter event for that node even if the mouse never actually was moved.
* This will in turn cause that event handler to hover that node rather than the one the user is trying to get to via keyboard.
* This is a dubious way to work around this. Effectively set this flag for a few hundred milliseconds after using keyboard movement
* to disable the onmouseenter -> hover behaviour temporarily
*/
isUsingKBToScroll.current = true;
newNodeDomRef?.scrollIntoView({block: 'nearest'});
setTimeout(() => {
isUsingKBToScroll.current = false;
}, 250);
}
}
}