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

[PLAY-863] MultiLevelSelect: expansion improvement and no pills option #2628

Merged
merged 8 commits into from
Jul 11, 2023
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
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