@angular/cdk 18.1.1 → 18.2.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/a11y/index.d.ts +283 -2
  2. package/coercion/private/index.d.ts +9 -0
  3. package/drag-drop/index.d.ts +12 -1
  4. package/esm2022/a11y/key-manager/list-key-manager.mjs +18 -38
  5. package/esm2022/a11y/key-manager/noop-tree-key-manager.mjs +94 -0
  6. package/esm2022/a11y/key-manager/tree-key-manager-strategy.mjs +9 -0
  7. package/esm2022/a11y/key-manager/tree-key-manager.mjs +345 -0
  8. package/esm2022/a11y/key-manager/typeahead.mjs +91 -0
  9. package/esm2022/a11y/public-api.mjs +4 -1
  10. package/esm2022/coercion/private/index.mjs +9 -0
  11. package/esm2022/coercion/private/observable.mjs +19 -0
  12. package/esm2022/coercion/private/private_public_index.mjs +5 -0
  13. package/esm2022/drag-drop/directives/drag.mjs +16 -3
  14. package/esm2022/drag-drop/drag-ref.mjs +8 -2
  15. package/esm2022/drag-drop/sorting/single-axis-sort-strategy.mjs +4 -3
  16. package/esm2022/tree/control/base-tree-control.mjs +7 -2
  17. package/esm2022/tree/control/flat-tree-control.mjs +8 -2
  18. package/esm2022/tree/control/nested-tree-control.mjs +11 -2
  19. package/esm2022/tree/control/tree-control.mjs +1 -1
  20. package/esm2022/tree/nested-node.mjs +6 -15
  21. package/esm2022/tree/padding.mjs +2 -4
  22. package/esm2022/tree/toggle.mjs +15 -8
  23. package/esm2022/tree/tree-errors.mjs +7 -6
  24. package/esm2022/tree/tree.mjs +817 -63
  25. package/esm2022/version.mjs +1 -1
  26. package/fesm2022/a11y.mjs +520 -40
  27. package/fesm2022/a11y.mjs.map +1 -1
  28. package/fesm2022/cdk.mjs +1 -1
  29. package/fesm2022/cdk.mjs.map +1 -1
  30. package/fesm2022/coercion/private.mjs +19 -0
  31. package/fesm2022/coercion/private.mjs.map +1 -0
  32. package/fesm2022/drag-drop.mjs +25 -5
  33. package/fesm2022/drag-drop.mjs.map +1 -1
  34. package/fesm2022/tree.mjs +858 -94
  35. package/fesm2022/tree.mjs.map +1 -1
  36. package/package.json +9 -3
  37. package/schematics/ng-add/index.js +1 -1
  38. package/schematics/ng-add/index.mjs +1 -1
  39. package/tree/index.d.ts +304 -25
package/fesm2022/tree.mjs CHANGED
@@ -1,11 +1,18 @@
1
1
  import { SelectionModel, isDataSource } from '@angular/cdk/collections';
2
- import { isObservable, Subject, BehaviorSubject, of } from 'rxjs';
3
- import { take, filter, takeUntil, map, distinctUntilChanged } from 'rxjs/operators';
2
+ import { isObservable, Subject, BehaviorSubject, of, combineLatest, EMPTY, concat } from 'rxjs';
3
+ import { take, filter, takeUntil, startWith, tap, switchMap, map, reduce, concatMap, distinctUntilChanged } from 'rxjs/operators';
4
4
  import * as i0 from '@angular/core';
5
- import { InjectionToken, Directive, Inject, Optional, Component, ViewEncapsulation, ChangeDetectionStrategy, Input, ViewChild, ContentChildren, inject, ChangeDetectorRef, numberAttribute, booleanAttribute, NgModule } from '@angular/core';
6
- import * as i2 from '@angular/cdk/bidi';
5
+ import { InjectionToken, Directive, Inject, Optional, inject, Component, ViewEncapsulation, ChangeDetectionStrategy, Input, ViewChild, ContentChildren, EventEmitter, ChangeDetectorRef, booleanAttribute, Output, numberAttribute, NgModule } from '@angular/core';
6
+ import { TREE_KEY_MANAGER } from '@angular/cdk/a11y';
7
+ import * as i1 from '@angular/cdk/bidi';
8
+ import { coerceObservable } from '@angular/cdk/coercion/private';
7
9
 
8
- /** Base tree control. It has basic toggle/expand/collapse operations on a single data node. */
10
+ /**
11
+ * Base tree control. It has basic toggle/expand/collapse operations on a single data node.
12
+ *
13
+ * @deprecated Use one of levelAccessor or childrenAccessor. To be removed in a future version.
14
+ * @breaking-change 21.0.0
15
+ */
9
16
  class BaseTreeControl {
10
17
  constructor() {
11
18
  /** A selection model with multi-selection to track expansion status. */
@@ -54,7 +61,13 @@ class BaseTreeControl {
54
61
  }
55
62
  }
56
63
 
57
- /** Flat tree control. Able to expand/collapse a subtree recursively for flattened tree. */
64
+ /**
65
+ * Flat tree control. Able to expand/collapse a subtree recursively for flattened tree.
66
+ *
67
+ * @deprecated Use one of levelAccessor or childrenAccessor instead. To be removed in a future
68
+ * version.
69
+ * @breaking-change 21.0.0
70
+ */
58
71
  class FlatTreeControl extends BaseTreeControl {
59
72
  /** Construct with flat tree data node functions getLevel and isExpandable. */
60
73
  constructor(getLevel, isExpandable, options) {
@@ -97,7 +110,13 @@ class FlatTreeControl extends BaseTreeControl {
97
110
  }
98
111
  }
99
112
 
100
- /** Nested tree control. Able to expand/collapse a subtree recursively for NestedNode type. */
113
+ /**
114
+ * Nested tree control. Able to expand/collapse a subtree recursively for NestedNode type.
115
+ *
116
+ * @deprecated Use one of levelAccessor or childrenAccessor instead. To be removed in a future
117
+ * version.
118
+ * @breaking-change 21.0.0
119
+ */
101
120
  class NestedTreeControl extends BaseTreeControl {
102
121
  /** Construct with nested tree function getChildren. */
103
122
  constructor(getChildren, options) {
@@ -107,6 +126,9 @@ class NestedTreeControl extends BaseTreeControl {
107
126
  if (this.options) {
108
127
  this.trackBy = this.options.trackBy;
109
128
  }
129
+ if (this.options?.isExpandable) {
130
+ this.isExpandable = this.options.isExpandable;
131
+ }
110
132
  }
111
133
  /**
112
134
  * Expands all dataNodes in the tree.
@@ -225,18 +247,19 @@ function getTreeMissingMatchingNodeDefError() {
225
247
  return Error(`Could not find a matching node definition for the provided node data.`);
226
248
  }
227
249
  /**
228
- * Returns an error to be thrown when there are tree control.
250
+ * Returns an error to be thrown when there is no tree control.
229
251
  * @docs-private
230
252
  */
231
253
  function getTreeControlMissingError() {
232
- return Error(`Could not find a tree control for the tree.`);
254
+ return Error(`Could not find a tree control, levelAccessor, or childrenAccessor for the tree.`);
233
255
  }
234
256
  /**
235
- * Returns an error to be thrown when tree control did not implement functions for flat/nested node.
257
+ * Returns an error to be thrown when there are multiple ways of specifying children or level
258
+ * provided to the tree.
236
259
  * @docs-private
237
260
  */
238
- function getTreeControlFunctionsMissingError() {
239
- return Error(`Could not find functions for nested/flat tree in tree control.`);
261
+ function getMultipleTreeControlsError() {
262
+ return Error(`More than one of tree control, levelAccessor, or childrenAccessor were provided.`);
240
263
  }
241
264
 
242
265
  /**
@@ -257,13 +280,25 @@ class CdkTree {
257
280
  this._switchDataSource(dataSource);
258
281
  }
259
282
  }
260
- constructor(_differs, _changeDetectorRef) {
283
+ constructor(_differs, _changeDetectorRef, _dir) {
261
284
  this._differs = _differs;
262
285
  this._changeDetectorRef = _changeDetectorRef;
286
+ this._dir = _dir;
263
287
  /** Subject that emits when the component has been destroyed. */
264
288
  this._onDestroy = new Subject();
265
289
  /** Level of nodes */
266
290
  this._levels = new Map();
291
+ /** The immediate parents for a node. This is `null` if there is no parent. */
292
+ this._parents = new Map();
293
+ /**
294
+ * Nodes grouped into each set, which is a list of nodes displayed together in the DOM.
295
+ *
296
+ * Lookup key is the parent of a set. Root nodes have key of null.
297
+ *
298
+ * Values is a 'set' of tree nodes. Each tree node maps to a treeitem element. Sets are in the
299
+ * order that it is rendered. Each set maps directly to aria-posinset and aria-setsize attributes.
300
+ */
301
+ this._ariaSets = new Map();
267
302
  // TODO(tinayuangao): Setup a listener for scrolling, emit the calculated view to viewChange.
268
303
  // Remove the MAX_VALUE in viewChange
269
304
  /**
@@ -274,12 +309,31 @@ class CdkTree {
274
309
  start: 0,
275
310
  end: Number.MAX_VALUE,
276
311
  });
312
+ /**
313
+ * Maintain a synchronous cache of flattened data nodes. This will only be
314
+ * populated after initial render, and in certain cases, will be delayed due to
315
+ * relying on Observable `getChildren` calls.
316
+ */
317
+ this._flattenedNodes = new BehaviorSubject([]);
318
+ /** The automatically determined node type for the tree. */
319
+ this._nodeType = new BehaviorSubject(null);
320
+ /** The mapping between data and the node that is rendered. */
321
+ this._nodes = new BehaviorSubject(new Map());
322
+ /**
323
+ * Synchronous cache of nodes for the `TreeKeyManager`. This is separate
324
+ * from `_flattenedNodes` so they can be independently updated at different
325
+ * times.
326
+ */
327
+ this._keyManagerNodes = new BehaviorSubject([]);
328
+ this._keyManagerFactory = inject(TREE_KEY_MANAGER);
329
+ this._viewInit = false;
277
330
  }
278
- ngOnInit() {
279
- this._dataDiffer = this._differs.find([]).create(this.trackBy);
280
- if (!this.treeControl && (typeof ngDevMode === 'undefined' || ngDevMode)) {
281
- throw getTreeControlMissingError();
282
- }
331
+ ngAfterContentInit() {
332
+ this._initializeKeyManager();
333
+ }
334
+ ngAfterContentChecked() {
335
+ this._updateDefaultNodeDefinition();
336
+ this._subscribeToDataChanges();
283
337
  }
284
338
  ngOnDestroy() {
285
339
  this._nodeOutlet.viewContainer.clear();
@@ -293,19 +347,35 @@ class CdkTree {
293
347
  this._dataSubscription.unsubscribe();
294
348
  this._dataSubscription = null;
295
349
  }
350
+ // In certain tests, the tree might be destroyed before this is initialized
351
+ // in `ngAfterContentInit`.
352
+ this._keyManager?.destroy();
296
353
  }
297
- ngAfterContentChecked() {
354
+ ngOnInit() {
355
+ this._checkTreeControlUsage();
356
+ this._initializeDataDiffer();
357
+ }
358
+ ngAfterViewInit() {
359
+ this._viewInit = true;
360
+ }
361
+ _updateDefaultNodeDefinition() {
298
362
  const defaultNodeDefs = this._nodeDefs.filter(def => !def.when);
299
363
  if (defaultNodeDefs.length > 1 && (typeof ngDevMode === 'undefined' || ngDevMode)) {
300
364
  throw getTreeMultipleDefaultNodeDefsError();
301
365
  }
302
366
  this._defaultNodeDef = defaultNodeDefs[0];
303
- if (this.dataSource && this._nodeDefs && !this._dataSubscription) {
304
- this._observeRenderChanges();
367
+ }
368
+ /**
369
+ * Sets the node type for the tree, if it hasn't been set yet.
370
+ *
371
+ * This will be called by the first node that's rendered in order for the tree
372
+ * to determine what data transformations are required.
373
+ */
374
+ _setNodeTypeIfUnset(nodeType) {
375
+ if (this._nodeType.value === null) {
376
+ this._nodeType.next(nodeType);
305
377
  }
306
378
  }
307
- // TODO(tinayuangao): Work on keyboard traversal and actions, make sure it's working for RTL
308
- // and nested trees.
309
379
  /**
310
380
  * Switch to the provided data source by resetting the data and unsubscribing from the current
311
381
  * render change subscription if one exists. If the data source is null, interpret this by
@@ -325,11 +395,21 @@ class CdkTree {
325
395
  }
326
396
  this._dataSource = dataSource;
327
397
  if (this._nodeDefs) {
328
- this._observeRenderChanges();
398
+ this._subscribeToDataChanges();
329
399
  }
330
400
  }
401
+ _getExpansionModel() {
402
+ if (!this.treeControl) {
403
+ this._expansionModel ??= new SelectionModel(true);
404
+ return this._expansionModel;
405
+ }
406
+ return this.treeControl.expansionModel;
407
+ }
331
408
  /** Set up a subscription for the data provided by the data source. */
332
- _observeRenderChanges() {
409
+ _subscribeToDataChanges() {
410
+ if (this._dataSubscription) {
411
+ return;
412
+ }
333
413
  let dataStream;
334
414
  if (isDataSource(this._dataSource)) {
335
415
  dataStream = this._dataSource.connect(this);
@@ -340,34 +420,140 @@ class CdkTree {
340
420
  else if (Array.isArray(this._dataSource)) {
341
421
  dataStream = of(this._dataSource);
342
422
  }
343
- if (dataStream) {
344
- this._dataSubscription = dataStream
345
- .pipe(takeUntil(this._onDestroy))
346
- .subscribe(data => this.renderNodeChanges(data));
423
+ if (!dataStream) {
424
+ if (typeof ngDevMode === 'undefined' || ngDevMode) {
425
+ throw getTreeNoValidDataSourceError();
426
+ }
427
+ return;
347
428
  }
348
- else if (typeof ngDevMode === 'undefined' || ngDevMode) {
349
- throw getTreeNoValidDataSourceError();
429
+ this._dataSubscription = this._getRenderData(dataStream)
430
+ .pipe(takeUntil(this._onDestroy))
431
+ .subscribe(renderingData => {
432
+ this._renderDataChanges(renderingData);
433
+ });
434
+ }
435
+ /** Given an Observable containing a stream of the raw data, returns an Observable containing the RenderingData */
436
+ _getRenderData(dataStream) {
437
+ const expansionModel = this._getExpansionModel();
438
+ return combineLatest([
439
+ dataStream,
440
+ this._nodeType,
441
+ // We don't use the expansion data directly, however we add it here to essentially
442
+ // trigger data rendering when expansion changes occur.
443
+ expansionModel.changed.pipe(startWith(null), tap(expansionChanges => {
444
+ this._emitExpansionChanges(expansionChanges);
445
+ })),
446
+ ]).pipe(switchMap(([data, nodeType]) => {
447
+ if (nodeType === null) {
448
+ return of({ renderNodes: data, flattenedNodes: null, nodeType });
449
+ }
450
+ // If we're here, then we know what our node type is, and therefore can
451
+ // perform our usual rendering pipeline, which necessitates converting the data
452
+ return this._computeRenderingData(data, nodeType).pipe(map(convertedData => ({ ...convertedData, nodeType })));
453
+ }));
454
+ }
455
+ _renderDataChanges(data) {
456
+ if (data.nodeType === null) {
457
+ this.renderNodeChanges(data.renderNodes);
458
+ return;
459
+ }
460
+ // If we're here, then we know what our node type is, and therefore can
461
+ // perform our usual rendering pipeline.
462
+ this._updateCachedData(data.flattenedNodes);
463
+ this.renderNodeChanges(data.renderNodes);
464
+ this._updateKeyManagerItems(data.flattenedNodes);
465
+ }
466
+ _emitExpansionChanges(expansionChanges) {
467
+ if (!expansionChanges) {
468
+ return;
469
+ }
470
+ const nodes = this._nodes.value;
471
+ for (const added of expansionChanges.added) {
472
+ const node = nodes.get(added);
473
+ node?._emitExpansionState(true);
474
+ }
475
+ for (const removed of expansionChanges.removed) {
476
+ const node = nodes.get(removed);
477
+ node?._emitExpansionState(false);
478
+ }
479
+ }
480
+ _initializeKeyManager() {
481
+ const items = combineLatest([this._keyManagerNodes, this._nodes]).pipe(map(([keyManagerNodes, renderNodes]) => keyManagerNodes.reduce((items, data) => {
482
+ const node = renderNodes.get(this._getExpansionKey(data));
483
+ if (node) {
484
+ items.push(node);
485
+ }
486
+ return items;
487
+ }, [])));
488
+ const keyManagerOptions = {
489
+ trackBy: node => this._getExpansionKey(node.data),
490
+ skipPredicate: node => !!node.isDisabled,
491
+ typeAheadDebounceInterval: true,
492
+ horizontalOrientation: this._dir.value,
493
+ };
494
+ this._keyManager = this._keyManagerFactory(items, keyManagerOptions);
495
+ }
496
+ _initializeDataDiffer() {
497
+ // Provide a default trackBy based on `_getExpansionKey` if one isn't provided.
498
+ const trackBy = this.trackBy ?? ((_index, item) => this._getExpansionKey(item));
499
+ this._dataDiffer = this._differs.find([]).create(trackBy);
500
+ }
501
+ _checkTreeControlUsage() {
502
+ if (typeof ngDevMode === 'undefined' || ngDevMode) {
503
+ // Verify that Tree follows API contract of using one of TreeControl, levelAccessor or
504
+ // childrenAccessor. Throw an appropriate error if contract is not met.
505
+ let numTreeControls = 0;
506
+ if (this.treeControl) {
507
+ numTreeControls++;
508
+ }
509
+ if (this.levelAccessor) {
510
+ numTreeControls++;
511
+ }
512
+ if (this.childrenAccessor) {
513
+ numTreeControls++;
514
+ }
515
+ if (!numTreeControls) {
516
+ throw getTreeControlMissingError();
517
+ }
518
+ else if (numTreeControls > 1) {
519
+ throw getMultipleTreeControlsError();
520
+ }
350
521
  }
351
522
  }
352
523
  /** Check for changes made in the data and render each change (node added/removed/moved). */
353
524
  renderNodeChanges(data, dataDiffer = this._dataDiffer, viewContainer = this._nodeOutlet.viewContainer, parentData) {
354
525
  const changes = dataDiffer.diff(data);
355
- if (!changes) {
526
+ // Some tree consumers expect change detection to propagate to nodes
527
+ // even when the array itself hasn't changed; we explicitly detect changes
528
+ // anyways in order for nodes to update their data.
529
+ //
530
+ // However, if change detection is called while the component's view is
531
+ // still initing, then the order of child views initing will be incorrect;
532
+ // to prevent this, we only exit early if the view hasn't initialized yet.
533
+ if (!changes && !this._viewInit) {
356
534
  return;
357
535
  }
358
- changes.forEachOperation((item, adjustedPreviousIndex, currentIndex) => {
536
+ changes?.forEachOperation((item, adjustedPreviousIndex, currentIndex) => {
359
537
  if (item.previousIndex == null) {
360
538
  this.insertNode(data[currentIndex], currentIndex, viewContainer, parentData);
361
539
  }
362
540
  else if (currentIndex == null) {
363
541
  viewContainer.remove(adjustedPreviousIndex);
364
- this._levels.delete(item.item);
365
542
  }
366
543
  else {
367
544
  const view = viewContainer.get(adjustedPreviousIndex);
368
545
  viewContainer.move(view, currentIndex);
369
546
  }
370
547
  });
548
+ // If the data itself changes, but keeps the same trackBy, we need to update the templates'
549
+ // context to reflect the new object.
550
+ changes?.forEachIdentityChange((record) => {
551
+ const newData = record.item;
552
+ if (record.currentIndex != undefined) {
553
+ const view = viewContainer.get(record.currentIndex);
554
+ view.context.$implicit = newData;
555
+ }
556
+ });
371
557
  // TODO: change to `this._changeDetectorRef.markForCheck()`, or just switch this component to
372
558
  // use signals.
373
559
  this._changeDetectorRef.detectChanges();
@@ -393,21 +579,24 @@ class CdkTree {
393
579
  * within the data node view container.
394
580
  */
395
581
  insertNode(nodeData, index, viewContainer, parentData) {
582
+ const levelAccessor = this._getLevelAccessor();
396
583
  const node = this._getNodeDef(nodeData, index);
584
+ const key = this._getExpansionKey(nodeData);
397
585
  // Node context that will be provided to created embedded view
398
586
  const context = new CdkTreeNodeOutletContext(nodeData);
587
+ parentData ??= this._parents.get(key) ?? undefined;
399
588
  // If the tree is flat tree, then use the `getLevel` function in flat tree control
400
589
  // Otherwise, use the level of parent node.
401
- if (this.treeControl.getLevel) {
402
- context.level = this.treeControl.getLevel(nodeData);
590
+ if (levelAccessor) {
591
+ context.level = levelAccessor(nodeData);
403
592
  }
404
- else if (typeof parentData !== 'undefined' && this._levels.has(parentData)) {
405
- context.level = this._levels.get(parentData) + 1;
593
+ else if (parentData !== undefined && this._levels.has(this._getExpansionKey(parentData))) {
594
+ context.level = this._levels.get(this._getExpansionKey(parentData)) + 1;
406
595
  }
407
596
  else {
408
597
  context.level = 0;
409
598
  }
410
- this._levels.set(nodeData, context.level);
599
+ this._levels.set(key, context.level);
411
600
  // Use default tree nodeOutlet, or nested node's nodeOutlet
412
601
  const container = viewContainer ? viewContainer : this._nodeOutlet.viewContainer;
413
602
  container.createEmbeddedView(node.template, context, index);
@@ -418,8 +607,427 @@ class CdkTree {
418
607
  CdkTreeNode.mostRecentTreeNode.data = nodeData;
419
608
  }
420
609
  }
421
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.0", ngImport: i0, type: CdkTree, deps: [{ token: i0.IterableDiffers }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
422
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.1.0", type: CdkTree, isStandalone: true, selector: "cdk-tree", inputs: { dataSource: "dataSource", treeControl: "treeControl", trackBy: "trackBy" }, host: { attributes: { "role": "tree" }, classAttribute: "cdk-tree" }, queries: [{ propertyName: "_nodeDefs", predicate: CdkTreeNodeDef, descendants: true }], viewQueries: [{ propertyName: "_nodeOutlet", first: true, predicate: CdkTreeNodeOutlet, descendants: true, static: true }], exportAs: ["cdkTree"], ngImport: i0, template: `<ng-container cdkTreeNodeOutlet></ng-container>`, isInline: true, dependencies: [{ kind: "directive", type: CdkTreeNodeOutlet, selector: "[cdkTreeNodeOutlet]" }], changeDetection: i0.ChangeDetectionStrategy.Default, encapsulation: i0.ViewEncapsulation.None }); }
610
+ /** Whether the data node is expanded or collapsed. Returns true if it's expanded. */
611
+ isExpanded(dataNode) {
612
+ return !!(this.treeControl?.isExpanded(dataNode) ||
613
+ this._expansionModel?.isSelected(this._getExpansionKey(dataNode)));
614
+ }
615
+ /** If the data node is currently expanded, collapse it. Otherwise, expand it. */
616
+ toggle(dataNode) {
617
+ if (this.treeControl) {
618
+ this.treeControl.toggle(dataNode);
619
+ }
620
+ else if (this._expansionModel) {
621
+ this._expansionModel.toggle(this._getExpansionKey(dataNode));
622
+ }
623
+ }
624
+ /** Expand the data node. If it is already expanded, does nothing. */
625
+ expand(dataNode) {
626
+ if (this.treeControl) {
627
+ this.treeControl.expand(dataNode);
628
+ }
629
+ else if (this._expansionModel) {
630
+ this._expansionModel.select(this._getExpansionKey(dataNode));
631
+ }
632
+ }
633
+ /** Collapse the data node. If it is already collapsed, does nothing. */
634
+ collapse(dataNode) {
635
+ if (this.treeControl) {
636
+ this.treeControl.collapse(dataNode);
637
+ }
638
+ else if (this._expansionModel) {
639
+ this._expansionModel.deselect(this._getExpansionKey(dataNode));
640
+ }
641
+ }
642
+ /**
643
+ * If the data node is currently expanded, collapse it and all its descendants.
644
+ * Otherwise, expand it and all its descendants.
645
+ */
646
+ toggleDescendants(dataNode) {
647
+ if (this.treeControl) {
648
+ this.treeControl.toggleDescendants(dataNode);
649
+ }
650
+ else if (this._expansionModel) {
651
+ if (this.isExpanded(dataNode)) {
652
+ this.collapseDescendants(dataNode);
653
+ }
654
+ else {
655
+ this.expandDescendants(dataNode);
656
+ }
657
+ }
658
+ }
659
+ /**
660
+ * Expand the data node and all its descendants. If they are already expanded, does nothing.
661
+ */
662
+ expandDescendants(dataNode) {
663
+ if (this.treeControl) {
664
+ this.treeControl.expandDescendants(dataNode);
665
+ }
666
+ else if (this._expansionModel) {
667
+ const expansionModel = this._expansionModel;
668
+ expansionModel.select(this._getExpansionKey(dataNode));
669
+ this._getDescendants(dataNode)
670
+ .pipe(take(1), takeUntil(this._onDestroy))
671
+ .subscribe(children => {
672
+ expansionModel.select(...children.map(child => this._getExpansionKey(child)));
673
+ });
674
+ }
675
+ }
676
+ /** Collapse the data node and all its descendants. If it is already collapsed, does nothing. */
677
+ collapseDescendants(dataNode) {
678
+ if (this.treeControl) {
679
+ this.treeControl.collapseDescendants(dataNode);
680
+ }
681
+ else if (this._expansionModel) {
682
+ const expansionModel = this._expansionModel;
683
+ expansionModel.deselect(this._getExpansionKey(dataNode));
684
+ this._getDescendants(dataNode)
685
+ .pipe(take(1), takeUntil(this._onDestroy))
686
+ .subscribe(children => {
687
+ expansionModel.deselect(...children.map(child => this._getExpansionKey(child)));
688
+ });
689
+ }
690
+ }
691
+ /** Expands all data nodes in the tree. */
692
+ expandAll() {
693
+ if (this.treeControl) {
694
+ this.treeControl.expandAll();
695
+ }
696
+ else if (this._expansionModel) {
697
+ const expansionModel = this._expansionModel;
698
+ expansionModel.select(...this._flattenedNodes.value.map(child => this._getExpansionKey(child)));
699
+ }
700
+ }
701
+ /** Collapse all data nodes in the tree. */
702
+ collapseAll() {
703
+ if (this.treeControl) {
704
+ this.treeControl.collapseAll();
705
+ }
706
+ else if (this._expansionModel) {
707
+ const expansionModel = this._expansionModel;
708
+ expansionModel.deselect(...this._flattenedNodes.value.map(child => this._getExpansionKey(child)));
709
+ }
710
+ }
711
+ /** Level accessor, used for compatibility between the old Tree and new Tree */
712
+ _getLevelAccessor() {
713
+ return this.treeControl?.getLevel?.bind(this.treeControl) ?? this.levelAccessor;
714
+ }
715
+ /** Children accessor, used for compatibility between the old Tree and new Tree */
716
+ _getChildrenAccessor() {
717
+ return this.treeControl?.getChildren?.bind(this.treeControl) ?? this.childrenAccessor;
718
+ }
719
+ /**
720
+ * Gets the direct children of a node; used for compatibility between the old tree and the
721
+ * new tree.
722
+ */
723
+ _getDirectChildren(dataNode) {
724
+ const levelAccessor = this._getLevelAccessor();
725
+ const expansionModel = this._expansionModel ?? this.treeControl?.expansionModel;
726
+ if (!expansionModel) {
727
+ return of([]);
728
+ }
729
+ const key = this._getExpansionKey(dataNode);
730
+ const isExpanded = expansionModel.changed.pipe(switchMap(changes => {
731
+ if (changes.added.includes(key)) {
732
+ return of(true);
733
+ }
734
+ else if (changes.removed.includes(key)) {
735
+ return of(false);
736
+ }
737
+ return EMPTY;
738
+ }), startWith(this.isExpanded(dataNode)));
739
+ if (levelAccessor) {
740
+ return combineLatest([isExpanded, this._flattenedNodes]).pipe(map(([expanded, flattenedNodes]) => {
741
+ if (!expanded) {
742
+ return [];
743
+ }
744
+ return this._findChildrenByLevel(levelAccessor, flattenedNodes, dataNode, 1);
745
+ }));
746
+ }
747
+ const childrenAccessor = this._getChildrenAccessor();
748
+ if (childrenAccessor) {
749
+ return coerceObservable(childrenAccessor(dataNode) ?? []);
750
+ }
751
+ throw getTreeControlMissingError();
752
+ }
753
+ /**
754
+ * Given the list of flattened nodes, the level accessor, and the level range within
755
+ * which to consider children, finds the children for a given node.
756
+ *
757
+ * For example, for direct children, `levelDelta` would be 1. For all descendants,
758
+ * `levelDelta` would be Infinity.
759
+ */
760
+ _findChildrenByLevel(levelAccessor, flattenedNodes, dataNode, levelDelta) {
761
+ const key = this._getExpansionKey(dataNode);
762
+ const startIndex = flattenedNodes.findIndex(node => this._getExpansionKey(node) === key);
763
+ const dataNodeLevel = levelAccessor(dataNode);
764
+ const expectedLevel = dataNodeLevel + levelDelta;
765
+ const results = [];
766
+ // Goes through flattened tree nodes in the `flattenedNodes` array, and get all
767
+ // descendants within a certain level range.
768
+ //
769
+ // If we reach a node whose level is equal to or less than the level of the tree node,
770
+ // we hit a sibling or parent's sibling, and should stop.
771
+ for (let i = startIndex + 1; i < flattenedNodes.length; i++) {
772
+ const currentLevel = levelAccessor(flattenedNodes[i]);
773
+ if (currentLevel <= dataNodeLevel) {
774
+ break;
775
+ }
776
+ if (currentLevel <= expectedLevel) {
777
+ results.push(flattenedNodes[i]);
778
+ }
779
+ }
780
+ return results;
781
+ }
782
+ /**
783
+ * Adds the specified node component to the tree's internal registry.
784
+ *
785
+ * This primarily facilitates keyboard navigation.
786
+ */
787
+ _registerNode(node) {
788
+ this._nodes.value.set(this._getExpansionKey(node.data), node);
789
+ this._nodes.next(this._nodes.value);
790
+ }
791
+ /** Removes the specified node component from the tree's internal registry. */
792
+ _unregisterNode(node) {
793
+ this._nodes.value.delete(this._getExpansionKey(node.data));
794
+ this._nodes.next(this._nodes.value);
795
+ }
796
+ /**
797
+ * For the given node, determine the level where this node appears in the tree.
798
+ *
799
+ * This is intended to be used for `aria-level` but is 0-indexed.
800
+ */
801
+ _getLevel(node) {
802
+ return this._levels.get(this._getExpansionKey(node));
803
+ }
804
+ /**
805
+ * For the given node, determine the size of the parent's child set.
806
+ *
807
+ * This is intended to be used for `aria-setsize`.
808
+ */
809
+ _getSetSize(dataNode) {
810
+ const set = this._getAriaSet(dataNode);
811
+ return set.length;
812
+ }
813
+ /**
814
+ * For the given node, determine the index (starting from 1) of the node in its parent's child set.
815
+ *
816
+ * This is intended to be used for `aria-posinset`.
817
+ */
818
+ _getPositionInSet(dataNode) {
819
+ const set = this._getAriaSet(dataNode);
820
+ const key = this._getExpansionKey(dataNode);
821
+ return set.findIndex(node => this._getExpansionKey(node) === key) + 1;
822
+ }
823
+ /** Given a CdkTreeNode, gets the node that renders that node's parent's data. */
824
+ _getNodeParent(node) {
825
+ const parent = this._parents.get(this._getExpansionKey(node.data));
826
+ return parent && this._nodes.value.get(this._getExpansionKey(parent));
827
+ }
828
+ /** Given a CdkTreeNode, gets the nodes that renders that node's child data. */
829
+ _getNodeChildren(node) {
830
+ return this._getDirectChildren(node.data).pipe(map(children => children.reduce((nodes, child) => {
831
+ const value = this._nodes.value.get(this._getExpansionKey(child));
832
+ if (value) {
833
+ nodes.push(value);
834
+ }
835
+ return nodes;
836
+ }, [])));
837
+ }
838
+ /** `keydown` event handler; this just passes the event to the `TreeKeyManager`. */
839
+ _sendKeydownToKeyManager(event) {
840
+ this._keyManager.onKeydown(event);
841
+ }
842
+ /** Gets all nested descendants of a given node. */
843
+ _getDescendants(dataNode) {
844
+ if (this.treeControl) {
845
+ return of(this.treeControl.getDescendants(dataNode));
846
+ }
847
+ if (this.levelAccessor) {
848
+ const results = this._findChildrenByLevel(this.levelAccessor, this._flattenedNodes.value, dataNode, Infinity);
849
+ return of(results);
850
+ }
851
+ if (this.childrenAccessor) {
852
+ return this._getAllChildrenRecursively(dataNode).pipe(reduce((allChildren, nextChildren) => {
853
+ allChildren.push(...nextChildren);
854
+ return allChildren;
855
+ }, []));
856
+ }
857
+ throw getTreeControlMissingError();
858
+ }
859
+ /**
860
+ * Gets all children and sub-children of the provided node.
861
+ *
862
+ * This will emit multiple times, in the order that the children will appear
863
+ * in the tree, and can be combined with a `reduce` operator.
864
+ */
865
+ _getAllChildrenRecursively(dataNode) {
866
+ if (!this.childrenAccessor) {
867
+ return of([]);
868
+ }
869
+ return coerceObservable(this.childrenAccessor(dataNode)).pipe(take(1), switchMap(children => {
870
+ // Here, we cache the parents of a particular child so that we can compute the levels.
871
+ for (const child of children) {
872
+ this._parents.set(this._getExpansionKey(child), dataNode);
873
+ }
874
+ return of(...children).pipe(concatMap(child => concat(of([child]), this._getAllChildrenRecursively(child))));
875
+ }));
876
+ }
877
+ _getExpansionKey(dataNode) {
878
+ // In the case that a key accessor function was not provided by the
879
+ // tree user, we'll default to using the node object itself as the key.
880
+ //
881
+ // This cast is safe since:
882
+ // - if an expansionKey is provided, TS will infer the type of K to be
883
+ // the return type.
884
+ // - if it's not, then K will be defaulted to T.
885
+ return this.expansionKey?.(dataNode) ?? dataNode;
886
+ }
887
+ _getAriaSet(node) {
888
+ const key = this._getExpansionKey(node);
889
+ const parent = this._parents.get(key);
890
+ const parentKey = parent ? this._getExpansionKey(parent) : null;
891
+ const set = this._ariaSets.get(parentKey);
892
+ return set ?? [node];
893
+ }
894
+ /**
895
+ * Finds the parent for the given node. If this is a root node, this
896
+ * returns null. If we're unable to determine the parent, for example,
897
+ * if we don't have cached node data, this returns undefined.
898
+ */
899
+ _findParentForNode(node, index, cachedNodes) {
900
+ // In all cases, we have a mapping from node to level; all we need to do here is backtrack in
901
+ // our flattened list of nodes to determine the first node that's of a level lower than the
902
+ // provided node.
903
+ if (!cachedNodes.length) {
904
+ return null;
905
+ }
906
+ const currentLevel = this._levels.get(this._getExpansionKey(node)) ?? 0;
907
+ for (let parentIndex = index - 1; parentIndex >= 0; parentIndex--) {
908
+ const parentNode = cachedNodes[parentIndex];
909
+ const parentLevel = this._levels.get(this._getExpansionKey(parentNode)) ?? 0;
910
+ if (parentLevel < currentLevel) {
911
+ return parentNode;
912
+ }
913
+ }
914
+ return null;
915
+ }
916
+ /**
917
+ * Given a set of root nodes and the current node level, flattens any nested
918
+ * nodes into a single array.
919
+ *
920
+ * If any nodes are not expanded, then their children will not be added into the array.
921
+ * This will still traverse all nested children in order to build up our internal data
922
+ * models, but will not include them in the returned array.
923
+ */
924
+ _flattenNestedNodesWithExpansion(nodes, level = 0) {
925
+ const childrenAccessor = this._getChildrenAccessor();
926
+ // If we're using a level accessor, we don't need to flatten anything.
927
+ if (!childrenAccessor) {
928
+ return of([...nodes]);
929
+ }
930
+ return of(...nodes).pipe(concatMap(node => {
931
+ const parentKey = this._getExpansionKey(node);
932
+ if (!this._parents.has(parentKey)) {
933
+ this._parents.set(parentKey, null);
934
+ }
935
+ this._levels.set(parentKey, level);
936
+ const children = coerceObservable(childrenAccessor(node));
937
+ return concat(of([node]), children.pipe(take(1), tap(childNodes => {
938
+ this._ariaSets.set(parentKey, [...(childNodes ?? [])]);
939
+ for (const child of childNodes ?? []) {
940
+ const childKey = this._getExpansionKey(child);
941
+ this._parents.set(childKey, node);
942
+ this._levels.set(childKey, level + 1);
943
+ }
944
+ }), switchMap(childNodes => {
945
+ if (!childNodes) {
946
+ return of([]);
947
+ }
948
+ return this._flattenNestedNodesWithExpansion(childNodes, level + 1).pipe(map(nestedNodes => (this.isExpanded(node) ? nestedNodes : [])));
949
+ })));
950
+ }), reduce((results, children) => {
951
+ results.push(...children);
952
+ return results;
953
+ }, []));
954
+ }
955
+ /**
956
+ * Converts children for certain tree configurations.
957
+ *
958
+ * This also computes parent, level, and group data.
959
+ */
960
+ _computeRenderingData(nodes, nodeType) {
961
+ // The only situations where we have to convert children types is when
962
+ // they're mismatched; i.e. if the tree is using a childrenAccessor and the
963
+ // nodes are flat, or if the tree is using a levelAccessor and the nodes are
964
+ // nested.
965
+ if (this.childrenAccessor && nodeType === 'flat') {
966
+ // This flattens children into a single array.
967
+ this._ariaSets.set(null, [...nodes]);
968
+ return this._flattenNestedNodesWithExpansion(nodes).pipe(map(flattenedNodes => ({
969
+ renderNodes: flattenedNodes,
970
+ flattenedNodes,
971
+ })));
972
+ }
973
+ else if (this.levelAccessor && nodeType === 'nested') {
974
+ // In the nested case, we only look for root nodes. The CdkNestedNode
975
+ // itself will handle rendering each individual node's children.
976
+ const levelAccessor = this.levelAccessor;
977
+ return of(nodes.filter(node => levelAccessor(node) === 0)).pipe(map(rootNodes => ({
978
+ renderNodes: rootNodes,
979
+ flattenedNodes: nodes,
980
+ })), tap(({ flattenedNodes }) => {
981
+ this._calculateParents(flattenedNodes);
982
+ }));
983
+ }
984
+ else if (nodeType === 'flat') {
985
+ // In the case of a TreeControl, we know that the node type matches up
986
+ // with the TreeControl, and so no conversions are necessary. Otherwise,
987
+ // we've already confirmed that the data model matches up with the
988
+ // desired node type here.
989
+ return of({ renderNodes: nodes, flattenedNodes: nodes }).pipe(tap(({ flattenedNodes }) => {
990
+ this._calculateParents(flattenedNodes);
991
+ }));
992
+ }
993
+ else {
994
+ // For nested nodes, we still need to perform the node flattening in order
995
+ // to maintain our caches for various tree operations.
996
+ this._ariaSets.set(null, [...nodes]);
997
+ return this._flattenNestedNodesWithExpansion(nodes).pipe(map(flattenedNodes => ({
998
+ renderNodes: nodes,
999
+ flattenedNodes,
1000
+ })));
1001
+ }
1002
+ }
1003
+ _updateCachedData(flattenedNodes) {
1004
+ this._flattenedNodes.next(flattenedNodes);
1005
+ }
1006
+ _updateKeyManagerItems(flattenedNodes) {
1007
+ this._keyManagerNodes.next(flattenedNodes);
1008
+ }
1009
+ /** Traverse the flattened node data and compute parents, levels, and group data. */
1010
+ _calculateParents(flattenedNodes) {
1011
+ const levelAccessor = this._getLevelAccessor();
1012
+ if (!levelAccessor) {
1013
+ return;
1014
+ }
1015
+ this._parents.clear();
1016
+ this._ariaSets.clear();
1017
+ for (let index = 0; index < flattenedNodes.length; index++) {
1018
+ const dataNode = flattenedNodes[index];
1019
+ const key = this._getExpansionKey(dataNode);
1020
+ this._levels.set(key, levelAccessor(dataNode));
1021
+ const parent = this._findParentForNode(dataNode, index, flattenedNodes);
1022
+ this._parents.set(key, parent);
1023
+ const parentKey = parent ? this._getExpansionKey(parent) : null;
1024
+ const group = this._ariaSets.get(parentKey) ?? [];
1025
+ group.splice(index, 0, dataNode);
1026
+ this._ariaSets.set(parentKey, group);
1027
+ }
1028
+ }
1029
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.0", ngImport: i0, type: CdkTree, deps: [{ token: i0.IterableDiffers }, { token: i0.ChangeDetectorRef }, { token: i1.Directionality }], target: i0.ɵɵFactoryTarget.Component }); }
1030
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.1.0", type: CdkTree, isStandalone: true, selector: "cdk-tree", inputs: { dataSource: "dataSource", treeControl: "treeControl", levelAccessor: "levelAccessor", childrenAccessor: "childrenAccessor", trackBy: "trackBy", expansionKey: "expansionKey" }, host: { attributes: { "role": "tree" }, listeners: { "keydown": "_sendKeydownToKeyManager($event)" }, classAttribute: "cdk-tree" }, queries: [{ propertyName: "_nodeDefs", predicate: CdkTreeNodeDef, descendants: true }], viewQueries: [{ propertyName: "_nodeOutlet", first: true, predicate: CdkTreeNodeOutlet, descendants: true, static: true }], exportAs: ["cdkTree"], ngImport: i0, template: `<ng-container cdkTreeNodeOutlet></ng-container>`, isInline: true, dependencies: [{ kind: "directive", type: CdkTreeNodeOutlet, selector: "[cdkTreeNodeOutlet]" }], changeDetection: i0.ChangeDetectionStrategy.Default, encapsulation: i0.ViewEncapsulation.None }); }
423
1031
  }
424
1032
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.0", ngImport: i0, type: CdkTree, decorators: [{
425
1033
  type: Component,
@@ -430,6 +1038,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.0", ngImpor
430
1038
  host: {
431
1039
  'class': 'cdk-tree',
432
1040
  'role': 'tree',
1041
+ '(keydown)': '_sendKeydownToKeyManager($event)',
433
1042
  },
434
1043
  encapsulation: ViewEncapsulation.None,
435
1044
  // The "OnPush" status for the `CdkTree` component is effectively a noop, so we are removing it.
@@ -440,12 +1049,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.0", ngImpor
440
1049
  standalone: true,
441
1050
  imports: [CdkTreeNodeOutlet],
442
1051
  }]
443
- }], ctorParameters: () => [{ type: i0.IterableDiffers }, { type: i0.ChangeDetectorRef }], propDecorators: { dataSource: [{
1052
+ }], ctorParameters: () => [{ type: i0.IterableDiffers }, { type: i0.ChangeDetectorRef }, { type: i1.Directionality }], propDecorators: { dataSource: [{
444
1053
  type: Input
445
1054
  }], treeControl: [{
446
1055
  type: Input
1056
+ }], levelAccessor: [{
1057
+ type: Input
1058
+ }], childrenAccessor: [{
1059
+ type: Input
447
1060
  }], trackBy: [{
448
1061
  type: Input
1062
+ }], expansionKey: [{
1063
+ type: Input
449
1064
  }], _nodeOutlet: [{
450
1065
  type: ViewChild,
451
1066
  args: [CdkTreeNodeOutlet, { static: true }]
@@ -463,16 +1078,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.0", ngImpor
463
1078
  class CdkTreeNode {
464
1079
  /**
465
1080
  * The role of the tree node.
466
- * @deprecated The correct role is 'treeitem', 'group' should not be used. This input will be
467
- * removed in a future version.
468
- * @breaking-change 12.0.0 Remove this input
1081
+ *
1082
+ * @deprecated This will be ignored; the tree will automatically determine the appropriate role
1083
+ * for tree node. This input will be removed in a future version.
1084
+ * @breaking-change 21.0.0
469
1085
  */
470
1086
  get role() {
471
1087
  return 'treeitem';
472
1088
  }
473
1089
  set role(_role) {
474
- // TODO: move to host after View Engine deprecation
475
- this._elementRef.nativeElement.setAttribute('role', _role);
1090
+ // ignore any role setting, we handle this internally.
1091
+ }
1092
+ /**
1093
+ * Whether or not this node is expandable.
1094
+ *
1095
+ * If not using `FlatTreeControl`, or if `isExpandable` is not provided to
1096
+ * `NestedTreeControl`, this should be provided for correct node a11y.
1097
+ */
1098
+ get isExpandable() {
1099
+ return this._isExpandable();
1100
+ }
1101
+ set isExpandable(isExpandable) {
1102
+ this._inputIsExpandable = isExpandable;
1103
+ }
1104
+ get isExpanded() {
1105
+ return this._tree.isExpanded(this._data);
1106
+ }
1107
+ set isExpanded(isExpanded) {
1108
+ if (isExpanded) {
1109
+ this.expand();
1110
+ }
1111
+ else {
1112
+ this.collapse();
1113
+ }
1114
+ }
1115
+ getLabel() {
1116
+ return this.typeaheadLabel || this._elementRef.nativeElement.textContent?.trim() || '';
476
1117
  }
477
1118
  /**
478
1119
  * The most recently created `CdkTreeNode`. We save it in static variable so we can retrieve it
@@ -486,40 +1127,101 @@ class CdkTreeNode {
486
1127
  set data(value) {
487
1128
  if (value !== this._data) {
488
1129
  this._data = value;
489
- this._setRoleFromData();
490
1130
  this._dataChanges.next();
491
1131
  }
492
1132
  }
493
- get isExpanded() {
494
- return this._tree.treeControl.isExpanded(this._data);
1133
+ /* If leaf node, return true to not assign aria-expanded attribute */
1134
+ get isLeafNode() {
1135
+ // If flat tree node data returns false for expandable property, it's a leaf node
1136
+ if (this._tree.treeControl?.isExpandable !== undefined &&
1137
+ !this._tree.treeControl.isExpandable(this._data)) {
1138
+ return true;
1139
+ // If nested tree node data returns 0 descendants, it's a leaf node
1140
+ }
1141
+ else if (this._tree.treeControl?.isExpandable === undefined &&
1142
+ this._tree.treeControl?.getDescendants(this._data).length === 0) {
1143
+ return true;
1144
+ }
1145
+ return false;
495
1146
  }
496
1147
  get level() {
497
- // If the treeControl has a getLevel method, use it to get the level. Otherwise read the
1148
+ // If the tree has a levelAccessor, use it to get the level. Otherwise read the
498
1149
  // aria-level off the parent node and use it as the level for this node (note aria-level is
499
1150
  // 1-indexed, while this property is 0-indexed, so we don't need to increment).
500
- return this._tree.treeControl.getLevel
501
- ? this._tree.treeControl.getLevel(this._data)
502
- : this._parentNodeAriaLevel;
1151
+ return this._tree._getLevel(this._data) ?? this._parentNodeAriaLevel;
1152
+ }
1153
+ /** Determines if the tree node is expandable. */
1154
+ _isExpandable() {
1155
+ if (this._tree.treeControl) {
1156
+ if (this.isLeafNode) {
1157
+ return false;
1158
+ }
1159
+ // For compatibility with trees created using TreeControl before we added
1160
+ // CdkTreeNode#isExpandable.
1161
+ return true;
1162
+ }
1163
+ return this._inputIsExpandable;
1164
+ }
1165
+ /**
1166
+ * Determines the value for `aria-expanded`.
1167
+ *
1168
+ * For non-expandable nodes, this is `null`.
1169
+ */
1170
+ _getAriaExpanded() {
1171
+ if (!this._isExpandable()) {
1172
+ return null;
1173
+ }
1174
+ return String(this.isExpanded);
1175
+ }
1176
+ /**
1177
+ * Determines the size of this node's parent's child set.
1178
+ *
1179
+ * This is intended to be used for `aria-setsize`.
1180
+ */
1181
+ _getSetSize() {
1182
+ return this._tree._getSetSize(this._data);
1183
+ }
1184
+ /**
1185
+ * Determines the index (starting from 1) of this node in its parent's child set.
1186
+ *
1187
+ * This is intended to be used for `aria-posinset`.
1188
+ */
1189
+ _getPositionInSet() {
1190
+ return this._tree._getPositionInSet(this._data);
503
1191
  }
504
1192
  constructor(_elementRef, _tree) {
505
1193
  this._elementRef = _elementRef;
506
1194
  this._tree = _tree;
1195
+ this._tabindex = -1;
1196
+ /** This emits when the node has been programatically activated or activated by keyboard. */
1197
+ this.activation = new EventEmitter();
1198
+ /** This emits when the node's expansion status has been changed. */
1199
+ this.expandedChange = new EventEmitter();
507
1200
  /** Subject that emits when the component has been destroyed. */
508
1201
  this._destroyed = new Subject();
509
1202
  /** Emits when the node's data has changed. */
510
1203
  this._dataChanges = new Subject();
1204
+ this._inputIsExpandable = false;
1205
+ /**
1206
+ * Flag used to determine whether or not we should be focusing the actual element based on
1207
+ * some user interaction (click or focus). On click, we don't forcibly focus the element
1208
+ * since the click could trigger some other component that wants to grab its own focus
1209
+ * (e.g. menu, dialog).
1210
+ */
1211
+ this._shouldFocus = true;
511
1212
  this._changeDetectorRef = inject(ChangeDetectorRef);
512
1213
  CdkTreeNode.mostRecentTreeNode = this;
513
- this.role = 'treeitem';
514
1214
  }
515
1215
  ngOnInit() {
516
1216
  this._parentNodeAriaLevel = getParentNodeAriaLevel(this._elementRef.nativeElement);
517
- this._elementRef.nativeElement.setAttribute('aria-level', `${this.level + 1}`);
518
- this._tree.treeControl.expansionModel.changed
519
- .pipe(map(() => this.isExpanded), distinctUntilChanged())
1217
+ this._tree
1218
+ ._getExpansionModel()
1219
+ .changed.pipe(map(() => this.isExpanded), distinctUntilChanged())
520
1220
  .subscribe(() => {
521
1221
  this._changeDetectorRef.markForCheck();
522
1222
  });
1223
+ this._tree._setNodeTypeIfUnset('flat');
1224
+ this._tree._registerNode(this);
523
1225
  }
524
1226
  ngOnDestroy() {
525
1227
  // If this is the last tree node being destroyed,
@@ -531,21 +1233,63 @@ class CdkTreeNode {
531
1233
  this._destroyed.next();
532
1234
  this._destroyed.complete();
533
1235
  }
534
- /** Focuses the menu item. Implements for FocusableOption. */
1236
+ getParent() {
1237
+ return this._tree._getNodeParent(this) ?? null;
1238
+ }
1239
+ getChildren() {
1240
+ return this._tree._getNodeChildren(this);
1241
+ }
1242
+ /** Focuses this data node. Implemented for TreeKeyManagerItem. */
535
1243
  focus() {
536
- this._elementRef.nativeElement.focus();
1244
+ this._tabindex = 0;
1245
+ if (this._shouldFocus) {
1246
+ this._elementRef.nativeElement.focus();
1247
+ }
1248
+ this._changeDetectorRef.markForCheck();
537
1249
  }
538
- // TODO: role should eventually just be set in the component host
539
- _setRoleFromData() {
540
- if (!this._tree.treeControl.isExpandable &&
541
- !this._tree.treeControl.getChildren &&
542
- (typeof ngDevMode === 'undefined' || ngDevMode)) {
543
- throw getTreeControlFunctionsMissingError();
1250
+ /** Defocus this data node. */
1251
+ unfocus() {
1252
+ this._tabindex = -1;
1253
+ this._changeDetectorRef.markForCheck();
1254
+ }
1255
+ /** Emits an activation event. Implemented for TreeKeyManagerItem. */
1256
+ activate() {
1257
+ if (this.isDisabled) {
1258
+ return;
1259
+ }
1260
+ this.activation.next(this._data);
1261
+ }
1262
+ /** Collapses this data node. Implemented for TreeKeyManagerItem. */
1263
+ collapse() {
1264
+ if (this.isExpandable) {
1265
+ this._tree.collapse(this._data);
1266
+ }
1267
+ }
1268
+ /** Expands this data node. Implemented for TreeKeyManagerItem. */
1269
+ expand() {
1270
+ if (this.isExpandable) {
1271
+ this._tree.expand(this._data);
1272
+ }
1273
+ }
1274
+ _focusItem() {
1275
+ if (this.isDisabled) {
1276
+ return;
544
1277
  }
545
- this.role = 'treeitem';
1278
+ this._tree._keyManager.focusItem(this);
1279
+ }
1280
+ _setActiveItem() {
1281
+ if (this.isDisabled) {
1282
+ return;
1283
+ }
1284
+ this._shouldFocus = false;
1285
+ this._tree._keyManager.focusItem(this);
1286
+ this._shouldFocus = true;
1287
+ }
1288
+ _emitExpansionState(expanded) {
1289
+ this.expandedChange.emit(expanded);
546
1290
  }
547
1291
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.0", ngImport: i0, type: CdkTreeNode, deps: [{ token: i0.ElementRef }, { token: CdkTree }], target: i0.ɵɵFactoryTarget.Directive }); }
548
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.1.0", type: CdkTreeNode, isStandalone: true, selector: "cdk-tree-node", inputs: { role: "role" }, host: { properties: { "attr.aria-expanded": "isExpanded" }, classAttribute: "cdk-tree-node" }, exportAs: ["cdkTreeNode"], ngImport: i0 }); }
1292
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "18.1.0", type: CdkTreeNode, isStandalone: true, selector: "cdk-tree-node", inputs: { role: "role", isExpandable: ["isExpandable", "isExpandable", booleanAttribute], isExpanded: "isExpanded", isDisabled: ["isDisabled", "isDisabled", booleanAttribute], typeaheadLabel: ["cdkTreeNodeTypeaheadLabel", "typeaheadLabel"] }, outputs: { activation: "activation", expandedChange: "expandedChange" }, host: { attributes: { "role": "treeitem" }, listeners: { "click": "_setActiveItem()", "focus": "_focusItem()" }, properties: { "attr.aria-expanded": "_getAriaExpanded()", "attr.aria-level": "level + 1", "attr.aria-posinset": "_getPositionInSet()", "attr.aria-setsize": "_getSetSize()", "tabindex": "_tabindex" }, classAttribute: "cdk-tree-node" }, exportAs: ["cdkTreeNode"], ngImport: i0 }); }
549
1293
  }
550
1294
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.0", ngImport: i0, type: CdkTreeNode, decorators: [{
551
1295
  type: Directive,
@@ -554,12 +1298,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.0", ngImpor
554
1298
  exportAs: 'cdkTreeNode',
555
1299
  host: {
556
1300
  'class': 'cdk-tree-node',
557
- '[attr.aria-expanded]': 'isExpanded',
1301
+ '[attr.aria-expanded]': '_getAriaExpanded()',
1302
+ '[attr.aria-level]': 'level + 1',
1303
+ '[attr.aria-posinset]': '_getPositionInSet()',
1304
+ '[attr.aria-setsize]': '_getSetSize()',
1305
+ '[tabindex]': '_tabindex',
1306
+ 'role': 'treeitem',
1307
+ '(click)': '_setActiveItem()',
1308
+ '(focus)': '_focusItem()',
558
1309
  },
559
1310
  standalone: true,
560
1311
  }]
561
1312
  }], ctorParameters: () => [{ type: i0.ElementRef }, { type: CdkTree }], propDecorators: { role: [{
562
1313
  type: Input
1314
+ }], isExpandable: [{
1315
+ type: Input,
1316
+ args: [{ transform: booleanAttribute }]
1317
+ }], isExpanded: [{
1318
+ type: Input
1319
+ }], isDisabled: [{
1320
+ type: Input,
1321
+ args: [{ transform: booleanAttribute }]
1322
+ }], typeaheadLabel: [{
1323
+ type: Input,
1324
+ args: ['cdkTreeNodeTypeaheadLabel']
1325
+ }], activation: [{
1326
+ type: Output
1327
+ }], expandedChange: [{
1328
+ type: Output
563
1329
  }] } });
564
1330
  function getParentNodeAriaLevel(nodeElement) {
565
1331
  let parent = nodeElement.parentElement;
@@ -600,18 +1366,10 @@ class CdkNestedTreeNode extends CdkTreeNode {
600
1366
  }
601
1367
  ngAfterContentInit() {
602
1368
  this._dataDiffer = this._differs.find([]).create(this._tree.trackBy);
603
- if (!this._tree.treeControl.getChildren && (typeof ngDevMode === 'undefined' || ngDevMode)) {
604
- throw getTreeControlFunctionsMissingError();
605
- }
606
- const childrenNodes = this._tree.treeControl.getChildren(this.data);
607
- if (Array.isArray(childrenNodes)) {
608
- this.updateChildrenNodes(childrenNodes);
609
- }
610
- else if (isObservable(childrenNodes)) {
611
- childrenNodes
612
- .pipe(takeUntil(this._destroyed))
613
- .subscribe(result => this.updateChildrenNodes(result));
614
- }
1369
+ this._tree
1370
+ ._getDirectChildren(this.data)
1371
+ .pipe(takeUntil(this._destroyed))
1372
+ .subscribe(result => this.updateChildrenNodes(result));
615
1373
  this.nodeOutlet.changes
616
1374
  .pipe(takeUntil(this._destroyed))
617
1375
  .subscribe(() => this.updateChildrenNodes());
@@ -619,6 +1377,7 @@ class CdkNestedTreeNode extends CdkTreeNode {
619
1377
  // This is a workaround for https://github.com/angular/angular/issues/23091
620
1378
  // In aot mode, the lifecycle hooks from parent class are not called.
621
1379
  ngOnInit() {
1380
+ this._tree._setNodeTypeIfUnset('nested');
622
1381
  super.ngOnInit();
623
1382
  }
624
1383
  ngOnDestroy() {
@@ -733,9 +1492,7 @@ class CdkTreeNodePadding {
733
1492
  }
734
1493
  /** The padding indent value for the tree node. Returns a string with px numbers if not null. */
735
1494
  _paddingIndent() {
736
- const nodeLevel = this._treeNode.data && this._tree.treeControl.getLevel
737
- ? this._tree.treeControl.getLevel(this._treeNode.data)
738
- : null;
1495
+ const nodeLevel = (this._treeNode.data && this._tree._getLevel(this._treeNode.data)) ?? null;
739
1496
  const level = this._level == null ? nodeLevel : this._level;
740
1497
  return typeof level === 'number' ? `${level * this._indent}${this.indentUnits}` : null;
741
1498
  }
@@ -781,7 +1538,7 @@ class CdkTreeNodePadding {
781
1538
  this._indent = numberAttribute(value);
782
1539
  this._setPadding();
783
1540
  }
784
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.0", ngImport: i0, type: CdkTreeNodePadding, deps: [{ token: CdkTreeNode }, { token: CdkTree }, { token: i0.ElementRef }, { token: i2.Directionality, optional: true }], target: i0.ɵɵFactoryTarget.Directive }); }
1541
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.0", ngImport: i0, type: CdkTreeNodePadding, deps: [{ token: CdkTreeNode }, { token: CdkTree }, { token: i0.ElementRef }, { token: i1.Directionality, optional: true }], target: i0.ɵɵFactoryTarget.Directive }); }
785
1542
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "18.1.0", type: CdkTreeNodePadding, isStandalone: true, selector: "[cdkTreeNodePadding]", inputs: { level: ["cdkTreeNodePadding", "level", numberAttribute], indent: ["cdkTreeNodePaddingIndent", "indent"] }, ngImport: i0 }); }
786
1543
  }
787
1544
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.0", ngImport: i0, type: CdkTreeNodePadding, decorators: [{
@@ -790,7 +1547,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.0", ngImpor
790
1547
  selector: '[cdkTreeNodePadding]',
791
1548
  standalone: true,
792
1549
  }]
793
- }], ctorParameters: () => [{ type: CdkTreeNode }, { type: CdkTree }, { type: i0.ElementRef }, { type: i2.Directionality, decorators: [{
1550
+ }], ctorParameters: () => [{ type: CdkTreeNode }, { type: CdkTree }, { type: i0.ElementRef }, { type: i1.Directionality, decorators: [{
794
1551
  type: Optional
795
1552
  }] }], propDecorators: { level: [{
796
1553
  type: Input,
@@ -801,7 +1558,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.0", ngImpor
801
1558
  }] } });
802
1559
 
803
1560
  /**
804
- * Node toggle to expand/collapse the node.
1561
+ * Node toggle to expand and collapse the node.
805
1562
  */
806
1563
  class CdkTreeNodeToggle {
807
1564
  constructor(_tree, _treeNode) {
@@ -810,21 +1567,28 @@ class CdkTreeNodeToggle {
810
1567
  /** Whether expand/collapse the node recursively. */
811
1568
  this.recursive = false;
812
1569
  }
813
- _toggle(event) {
1570
+ // Toggle the expanded or collapsed state of this node.
1571
+ //
1572
+ // Focus this node with expanding or collapsing it. This ensures that the active node will always
1573
+ // be visible when expanding and collapsing.
1574
+ _toggle() {
814
1575
  this.recursive
815
- ? this._tree.treeControl.toggleDescendants(this._treeNode.data)
816
- : this._tree.treeControl.toggle(this._treeNode.data);
817
- event.stopPropagation();
1576
+ ? this._tree.toggleDescendants(this._treeNode.data)
1577
+ : this._tree.toggle(this._treeNode.data);
1578
+ this._tree._keyManager.focusItem(this._treeNode);
818
1579
  }
819
1580
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.0", ngImport: i0, type: CdkTreeNodeToggle, deps: [{ token: CdkTree }, { token: CdkTreeNode }], target: i0.ɵɵFactoryTarget.Directive }); }
820
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "18.1.0", type: CdkTreeNodeToggle, isStandalone: true, selector: "[cdkTreeNodeToggle]", inputs: { recursive: ["cdkTreeNodeToggleRecursive", "recursive", booleanAttribute] }, host: { listeners: { "click": "_toggle($event)" } }, ngImport: i0 }); }
1581
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "18.1.0", type: CdkTreeNodeToggle, isStandalone: true, selector: "[cdkTreeNodeToggle]", inputs: { recursive: ["cdkTreeNodeToggleRecursive", "recursive", booleanAttribute] }, host: { attributes: { "tabindex": "-1" }, listeners: { "click": "_toggle(); $event.stopPropagation();", "keydown.Enter": "_toggle(); $event.preventDefault();", "keydown.Space": "_toggle(); $event.preventDefault();" } }, ngImport: i0 }); }
821
1582
  }
822
1583
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.0", ngImport: i0, type: CdkTreeNodeToggle, decorators: [{
823
1584
  type: Directive,
824
1585
  args: [{
825
1586
  selector: '[cdkTreeNodeToggle]',
826
1587
  host: {
827
- '(click)': '_toggle($event)',
1588
+ '(click)': '_toggle(); $event.stopPropagation();',
1589
+ '(keydown.Enter)': '_toggle(); $event.preventDefault();',
1590
+ '(keydown.Space)': '_toggle(); $event.preventDefault();',
1591
+ 'tabindex': '-1',
828
1592
  },
829
1593
  standalone: true,
830
1594
  }]
@@ -871,5 +1635,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.0", ngImpor
871
1635
  * Generated bundle index. Do not edit.
872
1636
  */
873
1637
 
874
- export { BaseTreeControl, CDK_TREE_NODE_OUTLET_NODE, CdkNestedTreeNode, CdkTree, CdkTreeModule, CdkTreeNode, CdkTreeNodeDef, CdkTreeNodeOutlet, CdkTreeNodeOutletContext, CdkTreeNodePadding, CdkTreeNodeToggle, FlatTreeControl, NestedTreeControl, getTreeControlFunctionsMissingError, getTreeControlMissingError, getTreeMissingMatchingNodeDefError, getTreeMultipleDefaultNodeDefsError, getTreeNoValidDataSourceError };
1638
+ export { BaseTreeControl, CDK_TREE_NODE_OUTLET_NODE, CdkNestedTreeNode, CdkTree, CdkTreeModule, CdkTreeNode, CdkTreeNodeDef, CdkTreeNodeOutlet, CdkTreeNodeOutletContext, CdkTreeNodePadding, CdkTreeNodeToggle, FlatTreeControl, NestedTreeControl, getMultipleTreeControlsError, getTreeControlMissingError, getTreeMissingMatchingNodeDefError, getTreeMultipleDefaultNodeDefsError, getTreeNoValidDataSourceError };
875
1639
  //# sourceMappingURL=tree.mjs.map