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

Closes eclipse-cdt-cloud#23
  • Loading branch information
martin-fleck-at committed Jan 9, 2025
1 parent 89663e2 commit a5330a7
Show file tree
Hide file tree
Showing 6 changed files with 608 additions and 320 deletions.
43 changes: 43 additions & 0 deletions src/components/tree/components/expand-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/********************************************************************************
* 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
********************************************************************************/
import { CDTTreeItem } from '../types';
import { classNames } from './utils';
import React from 'react';

export interface RenderExpandIconProps<RecordType> {
prefixCls: string;
expanded: boolean;
record: RecordType;
expandable: boolean;
onExpand: TriggerEventHandler<RecordType>;
}

export type TriggerEventHandler<RecordType> = (record: RecordType, event: React.MouseEvent<HTMLElement>) => void;

export function ExpandIcon({ expanded, onExpand, record, expandable }: RenderExpandIconProps<CDTTreeItem>): React.ReactElement {
if (!expandable) {
// simulate spacing to the left that we gain through expand icon so that leaf items look correctly intended
return <span className='leaf-item-spacer' />;
}

const doExpand = (event: React.MouseEvent<HTMLElement>) => {
event.stopPropagation();
onExpand(record, event);
};

const iconClass = expanded ? 'codicon-chevron-down' : 'codicon-chevron-right';
return (
<div
className={classNames('tree-toggler-container', 'codicon', iconClass)}
onClick={doExpand}
role="button"
tabIndex={0}
aria-label={expanded ? 'Collapse row' : 'Expand row'}
onKeyDown={event => { if (event.key === 'Enter' || event.key === ' ') doExpand(event as unknown as React.MouseEvent<HTMLElement>); }}
></div>
);
}
88 changes: 88 additions & 0 deletions src/components/tree/components/search-overlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*********************************************************************
* 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 {
input: () => HTMLInputElement | null;
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, () => ({
input: () => searchTextRef.current,
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>
);
});
84 changes: 84 additions & 0 deletions src/components/tree/components/search.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/********************************************************************************
* 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: 20px;
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: top 0.2s ease, opacity 0.2s ease;
-moz-transition: top 0.2s ease, opacity 0.2s ease;
-ms-transition: top 0.2s ease, opacity 0.2s ease;
-o-transition: top 0.2s ease, opacity 0.2s ease;
transition: top 0.2s ease, opacity 0.2s ease;
}

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

body.has-scrollbar .search-overlay {
right: 5px;
}

.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);
}
171 changes: 171 additions & 0 deletions src/components/tree/components/treetable-navigator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/********************************************************************************
* 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
********************************************************************************/
import { CDTTreeItem } from '../types';

export interface TreeNavigatorProps {
ref: React.RefObject<HTMLDivElement>;
rowIndex: Map<string, number>;
expandedRowKeys: string[];
expand: (expanded: boolean, record: CDTTreeItem) => void;
select: (record: CDTTreeItem) => void;
}

/**
* TreeNavigator is a helper class to navigate
* through a tree table.
*/
export class TreeNavigator {
constructor(private readonly props: TreeNavigatorProps) {
}

next(node: CDTTreeItem) {
if (node.children && node.children.length > 0 && this.props.expandedRowKeys.includes(node.id)) {
// Go deeper
this.select(node.children[0]);
} else {
let nextNode = this.getNext(node);
if (nextNode) {
this.select(nextNode);
} else {
// Go to parent sibling recursively
nextNode = this.getParentNext(node.parent);
if (nextNode) {
this.select(nextNode);
}
}
}
}

nextPage() {
this.scrollRelative(this.visibleDomElementCount);
}

private getSiblings(node: CDTTreeItem): CDTTreeItem[] {
return node.parent?.children?.filter(child => this.props.rowIndex.has(child.id)) ?? [];
}

private getNext(node: CDTTreeItem): CDTTreeItem | undefined {
const siblings = this.getSiblings(node);
const index = siblings.findIndex(n => n.id === node.id);
return siblings[index + 1];
}

private getParentNext(node: CDTTreeItem | undefined): CDTTreeItem | undefined {
if (!node) return undefined;
const nextSibling = this.getNext(node);
if (nextSibling) {
return nextSibling;
} else {
return this.getParentNext(node.parent);
}
}

previous(node: CDTTreeItem) {
let prevNode = this.getPrevious(node);
if (prevNode) {
// Go deeper to the last child if the previous node has children and is expanded
while (prevNode.children && prevNode.children.length > 0 && this.props.expandedRowKeys.includes(prevNode.id)) {
prevNode = prevNode.children[prevNode.children.length - 1];
}
this.select(prevNode);
} else {
const parent = node.parent;
// Go to parent if no previous sibling
if (parent && !CDTTreeItem.isRoot(parent)) {
this.select(parent);
}
}
}

previousPage() {
this.scrollRelative(-(this.visibleDomElementCount - 1));
}

private getPrevious(node: CDTTreeItem): CDTTreeItem | undefined {
const siblings = this.getSiblings(node);
const index = siblings.findIndex(n => n.id === node.id);
return siblings[index - 1];
}

toggle(node: CDTTreeItem) {
if (this.props.expandedRowKeys.includes(node.id)) {
this.collapse(node);
} else {
this.expand(node);
}
}

expand(node: CDTTreeItem) {
if (node.children && node.children.length > 0) {
if (this.props.expandedRowKeys.includes(node.id)) {
this.next(node);
} else {
this.props.expand(true, node);
}
}
}

collapse(node: CDTTreeItem) {
if (node.children && node.children.length > 0 && this.props.expandedRowKeys.includes(node.id)) {
this.props.expand(false, node);
} else if (node.parent && !CDTTreeItem.isRoot(node.parent)) {
this.select(node.parent, 'absolute');
}
}

private select(node: CDTTreeItem, scrollMode: 'relative' | 'absolute' = 'absolute') {
// Virtual scrolling may have hidden the node
if (!this.isDomVisible(node)) {
if (scrollMode === 'absolute') {
this.scrollAbsolute(node);
this.props.select(node);
} else {
this.scrollRelative(-(this.visibleDomElementCount / 2));
}

this.props.select(node);
// Allow the DOM to update before focusing
setTimeout(() => this.getDomElement(node)?.focus(), 100);
} else {
this.props.select(node);
this.getDomElement(node)?.focus();
}

this.getDomElement(node)?.addEventListener;
}

// ==== DOM ====

private scrollRelative(count = this.visibleDomElementCount) {
const rowHeight = this.props.ref.current?.querySelector<HTMLDivElement>('.ant-table-row')?.clientHeight ?? 22;
const body = this.props.ref.current?.querySelector<HTMLDivElement>('.ant-table-tbody-virtual-holder');
if (body) {
body.scrollTop = Math.max(body.scrollTop + count * rowHeight, 0);
}
}

private scrollAbsolute(node: CDTTreeItem) {
const rowHeight = this.props.ref.current?.querySelector<HTMLDivElement>('.ant-table-row')?.clientHeight ?? 22;
const body = this.props.ref.current?.querySelector<HTMLDivElement>('.ant-table-tbody-virtual-holder');
if (body) {
const index = this.props.rowIndex.get(node.id) ?? 1;
body.scrollTop = Math.max(index * rowHeight, 0);
}
}

private getDomElement(record: CDTTreeItem) {
return this.props.ref.current?.querySelector<HTMLDivElement>(`[data-row-key="${record.key}"]`);
}

private isDomVisible(record: CDTTreeItem) {
return !!this.getDomElement(record);
}

private get visibleDomElementCount() {
return this.props.ref.current?.querySelectorAll<HTMLDivElement>('.ant-table-row').length ?? 1;
}
}
Loading

0 comments on commit a5330a7

Please sign in to comment.