diff --git a/src/backend/renderer.js b/src/backend/renderer.js
index ec6ec23049..9a5ac9da77 100644
--- a/src/backend/renderer.js
+++ b/src/backend/renderer.js
@@ -543,6 +543,10 @@ export function attach(
addOperation(operation);
} else {
const { displayName, key, type } = getDataForFiber(fiber);
+ const { _debugOwner } = fiber;
+
+ const ownerID =
+ _debugOwner !== null ? getFiberID(getPrimaryFiber(_debugOwner)) : 0;
let encodedDisplayName = ((null: any): Uint8Array);
let encodedKey = ((null: any): Uint8Array);
@@ -565,19 +569,20 @@ export function attach(
const encodedKeySize = key === null ? 0 : encodedKey.length;
const operation = new Uint32Array(
- 6 + encodedDisplayNameSize + encodedKeySize
+ 7 + encodedDisplayNameSize + encodedKeySize
);
operation[0] = TREE_OPERATION_ADD;
operation[1] = id;
operation[2] = type;
operation[3] = getFiberID(getPrimaryFiber(parentFiber));
- operation[4] = encodedDisplayNameSize;
+ operation[4] = ownerID;
+ operation[5] = encodedDisplayNameSize;
if (displayName !== null) {
- operation.set(encodedDisplayName, 5);
+ operation.set(encodedDisplayName, 6);
}
- operation[5 + encodedDisplayNameSize] = encodedKeySize;
+ operation[6 + encodedDisplayNameSize] = encodedKeySize;
if (key !== null) {
- operation.set(encodedKey, 5 + encodedDisplayNameSize + 1);
+ operation.set(encodedKey, 6 + encodedDisplayNameSize + 1);
}
addOperation(operation);
}
diff --git a/src/devtools/store.js b/src/devtools/store.js
index 645a170a46..8e3166970e 100644
--- a/src/devtools/store.js
+++ b/src/devtools/store.js
@@ -202,6 +202,7 @@ export default class Store extends EventEmitter {
while (i < operations.length) {
let id: number = ((null: any): number);
let element: Element = ((null: any): Element);
+ let ownerID: number = 0;
let parentID: number = ((null: any): number);
let parentElement: Element = ((null: any): Element);
let type: ElementType = ((null: any): ElementType);
@@ -229,6 +230,7 @@ export default class Store extends EventEmitter {
displayName: null,
id,
key: null,
+ ownerID: 0,
parentID: 0,
type,
weight: 0,
@@ -236,6 +238,9 @@ export default class Store extends EventEmitter {
haveRootsChanged = true;
} else {
+ ownerID = ((operations[i]: any): number);
+ i++;
+
const displayNameLength = operations[i];
i++;
const displayName =
@@ -273,6 +278,7 @@ export default class Store extends EventEmitter {
displayName,
id,
key,
+ ownerID,
parentID: parentElement.id,
type,
weight: 1,
diff --git a/src/devtools/types.js b/src/devtools/types.js
index 58e421eab7..047fe86919 100644
--- a/src/devtools/types.js
+++ b/src/devtools/types.js
@@ -28,6 +28,9 @@ export type Element = {|
displayName: string | null,
key: number | string | null,
+ // Owner (if available)
+ ownerID: number,
+
// How many levels deep within the tree is this element?
// This determines how much indentation (left padding) should be used in the Elements tree.
depth: number,
diff --git a/src/devtools/views/ButtonIcon.js b/src/devtools/views/ButtonIcon.js
index 6f054b1d0a..c6056d72fc 100644
--- a/src/devtools/views/ButtonIcon.js
+++ b/src/devtools/views/ButtonIcon.js
@@ -4,12 +4,22 @@ import React from 'react';
import styles from './ButtonIcon.css';
type Props = {|
- type: 'close' | 'down' | 'search' | 'up' | 'view-dom' | 'view-source',
+ type:
+ | 'back'
+ | 'close'
+ | 'down'
+ | 'search'
+ | 'up'
+ | 'view-dom'
+ | 'view-source',
|};
export default function ButtonIcon({ type }: Props) {
let pathData = null;
switch (type) {
+ case 'back':
+ pathData = PATH_BACK;
+ break;
case 'close':
pathData = PATH_CLOSE;
break;
@@ -47,6 +57,12 @@ export default function ButtonIcon({ type }: Props) {
);
}
+const PATH_BACK = `
+ M22 3H7c-.69 0-1.23.35-1.59.88L0 12l5.41 8.11c.36.53.9.89 1.59.89h15c1.1 0 2-.9
+ 2-2V5c0-1.1-.9-2-2-2zm-3 12.59L17.59 17 14 13.41 10.41 17 9 15.59 12.59 12 9 8.41
+ 10.41 7 14 10.59 17.59 7 19 8.41 15.41 12 19 15.59z
+`;
+
const PATH_CLOSE =
'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z';
diff --git a/src/devtools/views/Element.js b/src/devtools/views/Element.js
index 7e92ad3013..5825e812aa 100644
--- a/src/devtools/views/Element.js
+++ b/src/devtools/views/Element.js
@@ -7,6 +7,8 @@ import { createRegExp } from './utils';
import { SearchAndSelectionContext } from './SearchAndSelectionContext';
import Icon from './Icon';
+import type { Element } from '../types';
+
import styles from './Element.css';
type Props = {
@@ -14,25 +16,32 @@ type Props = {
style: Object,
};
-export default function Element({ index, style }: Props) {
+export default function ElementView({ index, style }: Props) {
const { store } = useContext(TreeContext);
- const element = store.getElementAtIndex(index);
- // DevTools are rendered in concurrent mode.
- // It's possible the store has updated since the commit that triggered this render.
- // So we need to guard against an undefined element.
- // TODO: Handle this by switching to a Suspense based approach.
- if (element == null) {
- return null;
+ const {
+ ownerList,
+ pushOwnerList,
+ selectedElementID,
+ selectElementWithID,
+ } = useContext(SearchAndSelectionContext);
+
+ const element =
+ ownerList !== null
+ ? ((store.getElementByID(ownerList[index]): any): Element)
+ : ((store.getElementAtIndex(index): any): Element);
+ const { children, depth, displayName, id, key, type } = element;
+
+ let calculatedDepth = depth;
+ if (ownerList !== null) {
+ const owner = ((store.getElementByID(ownerList[0]): any): Element);
+ calculatedDepth = depth - owner.depth;
}
+ const handleDoubleClick = useCallback(() => pushOwnerList(id), [id]);
+
// TODO Add click and key handlers for toggling element open/close state.
- const { children, depth, displayName, id, key, type } = element;
-
- const { selectedElementID, selectElementWithID } = useContext(
- SearchAndSelectionContext
- );
const handleClick = useCallback(
({ metaKey }) => selectElementWithID(metaKey ? null : id),
[id]
@@ -45,9 +54,10 @@ export default function Element({ index, style }: Props) {
{children.length > 0 && (
diff --git a/src/devtools/views/OwnersStack.css b/src/devtools/views/OwnersStack.css
new file mode 100644
index 0000000000..c579c24225
--- /dev/null
+++ b/src/devtools/views/OwnersStack.css
@@ -0,0 +1,49 @@
+.OwnerStack {
+ flex: 1;
+ display: flex;
+ align-items: center;
+}
+
+.IconButton {
+ border: none;
+ background: none;
+ display: inline-flex;
+ align-items: center;
+ padding: 0.25rem;
+ cursor: pointer;
+ color: var(--color-button);
+ flex: 0 0 auto;
+}
+.IconButton:hover {
+ color: var(--color-button-hover);
+}
+.IconButton:disabled {
+ color: var(--color-dimmer);
+ cursor: default;
+}
+
+.Component,
+.FocusedComponent {
+ padding: 0.25rem;
+ margin-right: 0.5rem;
+ color: var(--color-component-name);
+ font-family: var(--font-family-monospace);
+ font-size: var(--font-size-normal);
+}
+
+.Component {
+ cursor: pointer;
+ color: var(--color-component-name);
+ background: var(--color-dimmest);
+ border-radius: 0.125rem;
+}
+.Component:hover {
+ background: var(--color-dimmer);
+}
+
+.VRule {
+ height: 20px;
+ width: 1px;
+ background-color: var(--color-border);
+ margin-right: 0.5rem;
+}
diff --git a/src/devtools/views/OwnersStack.js b/src/devtools/views/OwnersStack.js
new file mode 100644
index 0000000000..6247300a09
--- /dev/null
+++ b/src/devtools/views/OwnersStack.js
@@ -0,0 +1,63 @@
+// @flow
+
+import React, { useCallback, useContext } from 'react';
+import ButtonIcon from './ButtonIcon';
+import { SearchAndSelectionContext } from './SearchAndSelectionContext';
+import { TreeContext } from './context';
+
+import type { Element } from '../types';
+
+import styles from './OwnersStack.css';
+
+export default function OwnerStack() {
+ const { clearOwnerList, ownerIDStack } = useContext(
+ SearchAndSelectionContext
+ );
+
+ // $FlowFixMe "Missing type annotation for U" whatever that means
+ const elements = ownerIDStack.map(id =>
);
+
+ return (
+
+ );
+}
+
+type Props = {
+ id: number,
+};
+
+function ElementView({ id }: Props) {
+ const { ownerIDStack, popToOwnerList } = useContext(
+ SearchAndSelectionContext
+ );
+ const { store } = useContext(TreeContext);
+ const { displayName } = ((store.getElementByID(id): any): Element);
+
+ const isCurrentlyFocusedOwner = ownerIDStack[ownerIDStack.length - 1] === id;
+
+ const handleClick = useCallback(() => popToOwnerList(id), [
+ id,
+ popToOwnerList,
+ ]);
+
+ return (
+
+ {displayName}
+
+ );
+}
diff --git a/src/devtools/views/SearchAndSelectionContext.js b/src/devtools/views/SearchAndSelectionContext.js
index 886c0a16be..30656d20cc 100644
--- a/src/devtools/views/SearchAndSelectionContext.js
+++ b/src/devtools/views/SearchAndSelectionContext.js
@@ -1,7 +1,7 @@
// @flow
-// This context contains both the current search state and the selected element.
-// These values are combined into a single context because changes in one often impact the other.
+// This context contains both the search, owners stack, and selected element states.
+// These values are combined into a single context because changes in one often impact the others.
// Combining them enables us to avoid cascading renders.
import React, {
@@ -19,7 +19,9 @@ import Store from '../store';
import type { Element } from 'src/devtools/types';
type Context = {|
- // Read current context values:
+ // Read current context values
+ ownerIDStack: Array
,
+ ownerList: Array | null,
searchIndex: number | null,
searchResults: Array,
searchText: string,
@@ -27,10 +29,14 @@ type Context = {|
selectedElementIndex: number | null,
// Update current context values:
+ clearOwnerList: () => void,
decrementSearchIndex(): void,
incrementSearchIndex(): void,
+ popToOwnerList(elementID: number): void,
+ pushOwnerList(elementID: number): void,
selectElementWithID(id: number | null): void,
- selectElementAtIndex(index: number | null): void,
+ selectNextElementInTree(): void,
+ selectPreviousElementInTree(): void,
updateSearchText(searchText: string): void,
|};
@@ -41,6 +47,8 @@ const SearchAndSelectionContext = createContext(
SearchAndSelectionContext.displayName = 'SearchAndSelectionContext';
type State = {|
+ ownerIDStack: Array,
+ ownerList: Array | null,
searchIndex: number | null,
searchResults: Array,
searchText: string,
@@ -50,11 +58,15 @@ type State = {|
type Action = {|
type:
+ | 'CLEAR_OWNER_LIST'
| 'DECREMENT_SEARCH_INDEX'
| 'INCREMENT_SEARCH_INDEX'
- | 'REFINE_SEARCH_RESULTS'
+ | 'POP_TO_OWNER_LIST'
+ | 'PUSH_OWNER_LIST'
+ | 'REFINE_AFTER_MUTATION'
| 'SELECT_ELEMENT_WITH_ID'
- | 'SELECT_ELEMENT_AT_INDEX'
+ | 'SELECT_NEXT_ELEMENT_IN_TREE'
+ | 'SELECT_PREVIOUS_ELEMENT_IN_TREE'
| 'UPDATE_SEARCH_TEXT',
payload?: any,
|};
@@ -73,16 +85,70 @@ function SearchAndSelectionController({ children }: Props) {
const reducer = useMemo(
() =>
function reducer(state: State, action: Action): State {
- const { searchIndex, searchResults, searchText } = state;
+ const {
+ ownerIDStack,
+ ownerList,
+ searchIndex,
+ searchResults,
+ searchText,
+ selectedElementID,
+ selectedElementIndex,
+ } = state;
const { payload, type } = action;
- let newSearchIndex,
+ let element,
+ elementID,
+ newOwnerList,
+ newOwnerIDStack,
+ newSearchIndex,
newSearchResults,
newSearchText,
- newSelectedElementID;
+ newSelectedElementID,
+ newSelectedElementIndex;
+
+ const updateSelectedElementIndexHelper = (
+ newSelectedElementIndex: number
+ ) => {
+ if (ownerList !== null) {
+ newSelectedElementID =
+ newSelectedElementIndex !== null
+ ? ownerList[newSelectedElementIndex]
+ : null;
+
+ return {
+ ...state,
+ selectedElementID: newSelectedElementID,
+ selectedElementIndex: newSelectedElementIndex,
+ };
+ } else {
+ element =
+ newSelectedElementIndex !== null
+ ? store.getElementAtIndex(newSelectedElementIndex)
+ : null;
+
+ return {
+ ...state,
+ selectedElementID: element !== null ? element.id : null,
+ selectedElementIndex: newSelectedElementIndex,
+ };
+ }
+ };
switch (type) {
+ case 'CLEAR_OWNER_LIST':
+ return {
+ ...state,
+ ownerList: null,
+ ownerIDStack: [],
+ };
+
case 'DECREMENT_SEARCH_INDEX':
+ if (ownerList !== null) {
+ throw Error(
+ `Can't update search index while owner list is active`
+ );
+ }
+
if (searchResults.length === 0) return state;
newSearchIndex =
@@ -101,6 +167,12 @@ function SearchAndSelectionController({ children }: Props) {
};
case 'INCREMENT_SEARCH_INDEX':
+ if (ownerList !== null) {
+ throw Error(
+ `Can't update search index while owner list is active`
+ );
+ }
+
if (searchResults.length === 0) return state;
newSearchIndex =
@@ -118,108 +190,245 @@ function SearchAndSelectionController({ children }: Props) {
),
};
- case 'REFINE_SEARCH_RESULTS':
+ case 'POP_TO_OWNER_LIST':
+ elementID = ((payload: any): number);
+ return {
+ ...state,
+ ownerList: calculateCurrentOwnerList(
+ store,
+ elementID,
+ elementID,
+ []
+ ),
+ ownerIDStack: ownerIDStack.slice(
+ 0,
+ ownerIDStack.indexOf(elementID) + 1
+ ),
+ selectedElementIndex: null,
+ selectedElementID: null,
+ };
+
+ case 'PUSH_OWNER_LIST':
+ elementID = ((payload: any): number);
+ if (ownerIDStack.includes(elementID)) return state;
+
+ // Add this new owner, and fill in the owners above it as well.
+ const ownerIDsToConcat = [];
+ let currentOwnerID = elementID;
+ while (
+ currentOwnerID !== 0 &&
+ ownerIDStack[ownerIDStack.length - 1] !== currentOwnerID
+ ) {
+ ownerIDsToConcat.unshift(currentOwnerID);
+ currentOwnerID = ((store.getElementByID(
+ currentOwnerID
+ ): any): Element).ownerID;
+ }
+
+ return {
+ ...state,
+ ownerList: calculateCurrentOwnerList(
+ store,
+ elementID,
+ elementID,
+ []
+ ),
+ ownerIDStack: ownerIDStack.concat(ownerIDsToConcat),
+ selectedElementIndex: null,
+ selectedElementID: null,
+ searchIndex: null,
+ searchResults: [],
+ searchText: '',
+ };
+
+ case 'REFINE_AFTER_MUTATION':
const [
addedElementIDs,
removedElementIDs,
] = ((payload: any): Array);
- newSearchResults = searchResults;
+ newOwnerList = ownerList;
+ newOwnerIDStack = ownerIDStack;
newSearchIndex = searchIndex;
+ newSearchResults = searchResults;
+ newSearchText = searchText;
+ newSelectedElementID = selectedElementID;
+ newSelectedElementIndex = selectedElementIndex;
- removedElementIDs.forEach(id => {
- // Prune this item from the search results.
- const index = newSearchResults.indexOf(id);
- if (index >= 0) {
- newSearchResults = newSearchResults
- .slice(0, index)
- .concat(newSearchResults.slice(index + 1));
+ if (searchText !== '') {
+ removedElementIDs.forEach(id => {
+ // Prune this item from the search results.
+ const index = newSearchResults.indexOf(id);
+ if (index >= 0) {
+ newSearchResults = newSearchResults
+ .slice(0, index)
+ .concat(newSearchResults.slice(index + 1));
- // If the results are now empty, also deselect things.
- if (newSearchResults.length === 0) {
- newSearchIndex = null;
- } else if (
- ((newSearchIndex: any): number) >= newSearchResults.length
- ) {
- newSearchIndex = newSearchResults.length - 1;
- }
- }
- });
-
- addedElementIDs.forEach(id => {
- const { displayName } = ((store.getElementByID(
- id
- ): any): Element);
-
- // Add this item to the search results if it matches.
- const regExp = createRegExp(searchText);
- if (displayName !== null && regExp.test(displayName)) {
- const newElementIndex = ((store.getIndexOfElementID(
- id
- ): any): number);
-
- let foundMatch = false;
- for (let index = 0; index < newSearchResults.length; index++) {
- const id = newSearchResults[index];
- if (
- newElementIndex <
- ((store.getIndexOfElementID(id): any): number)
+ // If the results are now empty, also deselect things.
+ if (newSearchResults.length === 0) {
+ newSearchIndex = null;
+ } else if (
+ ((newSearchIndex: any): number) >= newSearchResults.length
) {
- foundMatch = true;
- newSearchResults = newSearchResults
- .slice(0, index)
- .concat(id)
- .concat(newSearchResults.slice(index));
- break;
+ newSearchIndex = newSearchResults.length - 1;
}
}
- if (!foundMatch) {
- newSearchResults = newSearchResults.concat(id);
+ });
+
+ addedElementIDs.forEach(id => {
+ const { displayName } = ((store.getElementByID(
+ id
+ ): any): Element);
+
+ // Add this item to the search results if it matches.
+ const regExp = createRegExp(searchText);
+ if (displayName !== null && regExp.test(displayName)) {
+ const newElementIndex = ((store.getIndexOfElementID(
+ id
+ ): any): number);
+
+ let foundMatch = false;
+ for (
+ let index = 0;
+ index < newSearchResults.length;
+ index++
+ ) {
+ const id = newSearchResults[index];
+ if (
+ newElementIndex <
+ ((store.getIndexOfElementID(id): any): number)
+ ) {
+ foundMatch = true;
+ newSearchResults = newSearchResults
+ .slice(0, index)
+ .concat(id)
+ .concat(newSearchResults.slice(index));
+ break;
+ }
+ }
+ if (!foundMatch) {
+ newSearchResults = newSearchResults.concat(id);
+ }
+
+ newSearchIndex = newSearchIndex === null ? 0 : newSearchIndex;
}
+ });
- newSearchIndex = newSearchIndex === null ? 0 : newSearchIndex;
+ newSelectedElementID =
+ newSearchIndex !== null
+ ? newSearchResults[newSearchIndex]
+ : null;
+ newSelectedElementIndex =
+ newSelectedElementID !== null
+ ? store.getIndexOfElementID(newSelectedElementID)
+ : null;
+ }
+
+ // If an item in the owner stack has been removed from the tree, unwind it.
+ if (ownerIDStack.length > 0) {
+ let indexOfRemovedItem = -1;
+ for (let i = 0; i < ownerIDStack.length; i++) {
+ if (removedElementIDs.includes(ownerIDStack[i])) {
+ indexOfRemovedItem = i;
+ break;
+ }
}
- });
- newSelectedElementID =
- newSearchIndex !== null ? newSearchResults[newSearchIndex] : null;
+ if (indexOfRemovedItem >= 0) {
+ newOwnerIDStack = ownerIDStack.slice(0, indexOfRemovedItem);
+ }
+
+ if (newOwnerIDStack.length === 0) {
+ newOwnerList = null;
+ } else {
+ elementID = newOwnerIDStack[newOwnerIDStack.length - 1];
+
+ newOwnerList = calculateCurrentOwnerList(
+ store,
+ elementID,
+ elementID,
+ []
+ );
+
+ newSelectedElementIndex =
+ newSelectedElementID !== null
+ ? newOwnerList.indexOf(newSelectedElementID)
+ : null;
+ }
+ }
return {
...state,
+ ownerIDStack: newOwnerIDStack,
+ ownerList: newOwnerList,
searchIndex: newSearchIndex,
searchResults: newSearchResults,
selectedElementID: newSelectedElementID,
- selectedElementIndex:
- newSelectedElementID !== null
- ? store.getIndexOfElementID(newSelectedElementID)
- : null,
+ selectedElementIndex: newSelectedElementIndex,
};
case 'SELECT_ELEMENT_WITH_ID':
newSelectedElementID = ((payload: any): number | null);
+ newSelectedElementIndex = null;
+
+ if (newSelectedElementID !== null) {
+ if (ownerList !== null) {
+ newSelectedElementIndex = ownerList.indexOf(
+ newSelectedElementID
+ );
+ } else {
+ newSelectedElementIndex = store.getIndexOfElementID(
+ newSelectedElementID
+ );
+ }
+ }
+
+ const isInOwnerList =
+ newSelectedElementID !== null &&
+ ownerList !== null &&
+ ownerList.includes(newSelectedElementID);
return {
...state,
+ ownerIDStack: isInOwnerList ? ownerIDStack : [],
+ ownerList: isInOwnerList ? ownerList : null,
selectedElementID: newSelectedElementID,
- selectedElementIndex:
- newSelectedElementID !== null
- ? store.getIndexOfElementID(newSelectedElementID)
- : null,
+ selectedElementIndex: newSelectedElementIndex,
};
- case 'SELECT_ELEMENT_AT_INDEX':
- newSearchIndex = ((payload: any): number | null);
+ case 'SELECT_NEXT_ELEMENT_IN_TREE':
+ if (selectedElementIndex === null) {
+ return state;
+ } else if (ownerList !== null) {
+ return updateSelectedElementIndexHelper(
+ selectedElementIndex + 1 < ownerList.length
+ ? selectedElementIndex + 1
+ : selectedElementIndex
+ );
+ } else {
+ return updateSelectedElementIndexHelper(
+ selectedElementIndex + 1 < store.numElements
+ ? selectedElementIndex + 1
+ : selectedElementIndex
+ );
+ }
- const element =
- newSearchIndex !== null
- ? store.getElementAtIndex(newSearchIndex)
- : null;
-
- return {
- ...state,
- selectedElementID: element !== null ? element.id : null,
- selectedElementIndex: newSearchIndex,
- };
+ case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
+ if (selectedElementIndex === null) {
+ return state;
+ } else if (ownerList !== null) {
+ return updateSelectedElementIndexHelper(
+ selectedElementIndex > 0
+ ? selectedElementIndex - 1
+ : selectedElementIndex
+ );
+ } else {
+ return updateSelectedElementIndexHelper(
+ selectedElementIndex > 0
+ ? selectedElementIndex - 1
+ : selectedElementIndex
+ );
+ }
case 'UPDATE_SEARCH_TEXT':
newSearchIndex = searchIndex;
@@ -275,6 +484,8 @@ function SearchAndSelectionController({ children }: Props) {
);
const [state, dispatch] = useReducer(reducer, {
+ ownerIDStack: [],
+ ownerList: null,
searchIndex: null,
searchResults: [],
searchText: '',
@@ -282,6 +493,10 @@ function SearchAndSelectionController({ children }: Props) {
selectedElementIndex: null,
});
+ const clearOwnerList = useCallback(
+ () => dispatch({ type: 'CLEAR_OWNER_LIST' }),
+ [dispatch]
+ );
const decrementSearchIndex = useCallback(
() => dispatch({ type: 'DECREMENT_SEARCH_INDEX' }),
[dispatch]
@@ -290,14 +505,27 @@ function SearchAndSelectionController({ children }: Props) {
() => dispatch({ type: 'INCREMENT_SEARCH_INDEX' }),
[dispatch]
);
+ const popToOwnerList = useCallback(
+ (elementID: number) =>
+ dispatch({ type: 'POP_TO_OWNER_LIST', payload: elementID }),
+ [dispatch]
+ );
+ const pushOwnerList = useCallback(
+ (elementID: number) =>
+ dispatch({ type: 'PUSH_OWNER_LIST', payload: elementID }),
+ [dispatch]
+ );
const selectElementWithID = useCallback(
(id: number | null) =>
dispatch({ type: 'SELECT_ELEMENT_WITH_ID', payload: id }),
[dispatch]
);
- const selectElementAtIndex = useCallback(
- (index: number | null) =>
- dispatch({ type: 'SELECT_ELEMENT_AT_INDEX', payload: index }),
+ const selectNextElementInTree = useCallback(
+ () => dispatch({ type: 'SELECT_NEXT_ELEMENT_IN_TREE' }),
+ [dispatch]
+ );
+ const selectPreviousElementInTree = useCallback(
+ () => dispatch({ type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE' }),
[dispatch]
);
const updateSearchText = useCallback(
@@ -308,16 +536,22 @@ function SearchAndSelectionController({ children }: Props) {
const value = useMemo(
() => ({
+ ownerList: state.ownerList,
+ ownerIDStack: state.ownerIDStack,
searchIndex: state.searchIndex,
searchResults: state.searchResults,
searchText: state.searchText,
selectedElementID: state.selectedElementID,
selectedElementIndex: state.selectedElementIndex,
+ clearOwnerList,
decrementSearchIndex,
incrementSearchIndex,
+ popToOwnerList,
+ pushOwnerList,
selectElementWithID,
- selectElementAtIndex,
+ selectNextElementInTree,
+ selectPreviousElementInTree,
updateSearchText,
}),
[state]
@@ -329,12 +563,12 @@ function SearchAndSelectionController({ children }: Props) {
addedElementIDs,
removedElementIDs,
]: Array) => {
- if (!state.searchText) {
+ if (!state.searchText && state.ownerList === null) {
return;
}
dispatch({
- type: 'REFINE_SEARCH_RESULTS',
+ type: 'REFINE_AFTER_MUTATION',
payload: [addedElementIDs, removedElementIDs],
});
};
@@ -351,6 +585,33 @@ function SearchAndSelectionController({ children }: Props) {
);
}
+function calculateCurrentOwnerList(
+ store: Store,
+ rootOwnerID: number,
+ elementID: number,
+ ownerList: Array
+): Array {
+ if (elementID === rootOwnerID) {
+ ownerList.push(elementID);
+ const { children } = ((store.getElementByID(elementID): any): Element);
+ children.forEach(childID =>
+ calculateCurrentOwnerList(store, rootOwnerID, childID, ownerList)
+ );
+ } else {
+ const { children, ownerID } = ((store.getElementByID(
+ elementID
+ ): any): Element);
+ if (ownerID === rootOwnerID) {
+ ownerList.push(elementID);
+ children.forEach(childID =>
+ calculateCurrentOwnerList(store, rootOwnerID, childID, ownerList)
+ );
+ }
+ }
+
+ return ownerList;
+}
+
function recursivelySearchTree(
store: Store,
elementID: number,
diff --git a/src/devtools/views/SelectedElement.js b/src/devtools/views/SelectedElement.js
index 7f9652ff04..a107bb0768 100644
--- a/src/devtools/views/SelectedElement.js
+++ b/src/devtools/views/SelectedElement.js
@@ -1,6 +1,12 @@
// @flow
-import React, { useContext, useLayoutEffect, useRef, useState } from 'react';
+import React, {
+ useCallback,
+ useContext,
+ useLayoutEffect,
+ useRef,
+ useState,
+} from 'react';
import { SearchAndSelectionContext } from './SearchAndSelectionContext';
import { BridgeContext, StoreContext } from './context';
import ButtonIcon from './ButtonIcon';
@@ -75,8 +81,6 @@ type InspectedElementViewProps = {|
|};
function InspectedElementView({ inspectedElement }: InspectedElementViewProps) {
- const { selectElementWithID } = useContext(SearchAndSelectionContext);
-
let { context, hooks, owners, props, state } = inspectedElement;
return (
@@ -90,14 +94,11 @@ function InspectedElementView({ inspectedElement }: InspectedElementViewProps) {
owner stack
{owners.map(owner => (
-
selectElementWithID(owner.id)}
- title={owner.displayName}
- >
- {owner.displayName}
-
+ displayName={owner.displayName}
+ id={owner.id}
+ />
))}
)}
@@ -105,6 +106,23 @@ function InspectedElementView({ inspectedElement }: InspectedElementViewProps) {
);
}
+function OwnerView({ displayName, id }: { displayName: string, id: number }) {
+ const { selectElementWithID } = useContext(SearchAndSelectionContext);
+
+ const handleClick = useCallback(() => selectElementWithID(id), [id]);
+
+ return (
+
+ {displayName}
+
+ );
+}
+
function hydrateHelper(dehydratedData: DehydratedData | null): Object | null {
if (dehydratedData !== null) {
return hydrate(dehydratedData.data, dehydratedData.cleaned);
diff --git a/src/devtools/views/Tree.js b/src/devtools/views/Tree.js
index 5464701ad4..b4e4e93a73 100644
--- a/src/devtools/views/Tree.js
+++ b/src/devtools/views/Tree.js
@@ -1,11 +1,18 @@
// @flow
-import React, { useContext, useEffect, useLayoutEffect, useRef } from 'react';
+import React, {
+ useContext,
+ useEffect,
+ useMemo,
+ useLayoutEffect,
+ useRef,
+} from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { FixedSizeList } from 'react-window';
import { SearchAndSelectionContext } from './SearchAndSelectionContext';
import ButtonIcon from './ButtonIcon';
import Element from './Element';
+import OwnersStack from './OwnersStack';
import SearchInput from './SearchInput';
import { TreeContext } from './context';
@@ -14,9 +21,13 @@ import styles from './Tree.css';
type Props = {||};
export default function Tree(props: Props) {
- const { selectedElementIndex, selectElementAtIndex } = useContext(
- SearchAndSelectionContext
- );
+ const {
+ ownerIDStack,
+ ownerList,
+ selectedElementIndex,
+ selectNextElementInTree,
+ selectPreviousElementInTree,
+ } = useContext(SearchAndSelectionContext);
const treeContext = useContext(TreeContext);
const listRef = useRef>();
@@ -30,22 +41,17 @@ export default function Tree(props: Props) {
// Navigate the tree with up/down arrow keys.
useEffect(() => {
+ // TODO Use selectNextElement / selectPreviousElement (with dispatch)
+
const handleKeyDown = event => {
// eslint-disable-next-line default-case
switch (event.key) {
case 'ArrowDown':
- if (
- selectedElementIndex !== null &&
- selectedElementIndex + 1 < treeContext.size
- ) {
- selectElementAtIndex(((selectedElementIndex: any): number) + 1);
- }
+ selectNextElementInTree();
event.preventDefault();
break;
case 'ArrowUp':
- if (selectedElementIndex !== null && selectedElementIndex > 0) {
- selectElementAtIndex(((selectedElementIndex: any): number) - 1);
- }
+ selectPreviousElementInTree();
event.preventDefault();
break;
}
@@ -56,12 +62,26 @@ export default function Tree(props: Props) {
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
- }, [selectedElementIndex, selectElementAtIndex, treeContext]);
+ }, [
+ selectedElementIndex,
+ selectNextElementInTree,
+ selectPreviousElementInTree,
+ treeContext,
+ ]);
+
+ const itemCount = ownerList !== null ? ownerList.length : treeContext.size;
+
+ // Let react-window know to re-render any time the underlying tree data changes.
+ // This includes the owner context, since it controls a filtered view of the tree.
+ const itemData = useMemo(() => ({ treeContext, ownerList }), [
+ treeContext,
+ ownerList,
+ ]);
return (
-
+ {ownerIDStack.length > 0 ? : }