Files
flipper/desktop/plugins/public/ui-debugger/components/Tree.tsx
Luke De Feo 477eae1993 Hit test can produce multiple nodes
Summary:
There are situations where multiple siblings overlap and they are both hit. Previously we picked the first one in the hierachy. Now we produce a list of hit children. The list will not have 2 nodes in the same ancestor path.

We store the hovered nodes as a list as we may want to present a modal in future to ask user which node they indented to select. That said simply sorting nodes by area seems to give decent results so we can start with this

Reviewed By: lblasa

Differential Revision: D41220271

fbshipit-source-id: 643a369113da28e8c4749725a7aee7aa5d08c401
2022-11-14 07:05:58 -08:00

120 lines
3.2 KiB
TypeScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {Id, UINode} from '../types';
import React, {useEffect, useMemo, useRef} from 'react';
import {
Tree as ComplexTree,
ControlledTreeEnvironment,
TreeItem,
} from 'react-complex-tree';
import {plugin} from '../index';
import {usePlugin, useValue} from 'flipper-plugin';
import {
InteractionMode,
TreeEnvironmentRef,
} from 'react-complex-tree/lib/esm/types';
import {head} from 'lodash';
export function Tree(props: {
rootId: Id;
nodes: Map<Id, UINode>;
selectedNode?: Id;
onSelectNode: (id: Id) => void;
}) {
const instance = usePlugin(plugin);
const expandedItems = useValue(instance.treeState).expandedNodes;
const items = useMemo(() => toComplexTree(props.nodes), [props.nodes]);
const hoveredNodes = useValue(instance.hoveredNodes);
const treeRef = useRef<TreeEnvironmentRef>();
useEffect(() => {
//this makes the keyboard arrow controls work always, even when using the visualiser
treeRef.current?.focusTree('tree', true);
}, [hoveredNodes, props.selectedNode]);
return (
<ControlledTreeEnvironment
ref={treeRef as any}
items={items}
getItemTitle={(item) => item.data.name}
canRename={false}
canDragAndDrop={false}
canSearch
autoFocus
viewState={{
tree: {
focusedItem: head(hoveredNodes),
expandedItems,
selectedItems: props.selectedNode ? [props.selectedNode] : [],
},
}}
onFocusItem={(item) => {
instance.hoveredNodes.set([item.index]);
}}
onExpandItem={(item) => {
instance.treeState.update((draft) => {
draft.expandedNodes.push(item.index);
});
}}
onCollapseItem={(item) =>
instance.treeState.update((draft) => {
draft.expandedNodes = draft.expandedNodes.filter(
(expandedItemIndex) => expandedItemIndex !== item.index,
);
})
}
onSelectItems={(items) => props.onSelectNode(items[0])}
defaultInteractionMode={{
mode: 'custom',
extends: InteractionMode.DoubleClickItemToExpand,
createInteractiveElementProps: (
item,
treeId,
actions,
renderFlags,
) => ({
onClick: () => {
if (renderFlags.isSelected) {
actions.unselectItem();
} else {
actions.selectItem();
}
},
onMouseOver: () => {
instance.hoveredNodes.set([item.index]);
},
}),
}}>
<ComplexTree
treeId="tree"
rootItem={props.rootId as any} //the typing in in the library is wrong here
treeLabel="UI"
/>
</ControlledTreeEnvironment>
);
}
function toComplexTree(nodes: Map<Id, UINode>): Record<Id, TreeItem<UINode>> {
const res: Record<Id, TreeItem<UINode>> = {};
for (const node of nodes.values()) {
res[node.id] = {
index: node.id,
canMove: false,
canRename: false,
children: node.children,
data: node,
hasChildren: node.children.length > 0,
};
}
return res;
}