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 ( +
+ +
+ {elements} +
+ ); +} + +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 ? : }