Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TreeViewList - expand search items #808

Merged
merged 4 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/TreeViewList/TreeViewList.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,49 @@ test("should hide child when is collapsed", async ({ page }) => {
.getByText("GearSpred", { exact: true })
).toBeHidden();
});

test("should expand the parent and child nodes that match the search term", async ({
page
}) => {
await page.goto(
"http://localhost:6006/?path=/story/lists-treeviewlist--default&args=expandSearchTerm:true;searchTerm:ratio"
);
await expect(
page
.frameLocator('iframe[title="storybook-preview-iframe"]')
.getByText("BRK")
).toBeVisible();
await expect(
page
.frameLocator('iframe[title="storybook-preview-iframe"]')
.getByText("Pedal")
).toBeVisible();
await expect(
page
.frameLocator('iframe[title="storybook-preview-iframe"]')
.getByText("Ratio", { exact: true })
).toBeVisible();
});

test("should not expand the child nodes when expand for search is not enabled", async ({
page
}) => {
await page.goto(
"http://localhost:6006/?path=/story/lists-treeviewlist--default&args=expandSearchTerm:false;searchTerm:ratio"
);
await expect(
page
.frameLocator('iframe[title="storybook-preview-iframe"]')
.getByText("BRK")
).toBeVisible();
await expect(
page
.frameLocator('iframe[title="storybook-preview-iframe"]')
.getByText("Pedal")
).toBeHidden();
await expect(
page
.frameLocator('iframe[title="storybook-preview-iframe"]')
.getByText("Ratio", { exact: true })
).toBeHidden();
});
6 changes: 6 additions & 0 deletions src/TreeViewList/TreeViewList.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export const Default = {
}
],
name: "HydESPModel"
},
{
name: "BoreTravel",
options: [],
tooltip:
"Piston travel to close compensation bore inside master cylinder. Parameter needed for CarMaker hydraulic ESC (Name: 'MC.xCompBore')."
}
],
name: "MCbooster"
Expand Down
124 changes: 83 additions & 41 deletions src/TreeViewList/TreeViewList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
TreeNode,
TreeViewListProps
} from "./TreeViewList.types";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { TreeItem, TreeView } from "@mui/x-tree-view";

import AddIcon from "@mui/icons-material/Add";
Expand All @@ -22,14 +22,17 @@ import { alpha } from "@mui/material/styles";
* @property props.selected - The ID of the currently selected item.
* @property props.searchTerm - The term to search for in the items.
* @property [props.defaultExpanded=[]] - The IDs of the items that should be expanded by default.
* @property props.expandSearchTerm - Weather to expand the tree when searching.
* @property props.width - The width of the tree view list.
* @property props.onSelectionChange - The function to call when the selection changes.
* @returns The tree view list component.
*/
const TreeViewList = <T,>({
items,
selected,
searchTerm,
searchTerm = "",
defaultExpanded = [],
expandSearchTerm = false,
width,
onSelectionChange
}: TreeViewListProps<T>) => {
Expand All @@ -43,6 +46,40 @@ const TreeViewList = <T,>({
setExpanded(nodeIds);
};

// search the tree and return an array of matching nodeIds
const searchTree = (
nodes: TreeNode[],
term: string,
isChildSearch: boolean = false // Additional parameter to indicate if we are searching within child nodes
): string[] => {
let result: string[] = [];
const terms = term.toLowerCase().split(" "); // Convert term to lower case and split into words

nodes.forEach(node => {
const nodeNameLower = node.name.toLowerCase(); // Convert node name to lower case
const nodeMatched = terms.some(t => nodeNameLower.includes(t));

// If the current node matches, or if we're in a child search and there's a match in children, add the node ID
if (nodeMatched || isChildSearch) {
result.push(node.id);
}

// If the node has children, search them too
if (node.children) {
const childResult = searchTree(node.children, term, true);
// If there's a match in children, ensure the parent node is included for expansion
if (childResult.length > 0 && !nodeMatched) {
result.push(node.id);
}
// Concatenate the child results (which includes only matching children and their parents)
result = result.concat(childResult);
}
});

// Remove duplicates and return
return Array.from(new Set(result));
};

// tooltip tree item
const TooltipTreeItem = (props: TooltipTreeItemProps) => {
const handleClick = () => {
Expand Down Expand Up @@ -84,49 +121,39 @@ const TreeViewList = <T,>({

// recursive search function
const applySearch = (search: string, parameters: Item<T>[]): Item<T>[] => {
const terms = search
.toUpperCase()
.trim()
.split(/(?:\.| )+/);

const searchInItem = (item: Item<T>): Item<T> | null => {
const match = terms.some(term =>
typeof item.name === "string"
? item.name.toUpperCase().includes(term)
: false
);

if (match) {
// if the item matches any of the search terms, return the whole item
return item;
}
// Split the search string into individual terms for comparison
const terms = search.toLowerCase().split(" ");

const children = Array.isArray(item.children)
? item.children.map(searchInItem).filter(Boolean)
: [];
const options = Array.isArray(item.options)
? item.options
.map((option: Item<T>) => {
if (typeof option === "object" && option !== null) {
return searchInItem(option as Item<T>);
}
return null;
})
.filter(Boolean)
: [];

if (children.length > 0 || options.length > 0) {
return {
...item,
children: children as T,
options: options as T
};
}
// Function to recursively search and filter the items
const filterItems = (items: Item<T>[]): Item<T>[] => {
return items.reduce((acc: Item<T>[], item) => {
const itemName = (item.name as string)?.toLowerCase() || "";

// Check if the item name matches any of the search terms
let matches = terms.some(term => itemName.toLowerCase().includes(term));

// If there are children, recursively filter them
if (Array.isArray(item.children) && item.children.length > 0) {
const filteredChildren = filterItems(item.children);

return null;
// If any children match, include this item in the result
if (filteredChildren.length > 0) {
matches = true;
// Update the item with the filtered children
item = { ...item, children: filteredChildren as T };
}
}

// If this item or any of its children match, add it to the accumulator
if (matches) {
acc.push(item);
}

return acc;
}, []);
};

return parameters.map(searchInItem).filter(Boolean) as Item<T>[];
return filterItems(parameters);
};

// build tree nodes
Expand All @@ -152,6 +179,21 @@ const TreeViewList = <T,>({
</TooltipTreeItem>
));

// expand nodes that match the search term
useEffect(() => {
if (expandSearchTerm) {
// Reset expanded ids
setExpanded([]);

// if the search term is not empty, get the ids of the matching nodes and set them as expanded
if (searchTerm !== "") {
const ids = searchTree(parameters, searchTerm);
setExpanded(ids);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
syedsalehinipg marked this conversation as resolved.
Show resolved Hide resolved
}, [searchTerm]);

return (
<TreeView
defaultCollapseIcon={<RemoveIcon />}
Expand Down
5 changes: 5 additions & 0 deletions src/TreeViewList/TreeViewList.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export type TreeViewListProps<T> = {
*/
searchTerm?: string;

/**
* Flag indicating whether the search term should be expanded in the tree view list. This is optional.
*/
expandSearchTerm?: boolean;

/**
* The IDs of the items that should be expanded by default. This is optional.
*/
Expand Down
Loading