diff --git a/.changeset/feat-treeview-add-support-for-isdisabled.md b/.changeset/feat-treeview-add-support-for-isdisabled.md
new file mode 100644
index 000000000..55cc0db18
--- /dev/null
+++ b/.changeset/feat-treeview-add-support-for-isdisabled.md
@@ -0,0 +1,5 @@
+---
+'react-magma-dom': minor
+---
+
+feat(TreeView): Add support for isDisabled
\ No newline at end of file
diff --git a/packages/react-magma-dom/src/components/TreeView/TreeItem.test.js b/packages/react-magma-dom/src/components/TreeView/TreeItem.test.js
index 3a276a6c0..03bd5ec9b 100644
--- a/packages/react-magma-dom/src/components/TreeView/TreeItem.test.js
+++ b/packages/react-magma-dom/src/components/TreeView/TreeItem.test.js
@@ -75,12 +75,14 @@ describe('TreeItem', () => {
describe('isDisabled', () => {
it('the label is disabled', () => {
const { getByTestId } = render(
-
+
+
+
);
expect(getByTestId(`${testId}-label`)).toHaveStyleRule(
diff --git a/packages/react-magma-dom/src/components/TreeView/TreeItem.tsx b/packages/react-magma-dom/src/components/TreeView/TreeItem.tsx
index 2aa2ea152..5b319950a 100644
--- a/packages/react-magma-dom/src/components/TreeView/TreeItem.tsx
+++ b/packages/react-magma-dom/src/components/TreeView/TreeItem.tsx
@@ -192,7 +192,6 @@ export const TreeItem = React.forwardRef(
children,
icon,
index,
- isDisabled,
label,
labelStyle,
style,
@@ -210,6 +209,8 @@ export const TreeItem = React.forwardRef(
props,
forwardedRef
);
+
+ const { isDisabled} = contextValue;
const {
checkboxChangeHandler,
@@ -253,7 +254,12 @@ export const TreeItem = React.forwardRef(
id={`${itemId}-label`}
data-testid={`${testId || itemId}-label`}
onClick={(e: any) => {
- if (selectable === TreeViewSelectable.single && !isDisabled) {
+ if (isDisabled) {
+ e.stopPropagation();
+ return;
+ }
+
+ if (selectable === TreeViewSelectable.single) {
handleClick(e, itemId);
}
}}
@@ -297,6 +303,14 @@ export const TreeItem = React.forwardRef(
onExpandedChange(event);
};
+ const tabIndex = React.useMemo(() => {
+ if (isDisabled) {
+ return undefined;
+ }
+
+ return itemToFocus === itemId ? 0 : -1;
+ }, [isDisabled, itemToFocus, itemId]);
+
return (
(
selectableType={selectable}
selected={selectedItem}
theme={theme}
- tabIndex={itemToFocus === itemId ? 0 : -1}
+ tabIndex={tabIndex}
onKeyDown={handleKeyDown}
onClick={event => {
if (selectable===TreeViewSelectable.off && hasOwnTreeItems) {
@@ -378,25 +392,18 @@ export const TreeItem = React.forwardRef(
{React.Children.map(
children,
(child: React.ReactElement, index) => {
- const component =
- child.type === TreeItem ? (
-
-
- {React.cloneElement(child, {
- index,
- key: index,
- itemDepth,
- parentDepth,
- })}
-
-
- ) : (
- child
- );
- // hide the disabled item + the children
- if (isDisabled) return <>>;
-
- return component;
+ return child.type === TreeItem ? (
+
+
+ {React.cloneElement(child, {
+ index,
+ key: index,
+ itemDepth,
+ parentDepth,
+ })}
+
+
+ ) : child;
}
)}
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 620117a06..d9e007e58 100644
--- a/packages/react-magma-dom/src/components/TreeView/TreeView.stories.tsx
+++ b/packages/react-magma-dom/src/components/TreeView/TreeView.stories.tsx
@@ -277,6 +277,7 @@ export const Complex = args => {
>
}
itemId="pt2ch5.1"
+ isDisabled
>
}
@@ -287,6 +288,7 @@ export const Complex = args => {
>
}
itemId="pt2ch5.1.1"
+ isDisabled
/>
}
@@ -413,15 +415,18 @@ Complex.args = {
{ itemId: 'pt1ch1', checkedStatus: IndeterminateCheckboxStatus.checked },
{ itemId: 'pt1', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
{ itemId: 'pt2ch4', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'pt2ch5.1.1', checkedStatus: IndeterminateCheckboxStatus.checked },
{
- itemId: 'pt2ch5.1.1',
- checkedStatus: IndeterminateCheckboxStatus.checked,
+ itemId: 'pt2ch5.1.2',
+ checkedStatus: IndeterminateCheckboxStatus.unchecked,
+ isDisabled: true,
},
{ itemId: 'pt2ch5.2', checkedStatus: IndeterminateCheckboxStatus.checked },
{ itemId: 'pt2ch5.3', checkedStatus: IndeterminateCheckboxStatus.checked },
],
checkParents: true,
checkChildren: true,
+ isDisabled: false,
testId: 'complex-example',
};
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 41d876854..d316ba71b 100644
--- a/packages/react-magma-dom/src/components/TreeView/TreeView.test.js
+++ b/packages/react-magma-dom/src/components/TreeView/TreeView.test.js
@@ -92,6 +92,17 @@ const getTreeItemsWithDisabled = props => (
);
+const getTreeItemsWithDisabledChildren = props => (
+
+
+
+
+
+
+
+
+);
+
const TreeItemsMultiLevelControlledOutside = props => {
const apiRef = React.useRef(null);
@@ -135,6 +146,7 @@ const TreeItemsMultiLevelControlledOutside = props => {
label="Great-grandchild 1"
itemId="item-ggchild1"
testId="item-ggchild1"
+ isDisabled
/>
{
);
});
});
+
+ it('parent should have indeterminate checkbox state and toggle children selection when some disabled children are partially selected', () => {
+ const onSelectedItemChange = jest.fn();
+ const { getByTestId, debug } = render(
+ getTreeItemsWithDisabledChildren({
+ selectable: TreeViewSelectable.multi,
+ preselectedItems: [{
+ itemId: 'item-child1',
+ checkedStatus: IndeterminateCheckboxStatus.checked,
+ }],
+ onSelectedItemChange
+ })
+ );
+
+ const item1 = getByTestId('item1');
+
+ expect(item1).toHaveAttribute('aria-checked', 'mixed');
+
+ expect(onSelectedItemChange).toHaveBeenCalledTimes(1);
+ expect(onSelectedItemChange).toHaveBeenCalledWith([
+ { itemId: 'item1', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
+ { itemId: 'item-child1', checkedStatus: IndeterminateCheckboxStatus.checked }
+ ]);
+
+ userEvent.click(getByTestId('item1-checkbox'));
+ expect(item1).toHaveAttribute('aria-checked', 'mixed');
+
+ expect(onSelectedItemChange).toHaveBeenCalledTimes(2);
+ expect(onSelectedItemChange).toHaveBeenCalledWith([
+ { itemId: 'item1', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
+ { itemId: 'item-child1', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item-child3', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item-child4', checkedStatus: IndeterminateCheckboxStatus.checked }
+ ]);
+
+ userEvent.click(getByTestId('item1-checkbox'));
+ expect(item1).toHaveAttribute('aria-checked', 'mixed');
+
+ expect(onSelectedItemChange).toHaveBeenCalledTimes(3);
+ expect(onSelectedItemChange).toHaveBeenCalledWith([
+ { itemId: 'item1', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
+ { itemId: 'item-child1', checkedStatus: IndeterminateCheckboxStatus.checked }
+ ]);
+ });
+
+ it('parent should have unchecked checkbox state when all disabled children and all enabled children are not selected. parent should have indeterminate checkbox state when all disabled children are not selected and some enabled children are selected. parent should have indeterminate checkbox state when all disabled children are not selected and all enabled children are selected. and toggle children selection', () => {
+ const onSelectedItemChange = jest.fn();
+ const { getByTestId, debug } = render(
+ getTreeItemsWithDisabledChildren({
+ selectable: TreeViewSelectable.multi,
+ onSelectedItemChange
+ })
+ );
+
+ const item1 = getByTestId('item1');
+
+ expect(item1).toHaveAttribute('aria-checked', 'false');
+
+ expect(onSelectedItemChange).not.toHaveBeenCalled();
+
+ userEvent.click(getByTestId('item1-checkbox'));
+ expect(item1).toHaveAttribute('aria-checked', 'mixed');
+
+ expect(onSelectedItemChange).toHaveBeenCalledTimes(1)
+ expect(onSelectedItemChange).toHaveBeenCalledWith([
+ { itemId: 'item1', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
+ { itemId: 'item-child3', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item-child4', checkedStatus: IndeterminateCheckboxStatus.checked }
+ ]);
+
+ userEvent.click(getByTestId('item1-checkbox'));
+ expect(item1).toHaveAttribute('aria-checked', 'false');
+
+ expect(onSelectedItemChange).toHaveBeenCalledTimes(2)
+ expect(onSelectedItemChange).toHaveBeenCalledWith([]);
+ });
+
+ it('parent should have checked checkbox state when all disabled children are selected and all enabled children are selected. parent should have indeterminate checkbox state when all disabled children are selected and enabled children are partially selected. parent should have indeterminate checkbox state when all disabled children are selected and all enabled children are not selected. and toggle children selection', () => {
+ const onSelectedItemChange = jest.fn();
+ const { getByTestId, debug } = render(
+ getTreeItemsWithDisabledChildren({
+ selectable: TreeViewSelectable.multi,
+ preselectedItems: [
+ { itemId: 'item-child1', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item-child2', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item-child3', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item-child4', checkedStatus: IndeterminateCheckboxStatus.checked },
+ ],
+ onSelectedItemChange
+ })
+ );
+
+ const item1 = getByTestId('item1');
+
+ expect(item1).toHaveAttribute('aria-checked', 'true');
+
+ expect(onSelectedItemChange).toHaveBeenCalledTimes(1)
+ expect(onSelectedItemChange).toHaveBeenCalledWith([
+ { itemId: 'item1', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item-child1', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item-child2', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item-child3', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item-child4', checkedStatus: IndeterminateCheckboxStatus.checked }
+ ]);
+
+ userEvent.click(getByTestId('item1-checkbox'));
+ expect(item1).toHaveAttribute('aria-checked', 'mixed');
+
+ expect(onSelectedItemChange).toHaveBeenCalledTimes(2)
+ expect(onSelectedItemChange).toHaveBeenCalledWith([
+ { itemId: 'item1', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
+ { itemId: 'item-child1', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item-child2', checkedStatus: IndeterminateCheckboxStatus.checked }
+ ]);
+
+ userEvent.click(getByTestId('item1-checkbox'));
+ expect(item1).toHaveAttribute('aria-checked', 'true');
+
+ expect(onSelectedItemChange).toHaveBeenCalledTimes(3)
+ expect(onSelectedItemChange).toHaveBeenCalledWith([
+ { itemId: 'item1', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item-child1', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item-child2', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item-child3', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item-child4', checkedStatus: IndeterminateCheckboxStatus.checked }
+ ]);
+ });
+
+ it('an item can be selected and disabled through preselectedItems', () => {
+ const onSelectedItemChange = jest.fn();
+ const { getByTestId, debug } = render(
+ getTreeItemsWithDisabledChildren({
+ selectable: TreeViewSelectable.multi,
+ preselectedItems: [
+ { itemId: 'item-child1', checkedStatus: IndeterminateCheckboxStatus.unchecked, isDisabled: false },
+ { itemId: 'item-child2', checkedStatus: IndeterminateCheckboxStatus.unchecked, isDisabled: false },
+ { itemId: 'item-child3', checkedStatus: IndeterminateCheckboxStatus.checked, isDisabled: true },
+ ],
+ initialExpandedItems: ['item1'],
+ onSelectedItemChange
+ })
+ );
+
+ expect(getByTestId('item-child1-checkbox')).not.toHaveAttribute('disabled');
+ expect(getByTestId('item-child2-checkbox')).not.toHaveAttribute('disabled');
+ expect(getByTestId('item-child3-checkbox')).toHaveAttribute('disabled');
+ expect(getByTestId('item-child3-label')).toHaveStyleRule(
+ 'color',
+ transparentize(0.6, magma.colors.neutral500)
+ );
+ expect(getByTestId('item-child4-checkbox')).not.toHaveAttribute('disabled');
+ });
+
+ it('should disable all items if "isDisabled" prop set to true on TreeView', () => {
+ const onSelectedItemChange = jest.fn();
+ const { getByTestId, debug } = render(
+ getTreeItemsWithDisabledChildren({
+ isDisabled: true,
+ selectable: TreeViewSelectable.multi,
+ preselectedItems: [
+ { itemId: 'item-child1', checkedStatus: IndeterminateCheckboxStatus.unchecked, isDisabled: false },
+ { itemId: 'item-child2', checkedStatus: IndeterminateCheckboxStatus.unchecked, isDisabled: false },
+ { itemId: 'item-child3', checkedStatus: IndeterminateCheckboxStatus.unchecked, isDisabled: true },
+ ],
+ initialExpandedItems: ['item1'],
+ onSelectedItemChange
+ })
+ );
+
+ expect(getByTestId('item1-checkbox')).toHaveAttribute('disabled');
+ expect(getByTestId('item1-label')).toHaveStyleRule(
+ 'color',
+ transparentize(0.6, magma.colors.neutral500)
+ );
+ expect(getByTestId('item-child1-checkbox')).toHaveAttribute('disabled');
+ expect(getByTestId('item-child1-label')).toHaveStyleRule(
+ 'color',
+ transparentize(0.6, magma.colors.neutral500)
+ );
+ expect(getByTestId('item-child2-checkbox')).toHaveAttribute('disabled');
+ expect(getByTestId('item-child2-label')).toHaveStyleRule(
+ 'color',
+ transparentize(0.6, magma.colors.neutral500)
+ );
+ expect(getByTestId('item-child3-checkbox')).toHaveAttribute('disabled');
+ expect(getByTestId('item-child3-label')).toHaveStyleRule(
+ 'color',
+ transparentize(0.6, magma.colors.neutral500)
+ );
+ expect(getByTestId('item-child4-checkbox')).toHaveAttribute('disabled');
+ expect(getByTestId('item-child4-label')).toHaveStyleRule(
+ 'color',
+ transparentize(0.6, magma.colors.neutral500)
+ );
+ });
});
});
describe('when controlled outside', () => {
- it('should be able to select all and clear all outside of TreeView', () => {
+ it('should be able to select all enabled items outside of TreeView', () => {
const onSelectedItemChange = jest.fn();
const { getByTestId, debug } = render(
@@ -1958,19 +2165,15 @@ describe('TreeView', () => {
},
{
itemId: "item2",
- checkedStatus: IndeterminateCheckboxStatus.checked,
+ checkedStatus: IndeterminateCheckboxStatus.indeterminate,
},
{
itemId: "item-child2.1",
- checkedStatus: IndeterminateCheckboxStatus.checked,
+ checkedStatus: IndeterminateCheckboxStatus.indeterminate,
},
{
itemId: "item-gchild2",
- checkedStatus: IndeterminateCheckboxStatus.checked,
- },
- {
- itemId: "item-ggchild1",
- checkedStatus: IndeterminateCheckboxStatus.checked,
+ checkedStatus: IndeterminateCheckboxStatus.indeterminate,
},
{
itemId: "item-ggchild2",
@@ -1989,64 +2192,83 @@ describe('TreeView', () => {
checkedStatus: IndeterminateCheckboxStatus.checked,
},
]);
+ });
+
+ it('should be able to clear all enabled items outside of TreeView', () => {
+ const disabledItemId = 'item-ggchild1';
+
+ const onSelectedItemChange = jest.fn();
+ const { getByTestId, debug } = render(
+
+ );
+
+ expect(onSelectedItemChange).toHaveBeenCalledTimes(1);
+ expect(onSelectedItemChange).toHaveBeenCalledWith([
+ { itemId: 'item2', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
+ { itemId: 'item-child2.1', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
+ { itemId: 'item-gchild2', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
+ { itemId: "item-ggchild1", checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: "item-ggchild2", checkedStatus: IndeterminateCheckboxStatus.checked },
+ ]);
userEvent.click(getByTestId('clear-all'));
- expect(onSelectedItemChange).toHaveBeenCalledWith([]);
+ expect(onSelectedItemChange).toHaveBeenCalledTimes(2);
+ expect(onSelectedItemChange).toHaveBeenCalledWith([
+ { itemId: 'item2', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
+ { itemId: 'item-child2.1', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
+ { itemId: 'item-gchild2', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
+ { itemId: 'item-ggchild1', checkedStatus: IndeterminateCheckboxStatus.checked }
+
+ ]);
});
- it('should be able to unselect item outside of TreeView', () => {
+ it('should be able to unselect enabled item outside of TreeView', () => {
+ const disabledItemId = 'item-ggchild1';
+
const onSelectedItemChange = jest.fn();
const { getByTestId, debug } = render(
-
+
);
- expect(onSelectedItemChange).not.toHaveBeenCalled();
+ expect(onSelectedItemChange).toHaveBeenCalledTimes(1);
+ expect(onSelectedItemChange).toHaveBeenCalledWith([
+ { itemId: 'item2', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
+ { itemId: 'item-child2.1', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
+ { itemId: 'item-gchild2', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
+ { itemId: 'item-ggchild1', checkedStatus: IndeterminateCheckboxStatus.checked },
+ { itemId: 'item-ggchild2', checkedStatus: IndeterminateCheckboxStatus.checked }
+ ]);
- userEvent.click(getByTestId('select-all'));
+ userEvent.click(getByTestId(`${disabledItemId}-tag`));
+ expect(onSelectedItemChange).toHaveBeenCalledTimes(1);
+
userEvent.click(getByTestId('item-ggchild2-tag'));
+ expect(onSelectedItemChange).toHaveBeenCalledTimes(2);
expect(onSelectedItemChange).toHaveBeenCalledWith([
- {
- itemId: "item0",
- checkedStatus: IndeterminateCheckboxStatus.checked,
- },
- {
- itemId: "item1",
- checkedStatus: IndeterminateCheckboxStatus.checked,
- },
- {
- itemId: "item-child1",
- checkedStatus: IndeterminateCheckboxStatus.checked,
- },
- {
- itemId: "item2",
- checkedStatus: IndeterminateCheckboxStatus.indeterminate,
- },
- {
- itemId: "item-child2.1",
- checkedStatus: IndeterminateCheckboxStatus.indeterminate,
- },
- {
- itemId: "item-gchild2",
- checkedStatus: IndeterminateCheckboxStatus.indeterminate,
- },
- {
- itemId: "item-ggchild1",
- checkedStatus: IndeterminateCheckboxStatus.checked,
- },
- {
- itemId: "item-ggchild3",
- checkedStatus: IndeterminateCheckboxStatus.checked,
- },
- {
- itemId: "item3",
- checkedStatus: IndeterminateCheckboxStatus.checked,
- },
- {
- itemId: "item-child3",
- checkedStatus: IndeterminateCheckboxStatus.checked,
- },
+ { itemId: 'item2', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
+ { itemId: 'item-child2.1', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
+ { itemId: 'item-gchild2', checkedStatus: IndeterminateCheckboxStatus.indeterminate },
+ { itemId: 'item-ggchild1', checkedStatus: IndeterminateCheckboxStatus.checked }
]);
});
});
diff --git a/packages/react-magma-dom/src/components/TreeView/TreeViewContext.ts b/packages/react-magma-dom/src/components/TreeView/TreeViewContext.ts
index 9af380a06..054059039 100644
--- a/packages/react-magma-dom/src/components/TreeView/TreeViewContext.ts
+++ b/packages/react-magma-dom/src/components/TreeView/TreeViewContext.ts
@@ -5,6 +5,7 @@ import { IndeterminateCheckboxStatus } from '../IndeterminateCheckbox';
export interface TreeItemSelectedInterface {
itemId?: string;
checkedStatus: IndeterminateCheckboxStatus;
+ isDisabled?: boolean;
}
export interface TreeViewItemInterface {
@@ -13,6 +14,7 @@ export interface TreeViewItemInterface {
icon?: React.ReactNode;
checkedStatus: IndeterminateCheckboxStatus;
hasOwnTreeItems: boolean
+ isDisabled?: boolean
}
export interface TreeViewContextInterface {
diff --git a/packages/react-magma-dom/src/components/TreeView/useTreeItem.ts b/packages/react-magma-dom/src/components/TreeView/useTreeItem.ts
index 361e90199..3db5ac981 100644
--- a/packages/react-magma-dom/src/components/TreeView/useTreeItem.ts
+++ b/packages/react-magma-dom/src/components/TreeView/useTreeItem.ts
@@ -69,7 +69,6 @@ export const checkedStatusToBoolean = (
export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
const {
children,
- isDisabled = false,
itemDepth,
itemId,
onClick,
@@ -91,6 +90,8 @@ export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
const treeViewItemData = React.useMemo(() => {
return items.find((item) => item.itemId === itemId)
}, [itemId, items])
+
+ const isDisabled = treeViewItemData?.isDisabled;
const checkedStatus = React.useMemo(() => {
return treeViewItemData?.checkedStatus ?? IndeterminateCheckboxStatus.unchecked
@@ -127,7 +128,7 @@ export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
}, [initialExpandedItemsNeedUpdate]);
const updateInitialExpanded = () => {
- if (initialExpandedItems?.length !== 0 && !isDisabled) {
+ if (initialExpandedItems?.length !== 0) {
const childrenItemIds = getChildrenItemIdsFlat(treeItemChildren);
const allExpanded = [...initialExpandedItems, ...childrenItemIds];
if (allExpanded?.some(item => item === itemId)) {
@@ -368,6 +369,7 @@ export function useTreeItem(props: UseTreeItemProps, forwardedRef) {
selectedItems,
setExpanded,
treeItemChildren,
+ isDisabled,
};
return { contextValue, handleClick, handleKeyDown };
diff --git a/packages/react-magma-dom/src/components/TreeView/useTreeView.ts b/packages/react-magma-dom/src/components/TreeView/useTreeView.ts
index 3073d6f6c..da2d6ff37 100644
--- a/packages/react-magma-dom/src/components/TreeView/useTreeView.ts
+++ b/packages/react-magma-dom/src/components/TreeView/useTreeView.ts
@@ -1,7 +1,7 @@
import * as React from 'react';
import { useDescendants } from '../../hooks/useDescendants';
import { TreeItemSelectedInterface, TreeViewItemInterface } from './TreeViewContext';
-import { getInitialExpandedIds, getInitialItems, selectMulti, selectSingle } from './utils';
+import { getInitialExpandedIds, getInitialItems, toggleMulti, selectSingle, toggleAllMulti, isSelectedItemsChanged } from './utils';
import { TreeViewSelectable } from './types';
import { IndeterminateCheckboxStatus } from '../IndeterminateCheckbox';
@@ -79,6 +79,11 @@ export interface UseTreeViewProps {
* clearAll(): void - action that allows to unselect all items.
*/
apiRef?: React.MutableRefObject,
+ /**
+ * If true, every item is disabled
+ * @default false
+ */
+ isDisabled?: boolean;
}
export function useTreeView(props: UseTreeViewProps) {
@@ -92,15 +97,16 @@ export function useTreeView(props: UseTreeViewProps) {
checkParents = selectable !== TreeViewSelectable.single,
children,
apiRef,
+ isDisabled,
} = props;
const hasPreselectedItems = Boolean(preselectedItems);
const [items, setItems] = React.useState(
- () => getInitialItems({ children, preselectedItems, checkParents, selectable })
+ () => getInitialItems({ children, preselectedItems, checkParents, checkChildren, selectable, isDisabled })
);
const [hasIcons] = React.useState(() => {
- const initialItems = getInitialItems({ children, preselectedItems, checkParents, selectable })
+ const initialItems = getInitialItems({ children, preselectedItems, checkParents, checkChildren, selectable, isDisabled })
return initialItems.some((item) => item.icon);
});
@@ -114,15 +120,16 @@ export function useTreeView(props: UseTreeViewProps) {
}, [items, rawInitialExpandedItems]);
const itemToFocus = React.useMemo(() => {
- const [firstItem] = items;
+ const enabledItems = items.filter((item) => !item.isDisabled);
+ const [firstItem] = enabledItems;
if (selectable === TreeViewSelectable.off) {
- const firstExpandableItem = items.find((item) => item.hasOwnTreeItems)
+ const firstExpandableItem = enabledItems.find((item) => item.hasOwnTreeItems)
return firstExpandableItem ? firstExpandableItem.itemId : firstItem?.itemId;
}
- const firstNonUncheckedItem = items.find((item) => item.checkedStatus && item.checkedStatus !== IndeterminateCheckboxStatus.unchecked)
+ const firstNonUncheckedItem = enabledItems.find((item) => item.checkedStatus && item.checkedStatus !== IndeterminateCheckboxStatus.unchecked)
if (firstNonUncheckedItem) {
return firstNonUncheckedItem.itemId;
@@ -131,61 +138,104 @@ export function useTreeView(props: UseTreeViewProps) {
return firstItem?.itemId;
}, [items]);
+ const prevSelectedItemsRef = React.useRef(null);
+
const initializationRef = React.useRef(true);
React.useEffect(() => {
- if (initializationRef.current && !hasPreselectedItems) {
- initializationRef.current = false;
+ if (initializationRef.current) {
+ return;
+ }
+
+ const itemsWithUpdatedDisabledState = getInitialItems({ children, preselectedItems, checkParents, checkChildren, selectable, isDisabled });
+
+ setItems((prevItems) => {
+ return prevItems.map(prevItem => {
+ const itemWithUpdatedDisabledState = itemsWithUpdatedDisabledState.find((item) => item.itemId === prevItem.itemId);
+
+ if (itemWithUpdatedDisabledState?.isDisabled === prevItem.isDisabled) {
+ return prevItem;
+ }
+
+ return { ...prevItem, isDisabled: itemWithUpdatedDisabledState.isDisabled };
+ })
+ })
+ }, [isDisabled])
+
+ React.useEffect(() => {
+ const isInitialization = initializationRef.current;
+
+ initializationRef.current = false;
+
+ if (isInitialization && !hasPreselectedItems) {
return;
}
if (selectable === TreeViewSelectable.off) {
return
}
+ const nextSelectedItems = items
+ .filter(({ checkedStatus }) => checkedStatus && checkedStatus !== IndeterminateCheckboxStatus.unchecked)
+ .map(({ itemId, checkedStatus }) => ({ itemId, checkedStatus }))
+
+ if (!isSelectedItemsChanged(prevSelectedItemsRef.current, nextSelectedItems)) {
+ return;
+ }
- onSelectedItemChange && onSelectedItemChange(items.filter(({ checkedStatus }) => checkedStatus && checkedStatus !== IndeterminateCheckboxStatus.unchecked).map(({ itemId, checkedStatus }) => ({ itemId, checkedStatus })))
+ prevSelectedItemsRef.current = nextSelectedItems;
+ onSelectedItemChange && onSelectedItemChange(nextSelectedItems)
}, [items, selectable, hasPreselectedItems])
-
- const selectItem = React.useCallback(({ itemId, checkedStatus }: Pick) => {
+
+ const selectItem = React.useCallback(({ itemId, checkedStatus }: Pick & Partial>) => {
if(selectable === TreeViewSelectable.off) {
return;
}
+ const item = items.find((item) => item.itemId === itemId);
+
+ if (item?.isDisabled) {
+ return;
+ }
+
setItems(prevItems => {
if(selectable === TreeViewSelectable.single) {
- return selectSingle({ items: prevItems, itemId, checkedStatus });
+ return selectSingle({ items: prevItems, itemId, checkedStatus: checkedStatus ?? IndeterminateCheckboxStatus.checked });
}
if(selectable === TreeViewSelectable.multi) {
- return selectMulti({ items: prevItems, itemId, checkedStatus, checkChildren, checkParents });
+ return toggleMulti({ items: prevItems, itemId, checkedStatus, checkChildren, checkParents });
}
return prevItems;
});
- }, [selectable, checkChildren, checkParents])
+ }, [selectable, checkChildren, checkParents, items])
React.useEffect(() => {
if (apiRef) {
apiRef.current = {
selectItem,
selectAll() {
- if ([TreeViewSelectable.single, TreeViewSelectable.single].includes(selectable)) {
+ if ([TreeViewSelectable.single, TreeViewSelectable.single].includes(selectable) || isDisabled) {
return;
}
setItems(prevItems => {
- return prevItems.map(item => ({...item, checkedStatus: IndeterminateCheckboxStatus.checked }))
+ return toggleAllMulti({ items, checkedStatus: IndeterminateCheckboxStatus.checked, checkChildren, checkParents });
})
},
clearAll() {
+ if (isDisabled) {
+ return;
+ }
+
setItems(prevItems => {
- return prevItems.map(({ checkedStatus, ...itemWithoutCheckedStatus }) => itemWithoutCheckedStatus)
+ return toggleAllMulti({ items, checkedStatus: IndeterminateCheckboxStatus.unchecked, checkChildren, checkParents })
})
},
};
}
- }, [selectItem])
+ }, [selectItem, isDisabled])
const [initialExpandedItemsNeedUpdate, setInitialExpandedItemsNeedUpdate] =
React.useState(false);
diff --git a/packages/react-magma-dom/src/components/TreeView/utils.ts b/packages/react-magma-dom/src/components/TreeView/utils.ts
index 586e7add9..dda833d6f 100644
--- a/packages/react-magma-dom/src/components/TreeView/utils.ts
+++ b/packages/react-magma-dom/src/components/TreeView/utils.ts
@@ -4,7 +4,7 @@ import { UseTreeViewProps } from './useTreeView';
import { TreeViewSelectable } from './types';
import React from 'react';
import { IndeterminateCheckboxStatus } from '../IndeterminateCheckbox';
-import { TreeViewItemInterface } from './TreeViewContext';
+import { TreeItemSelectedInterface, TreeViewItemInterface } from './TreeViewContext';
import { TreeItem } from './TreeItem';
export enum TreeNodeType {
@@ -176,14 +176,84 @@ export function filterNullEntries(obj) {
return {};
}
-const getTreeViewData = (children: React.ReactNode[], parentId = null) => {
+const getIsDisabled = ({ selectable, props, preselectedItems, isTreeViewDisabled, isParentDisabled, checkChildren }: { props: TreeViewItemInterface; isParentDisabled?: TreeViewItemInterface['isDisabled'], isTreeViewDisabled: UseTreeViewProps['isDisabled'] } & Pick) => {
+ if (isTreeViewDisabled) {
+ return true;
+ }
+
+ const preselectedItem = preselectedItems?.find((item) => item.itemId === props.itemId);
+ const isDisabled = preselectedItem?.isDisabled !== undefined ? preselectedItem?.isDisabled : props.isDisabled;
+
+ if (selectable === TreeViewSelectable.multi && !checkChildren) {
+ return isDisabled
+ }
+
+ return isParentDisabled || isDisabled;
+}
+
+const getTreeViewData = ({ children, selectable, checkChildren, parentId = null, isParentDisabled, preselectedItems, isTreeViewDisabled }: { isParentDisabled?: TreeViewItemInterface['isDisabled'], isTreeViewDisabled: UseTreeViewProps['isDisabled'] } & Pick & Pick) => {
const treeItemChildren = React.Children.toArray(children).filter(
(child: React.ReactElement) => child.type === TreeItem
) as React.ReactElement[];
- return treeItemChildren.map(({ props }) => [{ itemId: props.itemId, parentId, icon: props.icon, hasOwnTreeItems: Boolean(props.children) }, ...(props.children ? getTreeViewData(props.children, props.itemId) : [])]).flat();
+ return treeItemChildren.map(({ props }) => {
+ const isDisabled = getIsDisabled({ selectable, props, preselectedItems, isTreeViewDisabled, isParentDisabled, checkChildren });
+
+ return [
+ {
+ itemId: props.itemId,
+ parentId,
+ icon: props.icon,
+ hasOwnTreeItems: Boolean(props.children),
+ isDisabled
+ },
+ ...(props.children ? getTreeViewData({
+ children: props.children,
+ parentId: props.itemId,
+ selectable,
+ checkChildren,
+ isParentDisabled: isDisabled,
+ preselectedItems,
+ isTreeViewDisabled
+ }) : [])
+ ]
+ }).flat();
+}
+
+const processItemCheckedStatus = ({ items, itemId, checkedStatus }) => {
+ const item = items.find((item) => item.itemId === itemId);
+
+ if (item.isDisabled) {
+ return items;
+ }
+
+ return items.map((item) => item.itemId === itemId ? { ...item, checkedStatus } : item);
}
+const processChildrenSelection = ({ items, itemId, checkedStatus }) => {
+ const item = items.find((item) => item.itemId === itemId);
+
+ const itemsWithProcessedItemCheckedStatus = processItemCheckedStatus({ items, itemId, checkedStatus });
+
+ if (!item.hasOwnTreeItems) {
+ return itemsWithProcessedItemCheckedStatus;
+ }
+
+ const directChildren = itemsWithProcessedItemCheckedStatus.filter((item) => item.parentId === itemId);
+
+ const itemsWithProcessedChildren = directChildren.reduce((result, directChild) => {
+ return processChildrenSelection({ items: result, itemId: directChild.itemId, checkedStatus })
+ }, itemsWithProcessedItemCheckedStatus);
+
+ const childrenIds = getChildrenIds({ items: itemsWithProcessedChildren, itemId });
+ const children = itemsWithProcessedChildren.filter((item) => childrenIds.includes(item.itemId));
+
+ const uniqueChildrenCheckedStatus = Array.from(new Set(children.map((children) => children.checkedStatus === IndeterminateCheckboxStatus.checked)));
+ const isAllChildrenWithTheSameCheckedStatus = uniqueChildrenCheckedStatus.length === 1;
+ const itemCheckedStatus = isAllChildrenWithTheSameCheckedStatus ? checkedStatus : IndeterminateCheckboxStatus.indeterminate;
+
+ return processItemCheckedStatus({ items: itemsWithProcessedChildren, itemId, checkedStatus: itemCheckedStatus });
+}
const getChildrenIds = ({ items, itemId }: { items: TreeViewItemInterface[]; itemId: TreeViewItemInterface['itemId']; }) => {
return items.reduce((result, item) => {
@@ -199,6 +269,11 @@ const getChildrenIds = ({ items, itemId }: { items: TreeViewItemInterface[]; ite
}, [itemId])
}
+const getChildren = ({ items, itemId }: { items: TreeViewItemInterface[]; itemId: TreeViewItemInterface['itemId']; }) => {
+ const childrenIds = getChildrenIds({ items, itemId });
+ return items.filter((item) => childrenIds.includes(item.itemId));
+}
+
const getChildrenUniqueStatuses = ({ items, itemId }: { items: TreeViewItemInterface[]; itemId: TreeViewItemInterface['itemId']; }) => {
const childrenAndItemIds = getChildrenIds({ items, itemId });
const leaves = items.filter((item) => {
@@ -231,14 +306,17 @@ const processInitialParentStatuses = ({ items }: { items: TreeViewItemInterface[
})
}
-export const getInitialItems = ({ children, preselectedItems: rawPreselectedItems, checkParents, selectable }: Pick) => {
- const treeViewData = getTreeViewData(children);
+export const getInitialItems = ({ children, preselectedItems: rawPreselectedItems, checkParents, checkChildren, selectable, isDisabled: isTreeViewDisabled }: Pick) => {
+ const treeViewData = getTreeViewData({ children, checkChildren, selectable, preselectedItems: rawPreselectedItems, isTreeViewDisabled });
const preselectedItems = rawPreselectedItems?.length && selectable === TreeViewSelectable.single ? [rawPreselectedItems[0]] : rawPreselectedItems;
const enhancedWithPreselectedItems = preselectedItems ? treeViewData.map((treeViewDataItem) => {
const preselectedItem = preselectedItems.find(({itemId}) => treeViewDataItem.itemId === itemId);
- return preselectedItem ? { ...treeViewDataItem, checkedStatus: preselectedItem.checkedStatus } : treeViewDataItem
+ return preselectedItem ? {
+ ...treeViewDataItem,
+ checkedStatus: preselectedItem.checkedStatus,
+ } : treeViewDataItem
}) : treeViewData
return selectable === TreeViewSelectable.multi && checkParents && preselectedItems ? processInitialParentStatuses({ items: enhancedWithPreselectedItems }) : enhancedWithPreselectedItems
@@ -248,12 +326,6 @@ export const selectSingle = ({items, itemId, checkedStatus}: { items: TreeViewIt
return items.map((item) => ({ ...item, checkedStatus: item.itemId === itemId ? checkedStatus : IndeterminateCheckboxStatus.unchecked }))
}
-const processChildrenSelection = ({items, itemId, checkedStatus}: { items: TreeViewItemInterface[]; itemId: TreeViewItemInterface['itemId']; checkedStatus: TreeViewItemInterface['checkedStatus'] }) => {
- const childrenAndItemIds = getChildrenIds({ items, itemId })
-
- return items.map((item) => childrenAndItemIds.includes(item.itemId) ? { ...item, checkedStatus } : item)
-}
-
const processParentsSelection = ({items, itemId, checkedStatus}: { items: TreeViewItemInterface[]; itemId: TreeViewItemInterface['itemId']; checkedStatus: TreeViewItemInterface['checkedStatus'] }) => {
const item = items.find(item => item.itemId === itemId);
const { parentId } = item;
@@ -273,7 +345,20 @@ const processParentsSelection = ({items, itemId, checkedStatus}: { items: TreeVi
return processParentsSelection({items: nextItems, itemId: parent.itemId, checkedStatus: parentStatus })
}
-export const selectMulti = ({items, itemId, checkedStatus, checkChildren, checkParents }: { items: TreeViewItemInterface[]; itemId: TreeViewItemInterface['itemId']; checkedStatus: TreeViewItemInterface['checkedStatus'] } & Pick) => {
+const getMultiToggledStatus = ({ items, itemId }) => {
+ const children = getChildren({ items, itemId });
+ const enabledChildren = children.filter(item => !item.isDisabled);
+
+ if (enabledChildren.some(item => !item.checkedStatus || item.checkedStatus === IndeterminateCheckboxStatus.unchecked)) {
+ return IndeterminateCheckboxStatus.checked;
+ }
+
+ return IndeterminateCheckboxStatus.unchecked;
+}
+
+export const toggleMulti = ({ items, itemId, checkedStatus: rawCheckedStatus, forceCheckedStatus, checkChildren, checkParents }: { items: TreeViewItemInterface[]; itemId: TreeViewItemInterface['itemId']; checkedStatus: TreeViewItemInterface['checkedStatus']; forceCheckedStatus?: boolean } & Pick) => {
+ const checkedStatus = checkChildren && !forceCheckedStatus ? getMultiToggledStatus({ items, itemId }) : rawCheckedStatus
+
const itemsWithProcessedItemSelection = items.map((item) => item.itemId === itemId ? { ...item, checkedStatus } : item)
const itemsWithProcessedChildrenSelection = checkChildren ? processChildrenSelection({ items: itemsWithProcessedItemSelection, itemId, checkedStatus }) : itemsWithProcessedItemSelection
return checkParents ? processParentsSelection({ items: itemsWithProcessedChildrenSelection, itemId, checkedStatus }) : itemsWithProcessedChildrenSelection;
@@ -291,6 +376,30 @@ const getParentIds = ({ items, itemId, prevParentIds = []}: { items: TreeViewIte
return parentId ? getParentIds({ itemId: parentId, items, prevParentIds: [...prevParentIds, parentId]}) : prevParentIds;
}
+const getRootParentIds = (items: TreeViewItemInterface[]) => {
+ const rootParents = items.filter(({ parentId }) => !parentId);
+
+ return rootParents.map(({ itemId }) => itemId);
+}
+
+export const toggleAllMulti = ({ items, checkedStatus, checkChildren, checkParents }: { items: TreeViewItemInterface[]; checkedStatus: TreeViewItemInterface['checkedStatus'] } & Pick) => {
+ if (!checkChildren) {
+ return items.map(item => {
+ if (item.isDisabled) {
+ return item;
+ }
+
+ return { ...item, ...(checkedStatus === IndeterminateCheckboxStatus.unchecked ? {} :{ checkedStatus }) }
+ })
+ }
+
+ const rootParentIds = getRootParentIds(items);
+
+ return rootParentIds.reduce((result, rootParentId) => {
+ return toggleMulti({ items: result, itemId: rootParentId, checkedStatus, forceCheckedStatus: true, checkChildren, checkParents })
+ }, items)
+}
+
export const getInitialExpandedIds = ({ items, initialExpandedItems }: { items: TreeViewItemInterface[]; } & Pick) => {
if (!initialExpandedItems) {
return initialExpandedItems;
@@ -299,4 +408,15 @@ export const getInitialExpandedIds = ({ items, initialExpandedItems }: { items:
return initialExpandedItems.reduce((result, itemId) => {
return [...result, itemId, ...getParentIds({ itemId, items })];
}, []);
+}
+
+export const isSelectedItemsChanged = (prevSelectedItems: TreeItemSelectedInterface[] | null, selectedItems: TreeItemSelectedInterface[]) => {
+ if (!prevSelectedItems && selectedItems) {
+ return true;
+ }
+
+ const stringifiedPrevSelectedItems = JSON.stringify(prevSelectedItems);
+ const stringifiedSelectedItems = JSON.stringify(selectedItems);
+
+ return stringifiedPrevSelectedItems !== stringifiedSelectedItems;
}
\ No newline at end of file
diff --git a/website/react-magma-docs/src/pages/api/tree-view.mdx b/website/react-magma-docs/src/pages/api/tree-view.mdx
index 742115fde..84b3c689f 100644
--- a/website/react-magma-docs/src/pages/api/tree-view.mdx
+++ b/website/react-magma-docs/src/pages/api/tree-view.mdx
@@ -648,6 +648,116 @@ export function Example() {
}
```
+## Disabling TreeView
+
+**Styling for disabled item may not be visible if the user is passing a node to the label.**
+
+Use the `isDisabled` prop to disable all items inside TreeView.
+
+```typescript
+import React from 'react';
+import {
+ TreeView,
+ TreeItem,
+ TreeViewSelectable,
+} from 'react-magma-dom';
+
+export function Example() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+```
+
+## Disabling items
+
+Pass the `isDisabled` prop to item inside `preselectedItems` prop to manage disabled state for the item.
+
+```typescript
+import React from 'react';
+import {
+ TreeView,
+ TreeItem,
+ TreeViewSelectable,
+ IndeterminateCheckboxStatus,
+} from 'react-magma-dom';
+
+export function Example() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+```
+
+
## Change Events
The following events are available: `onSelectedItemChange` and `onExpandedChange`.