Permanent search bar

Summary: Out of the box the library search has some issues. when search matches it steals focus from the input. Eventually we want to customise the rendering of the tree items anyway so this lays the foundation for taht

Reviewed By: antonk52

Differential Revision: D41336524

fbshipit-source-id: 194f67023edd0675cd9bd8d6134260439c6b2785
This commit is contained in:
Luke De Feo
2022-11-24 09:23:16 -08:00
committed by Facebook GitHub Bot
parent 11b12b4e38
commit 3722ac1fea
6 changed files with 202 additions and 74 deletions

View File

@@ -13,14 +13,23 @@ import {
Tree as ComplexTree,
ControlledTreeEnvironment,
TreeItem,
TreeInformation,
TreeItemRenderContext,
InteractionMode,
TreeEnvironmentRef,
} from 'react-complex-tree';
import {plugin} from '../index';
import {usePlugin, useValue} from 'flipper-plugin';
import {
InteractionMode,
TreeEnvironmentRef,
} from 'react-complex-tree/lib/esm/types';
usePlugin,
useValue,
HighlightManager,
HighlightProvider,
HighlightContext,
useHighlighter,
theme,
} from 'flipper-plugin';
import {head} from 'lodash';
export function Tree(props: {
@@ -32,77 +41,155 @@ export function Tree(props: {
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>();
const treeEnvRef = useRef<TreeEnvironmentRef>();
const searchTerm = useValue(instance.searchTerm);
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();
}
},
treeEnvRef.current?.focusTree('tree', true);
}, [props.selectedNode]);
onMouseOver: () => {
instance.hoveredNodes.set([item.index]);
return (
<HighlightProvider
text={searchTerm}
highlightColor={theme.searchHighlightBackground.yellow}>
<ControlledTreeEnvironment
ref={treeEnvRef as any}
items={items}
getItemTitle={(item) => item.data.name}
canRename={false}
canDragAndDrop={false}
viewState={{
tree: {
focusedItem: head(hoveredNodes),
expandedItems,
selectedItems: props.selectedNode ? [props.selectedNode] : [],
},
}),
}}>
<ComplexTree
treeId="tree"
rootItem={props.rootId as any} //the typing in in the library is wrong here
treeLabel="UI"
/>
</ControlledTreeEnvironment>
}}
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,
);
})
}
renderItem={renderItem}
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>
</HighlightProvider>
);
}
//copied from https://github.com/lukasbach/react-complex-tree/blob/e3dcc435933284376a0fc6e3cc651e67ead678b5/packages/core/src/renderers/createDefaultRenderers.tsx
const cx = (...classNames: Array<string | undefined | false>) =>
classNames.filter((cn) => !!cn).join(' ');
const renderDepthOffset = 5;
function renderItem<C extends string = never>({
item,
depth,
children,
arrow,
context,
}: {
item: TreeItem<UINode>;
depth: number;
children: React.ReactNode | null;
title: React.ReactNode;
arrow: React.ReactNode;
context: TreeItemRenderContext<C>;
info: TreeInformation;
}) {
return (
<li
{...(context.itemContainerWithChildrenProps as any)}
className={cx(
'rct-tree-item-li',
item.hasChildren && 'rct-tree-item-li-hasChildren',
context.isSelected && 'rct-tree-item-li-selected',
context.isExpanded && 'rct-tree-item-li-expanded',
context.isFocused && 'rct-tree-item-li-focused',
context.isDraggingOver && 'rct-tree-item-li-dragging-over',
context.isSearchMatching && 'rct-tree-item-li-search-match',
)}>
<div
{...(context.itemContainerWithoutChildrenProps as any)}
style={{
paddingLeft: `${(depth + 1) * renderDepthOffset}px`,
}}
className={cx(
'rct-tree-item-title-container',
item.hasChildren && 'rct-tree-item-title-container-hasChildren',
context.isSelected && 'rct-tree-item-title-container-selected',
context.isExpanded && 'rct-tree-item-title-container-expanded',
context.isFocused && 'rct-tree-item-title-container-focused',
context.isDraggingOver &&
'rct-tree-item-title-container-dragging-over',
context.isSearchMatching &&
'rct-tree-item-title-container-search-match',
)}>
{arrow}
<div
{...(context.interactiveElementProps as any)}
className={cx(
'rct-tree-item-button',
item.hasChildren && 'rct-tree-item-button-hasChildren',
context.isSelected && 'rct-tree-item-button-selected',
context.isExpanded && 'rct-tree-item-button-expanded',
context.isFocused && 'rct-tree-item-button-focused',
context.isDraggingOver && 'rct-tree-item-button-dragging-over',
context.isSearchMatching && 'rct-tree-item-button-search-match',
)}>
<HighlightedText text={item.data.name} />
</div>
</div>
{children}
</li>
);
}
function HighlightedText(props: {text: string}) {
const highlightManager: HighlightManager = useHighlighter();
return <span>{highlightManager.render(props.text)}</span>;
}
function toComplexTree(nodes: Map<Id, UINode>): Record<Id, TreeItem<UINode>> {
const res: Record<Id, TreeItem<UINode>> = {};
for (const node of nodes.values()) {