Skip to content

Commit

Permalink
Add search capabilities to Peripheral Inspector view
Browse files Browse the repository at this point in the history
- Provide custom SearchOverlay component
- Add search overlay to filter tree and tree table
- Move custom data into the 'data' for filtering in tree table

Closes #23
  • Loading branch information
martin-fleck-at committed Jun 26, 2024
1 parent 05718b2 commit 1cb8105
Show file tree
Hide file tree
Showing 12 changed files with 243 additions and 32 deletions.
86 changes: 86 additions & 0 deletions src/components/tree/components/search-overlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*********************************************************************
* Copyright (c) 2024 Arm Limited and others
*
* This program and the accompanying materials are made available under the
* terms of the MIT License as outlined in the LICENSE File
********************************************************************************/

import { VSCodeButton } from '@vscode/webview-ui-toolkit/react';
import React from 'react';
import './search.css';

export interface SearchOverlayProps {
onChange?: (text: string) => void;
onShow?: () => void;
onHide?: () => void;
}

export interface SearchOverlay {
focus: () => void;
value(): string;
setValue: (value: string) => void;
show: () => void;
hide: () => void;
}

export const SearchOverlay = React.forwardRef<SearchOverlay, SearchOverlayProps>((props, ref) => {
const [showSearch, setShowSearch] = React.useState(false);
const searchTextRef = React.useRef<HTMLInputElement>(null);
const previousFocusedElementRef = React.useRef<HTMLElement | null>(null);

const show = () => {
previousFocusedElementRef.current = document.activeElement as HTMLElement;
setShowSearch(true);
setTimeout(() => searchTextRef.current?.select(), 100);
props.onShow?.();
};

const hide = () => {
setShowSearch(false);
props.onHide?.();
if (previousFocusedElementRef.current) {
previousFocusedElementRef.current.focus();
}
};

const onTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
props.onChange?.(value);
};

const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.ctrlKey && e.key === 'f') {
e.preventDefault();
e.stopPropagation();
show();
} else if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
hide();
}
};

const onFocus = (e: React.FocusEvent<HTMLInputElement>) => {
if (e.relatedTarget) {
previousFocusedElementRef.current = e.relatedTarget as HTMLElement;
}
};

React.useImperativeHandle(ref, () => ({
focus: () => searchTextRef.current?.focus(),
value: () => searchTextRef.current?.value ?? '',
setValue: (newValue: string) => {
if (searchTextRef.current) {
searchTextRef.current.value = newValue;
}
},
show: () => show(),
hide: () => hide()
}));

return (<div className={showSearch ? 'search-overlay visible' : 'search-overlay'} onKeyDown={onKeyDown}>
<input ref={searchTextRef} onChange={onTextChange} onFocus={onFocus} placeholder="Find" className="search-input" />
<VSCodeButton title='Close (Escape)' appearance='icon' aria-label='Close (Escape)'><span className='codicon codicon-close' onClick={() => hide()} /></VSCodeButton>
</div>
);
});
81 changes: 81 additions & 0 deletions src/components/tree/components/search.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/********************************************************************************
* Copyright (C) 2024 EclipseSource and others.
*
* This program and the accompanying materials are made available under the
* terms of the MIT License as outlined in the LICENSE File
********************************************************************************/

.search-overlay {
position: fixed;
top: -33px;
opacity: 0;
right: 5px;
background-color: var(--vscode-editorWidget-background);
box-shadow: 0 0 4px 1px var(--vscode-widget-shadow);
color: var(--vscode-editorWidget-foreground);
border-bottom: 1px solid var(--vscode-widget-border);
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-left: 1px solid var(--vscode-widget-border);
border-right: 1px solid var(--vscode-widget-border);
box-sizing: border-box;
height: 33px;
line-height: 19px;
overflow: hidden;
padding: 4px;
z-index: 35;
display: flex;
flex-direction: row;
gap: 5px;

-webkit-transition: all 0.2s ease;
-moz-transition: all 0.2s ease;
-ms-transition: all 0.2s ease;
-o-transition: all 0.2s ease;
transition: all 0.2s ease;
}

.search-overlay.visible {
top: 5px;
opacity: 1;
}

.search-overlay .search-input {
color: var(--vscode-input-foreground);
background-color: var(--vscode-input-background);
outline: none;
scrollbar-width: none;
border: none;
box-sizing: border-box;
display: inline-block;
font-family: inherit;
font-size: inherit;
height: 100%;
line-height: inherit;
resize: none;
width: 100%;
padding: 4px 6px;
margin: 0;
}

.search-overlay input.search-input:focus {
outline: 1px solid var(--vscode-focusBorder)
}


.search-input::placeholder {
color: var(--vscode-input-placeholderForeground);
}

.search-input::-moz-placeholder {
color: var(--vscode-input-placeholderForeground);
}

.search-input:-ms-input-placeholder {
color: var(--vscode-input-placeholderForeground);
}

.search-input:-webkit-input-placeholder {
color: var(--vscode-input-placeholderForeground);
}

44 changes: 32 additions & 12 deletions src/components/tree/components/tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,35 @@
* SPDX-License-Identifier: EPL-2.0
*********************************************************************/

import './common.css';
import './tree.css';

import { Tree, TreeEventNodeEvent, TreeNodeClickEvent } from 'primereact/tree';
import { TreeNode } from 'primereact/treenode';
import { classNames } from 'primereact/utils';
import React from 'react';
import { useCDTTreeContext } from '../tree-context';
import { CDTTreeItem, CTDTreeMessengerType, CTDTreeWebviewContext } from '../types';
import './common.css';
import { SearchOverlay } from './search-overlay';
import './tree.css';
import { createActions, createHighlightedText, createLabelWithTooltip } from './utils';

export type ComponentTreeProps = {
nodes?: CDTTreeItem[];
selectedNode?: CDTTreeItem;
};

export const ComponentTree = (props: ComponentTreeProps) => {
export const ComponentTree = ({ nodes, selectedNode }: ComponentTreeProps) => {
// Assemble the tree
if (props.nodes === undefined) {
if (nodes === undefined) {
return <div>loading</div>;
}

if (!props.nodes.length) {
if (!nodes.length) {
return <div>No children provided</div>;
}

const treeContext = useCDTTreeContext();
const [filter, setFilter] = React.useState<string | undefined>();
const searchRef = React.useRef<SearchOverlay>(null);

// Event handler
const onToggle = (event: TreeEventNodeEvent) => {
Expand All @@ -53,9 +55,9 @@ export const ComponentTree = (props: ComponentTreeProps) => {
const nodeTemplate = (node: TreeNode) => {
CDTTreeItem.assert(node);
return <div className='tree-node'
{...CTDTreeWebviewContext.create({ webviewSection: 'tree-item', cdtTreeItemId: node.id, cdtTreeItemPath: node.path })}
{...CTDTreeWebviewContext.create({ webviewSection: 'tree-item', cdtTreeItemId: node.id, cdtTreeItemPath: node.data.path })}
>
{createLabelWithTooltip(createHighlightedText(node.label, node.options?.highlights), node.options?.tooltip)}
{createLabelWithTooltip(createHighlightedText(node.label, node.data.options?.highlights), node.data.options?.tooltip)}
{createActions(treeContext, node)}
</div>;
};
Expand All @@ -70,19 +72,37 @@ export const ComponentTree = (props: ComponentTreeProps) => {
</div>;
};

return <div>
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.ctrlKey && e.key === 'f') {
e.preventDefault();
e.stopPropagation();
searchRef.current?.show();
}
};

const onSearchShow = () => setFilter(searchRef.current?.value());
const onSearchHide = () => setFilter(undefined);
const onSearchChange = (text: string) => setFilter(text);

return <div onKeyDown={onKeyDown}>
<SearchOverlay key={'search'} ref={searchRef} onHide={onSearchHide} onShow={onSearchShow} onChange={onSearchChange} />
<Tree
value={props.nodes}
value={nodes}
className="w-full md:w-30rem"
style={{ minWidth: '10rem' }}
nodeTemplate={nodeTemplate}
togglerTemplate={togglerTemplate}
selectionMode='single'
selectionKeys={props.selectedNode?.key?.toString()}
selectionKeys={selectedNode?.key?.toString()}
onNodeClick={event => onClick(event)}
onExpand={event => onToggle(event)}
onCollapse={event => onToggle(event)}
filter={true}
filterMode='strict'
filterValue={filter}
onFilterValueChange={() => { /* needed as otherwise the filter value is not taken into account */ }}
showHeader={false}
/>
</div >;
</div>;
};

30 changes: 24 additions & 6 deletions src/components/tree/components/treetable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import React from 'react';
import { useCDTTreeContext } from '../tree-context';
import { CDTTreeItem, CDTTreeTableColumnDefinition, CDTTreeTableExpanderColumn, CDTTreeTableStringColumn, CTDTreeMessengerType, CTDTreeWebviewContext } from '../types';
import { createActions, createHighlightedText, createIcon, createLabelWithTooltip } from './utils';
import { SearchOverlay } from './search-overlay';

export type ComponentTreeTableProps = {
nodes?: CDTTreeItem[];
Expand All @@ -37,6 +38,8 @@ export const ComponentTreeTable = (props: ComponentTreeTableProps) => {
}

const treeContext = useCDTTreeContext();
const [filter, setFilter] = React.useState<string | undefined>();
const searchRef = React.useRef<SearchOverlay>(null);

// Event handler
const onToggle = (event: TreeTableEvent) => {
Expand All @@ -55,7 +58,7 @@ export const ComponentTreeTable = (props: ComponentTreeTableProps) => {
const template = (node: TreeNode, field: string) => {
CDTTreeItem.assert(node);

const column = node.columns?.[field];
const column = node.data.columns?.[field];

if (column?.type === 'expander') {
return expanderTemplate(node, column);
Expand All @@ -69,7 +72,7 @@ export const ComponentTreeTable = (props: ComponentTreeTableProps) => {
const expanderTemplate = (node: TreeNode, column: CDTTreeTableExpanderColumn) => {
CDTTreeItem.assert(node);

return <div style={{ paddingLeft: `${((node.path.length ?? 1)) * 8}px` }}
return <div style={{ paddingLeft: `${((node.data.path.length ?? 1)) * 8}px` }}
>
<div className='treetable-node' >
<div
Expand All @@ -90,7 +93,7 @@ export const ComponentTreeTable = (props: ComponentTreeTableProps) => {
const text = createHighlightedText(column.label, column.highlight);

return <div
{...CTDTreeWebviewContext.create({ webviewSection: 'tree-item', cdtTreeItemId: node.id, cdtTreeItemPath: node.path })}
{...CTDTreeWebviewContext.create({ webviewSection: 'tree-item', cdtTreeItemId: node.id, cdtTreeItemPath: node.data.path })}
>
{createLabelWithTooltip(text, column.tooltip)}
</div>;
Expand All @@ -109,7 +112,20 @@ export const ComponentTreeTable = (props: ComponentTreeTableProps) => {
const expandedState = getExpandedState(props.nodes);
const selectedKey = props.selectedNode ? props.selectedNode.key as string : undefined;

return <div>
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.ctrlKey && e.key === 'f') {
e.preventDefault();
e.stopPropagation();
searchRef.current?.show();
}
};

const onSearchShow = () => setFilter(searchRef.current?.value());
const onSearchHide = () => setFilter(undefined);
const onSearchChange = (text: string) => setFilter(text);

return <div onKeyDown={onKeyDown}>
<SearchOverlay key={'search'} ref={searchRef} onHide={onSearchHide} onShow={onSearchShow} onChange={onSearchChange} />
<TreeTable
value={props.nodes}
selectionKeys={selectedKey}
Expand All @@ -124,11 +140,13 @@ export const ComponentTreeTable = (props: ComponentTreeTableProps) => {
onExpand={event => onToggle(event)}
onCollapse={event => onToggle(event)}
onRowClick={event => onClick(event)}
filterMode='strict' // continue searching on children
globalFilter={filter}
>
{props.columnDefinitions?.map(c => {
return <Column field={c.field} body={(node) => template(node, c.field)} expander={c.expander} />;
return <Column key={c.field} field={'columns.' + c.field + '.label'} body={(node) => template(node, c.field)} expander={c.expander} filter={true} />;
})}
<Column field="actions" style={{ width: '64px' }} body={actionsTemplate} />
<Column key={'actions'} field="actions" style={{ width: '64px' }} body={actionsTemplate} />
</TreeTable>
</div>;
};
Expand Down
2 changes: 1 addition & 1 deletion src/components/tree/components/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function createActions(context: CDTTreeContext, node: TreeNode): React.JS
};

return <div className="tree-actions">
{node.options?.commands?.map(a => <i key={a.commandId} className={`codicon codicon-${a.icon}`} onClick={(event) => onClick(event, a)}></i>)}
{node.data.options?.commands?.map(a => <i key={a.commandId} className={`codicon codicon-${a.icon}`} onClick={(event) => onClick(event, a)}></i>)}
</div>;
}

Expand Down
Loading

0 comments on commit 1cb8105

Please sign in to comment.