mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
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:
parent
53e622ca7f
commit
98dba66ee1
|
|
@ -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": "",
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user