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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 452 additions and 15 deletions

View File

@ -15,6 +15,7 @@ Object {
"numElements": 5,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -73,6 +74,7 @@ Object {
},
],
"ownerID": 4,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -131,6 +133,7 @@ Object {
},
],
"ownerID": 4,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -145,6 +148,7 @@ Object {
"numElements": 5,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -165,6 +169,7 @@ Object {
"numElements": 2,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -193,6 +198,7 @@ Object {
},
],
"ownerID": 3,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -207,6 +213,7 @@ Object {
"numElements": 1,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -235,6 +242,7 @@ Object {
},
],
"ownerID": 2,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -249,6 +257,7 @@ Object {
"numElements": 0,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -271,6 +280,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -328,6 +338,7 @@ Object {
},
],
"ownerID": 3,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -371,6 +382,7 @@ Object {
},
],
"ownerID": 3,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -399,6 +411,7 @@ Object {
},
],
"ownerID": 3,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -421,6 +434,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -478,6 +492,7 @@ Object {
},
],
"ownerID": 3,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -492,6 +507,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -512,6 +528,7 @@ Object {
"numElements": 2,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -526,6 +543,7 @@ Object {
"numElements": 2,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": 0,
"searchResults": Array [
3,
@ -542,6 +560,7 @@ Object {
"numElements": 3,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": 0,
"searchResults": Array [
3,
@ -567,6 +586,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -581,6 +601,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": 0,
"searchResults": Array [
3,
@ -598,6 +619,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": 0,
"searchResults": Array [
2,
@ -614,6 +636,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "y",
@ -628,6 +651,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": 0,
"searchResults": Array [
5,
@ -651,6 +675,7 @@ Object {
"numElements": 3,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -665,6 +690,7 @@ Object {
"numElements": 3,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": 0,
"searchResults": Array [
3,
@ -682,6 +708,7 @@ Object {
"numElements": 3,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": 1,
"searchResults": Array [
3,
@ -699,6 +726,7 @@ Object {
"numElements": 2,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": 0,
"searchResults": Array [
3,
@ -723,6 +751,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -737,6 +766,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": 0,
"searchResults": Array [
3,
@ -755,6 +785,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": 1,
"searchResults": Array [
3,
@ -773,6 +804,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": 2,
"searchResults": Array [
3,
@ -791,6 +823,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": 1,
"searchResults": Array [
3,
@ -809,6 +842,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": 0,
"searchResults": Array [
3,
@ -827,6 +861,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": 2,
"searchResults": Array [
3,
@ -845,6 +880,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": 0,
"searchResults": Array [
3,
@ -871,6 +907,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -885,6 +922,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -899,6 +937,7 @@ Object {
"numElements": 2,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -913,6 +952,7 @@ Object {
"numElements": 0,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -921,6 +961,37 @@ Object {
}
`;
exports[`TreeListContext tree state should navigate next/previous sibling and skip over children in between: 0: mount 1`] = `
[root]
▾ <Grandparent>
▾ <Parent>
<Child key="0">
▾ <Parent>
<Child key="0">
<Child key="1">
<Child key="2">
▾ <Parent>
<Child key="0">
<Child key="1">
`;
exports[`TreeListContext tree state should navigate the owner hierarchy: 0: mount 1`] = `
[root]
▾ <Grandparent>
▾ <Wrapper>
▾ <Parent>
<Child key="0">
▾ <Wrapper>
▾ <Parent>
<Child key="0">
<Child key="1">
<Child key="2">
▾ <Wrapper>
▾ <Parent>
<Child key="0">
<Child key="1">
`;
exports[`TreeListContext tree state should select child elements: 0: mount 1`] = `
[root]
▾ <Grandparent>
@ -938,6 +1009,7 @@ Object {
"numElements": 7,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -952,6 +1024,7 @@ Object {
"numElements": 7,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -966,6 +1039,7 @@ Object {
"numElements": 7,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -980,6 +1054,7 @@ Object {
"numElements": 7,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -1005,6 +1080,7 @@ Object {
"numElements": 7,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -1019,6 +1095,7 @@ Object {
"numElements": 7,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -1033,6 +1110,7 @@ Object {
"numElements": 7,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -1047,6 +1125,7 @@ Object {
"numElements": 7,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -1069,6 +1148,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -1083,6 +1163,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -1097,6 +1178,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -1111,6 +1193,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -1125,6 +1208,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -1139,6 +1223,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -1153,6 +1238,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -1167,6 +1253,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -1181,6 +1268,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",
@ -1195,6 +1283,7 @@ Object {
"numElements": 4,
"ownerFlatTree": null,
"ownerID": null,
"ownerSubtreeLeafElementID": null,
"searchIndex": null,
"searchResults": Array [],
"searchText": "",

View File

@ -268,6 +268,208 @@ describe('TreeListContext', () => {
done();
});
it('should navigate next/previous sibling and skip over children in between', () => {
const Grandparent = () => (
<React.Fragment>
<Parent numChildren={1} />
<Parent numChildren={3} />
<Parent numChildren={2} />
</React.Fragment>
);
const Parent = ({numChildren}) =>
new Array(numChildren)
.fill(true)
.map((_, index) => <Child key={index} />);
const Child = () => null;
utils.act(() =>
ReactDOM.render(<Grandparent />, document.createElement('div')),
);
/*
* 0 <Grandparent>
* 1 <Parent>
* 2 <Child key="0">
* 3 <Parent>
* 4 <Child key="0">
* 5 <Child key="1">
* 6 <Child key="2">
* 7 <Parent>
* 8 <Child key="0">
* 9 <Child key="1">
*/
expect(store).toMatchSnapshot('0: mount');
let renderer;
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
const firstParentID = ((store.getElementIDAtIndex(1): any): number);
utils.act(() =>
dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: firstParentID}),
);
utils.act(() => renderer.update(<Contexts />));
expect(state.selectedElementIndex).toBe(1);
utils.act(() => dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state.selectedElementIndex).toBe(3);
utils.act(() => dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state.selectedElementIndex).toBe(7);
utils.act(() => dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state.selectedElementIndex).toBe(1);
utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state.selectedElementIndex).toBe(7);
utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state.selectedElementIndex).toBe(3);
utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state.selectedElementIndex).toBe(1);
});
it('should navigate the owner hierarchy', () => {
const Wrapper = ({children}) => children;
const Grandparent = () => (
<React.Fragment>
<Wrapper>
<Parent numChildren={1} />
</Wrapper>
<Wrapper>
<Parent numChildren={3} />
</Wrapper>
<Wrapper>
<Parent numChildren={2} />
</Wrapper>
</React.Fragment>
);
const Parent = ({numChildren}) =>
new Array(numChildren)
.fill(true)
.map((_, index) => <Child key={index} />);
const Child = () => null;
utils.act(() =>
ReactDOM.render(<Grandparent />, document.createElement('div')),
);
/*
* 0 <Grandparent>
* 1 <Wrapper>
* 2 <Parent>
* 3 <Child key="0">
* 4 <Wrapper>
* 5 <Parent>
* 6 <Child key="0">
* 7 <Child key="1">
* 8 <Child key="2">
* 9 <Wrapper>
* 10 <Parent>
* 11 <Child key="0">
* 12 <Child key="1">
*/
expect(store).toMatchSnapshot('0: mount');
let renderer;
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
const childID = ((store.getElementIDAtIndex(7): any): number);
utils.act(() =>
dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: childID}),
);
utils.act(() => renderer.update(<Contexts />));
expect(state.ownerSubtreeLeafElementID).toBeNull();
expect(state.selectedElementIndex).toBe(7);
// Basic navigation test
utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
);
utils.act(() => renderer.update(<Contexts />));
expect(state.ownerSubtreeLeafElementID).toBe(childID);
expect(state.selectedElementIndex).toBe(5);
utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
);
utils.act(() => renderer.update(<Contexts />));
expect(state.selectedElementIndex).toBe(0);
utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
);
utils.act(() => renderer.update(<Contexts />));
expect(state.selectedElementIndex).toBe(0); // noop since we're at the top
utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),
);
utils.act(() => renderer.update(<Contexts />));
expect(state.selectedElementIndex).toBe(5);
utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),
);
utils.act(() => renderer.update(<Contexts />));
expect(state.selectedElementIndex).toBe(7);
utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),
);
utils.act(() => renderer.update(<Contexts />));
expect(state.selectedElementIndex).toBe(7); // noop since we're at the leaf node
// Other navigational actions should clear out the temporary owner chain.
utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state.selectedElementIndex).toBe(6);
expect(state.ownerSubtreeLeafElementID).toBeNull();
const parentID = ((store.getElementIDAtIndex(5): any): number);
utils.act(() =>
dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: parentID}),
);
utils.act(() => renderer.update(<Contexts />));
expect(state.ownerSubtreeLeafElementID).toBeNull();
expect(state.selectedElementIndex).toBe(5);
// It should not be possible to navigate beyond the owner chain leaf.
utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
);
utils.act(() => renderer.update(<Contexts />));
expect(state.ownerSubtreeLeafElementID).toBe(parentID);
expect(state.selectedElementIndex).toBe(0);
utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
);
utils.act(() => renderer.update(<Contexts />));
expect(state.selectedElementIndex).toBe(0); // noop since we're at the top
utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),
);
utils.act(() => renderer.update(<Contexts />));
expect(state.selectedElementIndex).toBe(5);
utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),
);
utils.act(() => renderer.update(<Contexts />));
expect(state.selectedElementIndex).toBe(5); // noop since we're at the leaf node
});
});
describe('search state', () => {

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: