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:
committed by
Facebook GitHub Bot
parent
11b12b4e38
commit
3722ac1fea
@@ -39,6 +39,8 @@ test('Correct top level API exposed', () => {
|
|||||||
"Dialog",
|
"Dialog",
|
||||||
"ElementsInspector",
|
"ElementsInspector",
|
||||||
"FileSelector",
|
"FileSelector",
|
||||||
|
"HighlightContext",
|
||||||
|
"HighlightProvider",
|
||||||
"Layout",
|
"Layout",
|
||||||
"MarkerTimeline",
|
"MarkerTimeline",
|
||||||
"MasterDetail",
|
"MasterDetail",
|
||||||
@@ -67,6 +69,7 @@ test('Correct top level API exposed', () => {
|
|||||||
"textContent",
|
"textContent",
|
||||||
"theme",
|
"theme",
|
||||||
"timeout",
|
"timeout",
|
||||||
|
"useHighlighter",
|
||||||
"useLocalStorageState",
|
"useLocalStorageState",
|
||||||
"useLogger",
|
"useLogger",
|
||||||
"useMemoize",
|
"useMemoize",
|
||||||
|
|||||||
@@ -70,7 +70,12 @@ export {Tabs, Tab} from './ui/Tabs';
|
|||||||
export {useLocalStorageState} from './utils/useLocalStorageState';
|
export {useLocalStorageState} from './utils/useLocalStorageState';
|
||||||
|
|
||||||
export {FileSelector} from './ui/FileSelector';
|
export {FileSelector} from './ui/FileSelector';
|
||||||
export {HighlightManager} from './ui/Highlight';
|
export {
|
||||||
|
HighlightManager,
|
||||||
|
HighlightContext,
|
||||||
|
HighlightProvider,
|
||||||
|
useHighlighter,
|
||||||
|
} from './ui/Highlight';
|
||||||
export {
|
export {
|
||||||
DataValueExtractor,
|
DataValueExtractor,
|
||||||
DataInspectorExpanded,
|
DataInspectorExpanded,
|
||||||
|
|||||||
@@ -13,14 +13,23 @@ import {
|
|||||||
Tree as ComplexTree,
|
Tree as ComplexTree,
|
||||||
ControlledTreeEnvironment,
|
ControlledTreeEnvironment,
|
||||||
TreeItem,
|
TreeItem,
|
||||||
|
TreeInformation,
|
||||||
|
TreeItemRenderContext,
|
||||||
|
InteractionMode,
|
||||||
|
TreeEnvironmentRef,
|
||||||
} from 'react-complex-tree';
|
} from 'react-complex-tree';
|
||||||
|
|
||||||
import {plugin} from '../index';
|
import {plugin} from '../index';
|
||||||
import {usePlugin, useValue} from 'flipper-plugin';
|
|
||||||
import {
|
import {
|
||||||
InteractionMode,
|
usePlugin,
|
||||||
TreeEnvironmentRef,
|
useValue,
|
||||||
} from 'react-complex-tree/lib/esm/types';
|
HighlightManager,
|
||||||
|
HighlightProvider,
|
||||||
|
HighlightContext,
|
||||||
|
useHighlighter,
|
||||||
|
theme,
|
||||||
|
} from 'flipper-plugin';
|
||||||
|
|
||||||
import {head} from 'lodash';
|
import {head} from 'lodash';
|
||||||
|
|
||||||
export function Tree(props: {
|
export function Tree(props: {
|
||||||
@@ -32,77 +41,155 @@ export function Tree(props: {
|
|||||||
const instance = usePlugin(plugin);
|
const instance = usePlugin(plugin);
|
||||||
const expandedItems = useValue(instance.treeState).expandedNodes;
|
const expandedItems = useValue(instance.treeState).expandedNodes;
|
||||||
const items = useMemo(() => toComplexTree(props.nodes), [props.nodes]);
|
const items = useMemo(() => toComplexTree(props.nodes), [props.nodes]);
|
||||||
|
|
||||||
const hoveredNodes = useValue(instance.hoveredNodes);
|
const hoveredNodes = useValue(instance.hoveredNodes);
|
||||||
const treeRef = useRef<TreeEnvironmentRef>();
|
const treeEnvRef = useRef<TreeEnvironmentRef>();
|
||||||
|
|
||||||
|
const searchTerm = useValue(instance.searchTerm);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//this makes the keyboard arrow controls work always, even when using the visualiser
|
//this makes the keyboard arrow controls work always, even when using the visualiser
|
||||||
treeRef.current?.focusTree('tree', true);
|
treeEnvRef.current?.focusTree('tree', true);
|
||||||
}, [hoveredNodes, props.selectedNode]);
|
}, [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: () => {
|
return (
|
||||||
instance.hoveredNodes.set([item.index]);
|
<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] : [],
|
||||||
},
|
},
|
||||||
}),
|
}}
|
||||||
}}>
|
onFocusItem={(item) => {
|
||||||
<ComplexTree
|
instance.hoveredNodes.set([item.index]);
|
||||||
treeId="tree"
|
}}
|
||||||
rootItem={props.rootId as any} //the typing in in the library is wrong here
|
onExpandItem={(item) => {
|
||||||
treeLabel="UI"
|
instance.treeState.update((draft) => {
|
||||||
/>
|
draft.expandedNodes.push(item.index);
|
||||||
</ControlledTreeEnvironment>
|
});
|
||||||
|
}}
|
||||||
|
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>> {
|
function toComplexTree(nodes: Map<Id, UINode>): Record<Id, TreeItem<UINode>> {
|
||||||
const res: Record<Id, TreeItem<UINode>> = {};
|
const res: Record<Id, TreeItem<UINode>> = {};
|
||||||
for (const node of nodes.values()) {
|
for (const node of nodes.values()) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {Tree} from './Tree';
|
|||||||
import {Visualization2D} from './Visualization2D';
|
import {Visualization2D} from './Visualization2D';
|
||||||
import {useKeyboardModifiers} from '../hooks/useKeyboardModifiers';
|
import {useKeyboardModifiers} from '../hooks/useKeyboardModifiers';
|
||||||
import {Inspector} from './sidebar/Inspector';
|
import {Inspector} from './sidebar/Inspector';
|
||||||
|
import {Input} from 'antd';
|
||||||
|
|
||||||
export function Component() {
|
export function Component() {
|
||||||
const instance = usePlugin(plugin);
|
const instance = usePlugin(plugin);
|
||||||
@@ -30,6 +31,7 @@ export function Component() {
|
|||||||
|
|
||||||
useHotkeys('ctrl+i', () => setShowPerfStats((show) => !show));
|
useHotkeys('ctrl+i', () => setShowPerfStats((show) => !show));
|
||||||
|
|
||||||
|
const searchTerm = useValue(instance.searchTerm);
|
||||||
const {ctrlPressed} = useKeyboardModifiers();
|
const {ctrlPressed} = useKeyboardModifiers();
|
||||||
|
|
||||||
function renderSidebar(
|
function renderSidebar(
|
||||||
@@ -51,14 +53,20 @@ export function Component() {
|
|||||||
if (rootId) {
|
if (rootId) {
|
||||||
return (
|
return (
|
||||||
<Layout.Horizontal grow>
|
<Layout.Horizontal grow>
|
||||||
<Layout.ScrollContainer>
|
<Layout.Container grow pad="medium" gap="small">
|
||||||
<Tree
|
<Input
|
||||||
selectedNode={selectedNode}
|
value={searchTerm}
|
||||||
onSelectNode={setSelectedNode}
|
onChange={(e) => instance.searchTerm.set(e.target.value)}
|
||||||
nodes={nodes}
|
|
||||||
rootId={rootId}
|
|
||||||
/>
|
/>
|
||||||
</Layout.ScrollContainer>
|
<Layout.ScrollContainer>
|
||||||
|
<Tree
|
||||||
|
selectedNode={selectedNode}
|
||||||
|
onSelectNode={setSelectedNode}
|
||||||
|
nodes={nodes}
|
||||||
|
rootId={rootId}
|
||||||
|
/>
|
||||||
|
</Layout.ScrollContainer>
|
||||||
|
</Layout.Container>
|
||||||
<Visualization2D
|
<Visualization2D
|
||||||
rootId={rootId}
|
rootId={rootId}
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import './node_modules/react-complex-tree/lib/style.css';
|
|||||||
export function plugin(client: PluginClient<Events>) {
|
export function plugin(client: PluginClient<Events>) {
|
||||||
const rootId = createState<Id | undefined>(undefined);
|
const rootId = createState<Id | undefined>(undefined);
|
||||||
const metadata = createState<Map<MetadataId, Metadata>>(new Map());
|
const metadata = createState<Map<MetadataId, Metadata>>(new Map());
|
||||||
|
const searchTerm = createState<string>('');
|
||||||
|
|
||||||
client.onMessage('init', (event) => {
|
client.onMessage('init', (event) => {
|
||||||
rootId.set(event.rootId);
|
rootId.set(event.rootId);
|
||||||
@@ -108,6 +109,7 @@ export function plugin(client: PluginClient<Events>) {
|
|||||||
hoveredNodes,
|
hoveredNodes,
|
||||||
perfEvents,
|
perfEvents,
|
||||||
treeState,
|
treeState,
|
||||||
|
searchTerm,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -889,6 +889,29 @@ const [showWhitespace, setShowWhitespace] = useLocalStorageState(
|
|||||||
Layout elements can be used to organize the screen layout.
|
Layout elements can be used to organize the screen layout.
|
||||||
See the [Style Guide](style-guide.mdx) for more details.
|
See the [Style Guide](style-guide.mdx) for more details.
|
||||||
|
|
||||||
|
### HighlightContext
|
||||||
|
|
||||||
|
### HighlightProvider
|
||||||
|
|
||||||
|
React context provider for Highlight context. All wrapped componets can access context or use the useHighligher helper. Example
|
||||||
|
```typescript jsx
|
||||||
|
<HighlightProvider
|
||||||
|
text={searchTerm}
|
||||||
|
highlightColor={theme.searchHighlightBackground.yellow}>
|
||||||
|
<HighlightedText text='Lorem itsum'/>
|
||||||
|
</HighlightProvider>
|
||||||
|
````
|
||||||
|
|
||||||
|
### useHighlighter
|
||||||
|
|
||||||
|
Hook to be used inside a Highlight context to render text with highlighting applied. Example
|
||||||
|
```typescript jsx
|
||||||
|
function HighlightedText(props: {text: string}) {
|
||||||
|
const highlightManager: HighlightManager = useHighlighter();
|
||||||
|
return <span>{highlightManager.render(props.text)}</span>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### DataTable
|
### DataTable
|
||||||
|
|
||||||
### DataFormatter
|
### DataFormatter
|
||||||
|
|||||||
Reference in New Issue
Block a user