Skip to content
Open
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
6 changes: 6 additions & 0 deletions packages/react-core/src/components/TreeView/TreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export interface TreeViewDataItem {
name: React.ReactNode;
/** Title of a tree view item. Only used in compact presentations. */
title?: React.ReactNode;
/** Flag indicating if the tree view item is disabled. */
isDisabled?: boolean;
/** Flag indicating if the tree view item toggle is disabled. */
isToggleDisabled?: boolean;
}

/** The main tree view component. */
Expand Down Expand Up @@ -158,6 +162,8 @@ export const TreeView: React.FunctionComponent<TreeViewProps> = ({
id={item.id}
isExpanded={allExpanded}
isSelectable={hasSelectableNodes}
isDisabled={item.isDisabled}
isToggleDisabled={item.isToggleDisabled}
defaultExpanded={item.defaultExpanded !== undefined ? item.defaultExpanded : defaultAllExpanded}
onSelect={onSelect}
onCheck={onCheck}
Expand Down
33 changes: 24 additions & 9 deletions packages/react-core/src/components/TreeView/TreeViewListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export interface TreeViewListItemProps {
* children.
*/
isSelectable?: boolean;
/** Flag indicating if the tree view item is disabled. */
isDisabled?: boolean;
/** Flag indicating if the tree view item toggle is disabled. */
isToggleDisabled?: boolean;
/** Data structure of tree view item. */
itemData?: TreeViewDataItem;
/** Internal content of a tree view item. */
Expand Down Expand Up @@ -81,6 +85,8 @@ const TreeViewListItemBase: React.FunctionComponent<TreeViewListItemProps> = ({
title,
id,
isExpanded,
isDisabled = false,
isToggleDisabled = false,
defaultExpanded = false,
children = null,
onSelect,
Expand Down Expand Up @@ -128,22 +134,22 @@ const TreeViewListItemBase: React.FunctionComponent<TreeViewListItemProps> = ({

const renderToggle = (randomId: string) => (
<ToggleComponent
className={css(styles.treeViewNodeToggle)}
className={css(styles.treeViewNodeToggle, ToggleComponent === 'button' && isToggleDisabled && 'pf-m-disabled')}
onClick={(evt: React.MouseEvent) => {
if (isSelectable || hasCheckbox) {
if (!isToggleDisabled && (isSelectable || hasCheckbox)) {
if (internalIsExpanded) {
onCollapse && onCollapse(evt, itemData, parentItem);
} else {
onExpand && onExpand(evt, itemData, parentItem);
}
setIsExpanded(!internalIsExpanded);
}
if (isSelectable) {
if (!isToggleDisabled && isSelectable) {
evt.stopPropagation();
}
}}
{...((hasCheckbox || isSelectable) && { 'aria-labelledby': `label-${randomId}` })}
{...(ToggleComponent === 'button' && { type: 'button' })}
{...(ToggleComponent === 'button' && { disabled: isToggleDisabled, type: 'button' })}
tabIndex={-1}
>
<span className={css(styles.treeViewNodeToggleIcon)}>
Expand Down Expand Up @@ -180,7 +186,12 @@ const TreeViewListItemBase: React.FunctionComponent<TreeViewListItemProps> = ({
<>
{isCompact && title && <span className={css(styles.treeViewNodeTitle)}>{title}</span>}
{isSelectable ? (
<button tabIndex={-1} className={css(styles.treeViewNodeText)} type="button">
<button
tabIndex={-1}
className={css(styles.treeViewNodeText, isDisabled && 'pf-m-disabled')}
type="button"
disabled={isDisabled}
>
{name}
</button>
) : (
Expand Down Expand Up @@ -234,11 +245,15 @@ const TreeViewListItemBase: React.FunctionComponent<TreeViewListItemProps> = ({
<GenerateId prefix={isSelectable ? 'selectable-id' : 'checkbox-id'}>
{(randomId) => (
<Component
className={css(styles.treeViewNode, isSelected && styles.modifiers.current)}
className={css(
styles.treeViewNode,
isSelected && styles.modifiers.current,
Component === 'button' && isDisabled && 'pf-m-disabled'
)}
onClick={(evt: React.MouseEvent) => {
if (!hasCheckbox) {
onSelect && onSelect(evt, itemData, parentItem);
if (!isSelectable && children && evt.isDefaultPrevented() !== true) {
!isDisabled && onSelect && onSelect(evt, itemData, parentItem);
if (!isDisabled && !isSelectable && children && evt.isDefaultPrevented() !== true) {
if (internalIsExpanded) {
onCollapse && onCollapse(evt, itemData, parentItem);
} else {
Expand All @@ -250,7 +265,7 @@ const TreeViewListItemBase: React.FunctionComponent<TreeViewListItemProps> = ({
}}
{...(hasCheckbox && { htmlFor: randomId })}
{...((hasCheckbox || (isSelectable && children)) && { id: `label-${randomId}` })}
{...(Component === 'button' && { type: 'button' })}
{...(Component === 'button' && { type: 'button', disabled: isDisabled })}
>
<span className={css(styles.treeViewNodeContainer)}>
{children && renderToggle(randomId)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,172 @@ test(`Does not render ${styles.treeViewNode} element with ${styles.modifiers.cur
expect(treeViewNode).not.toHaveClass(styles.modifiers.current);
});

// Assisted by Cursor AI
describe('isDisabled prop', () => {
const user = userEvent.setup();
const onSelectMock = jest.fn();
const onExpandMock = jest.fn();
const onCollapseMock = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
});

test('Renders button with disabled attribute and pf-m-disabled class when isDisabled is true', () => {
render(<TreeViewListItem isDisabled {...requiredProps} />);

const button = screen.getByRole('button', { name: requiredProps.name });
expect(button).toBeDisabled();
expect(button).toHaveClass('pf-m-disabled');
});

test('Does not render button with disabled attribute when isDisabled is false', () => {
render(<TreeViewListItem isDisabled={false} {...requiredProps} />);

expect(screen.getByRole('button', { name: requiredProps.name })).not.toBeDisabled();
});

test('Renders selectable button with disabled attribute when isDisabled is true', () => {
render(<TreeViewListItem isSelectable isDisabled {...requiredProps} />);

const treeViewNode = screen.getByRole('treeitem').querySelector(`.${styles.treeViewNode}`);
const selectableButton = treeViewNode?.querySelector('button');
expect(selectableButton).toBeDisabled();
expect(selectableButton).toHaveClass('pf-m-disabled');
});

test('Does not call onSelect when isDisabled is true', async () => {
render(<TreeViewListItem isDisabled onSelect={onSelectMock} {...requiredProps} />);

await user.click(screen.getByRole('button', { name: requiredProps.name }));

expect(onSelectMock).not.toHaveBeenCalled();
});

test('Does not call onExpand when isDisabled is true and item is collapsed', async () => {
render(
<TreeViewListItem isDisabled onExpand={onExpandMock} {...requiredProps}>
Content
</TreeViewListItem>
);

await user.click(screen.getByRole('button', { name: requiredProps.name }));

expect(onExpandMock).not.toHaveBeenCalled();
});

test('Does not call onCollapse when isDisabled is true and item is expanded', async () => {
render(
<TreeViewListItem isDisabled isExpanded onCollapse={onCollapseMock} {...requiredProps}>
Content
</TreeViewListItem>
);

await user.click(screen.getByRole('button', { name: requiredProps.name }));

expect(onCollapseMock).not.toHaveBeenCalled();
});
});

// Assisted by Cursor AI
describe('isToggleDisabled prop', () => {
const user = userEvent.setup();
const onExpandMock = jest.fn();
const onCollapseMock = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
});

test('Renders toggle button with disabled attribute and pf-m-disabled class when isToggleDisabled is true and hasCheckbox is passed', () => {
render(
<TreeViewListItem hasCheckbox isToggleDisabled {...requiredProps}>
Content
</TreeViewListItem>
);

const toggle = screen.getByText(requiredProps.name).previousElementSibling?.previousElementSibling;
expect(toggle).toBeDisabled();
expect(toggle).toHaveClass('pf-m-disabled');
});

test('Renders toggle button with disabled attribute and pf-m-disabled class when isToggleDisabled is true and isSelectable is passed', () => {
render(
<TreeViewListItem isSelectable isToggleDisabled {...requiredProps}>
Content
</TreeViewListItem>
);

const toggle = screen.getByText(requiredProps.name).previousElementSibling;
expect(toggle).toBeDisabled();
expect(toggle).toHaveClass('pf-m-disabled');
});

test('Does not render toggle span with disabled attribute when isToggleDisabled is true (toggle is span by default)', () => {
render(
<TreeViewListItem isToggleDisabled {...requiredProps}>
Content
</TreeViewListItem>
);

const toggle = screen.getByText(requiredProps.name).previousElementSibling;
expect(toggle?.tagName).toBe('SPAN');
expect(toggle).not.toHaveAttribute('disabled');
});

test('Does not call onExpand when isToggleDisabled is true and hasCheckbox is passed', async () => {
render(
<TreeViewListItem hasCheckbox isToggleDisabled onExpand={onExpandMock} {...requiredProps}>
Content
</TreeViewListItem>
);

const toggle = screen.getByText(requiredProps.name).previousElementSibling?.previousElementSibling;
await user.click(toggle as Element);

expect(onExpandMock).not.toHaveBeenCalled();
});

test('Does not call onCollapse when isToggleDisabled is true and hasCheckbox is passed', async () => {
render(
<TreeViewListItem hasCheckbox isToggleDisabled isExpanded onCollapse={onCollapseMock} {...requiredProps}>
Content
</TreeViewListItem>
);

const toggle = screen.getByText(requiredProps.name).previousElementSibling?.previousElementSibling;
await user.click(toggle as Element);

expect(onCollapseMock).not.toHaveBeenCalled();
});

test('Does not call onExpand when isToggleDisabled is true and isSelectable is passed', async () => {
render(
<TreeViewListItem isSelectable isToggleDisabled onExpand={onExpandMock} {...requiredProps}>
Content
</TreeViewListItem>
);

const toggle = screen.getByText(requiredProps.name).previousElementSibling;
await user.click(toggle as Element);

expect(onExpandMock).not.toHaveBeenCalled();
});

test('Does not call onCollapse when isToggleDisabled is true and isSelectable is passed', async () => {
render(
<TreeViewListItem isSelectable isToggleDisabled isExpanded onCollapse={onCollapseMock} {...requiredProps}>
Content
</TreeViewListItem>
);

const toggle = screen.getByText(requiredProps.name).previousElementSibling;
await user.click(toggle as Element);

expect(onCollapseMock).not.toHaveBeenCalled();
});
});

describe('Callback props', () => {
const user = userEvent.setup();
const compareItemsMock = jest.fn();
Expand Down
Loading