Skip to content

Commit f25dfcb

Browse files
committed
feat(TreeView): add support for disabled TreeViewListItems
1 parent 6bcdcaf commit f25dfcb

File tree

3 files changed

+196
-9
lines changed

3 files changed

+196
-9
lines changed

packages/react-core/src/components/TreeView/TreeView.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ export interface TreeViewDataItem {
3535
name: React.ReactNode;
3636
/** Title of a tree view item. Only used in compact presentations. */
3737
title?: React.ReactNode;
38+
/** Flag indicating if the tree view item is disabled. */
39+
isDisabled?: boolean;
40+
/** Flag indicating if the tree view item toggle is disabled. */
41+
isToggleDisabled?: boolean;
3842
}
3943

4044
/** The main tree view component. */
@@ -158,6 +162,8 @@ export const TreeView: React.FunctionComponent<TreeViewProps> = ({
158162
id={item.id}
159163
isExpanded={allExpanded}
160164
isSelectable={hasSelectableNodes}
165+
isDisabled={item.isDisabled}
166+
isToggleDisabled={item.isToggleDisabled}
161167
defaultExpanded={item.defaultExpanded !== undefined ? item.defaultExpanded : defaultAllExpanded}
162168
onSelect={onSelect}
163169
onCheck={onCheck}

packages/react-core/src/components/TreeView/TreeViewListItem.tsx

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ export interface TreeViewListItemProps {
4646
* children.
4747
*/
4848
isSelectable?: boolean;
49+
/** Flag indicating if the tree view item is disabled. */
50+
isDisabled?: boolean;
51+
/** Flag indicating if the tree view item toggle is disabled. */
52+
isToggleDisabled?: boolean;
4953
/** Data structure of tree view item. */
5054
itemData?: TreeViewDataItem;
5155
/** Internal content of a tree view item. */
@@ -81,6 +85,8 @@ const TreeViewListItemBase: React.FunctionComponent<TreeViewListItemProps> = ({
8185
title,
8286
id,
8387
isExpanded,
88+
isDisabled = false,
89+
isToggleDisabled = false,
8490
defaultExpanded = false,
8591
children = null,
8692
onSelect,
@@ -128,22 +134,22 @@ const TreeViewListItemBase: React.FunctionComponent<TreeViewListItemProps> = ({
128134

129135
const renderToggle = (randomId: string) => (
130136
<ToggleComponent
131-
className={css(styles.treeViewNodeToggle)}
137+
className={css(styles.treeViewNodeToggle, ToggleComponent === 'button' && isToggleDisabled && 'pf-m-disabled')}
132138
onClick={(evt: React.MouseEvent) => {
133-
if (isSelectable || hasCheckbox) {
139+
if (!isToggleDisabled && (isSelectable || hasCheckbox)) {
134140
if (internalIsExpanded) {
135141
onCollapse && onCollapse(evt, itemData, parentItem);
136142
} else {
137143
onExpand && onExpand(evt, itemData, parentItem);
138144
}
139145
setIsExpanded(!internalIsExpanded);
140146
}
141-
if (isSelectable) {
147+
if (!isToggleDisabled && isSelectable) {
142148
evt.stopPropagation();
143149
}
144150
}}
145151
{...((hasCheckbox || isSelectable) && { 'aria-labelledby': `label-${randomId}` })}
146-
{...(ToggleComponent === 'button' && { type: 'button' })}
152+
{...(ToggleComponent === 'button' && { disabled: isToggleDisabled, type: 'button' })}
147153
tabIndex={-1}
148154
>
149155
<span className={css(styles.treeViewNodeToggleIcon)}>
@@ -180,7 +186,12 @@ const TreeViewListItemBase: React.FunctionComponent<TreeViewListItemProps> = ({
180186
<>
181187
{isCompact && title && <span className={css(styles.treeViewNodeTitle)}>{title}</span>}
182188
{isSelectable ? (
183-
<button tabIndex={-1} className={css(styles.treeViewNodeText)} type="button">
189+
<button
190+
tabIndex={-1}
191+
className={css(styles.treeViewNodeText, isDisabled && 'pf-m-disabled')}
192+
type="button"
193+
disabled={isDisabled}
194+
>
184195
{name}
185196
</button>
186197
) : (
@@ -234,11 +245,15 @@ const TreeViewListItemBase: React.FunctionComponent<TreeViewListItemProps> = ({
234245
<GenerateId prefix={isSelectable ? 'selectable-id' : 'checkbox-id'}>
235246
{(randomId) => (
236247
<Component
237-
className={css(styles.treeViewNode, isSelected && styles.modifiers.current)}
248+
className={css(
249+
styles.treeViewNode,
250+
isSelected && styles.modifiers.current,
251+
Component === 'button' && isDisabled && 'pf-m-disabled'
252+
)}
238253
onClick={(evt: React.MouseEvent) => {
239254
if (!hasCheckbox) {
240-
onSelect && onSelect(evt, itemData, parentItem);
241-
if (!isSelectable && children && evt.isDefaultPrevented() !== true) {
255+
!isDisabled && onSelect && onSelect(evt, itemData, parentItem);
256+
if (!isDisabled && !isSelectable && children && evt.isDefaultPrevented() !== true) {
242257
if (internalIsExpanded) {
243258
onCollapse && onCollapse(evt, itemData, parentItem);
244259
} else {
@@ -250,7 +265,7 @@ const TreeViewListItemBase: React.FunctionComponent<TreeViewListItemProps> = ({
250265
}}
251266
{...(hasCheckbox && { htmlFor: randomId })}
252267
{...((hasCheckbox || (isSelectable && children)) && { id: `label-${randomId}` })}
253-
{...(Component === 'button' && { type: 'button' })}
268+
{...(Component === 'button' && { type: 'button', disabled: isDisabled })}
254269
>
255270
<span className={css(styles.treeViewNodeContainer)}>
256271
{children && renderToggle(randomId)}

packages/react-core/src/components/TreeView/__tests__/TreeViewListItem.test.tsx

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,172 @@ test(`Does not render ${styles.treeViewNode} element with ${styles.modifiers.cur
415415
expect(treeViewNode).not.toHaveClass(styles.modifiers.current);
416416
});
417417

418+
// Assisted by Cursor AI
419+
describe('isDisabled prop', () => {
420+
const user = userEvent.setup();
421+
const onSelectMock = jest.fn();
422+
const onExpandMock = jest.fn();
423+
const onCollapseMock = jest.fn();
424+
425+
beforeEach(() => {
426+
jest.clearAllMocks();
427+
});
428+
429+
test('Renders button with disabled attribute and pf-m-disabled class when isDisabled is true', () => {
430+
render(<TreeViewListItem isDisabled {...requiredProps} />);
431+
432+
const button = screen.getByRole('button', { name: requiredProps.name });
433+
expect(button).toBeDisabled();
434+
expect(button).toHaveClass('pf-m-disabled');
435+
});
436+
437+
test('Does not render button with disabled attribute when isDisabled is false', () => {
438+
render(<TreeViewListItem isDisabled={false} {...requiredProps} />);
439+
440+
expect(screen.getByRole('button', { name: requiredProps.name })).not.toBeDisabled();
441+
});
442+
443+
test('Renders selectable button with disabled attribute when isDisabled is true', () => {
444+
render(<TreeViewListItem isSelectable isDisabled {...requiredProps} />);
445+
446+
const treeViewNode = screen.getByRole('treeitem').querySelector(`.${styles.treeViewNode}`);
447+
const selectableButton = treeViewNode?.querySelector('button');
448+
expect(selectableButton).toBeDisabled();
449+
expect(selectableButton).toHaveClass('pf-m-disabled');
450+
});
451+
452+
test('Does not call onSelect when isDisabled is true', async () => {
453+
render(<TreeViewListItem isDisabled onSelect={onSelectMock} {...requiredProps} />);
454+
455+
await user.click(screen.getByRole('button', { name: requiredProps.name }));
456+
457+
expect(onSelectMock).not.toHaveBeenCalled();
458+
});
459+
460+
test('Does not call onExpand when isDisabled is true and item is collapsed', async () => {
461+
render(
462+
<TreeViewListItem isDisabled onExpand={onExpandMock} {...requiredProps}>
463+
Content
464+
</TreeViewListItem>
465+
);
466+
467+
await user.click(screen.getByRole('button', { name: requiredProps.name }));
468+
469+
expect(onExpandMock).not.toHaveBeenCalled();
470+
});
471+
472+
test('Does not call onCollapse when isDisabled is true and item is expanded', async () => {
473+
render(
474+
<TreeViewListItem isDisabled isExpanded onCollapse={onCollapseMock} {...requiredProps}>
475+
Content
476+
</TreeViewListItem>
477+
);
478+
479+
await user.click(screen.getByRole('button', { name: requiredProps.name }));
480+
481+
expect(onCollapseMock).not.toHaveBeenCalled();
482+
});
483+
});
484+
485+
// Assisted by Cursor AI
486+
describe('isToggleDisabled prop', () => {
487+
const user = userEvent.setup();
488+
const onExpandMock = jest.fn();
489+
const onCollapseMock = jest.fn();
490+
491+
beforeEach(() => {
492+
jest.clearAllMocks();
493+
});
494+
495+
test('Renders toggle button with disabled attribute and pf-m-disabled class when isToggleDisabled is true and hasCheckbox is passed', () => {
496+
render(
497+
<TreeViewListItem hasCheckbox isToggleDisabled {...requiredProps}>
498+
Content
499+
</TreeViewListItem>
500+
);
501+
502+
const toggle = screen.getByText(requiredProps.name).previousElementSibling?.previousElementSibling;
503+
expect(toggle).toBeDisabled();
504+
expect(toggle).toHaveClass('pf-m-disabled');
505+
});
506+
507+
test('Renders toggle button with disabled attribute and pf-m-disabled class when isToggleDisabled is true and isSelectable is passed', () => {
508+
render(
509+
<TreeViewListItem isSelectable isToggleDisabled {...requiredProps}>
510+
Content
511+
</TreeViewListItem>
512+
);
513+
514+
const toggle = screen.getByText(requiredProps.name).previousElementSibling;
515+
expect(toggle).toBeDisabled();
516+
expect(toggle).toHaveClass('pf-m-disabled');
517+
});
518+
519+
test('Does not render toggle span with disabled attribute when isToggleDisabled is true (toggle is span by default)', () => {
520+
render(
521+
<TreeViewListItem isToggleDisabled {...requiredProps}>
522+
Content
523+
</TreeViewListItem>
524+
);
525+
526+
const toggle = screen.getByText(requiredProps.name).previousElementSibling;
527+
expect(toggle?.tagName).toBe('SPAN');
528+
expect(toggle).not.toHaveAttribute('disabled');
529+
});
530+
531+
test('Does not call onExpand when isToggleDisabled is true and hasCheckbox is passed', async () => {
532+
render(
533+
<TreeViewListItem hasCheckbox isToggleDisabled onExpand={onExpandMock} {...requiredProps}>
534+
Content
535+
</TreeViewListItem>
536+
);
537+
538+
const toggle = screen.getByText(requiredProps.name).previousElementSibling?.previousElementSibling;
539+
await user.click(toggle as Element);
540+
541+
expect(onExpandMock).not.toHaveBeenCalled();
542+
});
543+
544+
test('Does not call onCollapse when isToggleDisabled is true and hasCheckbox is passed', async () => {
545+
render(
546+
<TreeViewListItem hasCheckbox isToggleDisabled isExpanded onCollapse={onCollapseMock} {...requiredProps}>
547+
Content
548+
</TreeViewListItem>
549+
);
550+
551+
const toggle = screen.getByText(requiredProps.name).previousElementSibling?.previousElementSibling;
552+
await user.click(toggle as Element);
553+
554+
expect(onCollapseMock).not.toHaveBeenCalled();
555+
});
556+
557+
test('Does not call onExpand when isToggleDisabled is true and isSelectable is passed', async () => {
558+
render(
559+
<TreeViewListItem isSelectable isToggleDisabled onExpand={onExpandMock} {...requiredProps}>
560+
Content
561+
</TreeViewListItem>
562+
);
563+
564+
const toggle = screen.getByText(requiredProps.name).previousElementSibling;
565+
await user.click(toggle as Element);
566+
567+
expect(onExpandMock).not.toHaveBeenCalled();
568+
});
569+
570+
test('Does not call onCollapse when isToggleDisabled is true and isSelectable is passed', async () => {
571+
render(
572+
<TreeViewListItem isSelectable isToggleDisabled isExpanded onCollapse={onCollapseMock} {...requiredProps}>
573+
Content
574+
</TreeViewListItem>
575+
);
576+
577+
const toggle = screen.getByText(requiredProps.name).previousElementSibling;
578+
await user.click(toggle as Element);
579+
580+
expect(onCollapseMock).not.toHaveBeenCalled();
581+
});
582+
});
583+
418584
describe('Callback props', () => {
419585
const user = userEvent.setup();
420586
const compareItemsMock = jest.fn();

0 commit comments

Comments
 (0)