Skip to content
New issue

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

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

Already on GitHub? # to your account

[HierarchyList] Test coverage, conditional rendering, and defaultSelected expansion #904

Merged
merged 7 commits into from
Feb 17, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 0 additions & 9 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,12 @@ module.exports = {
lines: 73,
functions: 57,
},

'./src/components/Table/TableHead/ColumnResize.jsx': {
statements: 36,
branches: 11,
lines: 36,
functions: 20,
},

// The overrides below are to be fixed/deleted via https://github.com/IBM/carbon-addons-iot-react/issues/707
'./src/components/List/HierarchyList/HierarchyList.jsx': {
statements: 69.62,
branches: 65,
lines: 73.33,
functions: 56.25,
},
'./src/components/Table/TableHead/FilterHeaderRow/FilterHeaderRow.jsx': { branches: 70 },
'./src/components/Table/TableToolbar/TableToolbar.jsx': { functions: 66 },
'./src/components/Table/TableBody/RowActionsCell/RowActionsCell.jsx': {
Expand Down
93 changes: 64 additions & 29 deletions src/components/List/HierarchyList/HierarchyList.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, { useState, useRef, useEffect } from 'react';
import React, { useState, useRef, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import cloneDeep from 'lodash/cloneDeep';
import debounce from 'lodash/debounce';

import { caseInsensitiveSearch } from '../../../utils/componentUtilityFunctions';
import List from '../List';

const propTypes = {
Expand Down Expand Up @@ -46,36 +47,38 @@ const defaultProps = {
expand: 'Expand',
close: 'Close',
},
isFullHeight: true,
isFullHeight: false,
pageSize: null,
defaultSelectedId: null,
onSelect: null,
};

/**
* Searches an item for a specific value
* @param {Object} item to be searched
* @returns {Boolean} found or not
* Assumes that the first level of items is not searchable and is only uses to categorize the
* items. Because of that, only search through the children. If a child is found while filtering,
* it needs to be returned to the filtered array. Deep clone is required because spread syntax is
* only a shallow clone
* @param {Array<Object>} items
* @param {String} value what to search for
* @returns {Array<Object>}
*/
export const searchItem = (item, searchTerm) => {
// Check that the value is not empty
if (item.content.value !== '' && item.content.value !== undefined) {
// Check that the secondary value is not empty
if (
item.content.secondaryValue !== '' &&
item.content.secondaryValue !== undefined &&
// Check if the value or secondary value has a match
(item.content.value.toLowerCase().search(searchTerm.toLowerCase()) !== -1 ||
item.content.secondaryValue.toLowerCase().search(searchTerm.toLowerCase()) !== -1)
) {
return true;
}
if (item.content.value.toLowerCase().search(searchTerm.toLowerCase()) !== -1) {
return true;
export const searchForNestedItemValues = (items, value) => {
const filteredItems = [];
cloneDeep(items).forEach(item => {
// if the item has children, recurse and search children
if (item.children) {
// eslint-disable-next-line
item.children = searchForNestedItemValues(item.children, value);
// if it's children did, we still need the item
if (item.children.length > 0) {
filteredItems.push(item);
}
} // if the item matches, add it to the filterItems array
else if (caseInsensitiveSearch([item.content.value, item.content.secondaryValue], value)) {
filteredItems.push(item);
}
return false;
}
return false;
});
return filteredItems;
};

/**
Expand All @@ -87,19 +90,19 @@ export const searchItem = (item, searchTerm) => {
* @param {String} value what to search for
* @returns {Array<Object>}
*/
export const searchNestedItems = (items, value) => {
export const searchForNestedItemIds = (items, value) => {
const filteredItems = [];
cloneDeep(items).forEach(item => {
// if the item has children, recurse and search children
if (item.children) {
// eslint-disable-next-line
item.children = searchNestedItems(item.children, value);
item.children = searchForNestedItemIds(item.children, value);
// if it's children did, we still need the item
if (item.children.length > 0) {
filteredItems.push(item);
}
} // if the item matches, add it to the filterItems array
else if (searchItem(item, value)) {
else if (caseInsensitiveSearch([item.id], value)) {
filteredItems.push(item);
}
});
Expand All @@ -126,6 +129,39 @@ const HierarchyList = ({
const [selectedIds, setSelectedIds] = useState([]);
const [selectedId, setSelectedId] = useState(defaultSelectedId);

const selectedItemRef = useCallback(node => {
if (node) {
// Check if a node is actually passed. Otherwise node would be null.
// node is the text of the item
// parentNode is the container div with the button role
// parentNode.parentNode is the list content, which is also the element to be scrolled
// the offsetHeight needs to be multiplied by 3 to be able to view the whole element
const offset =
node.offsetTop -
node.parentNode?.offsetHeight -
node.parentNode?.offsetHeight -
node.parentNode?.offsetHeight;
// eslint-disable-next-line no-unused-expressions
node.parentNode?.parentNode?.scroll(0, offset);
}
}, []);

useEffect(
() => {
// Expand the parent elements of the defaultSelectedId
if (defaultSelectedId) {
const tempFilteredItems = searchForNestedItemIds(items, defaultSelectedId);
const tempExpandedIds = [];
// Expand the categories that have found results
tempFilteredItems.forEach(categoryItem => {
tempExpandedIds.push(categoryItem.id);
});
setExpandedIds(tempExpandedIds);
}
},
[defaultSelectedId, items]
);

const handleSelect = id => {
if (selectedIds.includes(id)) {
setSelectedId(null);
Expand Down Expand Up @@ -192,9 +228,7 @@ const HierarchyList = ({
* @param {String} text keyed values from search input
*/
const handleSearch = text => {
/**
*/
const tempFilteredItems = searchNestedItems(items, text);
const tempFilteredItems = searchForNestedItemValues(items, text);
const tempExpandedIds = [];
// Expand the categories that have found results
tempFilteredItems.forEach(categoryItem => {
Expand Down Expand Up @@ -247,6 +281,7 @@ const HierarchyList = ({
selectedId={selectedId}
selectedIds={selectedIds}
handleSelect={handleSelect}
ref={selectedItemRef}
/>
);
};
Expand Down
50 changes: 45 additions & 5 deletions src/components/List/HierarchyList/HierarchyList.story.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ const addButton = (
/>
);

storiesOf('Watson IoT Experimental|Hierarchy List', module).add(
'Stateful list with nested searching',
() => (
storiesOf('Watson IoT Experimental|HierarchyList', module)
.add('Stateful list with nested searching', () => (
<div style={{ width: 400, height: 400 }}>
<HierarchyList
title={text('Title', 'MLB Expanded List')}
Expand Down Expand Up @@ -64,5 +63,46 @@ storiesOf('Watson IoT Experimental|Hierarchy List', module).add(
pageSize={select('Page Size', ['sm', 'lg', 'xl'], 'sm')}
/>
</div>
)
);
))
.add('With defaultSelectedId', () => (
<div style={{ width: 400, height: 400 }}>
<HierarchyList
title={text('Title', 'MLB Expanded List')}
defaultSelectedId={text('Default Selected Id', 'New York Mets_Pete Alonso')}
items={[
...Object.keys(sampleHierarchy.MLB['American League']).map(team => ({
id: team,
isCategory: true,
content: {
value: team,
},
children: Object.keys(sampleHierarchy.MLB['American League'][team]).map(player => ({
id: `${team}_${player}`,
content: {
value: player,
secondaryValue: sampleHierarchy.MLB['American League'][team][player],
},
isSelectable: true,
})),
})),
...Object.keys(sampleHierarchy.MLB['National League']).map(team => ({
id: team,
isCategory: true,
content: {
value: team,
},
children: Object.keys(sampleHierarchy.MLB['National League'][team]).map(player => ({
id: `${team}_${player}`,
content: {
value: player,
secondaryValue: sampleHierarchy.MLB['National League'][team][player],
},
isSelectable: true,
})),
})),
]}
hasSearch
pageSize={select('Page Size', ['sm', 'lg', 'xl'], 'lg')}
/>
</div>
));
Loading