From dcaa44365efbd9a0d854d5890091b572f74e746f Mon Sep 17 00:00:00 2001
From: Laura Silva <91160746+silvalaura@users.noreply.github.com>
Date: Fri, 5 Apr 2024 17:21:25 -0400
Subject: [PATCH] feat(TreeView): Fixing initialSelectedItems (#1225)
---
.changeset/treeview-4.md | 5 +
.../components/TreeView/TreeView.stories.tsx | 389 +++++++++++++++---
.../src/components/TreeView/TreeView.test.js | 131 +++++-
.../components/TreeView/TreeViewContext.ts | 14 +-
.../src/components/TreeView/useTreeItem.ts | 125 +++---
.../src/components/TreeView/useTreeView.ts | 7 +
.../src/components/TreeView/utils.ts | 58 ++-
7 files changed, 584 insertions(+), 145 deletions(-)
create mode 100644 .changeset/treeview-4.md
diff --git a/.changeset/treeview-4.md b/.changeset/treeview-4.md
new file mode 100644
index 000000000..2d5e6c2d0
--- /dev/null
+++ b/.changeset/treeview-4.md
@@ -0,0 +1,5 @@
+---
+"react-magma-dom": minor
+---
+
+feat(TreeView): TreeView & TreeItem updates
\ No newline at end of file
diff --git a/packages/react-magma-dom/src/components/TreeView/TreeView.stories.tsx b/packages/react-magma-dom/src/components/TreeView/TreeView.stories.tsx
index 1359aa2ba..1097ae25d 100644
--- a/packages/react-magma-dom/src/components/TreeView/TreeView.stories.tsx
+++ b/packages/react-magma-dom/src/components/TreeView/TreeView.stories.tsx
@@ -90,25 +90,25 @@ export const Default = args => {
setSelectedItems(selected);
setIndeterminateItems(indet);
setTotal(items.length);
- // console.log('onSelection event:', items);
+ console.log('onSelection event:', items);
}
return (
<>
- {/* Part 1: Introduction>} itemId="pt1" testId="pt1">
+ Part 1: Introduction>} itemId="pt1" testId="pt1">
}
label={<>Chapter 1: I love tiramisu jelly beans soufflé>}
itemId="pt1ch1"
testId="pt1ch1"
+ isDisabled
>
}
label={<>Section 1.1: Cake donut lemon drops gingerbread>}
itemId="pt1ch1.1"
- // isDisabled
/>
{
itemId="pt1ch3.3"
/>
- */}
+
}
label={
@@ -310,7 +310,9 @@ export const Default = args => {
<>
{total} total
Selected: {selectedItems}
- {args.selectable === TreeViewSelectable.multi && (Indeterminate: {indeterminateItems}
)}
+ {args.selectable === TreeViewSelectable.multi && (
+ Indeterminate: {indeterminateItems}
+ )}
>
)}
>
@@ -320,13 +322,16 @@ export const Default = args => {
Default.args = {
selectable: TreeViewSelectable.multi,
ariaLabel: 'Textbook tree',
- initialExpandedItems: [],
+ initialExpandedItems: ['pt1', 'pt1ch1'],
initialSelectedItems: [
- // { itemId: 'pt1ch1', checkedStatus: IndeterminateCheckboxStatus.checked },
- // { itemId: 'pt1', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
+ { itemId: 'pt1ch1', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'pt1', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
{ itemId: 'pt2ch4', checkedStatus: IndeterminateCheckboxStatus.checked },
// { itemId: 'pt2ch5', checkedStatus: IndeterminateCheckboxStatus.checked },
- { itemId: 'pt2ch5.1.1', checkedStatus: IndeterminateCheckboxStatus.checked },
+ {
+ itemId: 'pt2ch5.1.1',
+ checkedStatus: IndeterminateCheckboxStatus.checked,
+ },
{ itemId: 'pt2ch5.2', checkedStatus: IndeterminateCheckboxStatus.checked },
{ itemId: 'pt2ch5.3', checkedStatus: IndeterminateCheckboxStatus.checked },
],
@@ -405,7 +410,9 @@ export const NoIcons = args => {
{args.selectable !== TreeViewSelectable.off && (
<>
Selected: {selectedItems}
- {args.selectable === TreeViewSelectable.multi && (Indeterminate: {indeterminateItems}
)}
+ {args.selectable === TreeViewSelectable.multi && (
+ Indeterminate: {indeterminateItems}
+ )}
>
)}
>
@@ -421,16 +428,32 @@ NoIcons.args = {
export const Textbook = args => {
const [selectedItems, setSelectedItems] = React.useState(null);
+ const [indeterminateItems, setIndeterminateItems] = React.useState(null);
+ const [total, setTotal] = React.useState(null);
function onSelection(items) {
- const allTags = items.map((i: any, key) => {
- return (
-
- {i}
+ const selected = items
+ .filter(i => i.checkedStatus === IndeterminateCheckboxStatus.checked)
+ .map((i, key) => (
+
+ {i.itemId}
+
+ ));
+
+ const indet = items
+ .filter(
+ i => i.checkedStatus === IndeterminateCheckboxStatus.indeterminate
+ )
+ .map((i, key) => (
+
+ {i.itemId}
- );
- });
- setSelectedItems(allTags);
+ ));
+
+ setSelectedItems(selected);
+ setIndeterminateItems(indet);
+ setTotal(items.length);
+ // console.log('onSelection event:', items);
}
return (
@@ -503,7 +526,13 @@ export const Textbook = args => {
{args.selectable !== TreeViewSelectable.off && (
- <>Selected: {selectedItems}>
+ <>
+
{total} total
+ Selected: {selectedItems}
+ {args.selectable === TreeViewSelectable.multi && (
+ Indeterminate: {indeterminateItems}
+ )}
+ >
)}
>
);
@@ -516,6 +545,7 @@ Textbook.args = {
export const Simple = args => {
const [selectedItems, setSelectedItems] = React.useState(null);
const [indeterminateItems, setIndeterminateItems] = React.useState(null);
+ const [total, setTotal] = React.useState(null);
function onSelection(items) {
const selected = items
@@ -538,7 +568,8 @@ export const Simple = args => {
setSelectedItems(selected);
setIndeterminateItems(indet);
- console.log('onSelection event:', items);
+ setTotal(items.length);
+ // console.log('onSelection event:', items);
}
return (
@@ -564,8 +595,11 @@ export const Simple = args => {
{args.selectable !== TreeViewSelectable.off && (
<>
+ {total} total
Selected: {selectedItems}
- {args.selectable === TreeViewSelectable.multi && (Indeterminate: {indeterminateItems}
)}
+ {args.selectable === TreeViewSelectable.multi && (
+ Indeterminate: {indeterminateItems}
+ )}
>
)}
>
@@ -637,7 +671,9 @@ export const DefaultIcon = args => {
{args.selectable !== TreeViewSelectable.off && (
<>
Selected: {selectedItems}
- {args.selectable === TreeViewSelectable.multi && (Indeterminate: {indeterminateItems}
)}
+ {args.selectable === TreeViewSelectable.multi && (
+ Indeterminate: {indeterminateItems}
+ )}
>
)}
>
@@ -648,16 +684,30 @@ DefaultIcon.parameters = { controls: { exclude: ['isInverse'] } };
export const FirstItemLeaf = args => {
const [selectedItems, setSelectedItems] = React.useState(null);
+ const [indeterminateItems, setIndeterminateItems] = React.useState(null);
function onSelection(items) {
- const allTags = items.map((i: any, key) => {
- return (
-
- {i}
+ const selected = items
+ .filter(i => i.checkedStatus === IndeterminateCheckboxStatus.checked)
+ .map((i, key) => (
+
+ {i.itemId}
- );
- });
- setSelectedItems(allTags);
+ ));
+
+ const indet = items
+ .filter(
+ i => i.checkedStatus === IndeterminateCheckboxStatus.indeterminate
+ )
+ .map((i, key) => (
+
+ {i.itemId}
+
+ ));
+
+ setSelectedItems(selected);
+ setIndeterminateItems(indet);
+ // console.log('onSelection event:', items);
}
return (
@@ -683,7 +733,12 @@ export const FirstItemLeaf = args => {
{args.selectable !== TreeViewSelectable.off && (
- <>Selected: {selectedItems}>
+ <>
+ Selected: {selectedItems}
+ {args.selectable === TreeViewSelectable.multi && (
+ Indeterminate: {indeterminateItems}
+ )}
+ >
)}
>
);
@@ -764,34 +819,254 @@ Flat.args = {
Flat.parameters = { controls: { exclude: ['isInverse'] } };
-export const UnitTest = () => {
+export const UnitTest = args => {
+ const [selectedItems, setSelectedItems] = React.useState(null);
+ const [indeterminateItems, setIndeterminateItems] = React.useState(null);
+
+ function onSelection(items) {
+ const selected = items
+ .filter(i => i.checkedStatus === IndeterminateCheckboxStatus.checked)
+ .map((i, key) => (
+
+ {i.itemId}
+
+ ));
+
+ const indet = items
+ .filter(
+ i => i.checkedStatus === IndeterminateCheckboxStatus.indeterminate
+ )
+ .map((i, key) => (
+
+ {i.itemId}
+
+ ));
+
+ setSelectedItems(selected);
+ setIndeterminateItems(indet);
+ console.log('onSelection event:', items);
+ }
+
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+ <>
+ {/*
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ */}
+
+ {/* one level */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* single item */}
+ {/*
+
+ */}
+
+
+
+ Selected: {selectedItems}
+ Indeterminate: {indeterminateItems}
+ >
+ );
+};
+
+export const Animals = () => {
+ const [selectedItems, setSelectedItems] = React.useState(null);
+ const [indeterminateItems, setIndeterminateItems] = React.useState(null);
+ const [total, setTotal] = React.useState(null);
+
+ function onSelection(items) {
+ const selected = items
+ .filter(i => i.checkedStatus === IndeterminateCheckboxStatus.checked)
+ .map((i, key) => (
+
+ {i.itemId}
+
+ ));
+
+ const indet = items
+ .filter(
+ i => i.checkedStatus === IndeterminateCheckboxStatus.indeterminate
+ )
+ .map((i, key) => (
+
+ {i.itemId}
+
+ ));
+
+ setSelectedItems(selected);
+ setIndeterminateItems(indet);
+ setTotal(items.length);
+ console.log('onSelection event:', items);
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <>
+ {total} total
+ Selected: {selectedItems}
+ Indeterminate: {indeterminateItems}
+ >
+ >
);
};
diff --git a/packages/react-magma-dom/src/components/TreeView/TreeView.test.js b/packages/react-magma-dom/src/components/TreeView/TreeView.test.js
index b8762ed62..85b2e5dfe 100644
--- a/packages/react-magma-dom/src/components/TreeView/TreeView.test.js
+++ b/packages/react-magma-dom/src/components/TreeView/TreeView.test.js
@@ -6,6 +6,7 @@ import { magma } from '../../theme/magma';
import userEvent from '@testing-library/user-event';
import { FavoriteIcon } from 'react-magma-icons';
import { transparentize } from 'polished';
+import { IndeterminateCheckboxStatus } from '../IndeterminateCheckbox';
const TEXT = 'Test Text Tree Item';
const testId = 'tree-view';
@@ -17,7 +18,7 @@ const getTreeItemsOneLevel = props => (
-
+
@@ -42,7 +43,7 @@ const getTreeItemsMultiLevel = props => (
-
+
(
-
+
);
@@ -137,14 +138,14 @@ describe('TreeView', () => {
it('when child item is part of the array, that item is expanded', () => {
const { getByTestId } = render(
getTreeItemsMultiLevel({
- initialExpandedItems: ['item2', 'item-child2'],
+ initialExpandedItems: ['item2', 'item-child2.1'],
})
);
expect(getByTestId('item1')).toHaveAttribute('aria-expanded', 'false');
expect(getByTestId('item2')).toHaveAttribute('aria-expanded', 'true');
expect(getByTestId('item3')).toHaveAttribute('aria-expanded', 'false');
- expect(getByTestId('item-child2')).toHaveAttribute(
+ expect(getByTestId('item-child2.1')).toHaveAttribute(
'aria-expanded',
'true'
);
@@ -217,7 +218,7 @@ describe('TreeView', () => {
const { getByTestId } = render(
getTreeItemsOneLevel({
initialSelectedItems: [
- { itemId: 'item2', checkedStatus: 'checked' },
+ { itemId: 'item2', checkedStatus: IndeterminateCheckboxStatus.checked },
],
selectable: TreeViewSelectable.off,
})
@@ -257,7 +258,7 @@ describe('TreeView', () => {
const { getByTestId } = render(
getTreeItemsOneLevel({
initialSelectedItems: [
- { itemId: 'item2', checkedStatus: 'checked' },
+ { itemId: 'item2', checkedStatus: IndeterminateCheckboxStatus.checked },
],
selectable: TreeViewSelectable.single,
})
@@ -277,8 +278,8 @@ describe('TreeView', () => {
const { getByTestId } = render(
getTreeItemsOneLevel({
initialSelectedItems: [
- { itemId: 'item2', checkedStatus: 'checked' },
- { itemId: 'item0', checkedStatus: 'checked' },
+ {itemId: 'item2', checkedStatus: IndeterminateCheckboxStatus.checked},
+ { itemId: 'item0', checkedStatus: IndeterminateCheckboxStatus.checked },
],
selectable: TreeViewSelectable.single,
})
@@ -318,7 +319,7 @@ describe('TreeView', () => {
const { getByTestId } = render(
getTreeItemsOneLevel({
initialSelectedItems: [
- { itemId: 'item2', checkedStatus: 'checked' },
+ {itemId: 'item2', checkedStatus: IndeterminateCheckboxStatus.checked},
],
selectable: TreeViewSelectable.multi,
})
@@ -338,9 +339,9 @@ describe('TreeView', () => {
const { getByTestId } = render(
getTreeItemsOneLevel({
initialSelectedItems: [
- { itemId: 'item0', checkedStatus: 'checked' },
- { itemId: 'item1', checkedStatus: 'checked' },
- { itemId: 'item2', checkedStatus: 'checked' },
+ { itemId: 'item0', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item1', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item2', checkedStatus: IndeterminateCheckboxStatus.checked },
],
initialExpandedItems: ['item1', 'item2'],
selectable: TreeViewSelectable.multi,
@@ -351,7 +352,7 @@ describe('TreeView', () => {
expect(getByTestId('item1')).toHaveAttribute('aria-checked', 'true');
expect(getByTestId('item-child1')).toHaveAttribute('aria-checked', 'true');
expect(getByTestId('item2')).toHaveAttribute('aria-checked', 'true');
- expect(getByTestId('item-child2')).toHaveAttribute('aria-checked', 'true');
+ expect(getByTestId('item-child2.1')).toHaveAttribute('aria-checked', 'true');
expect(getByTestId('item3')).toHaveAttribute('aria-checked', 'false');
expect(getByTestId('item0')).not.toHaveAttribute('aria-selected');
@@ -359,6 +360,28 @@ describe('TreeView', () => {
expect(getByTestId('item2')).not.toHaveAttribute('aria-selected');
expect(getByTestId('item3')).not.toHaveAttribute('aria-selected');
});
+
+ it('and initialSelectedItems is set to multiple items at different depths, all those TreeItems are selected', () => {
+ const { getByTestId } = render(
+ getTreeItemsOneLevel({
+ initialSelectedItems: [
+ { itemId: 'item2', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
+ { itemId: 'item-child2.1', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item-child3', checkedStatus: IndeterminateCheckboxStatus.checked },
+ ],
+ initialExpandedItems: ['item1', 'item2'],
+ selectable: TreeViewSelectable.multi,
+ })
+ );
+
+ expect(getByTestId('item2')).toHaveAttribute('aria-checked', 'false');
+ expect(getByTestId('item-child2.1')).toHaveAttribute('aria-checked', 'true');
+ expect(getByTestId('item-child2.2')).toHaveAttribute('aria-checked', 'false');
+ expect(getByTestId('item3')).toHaveAttribute('aria-checked', 'true');
+
+ userEvent.click(getByTestId('item3-expand'));
+ expect(getByTestId('item-child3')).toHaveAttribute('aria-checked', 'true');
+ });
});
});
@@ -426,7 +449,7 @@ describe('TreeView', () => {
userEvent.click(getByTestId('item0-label'));
expect(onSelectedItemChange).toHaveBeenCalledWith([
- { itemId: 'item0', checkedStatus: 'checked' },
+ { itemId: 'item0', checkedStatus: IndeterminateCheckboxStatus.checked },
]);
});
@@ -441,7 +464,7 @@ describe('TreeView', () => {
userEvent.click(getByTestId('item2-label'));
expect(onSelectedItemChange).toHaveBeenCalledWith([
- { itemId: 'item2', checkedStatus: 'checked' },
+ { itemId: 'item2', checkedStatus: IndeterminateCheckboxStatus.checked },
]);
});
@@ -490,7 +513,7 @@ describe('TreeView', () => {
userEvent.click(getByTestId('item0-checkbox'));
expect(onSelectedItemChange).toHaveBeenCalledWith([
- { itemId: 'item0', checkedStatus: 'checked' },
+ { itemId: 'item0', checkedStatus: IndeterminateCheckboxStatus.checked },
]);
});
@@ -505,11 +528,22 @@ describe('TreeView', () => {
userEvent.click(getByTestId('item2-checkbox'));
expect(onSelectedItemChange).toHaveBeenCalledWith([
- { itemId: 'item-child2', checkedStatus: 'checked' },
- { itemId: 'item-gchild2', checkedStatus: 'checked' },
- { itemId: 'item-ggchild2', checkedStatus: 'checked' },
- { itemId: 'item-ggchild3', checkedStatus: 'checked' },
- { itemId: 'item2', checkedStatus: 'checked' },
+ { itemId: 'item-child2.1', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item-gchild2', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item-ggchild2', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item-ggchild3', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item2', checkedStatus: IndeterminateCheckboxStatus.checked },
+ ]);
+ userEvent.click(getByTestId('item2-expand'));
+ userEvent.click(getByTestId('item-child2.1-expand'));
+ userEvent.click(getByTestId('item-gchild2-expand'));
+
+ userEvent.click(getByTestId('item-ggchild2-checkbox'));
+ expect(onSelectedItemChange).toHaveBeenCalledWith([
+ { itemId: 'item-ggchild3', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item-gchild2', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
+ { itemId: 'item-child2.1', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
+ { itemId: 'item2', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
]);
});
@@ -529,6 +563,59 @@ describe('TreeView', () => {
});
});
+ describe('initialExpandedItems and initialSelectedItems', () => {
+ it('when initialExpandedItems and initialSelectedItems are empty, no TreeItem is expanded or selected', () => {
+ const { getByTestId } = render(
+ getTreeItemsOneLevel({ selectable: TreeViewSelectable.multi, initialExpandedItems: [], initialSelectedItems: []})
+ );
+
+ expect(getByTestId('item1')).toHaveAttribute('aria-expanded', 'false');
+ expect(getByTestId('item2')).toHaveAttribute('aria-expanded', 'false');
+ expect(getByTestId('item3')).toHaveAttribute('aria-expanded', 'false');
+
+ expect(getByTestId('item0')).toHaveAttribute('aria-checked', 'false');
+ expect(getByTestId('item1')).toHaveAttribute('aria-checked', 'false');
+ expect(getByTestId('item2')).toHaveAttribute('aria-checked', 'false');
+ expect(getByTestId('item3')).toHaveAttribute('aria-checked', 'false');
+ });
+
+ it('when initialExpandedItems is set and initialSelectedItems is set, the items are expanded and selected', () => {
+ const { getByTestId } = render(
+ getTreeItemsOneLevel({
+ selectable: TreeViewSelectable.multi,
+ initialExpandedItems: ['item2', 'item1'],
+ initialSelectedItems: [
+ {
+ itemId: 'item2',
+ checkedStatus: IndeterminateCheckboxStatus.indeterminate,
+ },
+ {
+ itemId: 'item-child2.1',
+ checkedStatus: IndeterminateCheckboxStatus.checked,
+ },
+ {
+ itemId: 'item-child3',
+ checkedStatus: IndeterminateCheckboxStatus.checked,
+ },
+ ],
+ })
+ );
+
+ expect(getByTestId('item1')).toHaveAttribute('aria-expanded', 'true');
+ expect(getByTestId('item2')).toHaveAttribute('aria-expanded', 'true');
+ expect(getByTestId('item3')).toHaveAttribute('aria-expanded', 'false');
+
+ expect(getByTestId('item0')).toHaveAttribute('aria-checked', 'false');
+ expect(getByTestId('item1')).toHaveAttribute('aria-checked', 'false');
+ expect(getByTestId('item2')).toHaveAttribute('aria-checked', 'false');
+ expect(getByTestId('item-child2.1')).toHaveAttribute('aria-checked', 'true');
+ expect(getByTestId('item3')).toHaveAttribute('aria-checked', 'true');
+
+ userEvent.click(getByTestId('item3-expand'));
+ expect(getByTestId('item-child3')).toHaveAttribute('aria-checked', 'true');
+ });
+ });
+
describe('a11y', () => {
it('sets the ariaLabel', () => {
const testId = 'ariaLabelId';
diff --git a/packages/react-magma-dom/src/components/TreeView/TreeViewContext.ts b/packages/react-magma-dom/src/components/TreeView/TreeViewContext.ts
index 115480457..956c0e588 100644
--- a/packages/react-magma-dom/src/components/TreeView/TreeViewContext.ts
+++ b/packages/react-magma-dom/src/components/TreeView/TreeViewContext.ts
@@ -4,13 +4,15 @@ import { IndeterminateCheckboxStatus } from '../IndeterminateCheckbox';
export interface TreeItemSelectedInterface {
itemId?: string;
- checkedStatus: IndeterminateCheckboxStatus
-};
+ checkedStatus: IndeterminateCheckboxStatus;
+}
export interface TreeViewContextInterface {
children?: React.ReactNode | React.ReactNode[];
hasIcons: boolean;
- onSelectedItemChange?: (selectedItems: Array) => void;
+ onSelectedItemChange?: (
+ selectedItems: Array
+ ) => void;
onExpandedChange?: (event: React.SyntheticEvent) => void;
selectable: TreeViewSelectable;
setHasIcons: React.Dispatch>;
@@ -24,7 +26,9 @@ export interface TreeViewContextInterface {
itemRef: React.MutableRefObject
) => void;
initialSelectedItemsNeedUpdate: boolean;
- setInitialSelectedItemsNeedUpdate: React.Dispatch>;
+ setInitialSelectedItemsNeedUpdate: React.Dispatch>;
+ initialExpandedItemsNeedUpdate: boolean;
+ setInitialExpandedItemsNeedUpdate: React.Dispatch>;
}
export const TreeViewContext = React.createContext({
@@ -38,4 +42,6 @@ export const TreeViewContext = React.createContext({
registerTreeItem: (elements, element) => {},
initialSelectedItemsNeedUpdate: false,
setInitialSelectedItemsNeedUpdate: () => {},
+ initialExpandedItemsNeedUpdate: false,
+ setInitialExpandedItemsNeedUpdate: () => {},
});
diff --git a/packages/react-magma-dom/src/components/TreeView/useTreeItem.ts b/packages/react-magma-dom/src/components/TreeView/useTreeItem.ts
index 84f68e547..54787a469 100644
--- a/packages/react-magma-dom/src/components/TreeView/useTreeItem.ts
+++ b/packages/react-magma-dom/src/components/TreeView/useTreeItem.ts
@@ -14,13 +14,14 @@ import {
getChildrenCheckedStatus,
// getEnabledTreeItemChildrenLength,
getUniqueSelectedItemsArray,
- selectedItemsIncludesId,
+ arrayIncludesId,
getUpdatedSelectedItems,
findCommonItems,
areArraysEqual,
findChildByItemId,
getChildrenItemIdsInTree,
getAllParentIds,
+ getChildrenItemIdsFlat,
} from './utils';
export interface UseTreeItemProps extends React.HTMLAttributes {
@@ -121,9 +122,10 @@ export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
treeItemRefArray,
initialSelectedItemsNeedUpdate,
setInitialSelectedItemsNeedUpdate,
+ initialExpandedItemsNeedUpdate,
} = React.useContext(TreeViewContext);
- const [expanded, setExpanded] = React.useState(isDisabled);
+ const [expanded, setExpanded] = React.useState(false);
const [checkedStatus, setCheckedStatus] =
React.useState(
IndeterminateCheckboxStatus.unchecked
@@ -165,8 +167,8 @@ export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
updateParentCheckStatus(index, newStatus);
setSelectedItems(prev => {
return getUniqueSelectedItemsArray(
- prev,
[{ itemId: itemId, checkedStatus: newStatus }],
+ prev,
[]
);
});
@@ -196,22 +198,18 @@ export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
});
}
- React.useEffect(() => {
- if (isDisabled || initialExpandedItems?.length === 0) {
- setExpanded(false);
- } else if (initialExpandedItems?.includes(itemId)) {
- setExpanded(true);
- } else {
- setExpanded(false);
- }
- }, [initialExpandedItems]);
-
React.useEffect(() => {
if (initialSelectedItemsNeedUpdate) {
updateInitialSelected();
}
}, [initialSelectedItemsNeedUpdate]);
+ React.useEffect(() => {
+ if (initialExpandedItemsNeedUpdate) {
+ updateInitialExpanded();
+ }
+ }, [initialExpandedItemsNeedUpdate]);
+
const updateCheckedStatusFromChild = (
index: number,
status: IndeterminateCheckboxStatus
@@ -230,10 +228,13 @@ export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
itemId,
itemIdChildren
) => {
- const item = initialSelectedChildrenItems.find(child => child.itemId === itemId);
+ const item = initialSelectedChildrenItems.find(
+ child => child.itemId === itemId
+ );
+
const parentStatus =
item?.checkedStatus ||
- areArraysEqual(childrenItemIds, initialSelectedChildrenItems)
+ areArraysEqual(initialSelectedChildrenItems, childrenItemIds)
? IndeterminateCheckboxStatus.checked
: IndeterminateCheckboxStatus.indeterminate;
@@ -243,31 +244,51 @@ export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
if (!item) {
setSelectedItems(prev => {
- return getUniqueSelectedItemsArray(prev, initialSelectedChildrenItems, [
- { itemId: itemId, checkedStatus: parentStatus },
- ]);
+ return getUniqueSelectedItemsArray(
+ [{ itemId: itemId, checkedStatus: parentStatus }],
+ initialSelectedChildrenItems,
+ prev
+ );
});
- return
+ return;
}
- const itemThing = itemIdChildren.find(child => child.itemId === itemId);
+ const thisItem = itemIdChildren.find(child => child.itemId === itemId);
if (
- itemThing?.children.length > 0 &&
+ thisItem?.children.length > 0 &&
item?.checkedStatus === IndeterminateCheckboxStatus.checked
) {
- // TODO cleanup
- const itemNode = findChildByItemId(treeItemChildren, itemThing?.itemId);
- const newChildren = getChildrenItemIds(itemNode?.props.children);
+ const itemNode = findChildByItemId(treeItemChildren, thisItem?.itemId);
+ const newChildren = getChildrenItemIds(
+ itemNode?.props.children,
+ checkedStatus
+ );
setSelectedItems(prev => {
- return getUniqueSelectedItemsArray(prev, newChildren, [
- { itemId: itemId, checkedStatus: parentStatus },
- ]);
+ return getUniqueSelectedItemsArray(
+ [{ itemId: itemId, checkedStatus: parentStatus }],
+ newChildren,
+ prev
+ );
});
}
};
+ const updateInitialExpanded = () => {
+ if (initialExpandedItems?.length !== 0 && !isDisabled) {
+ const childrenItemIds = getChildrenItemIdsFlat(treeItemChildren);
+ const allExpanded = [...initialExpandedItems, ...childrenItemIds];
+ if (allExpanded?.some(item => item === itemId)) {
+ setExpanded(true);
+ } else {
+ setExpanded(false);
+ }
+ } else {
+ setExpanded(false);
+ }
+ };
+
const updateInitialSelected = () => {
if (selectable === TreeViewSelectable.single && initialSelectedItems) {
if (
@@ -285,17 +306,19 @@ export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
) {
const item = initialSelectedItems.find(obj => obj.itemId === itemId);
const status = item?.checkedStatus;
-
- const childrenItemIds = getChildrenItemIds(treeItemChildren);
+ const childrenItemIds = getChildrenItemIds(
+ treeItemChildren,
+ status || IndeterminateCheckboxStatus.checked
+ );
// Items from initialSelectedItems that are children
const initialSelectedChildrenItems = findCommonItems(
- initialSelectedItems,
- childrenItemIds
+ childrenItemIds,
+ initialSelectedItems
);
if (
!isDisabled &&
- (selectedItemsIncludesId(initialSelectedItems, itemId) ||
+ (arrayIncludesId(initialSelectedItems, itemId) ||
childrenItemIds?.includes(itemId))
) {
setStatusUpdatedBy(StatusUpdatedByOptions.checkboxChange);
@@ -314,9 +337,9 @@ export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
}
setSelectedItems(prev => {
const allItems = getUniqueSelectedItemsArray(
- prev,
+ childrenItemIds,
initialSelectedItems,
- childrenItemIds
+ prev
);
return allItems;
});
@@ -325,9 +348,10 @@ export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
const itemIdChildren = getChildrenItemIdsInTree(treeItemChildren);
for (const i of initialSelectedChildrenItems) {
- const itemIdNode = findChildByItemId(treeItemChildren, i.itemId);
+ const itemIdNode = findChildByItemId(treeItemChildren, i.itemId);
const childrenOfItemId = getChildrenItemIds(
- itemIdNode?.props?.children
+ itemIdNode?.props?.children,
+ status
);
const parentIds = getAllParentIds(itemIdChildren, i.itemId);
@@ -378,6 +402,7 @@ export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
);
} else {
const childrenIds = getChildrenItemIds(treeItemChildren);
+
const newChildrenCheckedStatus = getChildrenCheckedStatus(
childrenIds,
parentCheckedStatus
@@ -412,11 +437,13 @@ export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
statusFromChildren === IndeterminateCheckboxStatus.checked ||
statusFromChildren === IndeterminateCheckboxStatus.indeterminate
) {
- if (itemId && !selectedItemsIncludesId(selectedItems, itemId)) {
+ if (itemId && !arrayIncludesId(selectedItems, itemId)) {
setSelectedItems([
...selectedItems,
{ itemId, checkedStatus: statusFromChildren },
]);
+ } else {
+ setSelectedItems(updateItemStatus);
}
} else if (
statusFromChildren === IndeterminateCheckboxStatus.unchecked
@@ -428,7 +455,7 @@ export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
statusUpdatedBy !== StatusUpdatedByOptions.parent &&
statusFromChildren === IndeterminateCheckboxStatus.indeterminate
) {
- if (!selectedItemsIncludesId(selectedItems, itemId)) {
+ if (!arrayIncludesId(selectedItems, itemId)) {
setSelectedItems([
...selectedItems,
{ itemId, checkedStatus: statusFromChildren },
@@ -459,7 +486,8 @@ export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
Array(numberOfTreeItemChildren).fill(status)
);
} else {
- const childrenIds = getChildrenItemIds(treeItemChildren);
+ const childrenIds = getChildrenItemIds(treeItemChildren, 'something');
+
const newChildrenCheckedStatus = getChildrenCheckedStatus(
childrenIds,
status
@@ -475,7 +503,7 @@ export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
event: React.ChangeEvent,
itemId: any
): void => {
- if (!selectedItemsIncludesId(selectedItems, itemId)) {
+ if (!arrayIncludesId(selectedItems, itemId)) {
setSelectedItems([
{ itemId, checkedStatus: IndeterminateCheckboxStatus.checked },
]);
@@ -486,11 +514,14 @@ export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
event: React.ChangeEvent
): void => {
if (hasOwnTreeItems) {
- const childrenIds = getChildrenItemIds(treeItemChildren);
+ const childrenIds = getChildrenItemIds(
+ treeItemChildren,
+ IndeterminateCheckboxStatus.checked
+ );
if (event.target.checked) {
updateParentCheckStatus(index, IndeterminateCheckboxStatus.checked);
- if (!selectedItemsIncludesId(selectedItems, itemId)) {
+ if (!arrayIncludesId(selectedItems, itemId)) {
setSelectedItems([
...selectedItems,
...childrenIds,
@@ -514,7 +545,7 @@ export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
}
} else {
if (event.target.checked) {
- if (!selectedItemsIncludesId(selectedItems, itemId)) {
+ if (!arrayIncludesId(selectedItems, itemId)) {
setSelectedItems([
...selectedItems,
{ itemId, checkedStatus: IndeterminateCheckboxStatus.checked },
@@ -607,7 +638,7 @@ export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
};
const toggleMultiSelectItems = () => {
- const status = selectedItemsIncludesId(selectedItems, itemId)
+ const status = arrayIncludesId(selectedItems, itemId)
? IndeterminateCheckboxStatus.unchecked
: IndeterminateCheckboxStatus.checked;
setStatusUpdatedBy(StatusUpdatedByOptions.checkboxChange);
@@ -615,8 +646,8 @@ export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
updateParentCheckStatus(index, status);
if (hasOwnTreeItems) {
- const childrenIds = getChildrenItemIds(treeItemChildren);
- if (!selectedItemsIncludesId(selectedItems, itemId)) {
+ const childrenIds = getChildrenItemIds(treeItemChildren, status);
+ if (!arrayIncludesId(selectedItems, itemId)) {
setSelectedItems([
...selectedItems,
...childrenIds,
@@ -631,7 +662,7 @@ export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
setSelectedItems(newSelectedItems);
}
} else {
- if (!selectedItemsIncludesId(selectedItems, itemId)) {
+ if (!arrayIncludesId(selectedItems, itemId)) {
setSelectedItems([...selectedItems, { itemId, checkedStatus: status }]);
} else {
setSelectedItems(selectedItems.filter(obj => obj.itemId !== itemId));
diff --git a/packages/react-magma-dom/src/components/TreeView/useTreeView.ts b/packages/react-magma-dom/src/components/TreeView/useTreeView.ts
index 4c9aec636..bef343a5c 100644
--- a/packages/react-magma-dom/src/components/TreeView/useTreeView.ts
+++ b/packages/react-magma-dom/src/components/TreeView/useTreeView.ts
@@ -66,6 +66,8 @@ export function useTreeView(props: UseTreeViewProps) {
const [selectedItems, setSelectedItems] = React.useState([]);
const [initialSelectedItemsNeedUpdate, setInitialSelectedItemsNeedUpdate] =
React.useState(false);
+ const [initialExpandedItemsNeedUpdate, setInitialExpandedItemsNeedUpdate] =
+ React.useState(false);
const [treeItemRefArray, registerTreeItem] = useDescendants();
@@ -82,6 +84,9 @@ export function useTreeView(props: UseTreeViewProps) {
if (selectable !== TreeViewSelectable.off && initialSelectedItems) {
setInitialSelectedItemsNeedUpdate(true);
}
+ if (initialExpandedItems) {
+ setInitialExpandedItemsNeedUpdate(true)
+ }
}, []);
const contextValue = {
@@ -98,6 +103,8 @@ export function useTreeView(props: UseTreeViewProps) {
registerTreeItem,
initialSelectedItemsNeedUpdate,
setInitialSelectedItemsNeedUpdate,
+ initialExpandedItemsNeedUpdate,
+ setInitialExpandedItemsNeedUpdate,
};
return { contextValue };
diff --git a/packages/react-magma-dom/src/components/TreeView/utils.ts b/packages/react-magma-dom/src/components/TreeView/utils.ts
index 14d95ccec..54d449b98 100644
--- a/packages/react-magma-dom/src/components/TreeView/utils.ts
+++ b/packages/react-magma-dom/src/components/TreeView/utils.ts
@@ -92,26 +92,34 @@ export function getTreeItemWrapperCursor(
return 'default';
}
-// Returns boolean if selectedItems has itemId
-export function selectedItemsIncludesId(selectedItems, itemId) {
- return selectedItems?.some(item => item.itemId === itemId);
+// Returns boolean if itemsArray has itemId
+export function arrayIncludesId(itemsArray, itemId) {
+ return itemsArray?.some(item => item.itemId === itemId);
}
-// Return an array of all the enabled children ids recursively
-export function getChildrenItemIds(children) {
+// Return an array of objects of all the enabled children ids recursively
+export function getChildrenItemIds(children, status = '') {
let itemIds = [];
React.Children.forEach(children, child => {
if (!child.props?.isDisabled) {
+ const childStatus =
+ status === IndeterminateCheckboxStatus.checked
+ ? IndeterminateCheckboxStatus.checked
+ : IndeterminateCheckboxStatus.unchecked;
+
if (child.props?.itemId) {
itemIds.push({
itemId: child.props.itemId,
- checkedStatus: IndeterminateCheckboxStatus.checked,
+ checkedStatus: childStatus,
});
}
if (child.props?.children) {
- const nestedItemIds = getChildrenItemIds(child.props.children);
+ const nestedItemIds = getChildrenItemIds(
+ child.props.children,
+ childStatus
+ );
itemIds = itemIds.concat(nestedItemIds);
}
}
@@ -120,6 +128,27 @@ export function getChildrenItemIds(children) {
return itemIds;
}
+// Return an array of strings of all enabled children ids recursively
+export function getChildrenItemIdsFlat(children) {
+ let itemIds = [];
+
+ React.Children.forEach(children, child => {
+ if (!child.props?.isDisabled) {
+ if (child.props?.itemId) {
+ itemIds.push(child.props.itemId);
+ }
+
+ if (child.props?.children) {
+ const nestedItemIds = getChildrenItemIdsFlat(child.props.children);
+ itemIds = itemIds.concat(nestedItemIds);
+ }
+ }
+ });
+
+ return itemIds;
+}
+
+// Return an array of objects where all children are items are nested in the parents
export function getChildrenItemIdsInTree(children) {
let itemIds = [];
@@ -169,7 +198,8 @@ export function findChildByItemId(children, itemId) {
}
if (child?.props?.children) {
- const nestedChild = findChildByItemId(child?.props?.children, itemId);
+ const nestedChild = findChildByItemId([child?.props?.children], itemId);
+
if (nestedChild) {
return nestedChild;
}
@@ -200,7 +230,9 @@ export function getMissingChildrenIds(selectedItems, childrenIds) {
// Return an array of statuses for all enabled children
export function getChildrenCheckedStatus(childrenIds, status) {
- return childrenIds.map(child => (child.isDisabled ? IndeterminateCheckboxStatus.unchecked : status));
+ return childrenIds.map(child =>
+ child.isDisabled ? IndeterminateCheckboxStatus.unchecked : status
+ );
}
// Return the length of enabled children
@@ -226,12 +258,8 @@ export function getUpdatedSelectedItems(selectedItems, itemId, checkedStatus) {
}
// Return an array of unique items from the previous state, initially selected items and the childrem item ids
-export function getUniqueSelectedItemsArray(
- prev,
- initialSelectedItems,
- childrenItemIds
-) {
- const combinedArray = [...prev, ...initialSelectedItems, ...childrenItemIds];
+export function getUniqueSelectedItemsArray(itemArr0, itemArr1, itemArr2) {
+ const combinedArray = [...itemArr0, ...itemArr2, ...itemArr1];
const uniqueItemsMap = new Map();
for (const item of combinedArray) {
uniqueItemsMap.set(item.itemId, item);