Added owner tree exploration view

This commit is contained in:
Brian Vaughn
2019-02-07 14:34:24 +00:00
parent fc924d136a
commit 45641e98e5
11 changed files with 585 additions and 132 deletions
+10 -5
View File
@@ -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);
}
+6
View File
@@ -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,
+3
View File
@@ -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,
+17 -1
View File
@@ -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';
+24 -14
View File
@@ -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) {
<div
className={isSelected ? styles.SelectedElement : styles.Element}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
style={{
...style, // "style" comes from react-window
paddingLeft: `${1 + depth}rem`,
paddingLeft: `${1 + calculatedDepth}rem`,
}}
>
{children.length > 0 && (
+49
View File
@@ -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;
}
+63
View File
@@ -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 => <ElementView key={id} id={id} />);
return (
<div className={styles.OwnerStack}>
<button
className={styles.IconButton}
onClick={clearOwnerList}
title="Back to tree view"
>
<ButtonIcon type="back" />
</button>
<div className={styles.VRule} />
{elements}
</div>
);
}
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 (
<span
className={
isCurrentlyFocusedOwner ? styles.FocusedComponent : styles.Component
}
onClick={handleClick}
>
{displayName}
</span>
);
}
+346 -85
View File
@@ -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<number>,
ownerList: Array<number> | null,
searchIndex: number | null,
searchResults: Array<number>,
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<Context>(
SearchAndSelectionContext.displayName = 'SearchAndSelectionContext';
type State = {|
ownerIDStack: Array<number>,
ownerList: Array<number> | null,
searchIndex: number | null,
searchResults: Array<number>,
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<Uint32Array>);
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<Uint32Array>) => {
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<number>
): Array<number> {
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,
+28 -10
View File
@@ -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) {
<div className={styles.Owners}>
<div>owner stack</div>
{owners.map(owner => (
<div
<OwnerView
key={owner.id}
className={styles.Owner}
onClick={() => selectElementWithID(owner.id)}
title={owner.displayName}
>
{owner.displayName}
</div>
displayName={owner.displayName}
id={owner.id}
/>
))}
</div>
)}
@@ -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 (
<div
key={id}
className={styles.Owner}
onClick={handleClick}
title={displayName}
>
{displayName}
</div>
);
}
function hydrateHelper(dehydratedData: DehydratedData | null): Object | null {
if (dehydratedData !== null) {
return hydrate(dehydratedData.data, dehydratedData.cleaned);
+37 -17
View File
@@ -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<FixedSizeList<any>>();
@@ -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 (
<div className={styles.Tree}>
<div className={styles.SearchInput}>
<SearchInput />
{ownerIDStack.length > 0 ? <OwnersStack /> : <SearchInput />}
<button
className={styles.IconButton}
title="Select an element in the page to inspect it"
@@ -75,8 +95,8 @@ export default function Tree(props: Props) {
<FixedSizeList
className={styles.List}
height={height}
itemCount={treeContext.size}
itemData={treeContext}
itemCount={itemCount}
itemData={itemData}
itemSize={20}
ref={listRef}
width={width}
+2
View File
@@ -12,6 +12,7 @@
--theme-light-component-name-inverted: #ffffff;
--theme-light-dim: #777d88;
--theme-light-dimmer: #cfd1d5;
--theme-light-dimmest: #eff0f1;
--theme-light-jsx-arrow-brackets: #333333;
--theme-light-jsx-arrow-brackets-inverted: rgba(255, 255, 255, 0.7);
--theme-light-search-match: yellow;
@@ -34,6 +35,7 @@
--color-component-name-inverted: var(--theme-light-component-name-inverted);
--color-dim: var(--theme-light-dim);
--color-dimmer: var(--theme-light-dimmer);
--color-dimmest: var(--theme-light-dimmest);
--color-search-match: var(--theme-light-search-match);
--color-search-match-current: var(--theme-light-search-match-current);
--color-tree-jsx-arrow-brackets: var(--theme-light-jsx-arrow-brackets);