Skip to content

Commit

Permalink
Merge pull request #829 from IPG-Automotive-UK/enhancement/tree-view-…
Browse files Browse the repository at this point in the history
…list-selected-node-state

TreeViewList - Selected node state
  • Loading branch information
syedsalehinipg authored Mar 13, 2024
2 parents 360c57b + ce3d2df commit 5d9fe9f
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 11 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ipguk/react-ui",
"version": "7.2.0-0",
"version": "7.2.0-2",
"description": "React UI component library for IPG web applications",
"author": "IPG-Automotive-UK",
"license": "MIT",
Expand Down
30 changes: 30 additions & 0 deletions src/TreeViewList/TreeViewList.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,33 @@ test("should not expand the child nodes when expand for search is not enabled",
throw new Error("Frame element is null");
}
});

test("should expand selected node", async ({ page }) => {
// Navigate to the story
await page.goto(
"http://localhost:6006/?path=/story/lists-treeviewlist--default"
);

// Wait for the iframe to be attached in the DOM.
await page.waitForSelector('iframe[title="storybook-preview-iframe"]');

// Expect the test node to be hidden initially
await expect(
page
.frameLocator('iframe[title="storybook-preview-iframe"]')
.getByText("Mass", { exact: true })
).toBeHidden();

// Fill the selected field with an example node id and then lose focus to trigger the update
await page
.getByPlaceholder("Edit JSON string...")
.fill('"SUS.Damper.Front.Mass"');
await page.keyboard.press("Tab");

// Expect the selected test node to be visible after the update
await expect(
page
.frameLocator('iframe[title="storybook-preview-iframe"]')
.getByText("Mass", { exact: true })
).toBeVisible();
});
24 changes: 23 additions & 1 deletion src/TreeViewList/TreeViewList.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React from "react";
import TreeViewList from "./TreeViewList";
import { TreeViewListProps } from "./TreeViewList.types";
import items from "./example-items.json";
import { useArgs } from "@storybook/preview-api";

/**
* Story metadata
Expand All @@ -16,7 +17,15 @@ export default meta;

// Story Template
const Template: StoryFn<TreeViewListProps> = args => {
return <TreeViewList {...args} />;
// useArgs is a hook that returns the current state of the args object
const [{ selected }, updateArgs] = useArgs<TreeViewListProps>();

// update the args object with the new value value
React.useEffect(() => {
updateArgs({ selected });
}, [selected, updateArgs]);

return <TreeViewList {...args} selected={selected} />;
};

// Default
Expand Down Expand Up @@ -45,6 +54,19 @@ export const WithSearch = {
render: Template
};

// With Custom width
export const SelectedNode = {
args: {
enableSearch: true,
expandItems: 1,
expandSearchResults: true,
items,
selected: "SUS.Axle.WheelBase",
width: "90%"
},
render: Template
};

// With Custom width
export const CustomWidth = {
args: {
Expand Down
91 changes: 82 additions & 9 deletions src/TreeViewList/TreeViewList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ const TreeViewList = ({
const [treeDisplayItems, setTreeDisplayItems] =
useState<TreeViewListProps["items"]>(items);

// state for selected node
const [selectedNode, setSelectedNode] = useState<string>(selected ?? "");

// state for search input value
const [searchValue, setSearchValue] = useState("");

Expand Down Expand Up @@ -87,12 +90,14 @@ const TreeViewList = ({

// expand the nodes when the search input changes
const expandNodes = () => {
let searchedNodes: string[] = [];
// use a Set to store the searched nodes to prevent duplicates
const searchedNodes = new Set<string>();

const expandChildNodes = (items: TreeNodeItem[]) => {
// If the condition is not met for the entire items array, return early
if (countLastChild(items) > expandItems) {
// reset the searched nodes array
searchedNodes = [];
searchedNodes.clear();
return;
}

Expand All @@ -101,17 +106,24 @@ const TreeViewList = ({
for (const item of items) {
// if the node has children, expand it and call the function on its children
if (item.children) {
searchedNodes.push(item.nodeId);
searchedNodes.add(item.nodeId);
expandChildNodes(item.children);
}
}
}
};

// recursively expand the child nodes
expandChildNodes(treeDisplayItems);

// update the expanded nodes with the searched nodes
setExpandedNodes(prevState => [...prevState, ...searchedNodes]);
setExpandedNodes(prevState => {
const merged = new Set([...prevState, ...searchedNodes]);
return Array.from(merged);
});
};

// update the expanded nodes when the user expanded nodes change or when the search input changes
useEffect(() => {
if (searchValue !== "") {
// if the search input is not empty, update expanded nodes with the user expanded nodes
Expand All @@ -130,18 +142,41 @@ const TreeViewList = ({
if (enableSearch && expandSearchResults && searchValue !== "") {
debouncedExpandAllNodes();
} else {
// if enableSearch is false and expandSearchResults is false and searchValue is empty, update the expanded nodes with the user expanded nodes
setExpandedNodes(userExpanded);
// if enableSearch is false and expandSearchResults is false, searchValue is empty, and there isn't a selectedNode update the expanded nodes with the user expanded nodes
if (selectedNode === "") {
setExpandedNodes(userExpanded);
}
}
}, [
userExpanded,
debouncedExpandAllNodes,
expandedNodes,
enableSearch,
expandSearchResults,
searchValue
searchValue,
selectedNode,
items
]);

// update the selected node when the selected prop changes
useEffect(() => {
// if the selected node is not empty, update the selected node
if (selected) {
setSelectedNode(selected);
}
}, [selected]);

// update the expanded nodes when the selected node changes
useEffect(() => {
if (selectedNode) {
// update the expanded nodes when the selected node changes
const parentNodeIds = findPathToNodeId(items, selectedNode);

// update the expanded nodes with the parent node ids
setExpandedNodes(prev => [...prev, ...parentNodeIds]);
}
}, [items, selectedNode]);

// render the tree nodes with optional tooltips
const renderTree = (nodes: TreeNodeItem[]) =>
nodes.map(node => (
Expand Down Expand Up @@ -198,7 +233,10 @@ const TreeViewList = ({
{enableSearch ? (
<SearchBar
value={searchValue}
onChange={event => setSearchValue(event.target.value)}
onChange={event => {
setSelectedNode("");
setSearchValue(event.target.value);
}}
/>
) : null}
</Box>
Expand All @@ -208,18 +246,24 @@ const TreeViewList = ({
defaultCollapseIcon={<RemoveIcon />}
defaultExpandIcon={<AddIcon />}
expanded={expandedNodes}
selected={selected}
selected={selectedNode}
onNodeSelect={(event, nodeId) => {
const node = getNodeById(treeDisplayItems, nodeId);
const isChild = Boolean(
node && (!node.children || node.children.length === 0)
);
if (onNodeSelect) {
const nodeDetails = { isChild };
// update the selected node when a node is selected and it is a child
if (isChild) {
setSelectedNode(nodeId);
}
onNodeSelect(event, nodeId, nodeDetails);
}
}}
onNodeToggle={(event, nodeId) => {
// reset the selected node when a node is toggled
setSelectedNode("");
// update the user expanded nodes when a node is toggled
setUserExpanded(nodeId);
// update the expanded nodes when a node is toggled
Expand Down Expand Up @@ -380,4 +424,33 @@ const filterBySearchTerm = (items: TreeNodeItem[], searchTerm: string) => {
return filteredItems as TreeNodeItem[];
};

/**
* Recursively find the path from the root to the given nodeId
* @param items - The items to search.
* @param nodeId - The node id to find.
* @returns The path from the root to the node id.
* @example findPathToNodeId(items, "node_id");
*/
const findPathToNodeId = (items: TreeNodeItem[], nodeId: string): string[] => {
for (const item of items) {
if (item.nodeId === nodeId) {
// if the current item is the one we're looking for, return an array containing only its id
return [item.nodeId];
}

if (item.children) {
// if the current item has children, recursively call this function on them
const pathFromChild = findPathToNodeId(item.children, nodeId);

if (pathFromChild.length > 0) {
// if we found the node in the children, return an array containing the id of the current item and the path from the child
return [item.nodeId, ...pathFromChild];
}
}
}

// if no node found, return an empty array
return [];
};

export default TreeViewList;

0 comments on commit 5d9fe9f

Please sign in to comment.