Skip to content

Commit 3b4ade5

Browse files
authored
fix(cdk/tree): only handle keyboard events directly from the node (angular#29861)
Currently the CDK tree handle any keyboard event coming from a descendant. This problematic if there are interactive elements like inputs inside the tree, because their default behavior will be prevented. This change switches to only handling events coming either directly from the tree or directly from a tree node. Fixes angular#29828.
1 parent 56f7fe5 commit 3b4ade5

File tree

3 files changed

+40
-5
lines changed

3 files changed

+40
-5
lines changed

src/cdk/tree/tree.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,28 @@ describe('CdkTree', () => {
380380
.withContext(`Expect node collapsed`)
381381
.toBe(0);
382382
});
383+
384+
it('should not handle events coming from a descendant of a node', () => {
385+
expect(dataSource.data.length).toBe(3);
386+
387+
expect(getExpandedNodes(component.dataSource?.getRecursiveData(), component.tree).length)
388+
.withContext('Expect no expanded node on init')
389+
.toBe(0);
390+
391+
const node = getNodes(treeElement)[2] as HTMLElement;
392+
const input = document.createElement('input');
393+
node.appendChild(input);
394+
395+
const event = createKeyboardEvent('keydown', undefined, 'ArrowRight');
396+
spyOn(event, 'preventDefault').and.callThrough();
397+
input.dispatchEvent(event);
398+
fixture.detectChanges();
399+
400+
expect(getExpandedNodes(component.dataSource?.getRecursiveData(), component.tree).length)
401+
.withContext('Expect no expanded node after event')
402+
.toBe(0);
403+
expect(event.preventDefault).not.toHaveBeenCalled();
404+
});
383405
});
384406

385407
describe('with when node template', () => {

src/cdk/tree/tree.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export class CdkTree<T, K = T>
127127
{
128128
private _differs = inject(IterableDiffers);
129129
private _changeDetectorRef = inject(ChangeDetectorRef);
130+
private _elementRef = inject(ElementRef);
130131

131132
private _dir = inject(Directionality);
132133

@@ -890,8 +891,20 @@ export class CdkTree<T, K = T>
890891
}
891892

892893
/** `keydown` event handler; this just passes the event to the `TreeKeyManager`. */
893-
_sendKeydownToKeyManager(event: KeyboardEvent) {
894-
this._keyManager.onKeydown(event);
894+
protected _sendKeydownToKeyManager(event: KeyboardEvent): void {
895+
// Only handle events directly on the tree or directly on one of the nodes, otherwise
896+
// we risk interfering with events in the projected content (see #29828).
897+
if (event.target === this._elementRef.nativeElement) {
898+
this._keyManager.onKeydown(event);
899+
} else {
900+
const nodes = this._nodes.getValue();
901+
for (const [, node] of nodes) {
902+
if (event.target === node._elementRef.nativeElement) {
903+
this._keyManager.onKeydown(event);
904+
break;
905+
}
906+
}
907+
}
895908
}
896909

897910
/** Gets all nested descendants of a given node. */
@@ -1155,7 +1168,7 @@ export class CdkTree<T, K = T>
11551168
standalone: true,
11561169
})
11571170
export class CdkTreeNode<T, K = T> implements OnDestroy, OnInit, TreeKeyManagerItem {
1158-
protected _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
1171+
_elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
11591172
protected _tree = inject<CdkTree<T, K>>(CdkTree);
11601173
protected _tabindex: number | null = -1;
11611174

tools/public_api_guard/cdk/tree.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export class CdkTree<T, K = T> implements AfterContentChecked, AfterContentInit,
117117
_nodeOutlet: CdkTreeNodeOutlet;
118118
_registerNode(node: CdkTreeNode<T, K>): void;
119119
renderNodeChanges(data: readonly T[], dataDiffer?: IterableDiffer<T>, viewContainer?: ViewContainerRef, parentData?: T): void;
120-
_sendKeydownToKeyManager(event: KeyboardEvent): void;
120+
protected _sendKeydownToKeyManager(event: KeyboardEvent): void;
121121
_setNodeTypeIfUnset(nodeType: 'flat' | 'nested'): void;
122122
toggle(dataNode: T): void;
123123
toggleDescendants(dataNode: T): void;
@@ -158,7 +158,7 @@ export class CdkTreeNode<T, K = T> implements OnDestroy, OnInit, TreeKeyManagerI
158158
readonly _dataChanges: Subject<void>;
159159
protected readonly _destroyed: Subject<void>;
160160
// (undocumented)
161-
protected _elementRef: ElementRef<HTMLElement>;
161+
_elementRef: ElementRef<HTMLElement>;
162162
// (undocumented)
163163
_emitExpansionState(expanded: boolean): void;
164164
expand(): void;

0 commit comments

Comments
 (0)