From 98dba66ee1e5fcf3b88a0fe0331c8d84e9ddfbed Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 1 Sep 2020 20:03:44 -0400 Subject: [PATCH] =?UTF-8?q?Add=20=E2=8E=87=20+=20arrow=20key=20navigation?= =?UTF-8?q?=20to=20DevTools=20(#19741)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⎇ + left/right navigates between owners (similar to owners tree) and ⎇ + up/down navigations between siblings. --- .../__snapshots__/treeContext-test.js.snap | 89 ++++++++ .../src/__tests__/treeContext-test.js | 202 ++++++++++++++++++ .../src/devtools/views/Components/Tree.js | 34 ++- .../devtools/views/Components/TreeContext.js | 140 +++++++++++- 4 files changed, 451 insertions(+), 14 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/__snapshots__/treeContext-test.js.snap b/packages/react-devtools-shared/src/__tests__/__snapshots__/treeContext-test.js.snap index 3130bd29239f9..1e72710e4bd2b 100644 --- a/packages/react-devtools-shared/src/__tests__/__snapshots__/treeContext-test.js.snap +++ b/packages/react-devtools-shared/src/__tests__/__snapshots__/treeContext-test.js.snap @@ -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] + ▾ + ▾ + + ▾ + + + + ▾ + + +`; + +exports[`TreeListContext tree state should navigate the owner hierarchy: 0: mount 1`] = ` +[root] + ▾ + ▾ + ▾ + + ▾ + ▾ + + + + ▾ + ▾ + + +`; + exports[`TreeListContext tree state should select child elements: 0: mount 1`] = ` [root] ▾ @@ -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": "", diff --git a/packages/react-devtools-shared/src/__tests__/treeContext-test.js b/packages/react-devtools-shared/src/__tests__/treeContext-test.js index b42fb0a9b6f7a..0b672f91415f6 100644 --- a/packages/react-devtools-shared/src/__tests__/treeContext-test.js +++ b/packages/react-devtools-shared/src/__tests__/treeContext-test.js @@ -268,6 +268,208 @@ describe('TreeListContext', () => { done(); }); + + it('should navigate next/previous sibling and skip over children in between', () => { + const Grandparent = () => ( + + + + + + ); + const Parent = ({numChildren}) => + new Array(numChildren) + .fill(true) + .map((_, index) => ); + const Child = () => null; + + utils.act(() => + ReactDOM.render(, document.createElement('div')), + ); + + /* + * 0 ▾ + * 1 ▾ + * 2 + * 3 ▾ + * 4 + * 5 + * 6 + * 7 ▾ + * 8 + * 9 + */ + + expect(store).toMatchSnapshot('0: mount'); + + let renderer; + utils.act(() => (renderer = TestRenderer.create())); + + const firstParentID = ((store.getElementIDAtIndex(1): any): number); + + utils.act(() => + dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: firstParentID}), + ); + utils.act(() => renderer.update()); + expect(state.selectedElementIndex).toBe(1); + + utils.act(() => dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'})); + utils.act(() => renderer.update()); + expect(state.selectedElementIndex).toBe(3); + + utils.act(() => dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'})); + utils.act(() => renderer.update()); + expect(state.selectedElementIndex).toBe(7); + + utils.act(() => dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'})); + utils.act(() => renderer.update()); + expect(state.selectedElementIndex).toBe(1); + + utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'})); + utils.act(() => renderer.update()); + expect(state.selectedElementIndex).toBe(7); + + utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'})); + utils.act(() => renderer.update()); + expect(state.selectedElementIndex).toBe(3); + + utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'})); + utils.act(() => renderer.update()); + expect(state.selectedElementIndex).toBe(1); + }); + + it('should navigate the owner hierarchy', () => { + const Wrapper = ({children}) => children; + const Grandparent = () => ( + + + + + + + + + + + + ); + const Parent = ({numChildren}) => + new Array(numChildren) + .fill(true) + .map((_, index) => ); + const Child = () => null; + + utils.act(() => + ReactDOM.render(, document.createElement('div')), + ); + + /* + * 0 ▾ + * 1 ▾ + * 2 ▾ + * 3 + * 4 ▾ + * 5 ▾ + * 6 + * 7 + * 8 + * 9 ▾ + * 10 ▾ + * 11 + * 12 + */ + + expect(store).toMatchSnapshot('0: mount'); + + let renderer; + utils.act(() => (renderer = TestRenderer.create())); + + const childID = ((store.getElementIDAtIndex(7): any): number); + utils.act(() => + dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: childID}), + ); + utils.act(() => renderer.update()); + 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()); + 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()); + expect(state.selectedElementIndex).toBe(0); + + utils.act(() => + dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}), + ); + utils.act(() => renderer.update()); + 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()); + expect(state.selectedElementIndex).toBe(5); + + utils.act(() => + dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}), + ); + utils.act(() => renderer.update()); + expect(state.selectedElementIndex).toBe(7); + + utils.act(() => + dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}), + ); + utils.act(() => renderer.update()); + 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()); + 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()); + 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()); + 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()); + 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()); + expect(state.selectedElementIndex).toBe(5); + + utils.act(() => + dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}), + ); + utils.act(() => renderer.update()); + expect(state.selectedElementIndex).toBe(5); // noop since we're at the leaf node + }); }); describe('search state', () => { diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js index 8b9f66f39f844..fe5221a66f166 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js @@ -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; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js b/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js index a2aac21026459..f748a9f3548af 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js @@ -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_NEXT_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 < 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 !== null) { + 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: