Add ⎇ + arrow key navigation to DevTools (#19741)

⎇ + left/right navigates between owners (similar to owners tree) and ⎇ + up/down navigations between siblings.
This commit is contained in:
Brian Vaughn
2020-09-01 20:03:44 -04:00
committed by GitHub
parent 53e622ca7f
commit 98dba66ee1
4 changed files with 452 additions and 15 deletions

View File

@@ -130,7 +130,11 @@ export default function Tree(props: Props) {
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'});
if (event.altKey) {
dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'});
} else {
dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'});
}
break;
case 'ArrowLeft':
event.preventDefault();
@@ -139,10 +143,16 @@ export default function Tree(props: Props) {
? store.getElementByID(selectedElementID)
: null;
if (element !== null) {
if (element.children.length > 0 && !element.isCollapsed) {
store.toggleIsCollapsed(element.id, true);
if (event.altKey) {
if (element.ownerID !== null) {
dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'});
}
} else {
dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'});
if (element.children.length > 0 && !element.isCollapsed) {
store.toggleIsCollapsed(element.id, true);
} else {
dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'});
}
}
}
break;
@@ -153,16 +163,24 @@ export default function Tree(props: Props) {
? store.getElementByID(selectedElementID)
: null;
if (element !== null) {
if (element.children.length > 0 && element.isCollapsed) {
store.toggleIsCollapsed(element.id, false);
if (event.altKey) {
dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'});
} else {
dispatch({type: 'SELECT_CHILD_ELEMENT_IN_TREE'});
if (element.children.length > 0 && element.isCollapsed) {
store.toggleIsCollapsed(element.id, false);
} else {
dispatch({type: 'SELECT_CHILD_ELEMENT_IN_TREE'});
}
}
}
break;
case 'ArrowUp':
event.preventDefault();
dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'});
if (event.altKey) {
dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'});
} else {
dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'});
}
break;
default:
return;

View File

@@ -49,6 +49,7 @@ import type {Element} from './types';
export type StateContext = {|
// Tree
numElements: number,
ownerSubtreeLeafElementID: number | null,
selectedElementID: number | null,
selectedElementIndex: number | null,
@@ -92,15 +93,27 @@ type ACTION_SELECT_ELEMENT_BY_ID = {|
type ACTION_SELECT_NEXT_ELEMENT_IN_TREE = {|
type: 'SELECT_NEXT_ELEMENT_IN_TREE',
|};
type ACTION_SELECT_NEXT_SIBLING_IN_TREE = {|
type: 'SELECT_NEXT_SIBLING_IN_TREE',
|};
type ACTION_SELECT_OWNER = {|
type: 'SELECT_OWNER',
payload: number,
|};
type ACTION_SELECT_PARENT_ELEMENT_IN_TREE = {|
type: 'SELECT_PARENT_ELEMENT_IN_TREE',
|};
type ACTION_SELECT_PREVIOUS_ELEMENT_IN_TREE = {|
type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE',
|};
type ACTION_SELECT_OWNER = {|
type: 'SELECT_OWNER',
payload: number,
type ACTION_SELECT_PREVIOUS_SIBLING_IN_TREE = {|
type: 'SELECT_PREVIOUS_SIBLING_IN_TREE',
|};
type ACTION_SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE = {|
type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE',
|};
type ACTION_SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE = {|
type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE',
|};
type ACTION_SET_SEARCH_TEXT = {|
type: 'SET_SEARCH_TEXT',
@@ -119,9 +132,13 @@ type Action =
| ACTION_SELECT_ELEMENT_AT_INDEX
| ACTION_SELECT_ELEMENT_BY_ID
| ACTION_SELECT_NEXT_ELEMENT_IN_TREE
| ACTION_SELECT_NEXT_SIBLING_IN_TREE
| ACTION_SELECT_OWNER
| ACTION_SELECT_PARENT_ELEMENT_IN_TREE
| ACTION_SELECT_PREVIOUS_ELEMENT_IN_TREE
| ACTION_SELECT_OWNER
| ACTION_SELECT_PREVIOUS_SIBLING_IN_TREE
| ACTION_SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE
| ACTION_SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE
| ACTION_SET_SEARCH_TEXT
| ACTION_UPDATE_INSPECTED_ELEMENT_ID;
@@ -140,6 +157,7 @@ TreeDispatcherContext.displayName = 'TreeDispatcherContext';
type State = {|
// Tree
numElements: number,
ownerSubtreeLeafElementID: number | null,
selectedElementID: number | null,
selectedElementIndex: number | null,
@@ -157,7 +175,12 @@ type State = {|
|};
function reduceTreeState(store: Store, state: State, action: Action): State {
let {numElements, selectedElementIndex, selectedElementID} = state;
let {
numElements,
ownerSubtreeLeafElementID,
selectedElementIndex,
selectedElementID,
} = state;
const ownerID = state.ownerID;
let lookupIDForIndex = true;
@@ -187,6 +210,8 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
}
break;
case 'SELECT_CHILD_ELEMENT_IN_TREE':
ownerSubtreeLeafElementID = null;
if (selectedElementIndex !== null) {
const selectedElement = store.getElementAtIndex(
((selectedElementIndex: any): number),
@@ -205,9 +230,13 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
}
break;
case 'SELECT_ELEMENT_AT_INDEX':
ownerSubtreeLeafElementID = null;
selectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX).payload;
break;
case 'SELECT_ELEMENT_BY_ID':
ownerSubtreeLeafElementID = null;
// Skip lookup in this case; it would be redundant.
// It might also cause problems if the specified element was inside of a (not yet expanded) subtree.
lookupIDForIndex = false;
@@ -219,6 +248,8 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
: store.getIndexOfElementID(selectedElementID);
break;
case 'SELECT_NEXT_ELEMENT_IN_TREE':
ownerSubtreeLeafElementID = null;
if (
selectedElementIndex === null ||
selectedElementIndex + 1 >= numElements
@@ -228,12 +259,80 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
selectedElementIndex++;
}
break;
case 'SELECT_PARENT_ELEMENT_IN_TREE':
case 'SELECT_NEXT_SIBLING_IN_TREE':
ownerSubtreeLeafElementID = null;
if (selectedElementIndex !== null) {
const selectedElement = store.getElementAtIndex(
((selectedElementIndex: any): number),
);
if (selectedElement !== null && selectedElement.parentID !== null) {
if (selectedElement !== null && selectedElement.parentID !== 0) {
const parent = store.getElementByID(selectedElement.parentID);
if (parent !== null) {
const {children} = parent;
const selectedChildIndex = children.indexOf(selectedElement.id);
const nextChildID =
selectedChildIndex < children.length - 1
? children[selectedChildIndex + 1]
: children[0];
selectedElementIndex = store.getIndexOfElementID(nextChildID);
}
}
}
break;
case 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE':
if (selectedElementIndex !== null) {
if (
ownerSubtreeLeafElementID !== null &&
ownerSubtreeLeafElementID !== selectedElementID
) {
const leafElement = store.getElementByID(ownerSubtreeLeafElementID);
if (leafElement !== null) {
let currentElement = leafElement;
while (currentElement !== null) {
if (currentElement.ownerID === selectedElementID) {
selectedElementIndex = store.getIndexOfElementID(
currentElement.id,
);
break;
} else if (currentElement.ownerID !== 0) {
currentElement = store.getElementByID(currentElement.ownerID);
}
}
}
}
}
break;
case 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE':
if (selectedElementIndex !== null) {
if (ownerSubtreeLeafElementID === null) {
// If this is the first time we're stepping through the owners tree,
// pin the current component as the owners list leaf.
// This will enable us to step back down to this component.
ownerSubtreeLeafElementID = selectedElementID;
}
const selectedElement = store.getElementAtIndex(
((selectedElementIndex: any): number),
);
if (selectedElement !== null && selectedElement.ownerID !== 0) {
const ownerIndex = store.getIndexOfElementID(
selectedElement.ownerID,
);
if (ownerIndex !== null) {
selectedElementIndex = ownerIndex;
}
}
}
break;
case 'SELECT_PARENT_ELEMENT_IN_TREE':
ownerSubtreeLeafElementID = null;
if (selectedElementIndex !== null) {
const selectedElement = store.getElementAtIndex(
((selectedElementIndex: any): number),
);
if (selectedElement !== null && selectedElement.parentID !== 0) {
const parentIndex = store.getIndexOfElementID(
selectedElement.parentID,
);
@@ -244,12 +343,35 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
}
break;
case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
ownerSubtreeLeafElementID = null;
if (selectedElementIndex === null || selectedElementIndex === 0) {
selectedElementIndex = numElements - 1;
} else {
selectedElementIndex--;
}
break;
case 'SELECT_PREVIOUS_SIBLING_IN_TREE':
ownerSubtreeLeafElementID = null;
if (selectedElementIndex !== null) {
const selectedElement = store.getElementAtIndex(
((selectedElementIndex: any): number),
);
if (selectedElement !== null && selectedElement.parentID !== 0) {
const parent = store.getElementByID(selectedElement.parentID);
if (parent !== null) {
const {children} = parent;
const selectedChildIndex = children.indexOf(selectedElement.id);
const nextChildID =
selectedChildIndex > 0
? children[selectedChildIndex - 1]
: children[children.length - 1];
selectedElementIndex = store.getIndexOfElementID(nextChildID);
}
}
}
break;
default:
// React can bailout of no-op updates.
return state;
@@ -271,6 +393,7 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
...state,
numElements,
ownerSubtreeLeafElementID,
selectedElementIndex,
selectedElementID,
};
@@ -653,8 +776,12 @@ function TreeContextController({
case 'SELECT_ELEMENT_BY_ID':
case 'SELECT_CHILD_ELEMENT_IN_TREE':
case 'SELECT_NEXT_ELEMENT_IN_TREE':
case 'SELECT_NEXT_SIBLING_IN_TREE':
case 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE':
case 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE':
case 'SELECT_PARENT_ELEMENT_IN_TREE':
case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
case 'SELECT_PREVIOUS_SIBLING_IN_TREE':
case 'SELECT_OWNER':
case 'UPDATE_INSPECTED_ELEMENT_ID':
case 'SET_SEARCH_TEXT':
@@ -687,6 +814,7 @@ function TreeContextController({
const [state, dispatch] = useReducer(reducer, {
// Tree
numElements: store.numElements,
ownerSubtreeLeafElementID: null,
selectedElementID:
defaultSelectedElementID == null ? null : defaultSelectedElementID,
selectedElementIndex: