Skip to content

Commit 5d1bc76

Browse files
committed
fix(cdk/tree): only handle keyboard events directly from the node
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 #29828.
1 parent a98c886 commit 5d1bc76

File tree

3 files changed

+40
-5
lines changed

3 files changed

+40
-5
lines changed

src/cdk/tree/tree.spec.ts

+22
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

+16-3
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export class CdkTree<T, K = T>
125125
OnDestroy,
126126
OnInit
127127
{
128+
private _elementRef = inject(ElementRef);
128129
private _dir = inject(Directionality);
129130

130131
/** Subject that emits when the component has been destroyed. */
@@ -889,8 +890,20 @@ export class CdkTree<T, K = T>
889890
}
890891

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

896909
/** Gets all nested descendants of a given node. */
@@ -1341,7 +1354,7 @@ export class CdkTreeNode<T, K = T> implements OnDestroy, OnInit, TreeKeyManagerI
13411354
private _changeDetectorRef = inject(ChangeDetectorRef);
13421355

13431356
constructor(
1344-
protected _elementRef: ElementRef<HTMLElement>,
1357+
public _elementRef: ElementRef<HTMLElement>,
13451358
protected _tree: CdkTree<T, K>,
13461359
) {
13471360
CdkTreeNode.mostRecentTreeNode = this as CdkTreeNode<T, K>;

tools/public_api_guard/cdk/tree.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export class CdkTree<T, K = T> implements AfterContentChecked, AfterContentInit,
119119
_nodeOutlet: CdkTreeNodeOutlet;
120120
_registerNode(node: CdkTreeNode<T, K>): void;
121121
renderNodeChanges(data: readonly T[], dataDiffer?: IterableDiffer<T>, viewContainer?: ViewContainerRef, parentData?: T): void;
122-
_sendKeydownToKeyManager(event: KeyboardEvent): void;
122+
protected _sendKeydownToKeyManager(event: KeyboardEvent): void;
123123
_setNodeTypeIfUnset(nodeType: 'flat' | 'nested'): void;
124124
toggle(dataNode: T): void;
125125
toggleDescendants(dataNode: T): void;
@@ -160,7 +160,7 @@ export class CdkTreeNode<T, K = T> implements OnDestroy, OnInit, TreeKeyManagerI
160160
readonly _dataChanges: Subject<void>;
161161
protected readonly _destroyed: Subject<void>;
162162
// (undocumented)
163-
protected _elementRef: ElementRef<HTMLElement>;
163+
_elementRef: ElementRef<HTMLElement>;
164164
// (undocumented)
165165
_emitExpansionState(expanded: boolean): void;
166166
expand(): void;

0 commit comments

Comments
 (0)