Skip to content

Commit 3514831

Browse files
authored
feat(TreeView): add support for disabled TreeViewListItems (#12140)
* feat(TreeView): add support for disabled TreeViewListItems * pr feedback & css updates * bump core, replace hardcoded css with modifier obj * add disabled props to memo * update checkbox disabled to reflect prop
1 parent f3613a6 commit 3514831

File tree

9 files changed

+291
-24
lines changed

9 files changed

+291
-24
lines changed

packages/react-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"tslib": "^2.8.1"
5555
},
5656
"devDependencies": {
57-
"@patternfly/patternfly": "6.5.0-prerelease.33",
57+
"@patternfly/patternfly": "6.5.0-prerelease.34",
5858
"case-anything": "^3.1.2",
5959
"css": "^3.0.0",
6060
"fs-extra": "^11.3.0"

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: 27 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,
@@ -125,25 +131,26 @@ const TreeViewListItemBase: React.FunctionComponent<TreeViewListItemProps> = ({
125131
}
126132

127133
const ToggleComponent = hasCheckbox || isSelectable ? 'button' : 'span';
134+
const hasDisabledToggleClass = isToggleDisabled || (Component === 'button' && isDisabled);
128135

129136
const renderToggle = (randomId: string) => (
130137
<ToggleComponent
131-
className={css(styles.treeViewNodeToggle)}
138+
className={css(styles.treeViewNodeToggle, hasDisabledToggleClass && styles.modifiers.disabled)}
132139
onClick={(evt: React.MouseEvent) => {
133-
if (isSelectable || hasCheckbox) {
140+
if (!isToggleDisabled && (isSelectable || hasCheckbox)) {
134141
if (internalIsExpanded) {
135142
onCollapse && onCollapse(evt, itemData, parentItem);
136143
} else {
137144
onExpand && onExpand(evt, itemData, parentItem);
138145
}
139146
setIsExpanded(!internalIsExpanded);
140147
}
141-
if (isSelectable) {
148+
if (!isToggleDisabled && isSelectable) {
142149
evt.stopPropagation();
143150
}
144151
}}
145152
{...((hasCheckbox || isSelectable) && { 'aria-labelledby': `label-${randomId}` })}
146-
{...(ToggleComponent === 'button' && { type: 'button' })}
153+
{...(ToggleComponent === 'button' && { disabled: isToggleDisabled, type: 'button' })}
147154
tabIndex={-1}
148155
>
149156
<span className={css(styles.treeViewNodeToggleIcon)}>
@@ -162,6 +169,7 @@ const TreeViewListItemBase: React.FunctionComponent<TreeViewListItemProps> = ({
162169
ref={(elem) => {
163170
elem && (elem.indeterminate = checkProps.checked === null);
164171
}}
172+
disabled={isDisabled || checkProps.disabled}
165173
{...checkProps}
166174
checked={isCheckboxChecked}
167175
id={randomId}
@@ -180,7 +188,7 @@ const TreeViewListItemBase: React.FunctionComponent<TreeViewListItemProps> = ({
180188
<>
181189
{isCompact && title && <span className={css(styles.treeViewNodeTitle)}>{title}</span>}
182190
{isSelectable ? (
183-
<button tabIndex={-1} className={css(styles.treeViewNodeText)} type="button">
191+
<button tabIndex={-1} className={css(styles.treeViewNodeText)} type="button" disabled={isDisabled}>
184192
{name}
185193
</button>
186194
) : (
@@ -220,6 +228,9 @@ const TreeViewListItemBase: React.FunctionComponent<TreeViewListItemProps> = ({
220228
})
221229
);
222230

231+
const isFullyDisabled =
232+
(Component === 'button' && isDisabled) || (Component !== 'button' && isDisabled && isToggleDisabled);
233+
223234
return (
224235
<li
225236
id={id}
@@ -229,16 +240,21 @@ const TreeViewListItemBase: React.FunctionComponent<TreeViewListItemProps> = ({
229240
tabIndex={-1}
230241
{...(hasCheckbox && { 'aria-checked': isCheckboxChecked })}
231242
{...(!hasCheckbox && { 'aria-selected': isSelected })}
243+
{...(isFullyDisabled && { 'aria-disabled': true })}
232244
>
233245
<div className={css(styles.treeViewContent)}>
234246
<GenerateId prefix={isSelectable ? 'selectable-id' : 'checkbox-id'}>
235247
{(randomId) => (
236248
<Component
237-
className={css(styles.treeViewNode, isSelected && styles.modifiers.current)}
249+
className={css(
250+
styles.treeViewNode,
251+
isSelected && styles.modifiers.current,
252+
isDisabled && styles.modifiers.disabled
253+
)}
238254
onClick={(evt: React.MouseEvent) => {
239255
if (!hasCheckbox) {
240-
onSelect && onSelect(evt, itemData, parentItem);
241-
if (!isSelectable && children && evt.isDefaultPrevented() !== true) {
256+
!isDisabled && onSelect && onSelect(evt, itemData, parentItem);
257+
if (!isDisabled && !isSelectable && children && evt.isDefaultPrevented() !== true) {
242258
if (internalIsExpanded) {
243259
onCollapse && onCollapse(evt, itemData, parentItem);
244260
} else {
@@ -250,7 +266,7 @@ const TreeViewListItemBase: React.FunctionComponent<TreeViewListItemProps> = ({
250266
}}
251267
{...(hasCheckbox && { htmlFor: randomId })}
252268
{...((hasCheckbox || (isSelectable && children)) && { id: `label-${randomId}` })}
253-
{...(Component === 'button' && { type: 'button' })}
269+
{...(Component === 'button' && { type: 'button', disabled: isDisabled })}
254270
>
255271
<span className={css(styles.treeViewNodeContainer)}>
256272
{children && renderToggle(randomId)}
@@ -297,6 +313,8 @@ export const TreeViewListItem = memo(TreeViewListItemBase, (prevProps, nextProps
297313
prevProps.id !== nextProps.id ||
298314
prevProps.isExpanded !== nextProps.isExpanded ||
299315
prevProps.defaultExpanded !== nextProps.defaultExpanded ||
316+
prevProps.isDisabled !== nextProps.isDisabled ||
317+
prevProps.isToggleDisabled !== nextProps.isToggleDisabled ||
300318
prevProps.onSelect !== nextProps.onSelect ||
301319
prevProps.onCheck !== nextProps.onCheck ||
302320
prevProps.onExpand !== nextProps.onExpand ||

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

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,249 @@ 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 ${styles.modifiers.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(styles.modifiers.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('Does not call onSelect when isDisabled is true', async () => {
444+
render(<TreeViewListItem isDisabled onSelect={onSelectMock} {...requiredProps} />);
445+
446+
await user.click(screen.getByRole('button', { name: requiredProps.name }));
447+
448+
expect(onSelectMock).not.toHaveBeenCalled();
449+
});
450+
451+
test('Does not call onExpand when isDisabled is true and item is collapsed', async () => {
452+
render(
453+
<TreeViewListItem isDisabled onExpand={onExpandMock} {...requiredProps}>
454+
Content
455+
</TreeViewListItem>
456+
);
457+
458+
await user.click(screen.getByRole('button', { name: requiredProps.name }));
459+
460+
expect(onExpandMock).not.toHaveBeenCalled();
461+
});
462+
463+
test('Does not call onCollapse when isDisabled is true and item is expanded', async () => {
464+
render(
465+
<TreeViewListItem isDisabled isExpanded onCollapse={onCollapseMock} {...requiredProps}>
466+
Content
467+
</TreeViewListItem>
468+
);
469+
470+
await user.click(screen.getByRole('button', { name: requiredProps.name }));
471+
472+
expect(onCollapseMock).not.toHaveBeenCalled();
473+
});
474+
475+
test(`Renders toggle with ${styles.modifiers.disabled} class when isDisabled is true for default TreeViewListItem`, () => {
476+
render(
477+
<TreeViewListItem isDisabled {...requiredProps}>
478+
Content
479+
</TreeViewListItem>
480+
);
481+
482+
const toggle = screen.getByText(requiredProps.name).previousElementSibling;
483+
expect(toggle).toHaveClass(styles.modifiers.disabled);
484+
});
485+
486+
test('Renders treeitem with aria-disabled when isDisabled is true for default TreeViewListItem', () => {
487+
render(<TreeViewListItem isDisabled {...requiredProps} />);
488+
489+
expect(screen.getByRole('treeitem')).toHaveAttribute('aria-disabled', 'true');
490+
});
491+
492+
test('Renders treeitem with aria-disabled when isDisabled and isToggleDisabled are true and isSelectable is true', () => {
493+
render(
494+
<TreeViewListItem isSelectable isDisabled isToggleDisabled {...requiredProps}>
495+
Content
496+
</TreeViewListItem>
497+
);
498+
499+
expect(screen.getByRole('treeitem')).toHaveAttribute('aria-disabled', 'true');
500+
});
501+
502+
test('Renders treeitem with aria-disabled when isDisabled and isToggleDisabled are true and hasCheckbox is true', () => {
503+
render(
504+
<TreeViewListItem hasCheckbox isDisabled isToggleDisabled {...requiredProps}>
505+
Content
506+
</TreeViewListItem>
507+
);
508+
509+
expect(screen.getByRole('treeitem')).toHaveAttribute('aria-disabled', 'true');
510+
});
511+
512+
test('Does not render treeitem with aria-disabled when isDisabled is true, isToggleDisabled is false, and isSelectable is true', () => {
513+
render(
514+
<TreeViewListItem isSelectable isDisabled {...requiredProps}>
515+
Content
516+
</TreeViewListItem>
517+
);
518+
519+
expect(screen.getByRole('treeitem')).not.toHaveAttribute('aria-disabled');
520+
});
521+
522+
test('Does not render treeitem with aria-disabled when isDisabled is false', () => {
523+
render(<TreeViewListItem isDisabled={false} {...requiredProps} />);
524+
525+
expect(screen.getByRole('treeitem')).not.toHaveAttribute('aria-disabled');
526+
});
527+
});
528+
529+
// Assisted by Cursor AI
530+
describe('isToggleDisabled prop', () => {
531+
const user = userEvent.setup();
532+
const onExpandMock = jest.fn();
533+
const onCollapseMock = jest.fn();
534+
535+
beforeEach(() => {
536+
jest.clearAllMocks();
537+
});
538+
539+
test(`Renders toggle button with disabled attribute and ${styles.modifiers.disabled} class when isToggleDisabled is true and hasCheckbox is passed`, () => {
540+
render(
541+
<TreeViewListItem hasCheckbox isToggleDisabled {...requiredProps}>
542+
Content
543+
</TreeViewListItem>
544+
);
545+
546+
const toggle = screen.getByText(requiredProps.name).previousElementSibling?.previousElementSibling;
547+
expect(toggle).toBeDisabled();
548+
expect(toggle).toHaveClass(styles.modifiers.disabled);
549+
});
550+
551+
test(`Renders toggle button with disabled attribute and ${styles.modifiers.disabled} class when isToggleDisabled is true and isSelectable is passed`, () => {
552+
render(
553+
<TreeViewListItem isSelectable isToggleDisabled {...requiredProps}>
554+
Content
555+
</TreeViewListItem>
556+
);
557+
558+
const toggle = screen.getByText(requiredProps.name).previousElementSibling;
559+
expect(toggle).toBeDisabled();
560+
expect(toggle).toHaveClass(styles.modifiers.disabled);
561+
});
562+
563+
test('Does not render toggle span with disabled attribute when isToggleDisabled is true (toggle is span by default)', () => {
564+
render(
565+
<TreeViewListItem isToggleDisabled {...requiredProps}>
566+
Content
567+
</TreeViewListItem>
568+
);
569+
570+
const toggle = screen.getByText(requiredProps.name).previousElementSibling;
571+
expect(toggle?.tagName).toBe('SPAN');
572+
expect(toggle).not.toHaveAttribute('disabled');
573+
});
574+
575+
test('Does not call onExpand when isToggleDisabled is true and hasCheckbox is passed', async () => {
576+
render(
577+
<TreeViewListItem hasCheckbox isToggleDisabled onExpand={onExpandMock} {...requiredProps}>
578+
Content
579+
</TreeViewListItem>
580+
);
581+
582+
const toggle = screen.getByText(requiredProps.name).previousElementSibling?.previousElementSibling;
583+
await user.click(toggle as Element);
584+
585+
expect(onExpandMock).not.toHaveBeenCalled();
586+
});
587+
588+
test('Does not call onCollapse when isToggleDisabled is true and hasCheckbox is passed', async () => {
589+
render(
590+
<TreeViewListItem hasCheckbox isToggleDisabled isExpanded onCollapse={onCollapseMock} {...requiredProps}>
591+
Content
592+
</TreeViewListItem>
593+
);
594+
595+
const toggle = screen.getByText(requiredProps.name).previousElementSibling?.previousElementSibling;
596+
await user.click(toggle as Element);
597+
598+
expect(onCollapseMock).not.toHaveBeenCalled();
599+
});
600+
601+
test('Does not call onExpand when isToggleDisabled is true and isSelectable is passed', async () => {
602+
render(
603+
<TreeViewListItem isSelectable isToggleDisabled onExpand={onExpandMock} {...requiredProps}>
604+
Content
605+
</TreeViewListItem>
606+
);
607+
608+
const toggle = screen.getByText(requiredProps.name).previousElementSibling;
609+
await user.click(toggle as Element);
610+
611+
expect(onExpandMock).not.toHaveBeenCalled();
612+
});
613+
614+
test('Does not call onCollapse when isToggleDisabled is true and isSelectable is passed', async () => {
615+
render(
616+
<TreeViewListItem isSelectable isToggleDisabled isExpanded onCollapse={onCollapseMock} {...requiredProps}>
617+
Content
618+
</TreeViewListItem>
619+
);
620+
621+
const toggle = screen.getByText(requiredProps.name).previousElementSibling;
622+
await user.click(toggle as Element);
623+
624+
expect(onCollapseMock).not.toHaveBeenCalled();
625+
});
626+
627+
test(`Renders toggle span with ${styles.modifiers.disabled} class when isDisabled is true for default TreeViewListItem`, () => {
628+
render(
629+
<TreeViewListItem isDisabled {...requiredProps}>
630+
Content
631+
</TreeViewListItem>
632+
);
633+
634+
const toggle = screen.getByText(requiredProps.name).previousElementSibling;
635+
expect(toggle).toHaveClass(styles.modifiers.disabled);
636+
});
637+
638+
test(`Does not render toggle with ${styles.modifiers.disabled} class when isDisabled is true and hasCheckbox is true`, () => {
639+
render(
640+
<TreeViewListItem hasCheckbox isDisabled {...requiredProps}>
641+
Content
642+
</TreeViewListItem>
643+
);
644+
645+
const toggle = screen.getByText(requiredProps.name).previousElementSibling?.previousElementSibling;
646+
expect(toggle).not.toHaveClass(styles.modifiers.disabled);
647+
});
648+
649+
test(`Does not render toggle with ${styles.modifiers.disabled} class when isDisabled is true and isSelectable is true`, () => {
650+
render(
651+
<TreeViewListItem isSelectable isDisabled {...requiredProps}>
652+
Content
653+
</TreeViewListItem>
654+
);
655+
656+
const toggle = screen.getByText(requiredProps.name).previousElementSibling;
657+
expect(toggle).not.toHaveClass(styles.modifiers.disabled);
658+
});
659+
});
660+
418661
describe('Callback props', () => {
419662
const user = userEvent.setup();
420663
const compareItemsMock = jest.fn();

0 commit comments

Comments
 (0)