Skip to content

Commit

Permalink
[PLAY-863] MultiLevelSelect: expansion improvement and no pills option (
Browse files Browse the repository at this point in the history
#2628)

**What does this PR do?**

1- Adds in the the new inputDisplay prop with default set to 'pills'.
Can be optionally set to 'none' if no pills wanted in input field
2- If items already checked on first load, dropdown should show tree
expanded to show checked items
3- Fixed padding on input so it is closer UI wise to TextInupt

To see 1 and 2 in action, see codesandbox[
HERE](https://codesandbox.io/s/multilevelselect-inputdisplay-prop-expand-to-show-checked-gq4zkr)

[RUNWAY
TICKET](https://nitro.powerhrg.com/runway/backlog_items/PLAY-863)

**Screenshots:** 

![Screenshot 2023-07-05 at 3 39 08
PM](https://github.com/powerhome/playbook/assets/73710701/10b2f5f4-1060-407c-b8a5-f4f57559888b)

---------

Co-authored-by: Jasper Furniss <[email protected]>
  • Loading branch information
nidaqg and jasperfurniss authored Jul 11, 2023
1 parent 6229102 commit 8b76840
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ export const getAncestorsOfUnchecked = (
if (item.parent_id) {
const ancestor = filterFormattedDataById(data, item.parent_id);
ancestor[0].checked = false;
ancestor[0].parent_id && getAncestorsOfUnchecked(data, ancestor[0])
ancestor[0].parent_id && getAncestorsOfUnchecked(data, ancestor[0]);
}
return data;
};

//function is going over formattedData and returning all objects that match the
//id of the clicked item from the dropdown
export const filterFormattedDataById = (
Expand All @@ -20,9 +20,9 @@ export const filterFormattedDataById = (
const matched: { [key: string]: any }[] = [];
const recursiveSearch = (data: { [key: string]: any }[], term: string) => {
for (const item of data) {
if (item.id.toLowerCase() === (term.toLowerCase())) {
if (item.id.toLowerCase() === term.toLowerCase()) {
matched.push(item);
return
return;
}

if (item.children && item.children.length > 0) {
Expand Down Expand Up @@ -75,24 +75,29 @@ export const getCheckedItems = (
});
return checkedItems;
};
export const getDefaultCheckedItems = (treeData:{ [key: string]: any }[]) => {

export const getDefaultCheckedItems = (treeData: { [key: string]: any }[]) => {
const checkedDefault: { [key: string]: any }[] = [];

const traverseTree = (items:{ [key: string]: any }[]) => {
const traverseTree = (items: { [key: string]: any }[]) => {
if (!Array.isArray(items)) {
return;
}
items.forEach((item:{ [key: string]: any }) => {
items.forEach((item: { [key: string]: any }) => {
if (item.checked) {
if (item.children && item.children.length > 0) {
const uncheckedChildren = item.children.filter((child:{ [key: string]: any }) => !child.checked);
const uncheckedChildren = item.children.filter(
(child: { [key: string]: any }) => !child.checked
);
if (uncheckedChildren.length === 0) {
checkedDefault.push(item);
return;
}
} else {
const parent = items.find((parentItem:{ [key: string]: any }) => parentItem.id === item.parentId);
const parent = items.find(
(parentItem: { [key: string]: any }) =>
parentItem.id === item.parentId
);
if (!parent || !parent.checked) {
checkedDefault.push(item);
}
Expand All @@ -112,23 +117,47 @@ export const getDefaultCheckedItems = (treeData:{ [key: string]: any }[]) => {

export const recursiveCheckParent = (
item: { [key: string]: any },
data:any
data: any
) => {
if (item.parent_id !== null) {
const parent = filterFormattedDataById(data, item.parent_id);
const allChildrenChecked = parent[0].children.every(
(child: { [key: string]: any }) => child.checked
);
if (allChildrenChecked) {
parent[0].checked = true;
const parentHasParent = parent[0].parent_id !== null;
if (parentHasParent) {
recursiveCheckParent(
parent[0],
data
);
const parent = filterFormattedDataById(data, item.parent_id);
const allChildrenChecked = parent[0].children.every(
(child: { [key: string]: any }) => child.checked
);
if (allChildrenChecked) {
parent[0].checked = true;
const parentHasParent = parent[0].parent_id !== null;
if (parentHasParent) {
recursiveCheckParent(parent[0], data);
}
}
}
return data;
};

export const getExpandedItems = (treeData: { [key: string]: string }[]) => {
let expandedItems: any[] = [];

const traverse = (items: string | any[], ancestors: any[] = []) => {
for (let i = 0; i < items.length; i++) {
const item = items[i];
const itemAncestors = [...ancestors, item];

if (item.expanded) {
expandedItems.push(item.id);
}
if (Array.isArray(item.children)) {
const hasCheckedChildren = item.children.some(
(child: { [key: string]: string }) => child.checked
);
if (hasCheckedChildren) {
expandedItems.push(...itemAncestors.map((ancestor) => ancestor.id));
}
traverse(item.children, itemAncestors);
}
}
}
}
return data;
}
};

traverse(treeData);
return expandedItems;
};
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
.input_wrapper {
background-color: $white;
cursor: pointer;
padding: $space_xs + 4 $space_sm;
padding: $space_xs + 1 $space_sm;
border: 1px solid #e4e8f0;
border-radius: $border_rad_heavier;
display: flex;
Expand All @@ -36,7 +36,6 @@
input {
border: none;
font-family: $font_family_base;
padding: $space_xs;
&:focus {
outline: none;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import {
getCheckedItems,
getDefaultCheckedItems,
recursiveCheckParent,
getExpandedItems,
} from "./_helper_functions"

type MultiLevelSelectProps = {
aria?: { [key: string]: string }
className?: string
data?: { [key: string]: string }
id?: string
inputDisplay?: "pills" | "none"
name?: string
returnAllSelected?: boolean
treeData?: { [key: string]: string }[]
Expand All @@ -34,6 +36,7 @@ const MultiLevelSelect = (props: MultiLevelSelectProps) => {
className,
data = {},
id,
inputDisplay = "pills",
name,
returnAllSelected = false,
treeData,
Expand All @@ -50,26 +53,6 @@ const MultiLevelSelect = (props: MultiLevelSelectProps) => {

const dropdownRef = useRef(null)


const getExpandedItems = (treeData: { [key: string]: string }[]) => {
let expandedItems: any[] = [];

const traverse = (items: string | any[]) => {
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.expanded) {
expandedItems.push(item.id);
}
if (Array.isArray(item.children)) {
traverse(item.children);
}
}
}

traverse(treeData);
return expandedItems;
}

//state for whether dropdown is open or closed
const [isClosed, setIsClosed] = useState(true)
//state from onchange for textinput, to use for filtering to create typeahead
Expand Down Expand Up @@ -174,8 +157,6 @@ const MultiLevelSelect = (props: MultiLevelSelectProps) => {
return tree
}



//function to map over data and add parent_id + depth property to each item
const addCheckedAndParentProperty = (
treeData: { [key: string]: any }[],
Expand Down Expand Up @@ -331,7 +312,7 @@ const MultiLevelSelect = (props: MultiLevelSelectProps) => {
))
: null}

{returnedArray.length !== 0 && returnAllSelected
{returnedArray.length !== 0 && inputDisplay === "pills" && returnAllSelected
? returnedArray.map((item, index) => (
<FormPill
key={index}
Expand All @@ -342,17 +323,19 @@ const MultiLevelSelect = (props: MultiLevelSelectProps) => {
))
: null}
{!returnAllSelected &&
defaultReturn.length !== 0 &&
defaultReturn.length !== 0 && inputDisplay === "pills" ?
defaultReturn.map((item, index) => (
<FormPill
key={index}
text={item.label}
size='small'
onClick={(event: any) => handlePillClose(event, item)}
/>
))}
{returnedArray.length !== 0 && returnAllSelected && <br />}
{defaultReturn.length !== 0 && !returnAllSelected && <br />}
))
: null
}
{returnedArray.length !== 0 && returnAllSelected && inputDisplay === "pills" && <br />}
{defaultReturn.length !== 0 && !returnAllSelected && inputDisplay === "pills" && <br />}
<input
id='multiselect_input'
onChange={(e) => {
Expand All @@ -365,11 +348,11 @@ const MultiLevelSelect = (props: MultiLevelSelectProps) => {
</div>
{isClosed ? (
<div key='chevron-down'>
<Icon icon='chevron-down' />
<Icon icon='chevron-down' size="xs"/>
</div>
) : (
<div key='chevron-up'>
<Icon icon='chevron-up' />
<Icon icon='chevron-up' size="xs"/>
</div>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
The `returnAllSelected` or `return_all_selected` prop can be used when users want data on all checked nodes from the dropdown, irrespective of whether it is a parent or child node.

__NOTE__ :This variant also does not automatically uncheck the parent when any of the child nodes are unchecked. `returnAllSelected` is set to false by default.
__NOTE__: This variant also does not automatically uncheck the parent when any of the child nodes are unchecked. `returnAllSelected` is set to false by default.

__NOTE__: For larger trees that may return many pill selections, you can optionally set `input_display: "none"`(for Rails) or `inputDisplay = "none"`(for React) to hide all pills within the input.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ class MultiLevelSelect < Playbook::KitBase
default: []
prop :return_all_selected, type: Playbook::Props::Boolean,
default: false
prop :input_display, type: Playbook::Props::Enum,
values: %w[pills none],
default: "pills"

def classname
generate_classname("pb_multi_level_select")
Expand All @@ -17,6 +20,7 @@ def classname
def multi_level_select_options
{
id: id,
inputDisplay: input_display,
name: name,
treeData: tree_data,
returnAllSelected: return_all_selected,
Expand Down

0 comments on commit 8b76840

Please sign in to comment.