@clr/angular 18.3.0 → 18.4.0

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 (110) hide show
  1. package/fesm2022/clr-angular-accordion.mjs +28 -28
  2. package/fesm2022/clr-angular-accordion.mjs.map +1 -1
  3. package/fesm2022/clr-angular-button.mjs +27 -27
  4. package/fesm2022/clr-angular-button.mjs.map +1 -1
  5. package/fesm2022/clr-angular-card.mjs +417 -0
  6. package/fesm2022/clr-angular-card.mjs.map +1 -0
  7. package/fesm2022/clr-angular-collapsible-panel.mjs +6 -6
  8. package/fesm2022/clr-angular-collapsible-panel.mjs.map +1 -1
  9. package/fesm2022/clr-angular-data-datagrid.mjs +1105 -352
  10. package/fesm2022/clr-angular-data-datagrid.mjs.map +1 -1
  11. package/fesm2022/clr-angular-data-stack-view.mjs +23 -23
  12. package/fesm2022/clr-angular-data-stack-view.mjs.map +1 -1
  13. package/fesm2022/clr-angular-data-tree-view.mjs +283 -48
  14. package/fesm2022/clr-angular-data-tree-view.mjs.map +1 -1
  15. package/fesm2022/clr-angular-data.mjs +4 -4
  16. package/fesm2022/clr-angular-data.mjs.map +1 -1
  17. package/fesm2022/clr-angular-emphasis-alert.mjs +26 -26
  18. package/fesm2022/clr-angular-emphasis-alert.mjs.map +1 -1
  19. package/fesm2022/clr-angular-emphasis-badge.mjs +3 -3
  20. package/fesm2022/clr-angular-emphasis-badge.mjs.map +1 -1
  21. package/fesm2022/clr-angular-emphasis-label.mjs +3 -3
  22. package/fesm2022/clr-angular-emphasis-label.mjs.map +1 -1
  23. package/fesm2022/clr-angular-emphasis.mjs +4 -4
  24. package/fesm2022/clr-angular-emphasis.mjs.map +1 -1
  25. package/fesm2022/clr-angular-forms-checkbox.mjs +13 -13
  26. package/fesm2022/clr-angular-forms-checkbox.mjs.map +1 -1
  27. package/fesm2022/clr-angular-forms-combobox.mjs +35 -35
  28. package/fesm2022/clr-angular-forms-combobox.mjs.map +1 -1
  29. package/fesm2022/clr-angular-forms-common.mjs +69 -69
  30. package/fesm2022/clr-angular-forms-common.mjs.map +1 -1
  31. package/fesm2022/clr-angular-forms-datalist.mjs +17 -17
  32. package/fesm2022/clr-angular-forms-datalist.mjs.map +1 -1
  33. package/fesm2022/clr-angular-forms-datepicker.mjs +73 -73
  34. package/fesm2022/clr-angular-forms-datepicker.mjs.map +1 -1
  35. package/fesm2022/clr-angular-forms-file-input.mjs +36 -36
  36. package/fesm2022/clr-angular-forms-file-input.mjs.map +1 -1
  37. package/fesm2022/clr-angular-forms-input.mjs +10 -10
  38. package/fesm2022/clr-angular-forms-input.mjs.map +1 -1
  39. package/fesm2022/clr-angular-forms-number-input.mjs +11 -11
  40. package/fesm2022/clr-angular-forms-number-input.mjs.map +1 -1
  41. package/fesm2022/clr-angular-forms-password.mjs +11 -11
  42. package/fesm2022/clr-angular-forms-password.mjs.map +1 -1
  43. package/fesm2022/clr-angular-forms-radio.mjs +13 -13
  44. package/fesm2022/clr-angular-forms-radio.mjs.map +1 -1
  45. package/fesm2022/clr-angular-forms-range.mjs +10 -10
  46. package/fesm2022/clr-angular-forms-range.mjs.map +1 -1
  47. package/fesm2022/clr-angular-forms-select.mjs +10 -10
  48. package/fesm2022/clr-angular-forms-select.mjs.map +1 -1
  49. package/fesm2022/clr-angular-forms-textarea.mjs +10 -10
  50. package/fesm2022/clr-angular-forms-textarea.mjs.map +1 -1
  51. package/fesm2022/clr-angular-forms.mjs +4 -4
  52. package/fesm2022/clr-angular-forms.mjs.map +1 -1
  53. package/fesm2022/clr-angular-icon.mjs +81 -41
  54. package/fesm2022/clr-angular-icon.mjs.map +1 -1
  55. package/fesm2022/clr-angular-layout-breadcrumbs.mjs +10 -10
  56. package/fesm2022/clr-angular-layout-breadcrumbs.mjs.map +1 -1
  57. package/fesm2022/clr-angular-layout-main-container.mjs +7 -7
  58. package/fesm2022/clr-angular-layout-main-container.mjs.map +1 -1
  59. package/fesm2022/clr-angular-layout-nav.mjs +22 -22
  60. package/fesm2022/clr-angular-layout-nav.mjs.map +1 -1
  61. package/fesm2022/clr-angular-layout-tabs.mjs +35 -35
  62. package/fesm2022/clr-angular-layout-tabs.mjs.map +1 -1
  63. package/fesm2022/clr-angular-layout-vertical-nav.mjs +31 -31
  64. package/fesm2022/clr-angular-layout-vertical-nav.mjs.map +1 -1
  65. package/fesm2022/clr-angular-layout.mjs +4 -4
  66. package/fesm2022/clr-angular-layout.mjs.map +1 -1
  67. package/fesm2022/clr-angular-modal.mjs +26 -26
  68. package/fesm2022/clr-angular-modal.mjs.map +1 -1
  69. package/fesm2022/clr-angular-popover-common.mjs +69 -34
  70. package/fesm2022/clr-angular-popover-common.mjs.map +1 -1
  71. package/fesm2022/clr-angular-popover-dropdown.mjs +63 -26
  72. package/fesm2022/clr-angular-popover-dropdown.mjs.map +1 -1
  73. package/fesm2022/clr-angular-popover-signpost.mjs +27 -25
  74. package/fesm2022/clr-angular-popover-signpost.mjs.map +1 -1
  75. package/fesm2022/clr-angular-popover-tooltip.mjs +19 -19
  76. package/fesm2022/clr-angular-popover-tooltip.mjs.map +1 -1
  77. package/fesm2022/clr-angular-popover.mjs +4 -4
  78. package/fesm2022/clr-angular-popover.mjs.map +1 -1
  79. package/fesm2022/clr-angular-progress-progress-bars.mjs +7 -7
  80. package/fesm2022/clr-angular-progress-progress-bars.mjs.map +1 -1
  81. package/fesm2022/clr-angular-progress-spinner.mjs +7 -7
  82. package/fesm2022/clr-angular-progress-spinner.mjs.map +1 -1
  83. package/fesm2022/clr-angular-stepper.mjs +31 -31
  84. package/fesm2022/clr-angular-stepper.mjs.map +1 -1
  85. package/fesm2022/clr-angular-timeline.mjs +23 -23
  86. package/fesm2022/clr-angular-timeline.mjs.map +1 -1
  87. package/fesm2022/clr-angular-utils-conditional.mjs +16 -16
  88. package/fesm2022/clr-angular-utils-conditional.mjs.map +1 -1
  89. package/fesm2022/clr-angular-utils-loading.mjs +7 -7
  90. package/fesm2022/clr-angular-utils-loading.mjs.map +1 -1
  91. package/fesm2022/clr-angular-utils.mjs +144 -152
  92. package/fesm2022/clr-angular-utils.mjs.map +1 -1
  93. package/fesm2022/clr-angular-wizard.mjs +51 -51
  94. package/fesm2022/clr-angular-wizard.mjs.map +1 -1
  95. package/fesm2022/clr-angular.mjs +9 -4
  96. package/fesm2022/clr-angular.mjs.map +1 -1
  97. package/package.json +9 -5
  98. package/types/clr-angular-card.d.ts +110 -0
  99. package/types/clr-angular-data-datagrid.d.ts +293 -17
  100. package/types/clr-angular-data-tree-view.d.ts +126 -88
  101. package/types/clr-angular-forms-datepicker.d.ts +1 -1
  102. package/types/clr-angular-forms-file-input.d.ts +1 -1
  103. package/types/clr-angular-icon.d.ts +14 -2
  104. package/types/clr-angular-popover-common.d.ts +6 -0
  105. package/types/clr-angular-popover-dropdown.d.ts +33 -4
  106. package/types/clr-angular-utils.d.ts +61 -20
  107. package/types/clr-angular.d.ts +8 -6
  108. package/schematics/vitest.config.d.ts +0 -2
  109. package/schematics/vitest.config.js +0 -17
  110. package/schematics/vitest.config.js.map +0 -1
@@ -1,12 +1,12 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, Optional, SkipSelf, Directive, Input, Component, EventEmitter, PLATFORM_ID, ElementRef, ContentChildren, ViewChild, Output, Inject, NgModule } from '@angular/core';
3
- import { Subject, BehaviorSubject, fromEvent, isObservable } from 'rxjs';
2
+ import { Injectable, Optional, SkipSelf, HostBinding, Directive, Input, Component, EventEmitter, PLATFORM_ID, ElementRef, ContentChildren, ViewChild, Output, Inject, NgModule } from '@angular/core';
3
+ import { Subject, BehaviorSubject, asapScheduler, fromEvent, isObservable } from 'rxjs';
4
4
  import { trigger, transition, style, animate, state } from '@angular/animations';
5
5
  import * as i3 from '@angular/common';
6
6
  import { isPlatformBrowser, CommonModule } from '@angular/common';
7
7
  import * as i2 from '@clr/angular/utils';
8
8
  import { uniqueIdFactory, preventArrowKeyScroll, isKeyEitherLetterOrNumber, Keys, IfExpandService, LoadingListener, ClrLoadingModule } from '@clr/angular/utils';
9
- import { filter, debounceTime } from 'rxjs/operators';
9
+ import { filter, skip, debounceTime } from 'rxjs/operators';
10
10
  import * as i5 from '@clr/angular/icon';
11
11
  import { ClarityIcons, angleIcon, ClrIcon } from '@clr/angular/icon';
12
12
 
@@ -38,11 +38,17 @@ class TreeFeaturesService {
38
38
  this.selectable = false;
39
39
  this.eager = true;
40
40
  this.childrenFetched = new Subject();
41
+ /*
42
+ * Internal. Whether a bulk expansion is currently in effect for the whole tree, so that nodes created
43
+ * afterwards (lazy-loaded children, dynamic nodes) come in expanded. See `ClrTree.expandAll()`.
44
+ * Cleared as soon as any node of the tree is collapsed on its own.
45
+ */
46
+ this._allExpanded = false;
41
47
  }
42
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TreeFeaturesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
43
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TreeFeaturesService }); }
48
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: TreeFeaturesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
49
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: TreeFeaturesService }); }
44
50
  }
45
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TreeFeaturesService, decorators: [{
51
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: TreeFeaturesService, decorators: [{
46
52
  type: Injectable
47
53
  }] });
48
54
  function treeFeaturesFactory(existing) {
@@ -242,10 +248,10 @@ class TreeFocusManagerService {
242
248
  // Now look from its own direct parent
243
249
  return this.findNodeStartsWith(searchString, model.parent);
244
250
  }
245
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TreeFocusManagerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
246
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TreeFocusManagerService }); }
251
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: TreeFocusManagerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
252
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: TreeFocusManagerService }); }
247
253
  }
248
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TreeFocusManagerService, decorators: [{
254
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: TreeFocusManagerService, decorators: [{
249
255
  type: Injectable
250
256
  }] });
251
257
 
@@ -257,6 +263,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
257
263
  */
258
264
  class TreeNodeModel {
259
265
  constructor() {
266
+ /*
267
+ * Internal. Whether a bulk expansion is currently in effect for this node's subtree, so that descendants
268
+ * created afterwards (lazy-loaded children, dynamic nodes) come in expanded.
269
+ * Cleared as soon as any node of the subtree is collapsed on its own.
270
+ */
271
+ this._descendantsExpanded = false;
272
+ /*
273
+ * Internal, the node rendering this model. Bulk operations walk the model tree and need the node itself,
274
+ * for its expandable state and its animation.
275
+ * Imported as a type only, so the models pull in nothing from the components at runtime.
276
+ */
277
+ this._node = null;
260
278
  this.loading$ = new BehaviorSubject(false);
261
279
  this.selected = new BehaviorSubject(ClrSelectedState.UNSELECTED);
262
280
  /*
@@ -273,6 +291,12 @@ class TreeNodeModel {
273
291
  this._loading = isLoading;
274
292
  this.loading$.next(isLoading);
275
293
  }
294
+ /*
295
+ * Internal. The children that are already known, without triggering a lazy fetch.
296
+ */
297
+ get _loadedChildren() {
298
+ return this.children;
299
+ }
276
300
  get disabled() {
277
301
  // when both parameters are undefined, double negative is needed to cast to false, otherwise will return undefined.
278
302
  return !!(this._disabled || this.parent?.disabled);
@@ -281,9 +305,37 @@ class TreeNodeModel {
281
305
  this._disabled = value;
282
306
  }
283
307
  destroy() {
308
+ this._node = null;
284
309
  // Just to be safe
285
310
  this.selected.complete();
286
311
  }
312
+ /*
313
+ * Internal. Expands or collapses this node and every descendant that is already known.
314
+ * Disabled branches are left untouched and excluded, the same way selection excludes them.
315
+ */
316
+ _setExpandedRecursive(expanded) {
317
+ if (this.disabled) {
318
+ return;
319
+ }
320
+ if (this._node) {
321
+ this._node._setExpandedInBulk(expanded);
322
+ }
323
+ for (const child of this._loadedChildren) {
324
+ child._setExpandedRecursive(expanded);
325
+ }
326
+ this._descendantsExpanded = expanded;
327
+ }
328
+ /*
329
+ * Internal. Whether this node, or one of its ancestors, expects all of its descendants to be expanded.
330
+ */
331
+ _isInExpandedSubtree() {
332
+ for (let current = this; current; current = current.parent) {
333
+ if (current._descendantsExpanded) {
334
+ return true;
335
+ }
336
+ }
337
+ return false;
338
+ }
287
339
  // Propagate by default when eager, don't propagate in the lazy-loaded tree.
288
340
  setSelected(state, propagateUp, propagateDown) {
289
341
  if (state === this.selected.value) {
@@ -311,6 +363,15 @@ class TreeNodeModel {
311
363
  // It should never be called from lifecycle hooks or app-provided inputs.
312
364
  this.setSelected(newState, true, propagate);
313
365
  }
366
+ /*
367
+ * Internal, called when this node is collapsed on its own. No ancestor can still expect its whole subtree
368
+ * to be expanded, so nodes created afterwards must not keep cascading open.
369
+ */
370
+ _clearExpandedSubtree() {
371
+ for (let current = this; current; current = current.parent) {
372
+ current._descendantsExpanded = false;
373
+ }
374
+ }
314
375
  /*
315
376
  * Internal, but needs to be called by other nodes
316
377
  */
@@ -408,6 +469,10 @@ class DeclarativeTreeNodeModel extends TreeNodeModel {
408
469
  class ClrTreeNodeLink {
409
470
  constructor(el) {
410
471
  this.el = el;
472
+ // The tree node's content container owns the roving tabindex for the whole treeitem, so
473
+ // the link itself must be removed from the natural tab sequence to avoid a double Tab stop.
474
+ // It remains mouse-clickable and can still be activated programmatically via .click().
475
+ this.tabindex = -1;
411
476
  }
412
477
  get active() {
413
478
  return this.el.nativeElement.classList.contains('active');
@@ -417,16 +482,19 @@ class ClrTreeNodeLink {
417
482
  this.el.nativeElement.click();
418
483
  }
419
484
  }
420
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrTreeNodeLink, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); }
421
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: ClrTreeNodeLink, isStandalone: false, selector: ".clr-treenode-link", ngImport: i0 }); }
485
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: ClrTreeNodeLink, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); }
486
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.25", type: ClrTreeNodeLink, isStandalone: false, selector: ".clr-treenode-link", host: { properties: { "attr.tabindex": "this.tabindex" } }, ngImport: i0 }); }
422
487
  }
423
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrTreeNodeLink, decorators: [{
488
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: ClrTreeNodeLink, decorators: [{
424
489
  type: Directive,
425
490
  args: [{
426
491
  selector: '.clr-treenode-link',
427
492
  standalone: false,
428
493
  }]
429
- }], ctorParameters: () => [{ type: i0.ElementRef }] });
494
+ }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { tabindex: [{
495
+ type: HostBinding,
496
+ args: ['attr.tabindex']
497
+ }] } });
430
498
 
431
499
  /*
432
500
  * Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
@@ -442,6 +510,9 @@ class RecursiveChildren {
442
510
  constructor(featuresService, expandService) {
443
511
  this.featuresService = featuresService;
444
512
  this.expandService = expandService;
513
+ // One context object per node: a new object on every change detection pass would make ngTemplateOutlet
514
+ // run ngOnChanges and rewrite the context of every node on every pass.
515
+ this.contexts = new WeakMap();
445
516
  if (expandService) {
446
517
  this.subscription = expandService.expandChange.subscribe(value => {
447
518
  if (!value && this.parent && !featuresService.eager && featuresService.recursion) {
@@ -464,10 +535,18 @@ class RecursiveChildren {
464
535
  (this.featuresService.eager || !this.expandService || this.expandService.expanded));
465
536
  }
466
537
  getContext(node) {
467
- return {
468
- $implicit: node.model,
469
- clrModel: node,
470
- };
538
+ let context = this.contexts.get(node);
539
+ if (!context) {
540
+ context = {
541
+ // Read through, so the context keeps following the node if its model gets reassigned.
542
+ get $implicit() {
543
+ return node.model;
544
+ },
545
+ clrModel: node,
546
+ };
547
+ this.contexts.set(node, context);
548
+ }
549
+ return context;
471
550
  }
472
551
  ngOnDestroy() {
473
552
  if (this.subscription) {
@@ -477,8 +556,8 @@ class RecursiveChildren {
477
556
  setAriaRoles() {
478
557
  this.role = this.parent ? 'group' : null;
479
558
  }
480
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: RecursiveChildren, deps: [{ token: TreeFeaturesService }, { token: i2.IfExpandService, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
481
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: RecursiveChildren, isStandalone: false, selector: "clr-recursive-children", inputs: { parent: "parent", children: "children" }, host: { properties: { "attr.role": "role" } }, ngImport: i0, template: `
559
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: RecursiveChildren, deps: [{ token: TreeFeaturesService }, { token: i2.IfExpandService, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
560
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.25", type: RecursiveChildren, isStandalone: false, selector: "clr-recursive-children", inputs: { parent: "parent", children: "children" }, host: { properties: { "attr.role": "role" } }, ngImport: i0, template: `
482
561
  @if (shouldRender()) {
483
562
  @for (child of parent?.children || children; track child) {
484
563
  <ng-container *ngTemplateOutlet="featuresService.recursion.template; context: getContext(child)"></ng-container>
@@ -486,7 +565,7 @@ class RecursiveChildren {
486
565
  }
487
566
  `, isInline: true, dependencies: [{ kind: "directive", type: i3.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); }
488
567
  }
489
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: RecursiveChildren, decorators: [{
568
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: RecursiveChildren, decorators: [{
490
569
  type: Component,
491
570
  args: [{
492
571
  selector: 'clr-recursive-children',
@@ -536,6 +615,8 @@ class ClrTreeNode {
536
615
  this.isModelLoading = false;
537
616
  this.nodeId = uniqueIdFactory();
538
617
  this.contentContainerTabindex = -1;
618
+ this.bulkChange = false;
619
+ this.skipAnimation = false;
539
620
  this.skipEmitChange = false;
540
621
  this.typeAheadKeyBuffer = '';
541
622
  this.typeAheadKeyEvent = new Subject();
@@ -558,6 +639,7 @@ class ClrTreeNode {
558
639
  this._model = new DeclarativeTreeNodeModel(parent ? parent._model : null);
559
640
  }
560
641
  this._model.nodeId = this.nodeId;
642
+ this._model._node = this;
561
643
  }
562
644
  get disabled() {
563
645
  return this._model.disabled;
@@ -611,6 +693,12 @@ class ClrTreeNode {
611
693
  get treeNodeLink() {
612
694
  return this.treeNodeLinkList && this.treeNodeLinkList.first;
613
695
  }
696
+ get childrenAnimationState() {
697
+ if (this.expandService.expanded) {
698
+ return this.skipAnimation ? 'expandedInstant' : 'expanded';
699
+ }
700
+ return this.skipAnimation ? 'collapsedInstant' : 'collapsed';
701
+ }
614
702
  get isParent() {
615
703
  return this._model.children && this._model.children.length > 0;
616
704
  }
@@ -621,8 +709,15 @@ class ClrTreeNode {
621
709
  this.selectedChange.emit(value);
622
710
  }));
623
711
  this.subscriptions.push(this.expandService.expandChange.subscribe(value => {
712
+ this.skipAnimation = this.bulkChange;
624
713
  this.expandedChange.emit(value);
625
714
  this._model.expanded = value;
715
+ if (!this.bulkChange && !value) {
716
+ // Collapsed on its own, so nothing above can still expect its whole subtree to be expanded and
717
+ // nodes created afterwards must not keep cascading open.
718
+ this._model._clearExpandedSubtree();
719
+ this.featuresService._allExpanded = false;
720
+ }
626
721
  }));
627
722
  this.subscriptions.push(this.focusManager.focusRequest.subscribe(nodeId => {
628
723
  if (this.nodeId === nodeId) {
@@ -631,9 +726,27 @@ class ClrTreeNode {
631
726
  }), this.focusManager.focusChange.subscribe(nodeId => {
632
727
  this.checkTabIndex(nodeId);
633
728
  }));
634
- this.subscriptions.push(this._model.loading$.pipe(debounceTime(0)).subscribe(isLoading => (this.isModelLoading = isLoading)));
729
+ // The loading state can flip in the middle of a change detection pass (a lazy fetch starts when the template
730
+ // reads the children), so it is applied in a microtask rather than synchronously. Using a macrotask timer here
731
+ // would schedule one timer per node when a large tree gets created, and one extra change detection pass per
732
+ // fetched node in lazy trees. The current value is read directly instead of debouncing the replayed initial one.
733
+ this.isModelLoading = this._model.loading;
734
+ this.subscriptions.push(this._model.loading$
735
+ .pipe(skip(1), debounceTime(0, asapScheduler))
736
+ .subscribe(isLoading => (this.isModelLoading = isLoading)));
635
737
  }
636
738
  ngAfterContentInit() {
739
+ // Nodes created while everything above them is expected to be expanded (lazy-loaded children, dynamic
740
+ // nodes) come in expanded. Children are only known at this point, not in ngOnInit, which is why the check
741
+ // happens here. This is a continuation of the bulk operation, not a toggle of its own, so it goes through
742
+ // the same path: no animation, and the cascade keeps going for this node's own descendants.
743
+ if (!this.expanded &&
744
+ (this.featuresService._allExpanded || this._model._isInExpandedSubtree()) &&
745
+ !this.disabled &&
746
+ this.isExpandable()) {
747
+ this._setExpandedInBulk(true);
748
+ this._model._descendantsExpanded = true;
749
+ }
637
750
  this.subscriptions.push(this.typeAheadKeyEvent.pipe(debounceTime(TREE_TYPE_AHEAD_TIMEOUT)).subscribe((bufferedKeys) => {
638
751
  this.focusManager.focusNodeStartsWith(bufferedKeys, this._model);
639
752
  // reset once bufferedKeys are used
@@ -658,10 +771,54 @@ class ClrTreeNode {
658
771
  isSelectable() {
659
772
  return this.featuresService.selectable;
660
773
  }
774
+ /**
775
+ * Expands this node and every expandable node below it, without animation. Disabled nodes are left untouched.
776
+ * Descendants added afterwards, including lazy-loaded children, come in expanded until any node of the subtree
777
+ * gets collapsed.
778
+ */
779
+ expandDescendants() {
780
+ this._model._setExpandedRecursive(true);
781
+ }
782
+ /**
783
+ * Collapses this node and every node below it, without animation. Disabled nodes are left untouched.
784
+ */
785
+ collapseDescendants() {
786
+ this._model._setExpandedRecursive(false);
787
+ this.reclaimTabStop();
788
+ }
789
+ /*
790
+ * Internal, called while the model tree is walked during a bulk operation.
791
+ */
792
+ _setExpandedInBulk(expanded) {
793
+ // Leaves are left alone when expanding, so that they don't emit a meaningless clrExpandedChange.
794
+ if (expanded && !this.isExpandable()) {
795
+ return;
796
+ }
797
+ this.bulkChange = true;
798
+ try {
799
+ // Consumers run synchronously on clrExpandedChange from here, so the flag is restored even if they throw.
800
+ this.expandService.expanded = expanded;
801
+ }
802
+ finally {
803
+ this.bulkChange = false;
804
+ }
805
+ }
806
+ /*
807
+ * Internal. Takes over the tree's single tab stop without moving focus, so that a collapse cannot leave it
808
+ * on a node that is now hidden.
809
+ */
810
+ _takeTabStop() {
811
+ this.setTabIndex(0);
812
+ this.focusManager.broadcastFocusedNode(this.nodeId);
813
+ }
661
814
  focusTreeNode() {
815
+ if (!isPlatformBrowser(this.platformId)) {
816
+ return;
817
+ }
662
818
  const containerEl = this.contentContainer.nativeElement;
663
- if (isPlatformBrowser(this.platformId) && document.activeElement !== containerEl) {
664
- this.setTabIndex(0);
819
+ this.setTabIndex(0);
820
+ if (document.activeElement !== containerEl) {
821
+ this.focusManager.broadcastFocusedNode(this.nodeId);
665
822
  containerEl.focus();
666
823
  containerEl.scrollIntoView({ block: 'nearest', inline: 'nearest' });
667
824
  }
@@ -717,6 +874,17 @@ class ClrTreeNode {
717
874
  // if non-letter keys are pressed, do reset.
718
875
  this.typeAheadKeyBuffer = '';
719
876
  }
877
+ /*
878
+ * A collapsed subtree is made inert, so the tree's single tab stop must not be left inside the one that was
879
+ * just collapsed: the tree host gives up its own tabindex the first time it is focused, which would leave the
880
+ * whole tree unreachable by keyboard. This node is still visible, so it takes the tab stop over.
881
+ */
882
+ reclaimTabStop() {
883
+ const stranded = this.elementRef.nativeElement.querySelector('.clr-treenode-children .clr-tree-node-content-container[tabindex="0"]');
884
+ if (stranded) {
885
+ this._takeTabStop();
886
+ }
887
+ }
720
888
  setTabIndex(value) {
721
889
  this.contentContainerTabindex = value;
722
890
  this.contentContainer.nativeElement.setAttribute('tabindex', value.toString());
@@ -776,29 +944,53 @@ class ClrTreeNode {
776
944
  }
777
945
  }
778
946
  }
779
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrTreeNode, deps: [{ token: PLATFORM_ID }, { token: ClrTreeNode, optional: true, skipSelf: true }, { token: TreeFeaturesService }, { token: i2.IfExpandService }, { token: i2.ClrCommonStringsService }, { token: TreeFocusManagerService }, { token: i0.ElementRef }, { token: i0.Injector }], target: i0.ɵɵFactoryTarget.Component }); }
780
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: ClrTreeNode, isStandalone: false, selector: "clr-tree-node", inputs: { expandable: ["clrExpandable", "expandable"], disabled: ["clrDisabled", "disabled"], selected: ["clrSelected", "selected"], expanded: ["clrExpanded", "expanded"], clrForTypeAhead: "clrForTypeAhead" }, outputs: { selectedChange: "clrSelectedChange", expandedChange: "clrExpandedChange" }, host: { properties: { "class.clr-tree-node": "true", "class.disabled": "this._model.disabled" } }, providers: [TREE_FEATURES_PROVIDER, IfExpandService, { provide: LoadingListener, useExisting: IfExpandService }], queries: [{ propertyName: "treeNodeLinkList", predicate: ClrTreeNodeLink }], viewQueries: [{ propertyName: "contentContainer", first: true, predicate: ["contentContainer"], descendants: true, read: ElementRef, static: true }], ngImport: i0, template: "<!--\n ~ Copyright (c) 2016-2026 Broadcom. All Rights Reserved.\n ~ The term \"Broadcom\" refers to Broadcom Inc. and/or its subsidiaries.\n ~ This software is released under MIT license.\n ~ The full license information can be found in LICENSE in the root directory of this project.\n -->\n\n<div\n #contentContainer\n role=\"treeitem\"\n class=\"clr-tree-node-content-container\"\n tabindex=\"-1\"\n [class.clr-form-control-disabled]=\"disabled\"\n [attr.aria-disabled]=\"disabled\"\n [attr.aria-expanded]=\"isExpandable() ? expanded : null\"\n [attr.aria-selected]=\"ariaSelected\"\n (keydown)=\"onKeyDown($event)\"\n (focus)=\"broadcastFocusOnContainer()\"\n>\n @if (isExpandable() && !isModelLoading && !expandService.loading) {\n <button\n aria-hidden=\"true\"\n type=\"button\"\n tabindex=\"-1\"\n class=\"clr-treenode-caret\"\n (click)=\"expandService.toggle();\"\n (focus)=\"focusTreeNode()\"\n [disabled]=\"disabled\"\n >\n <cds-icon\n class=\"clr-treenode-caret-icon\"\n shape=\"angle\"\n [direction]=\"expandService.expanded ? 'down' : 'right'\"\n ></cds-icon>\n </button>\n } @if (expandService.loading || isModelLoading) {\n <div class=\"clr-treenode-spinner-container\">\n <span class=\"clr-treenode-spinner spinner\"></span>\n </div>\n } @if (featuresService.selectable) {\n <div class=\"clr-checkbox-wrapper clr-treenode-checkbox\">\n <input\n aria-hidden=\"true\"\n type=\"checkbox\"\n [id]=\"nodeId + '-check'\"\n class=\"clr-checkbox\"\n [disabled]=\"disabled\"\n [checked]=\"_model.selected.value === STATES.SELECTED\"\n [indeterminate]=\"_model.selected.value === STATES.INDETERMINATE\"\n (change)=\"_model.toggleSelection(featuresService.eager)\"\n (focus)=\"focusTreeNode()\"\n tabindex=\"-1\"\n />\n <label [for]=\"nodeId + '-check'\" class=\"clr-control-label\">\n <ng-container [ngTemplateOutlet]=\"treenodeContent\"></ng-container>\n </label>\n </div>\n } @if (!featuresService.selectable) {\n <div class=\"clr-treenode-content\" (mouseup)=\"focusTreeNode()\">\n <ng-container [ngTemplateOutlet]=\"treenodeContent\"></ng-container>\n </div>\n }\n\n <ng-template #treenodeContent>\n <ng-content></ng-content>\n @if (featuresService.selectable || ariaSelected) {\n <div class=\"clr-sr-only\">\n <span> {{ariaSelected ? commonStrings.keys.selectedTreeNode : commonStrings.keys.unselectedTreeNode}}</span>\n </div>\n }\n </ng-template>\n</div>\n<div\n class=\"clr-treenode-children\"\n [@toggleChildrenAnim]=\"expandService.expanded ? 'expanded' : 'collapsed'\"\n [attr.role]=\"isExpandable() && !featuresService.recursion ? 'group' : null\"\n [attr.inert]=\"expandService.expanded ? null : ''\"\n>\n <ng-content select=\"clr-tree-node\"></ng-content>\n <ng-content select=\"[clrIfExpanded]\"></ng-content>\n <clr-recursive-children [parent]=\"_model\"></clr-recursive-children>\n</div>\n", dependencies: [{ kind: "directive", type: i3.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i5.ClrIcon, selector: "clr-icon, cds-icon", inputs: ["shape", "size", "direction", "flip", "solid", "status", "inverse", "badge"] }, { kind: "component", type: RecursiveChildren, selector: "clr-recursive-children", inputs: ["parent", "children"] }], animations: [
947
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: ClrTreeNode, deps: [{ token: PLATFORM_ID }, { token: ClrTreeNode, optional: true, skipSelf: true }, { token: TreeFeaturesService }, { token: i2.IfExpandService }, { token: i2.ClrCommonStringsService }, { token: TreeFocusManagerService }, { token: i0.ElementRef }, { token: i0.Injector }], target: i0.ɵɵFactoryTarget.Component }); }
948
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.25", type: ClrTreeNode, isStandalone: false, selector: "clr-tree-node", inputs: { expandable: ["clrExpandable", "expandable"], disabled: ["clrDisabled", "disabled"], selected: ["clrSelected", "selected"], expanded: ["clrExpanded", "expanded"], clrForTypeAhead: "clrForTypeAhead" }, outputs: { selectedChange: "clrSelectedChange", expandedChange: "clrExpandedChange" }, host: { properties: { "class.clr-tree-node": "true", "class.disabled": "this._model.disabled" } }, providers: [TREE_FEATURES_PROVIDER, IfExpandService, { provide: LoadingListener, useExisting: IfExpandService }], queries: [{ propertyName: "treeNodeLinkList", predicate: ClrTreeNodeLink }], viewQueries: [{ propertyName: "contentContainer", first: true, predicate: ["contentContainer"], descendants: true, read: ElementRef, static: true }], ngImport: i0, template: "<!--\n ~ Copyright (c) 2016-2026 Broadcom. All Rights Reserved.\n ~ The term \"Broadcom\" refers to Broadcom Inc. and/or its subsidiaries.\n ~ This software is released under MIT license.\n ~ The full license information can be found in LICENSE in the root directory of this project.\n -->\n\n<div\n #contentContainer\n role=\"treeitem\"\n class=\"clr-tree-node-content-container\"\n tabindex=\"-1\"\n [class.clr-form-control-disabled]=\"disabled\"\n [attr.aria-disabled]=\"disabled\"\n [attr.aria-expanded]=\"isExpandable() ? expanded : null\"\n [attr.aria-selected]=\"ariaSelected\"\n (keydown)=\"onKeyDown($event)\"\n (focus)=\"broadcastFocusOnContainer()\"\n>\n @if (isExpandable() && !isModelLoading && !expandService.loading) {\n <button\n aria-hidden=\"true\"\n type=\"button\"\n tabindex=\"-1\"\n class=\"clr-treenode-caret\"\n (click)=\"expandService.toggle();\"\n (focus)=\"focusTreeNode()\"\n [disabled]=\"disabled\"\n >\n <cds-icon\n class=\"clr-treenode-caret-icon\"\n shape=\"angle\"\n [direction]=\"expandService.expanded ? 'down' : 'right'\"\n ></cds-icon>\n </button>\n } @if (expandService.loading || isModelLoading) {\n <div class=\"clr-treenode-spinner-container\">\n <span class=\"clr-treenode-spinner spinner\"></span>\n </div>\n } @if (featuresService.selectable) {\n <div class=\"clr-checkbox-wrapper clr-treenode-checkbox\">\n <input\n aria-hidden=\"true\"\n type=\"checkbox\"\n [id]=\"nodeId + '-check'\"\n class=\"clr-checkbox\"\n [disabled]=\"disabled\"\n [checked]=\"_model.selected.value === STATES.SELECTED\"\n [indeterminate]=\"_model.selected.value === STATES.INDETERMINATE\"\n (change)=\"_model.toggleSelection(featuresService.eager)\"\n (focus)=\"focusTreeNode()\"\n tabindex=\"-1\"\n />\n <label [for]=\"nodeId + '-check'\" class=\"clr-control-label\">\n <ng-container [ngTemplateOutlet]=\"treenodeContent\"></ng-container>\n </label>\n </div>\n } @if (!featuresService.selectable) {\n <div class=\"clr-treenode-content\" (mouseup)=\"focusTreeNode()\">\n <ng-container [ngTemplateOutlet]=\"treenodeContent\"></ng-container>\n </div>\n }\n\n <ng-template #treenodeContent>\n <ng-content></ng-content>\n @if (featuresService.selectable || ariaSelected) {\n <div class=\"clr-sr-only\">\n <span> {{ariaSelected ? commonStrings.keys.selectedTreeNode : commonStrings.keys.unselectedTreeNode}}</span>\n </div>\n }\n </ng-template>\n</div>\n<div\n class=\"clr-treenode-children\"\n [@toggleChildrenAnim]=\"childrenAnimationState\"\n [attr.role]=\"isExpandable() && !featuresService.recursion ? 'group' : null\"\n [attr.inert]=\"expandService.expanded ? null : ''\"\n>\n <ng-content select=\"clr-tree-node\"></ng-content>\n <ng-content select=\"[clrIfExpanded]\"></ng-content>\n <clr-recursive-children [parent]=\"_model\"></clr-recursive-children>\n</div>\n", dependencies: [{ kind: "directive", type: i3.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i5.ClrIcon, selector: "clr-icon, cds-icon", inputs: ["shape", "size", "direction", "flip", "solid", "status", "inverse", "badge", "innerOffset"] }, { kind: "component", type: RecursiveChildren, selector: "clr-recursive-children", inputs: ["parent", "children"] }], animations: [
781
949
  trigger('toggleChildrenAnim', [
782
- transition('collapsed => expanded', [style({ height: 0 }), animate(200, style({ height: '*' }))]),
783
- transition('expanded => collapsed', [style({ height: '*' }), animate(200, style({ height: 0 }))]),
784
- state('expanded', style({ height: '*', 'overflow-y': 'visible' })),
785
- state('collapsed', style({ height: 0 })),
950
+ // The "instant" states are used by bulk operations (expand all, expand descendants): they have the same
951
+ // styles but no transition leads to them, so hundreds of nested containers don't animate at once.
952
+ transition('collapsed => expanded, collapsedInstant => expanded', [
953
+ // The animation starts from the collapsed state's styles, so the children must be made visible explicitly.
954
+ style({ height: 0, 'content-visibility': 'visible' }),
955
+ animate(200, style({ height: '*', 'content-visibility': 'visible' })),
956
+ ]),
957
+ transition('expanded => collapsed, expandedInstant => collapsed', [
958
+ style({ height: '*' }),
959
+ animate(200, style({ height: 0 })),
960
+ ]),
961
+ state('expanded, expandedInstant', style({ height: '*', 'overflow-y': 'visible' })),
962
+ // Once collapsed, the browser skips the style, layout and paint of the whole subtree. The state style only
963
+ // applies when the collapse animation is over, so the children stay painted while they slide out; they are
964
+ // inert from the start (see the template), so they cannot be reached in the meantime.
965
+ state('collapsed, collapsedInstant', style({ height: 0, 'content-visibility': 'hidden' })),
786
966
  ]),
787
967
  ] }); }
788
968
  }
789
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrTreeNode, decorators: [{
969
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: ClrTreeNode, decorators: [{
790
970
  type: Component,
791
971
  args: [{ selector: 'clr-tree-node', providers: [TREE_FEATURES_PROVIDER, IfExpandService, { provide: LoadingListener, useExisting: IfExpandService }], animations: [
792
972
  trigger('toggleChildrenAnim', [
793
- transition('collapsed => expanded', [style({ height: 0 }), animate(200, style({ height: '*' }))]),
794
- transition('expanded => collapsed', [style({ height: '*' }), animate(200, style({ height: 0 }))]),
795
- state('expanded', style({ height: '*', 'overflow-y': 'visible' })),
796
- state('collapsed', style({ height: 0 })),
973
+ // The "instant" states are used by bulk operations (expand all, expand descendants): they have the same
974
+ // styles but no transition leads to them, so hundreds of nested containers don't animate at once.
975
+ transition('collapsed => expanded, collapsedInstant => expanded', [
976
+ // The animation starts from the collapsed state's styles, so the children must be made visible explicitly.
977
+ style({ height: 0, 'content-visibility': 'visible' }),
978
+ animate(200, style({ height: '*', 'content-visibility': 'visible' })),
979
+ ]),
980
+ transition('expanded => collapsed, expandedInstant => collapsed', [
981
+ style({ height: '*' }),
982
+ animate(200, style({ height: 0 })),
983
+ ]),
984
+ state('expanded, expandedInstant', style({ height: '*', 'overflow-y': 'visible' })),
985
+ // Once collapsed, the browser skips the style, layout and paint of the whole subtree. The state style only
986
+ // applies when the collapse animation is over, so the children stay painted while they slide out; they are
987
+ // inert from the start (see the template), so they cannot be reached in the meantime.
988
+ state('collapsed, collapsedInstant', style({ height: 0, 'content-visibility': 'hidden' })),
797
989
  ]),
798
990
  ], host: {
799
991
  '[class.clr-tree-node]': 'true',
800
992
  '[class.disabled]': 'this._model.disabled',
801
- }, standalone: false, template: "<!--\n ~ Copyright (c) 2016-2026 Broadcom. All Rights Reserved.\n ~ The term \"Broadcom\" refers to Broadcom Inc. and/or its subsidiaries.\n ~ This software is released under MIT license.\n ~ The full license information can be found in LICENSE in the root directory of this project.\n -->\n\n<div\n #contentContainer\n role=\"treeitem\"\n class=\"clr-tree-node-content-container\"\n tabindex=\"-1\"\n [class.clr-form-control-disabled]=\"disabled\"\n [attr.aria-disabled]=\"disabled\"\n [attr.aria-expanded]=\"isExpandable() ? expanded : null\"\n [attr.aria-selected]=\"ariaSelected\"\n (keydown)=\"onKeyDown($event)\"\n (focus)=\"broadcastFocusOnContainer()\"\n>\n @if (isExpandable() && !isModelLoading && !expandService.loading) {\n <button\n aria-hidden=\"true\"\n type=\"button\"\n tabindex=\"-1\"\n class=\"clr-treenode-caret\"\n (click)=\"expandService.toggle();\"\n (focus)=\"focusTreeNode()\"\n [disabled]=\"disabled\"\n >\n <cds-icon\n class=\"clr-treenode-caret-icon\"\n shape=\"angle\"\n [direction]=\"expandService.expanded ? 'down' : 'right'\"\n ></cds-icon>\n </button>\n } @if (expandService.loading || isModelLoading) {\n <div class=\"clr-treenode-spinner-container\">\n <span class=\"clr-treenode-spinner spinner\"></span>\n </div>\n } @if (featuresService.selectable) {\n <div class=\"clr-checkbox-wrapper clr-treenode-checkbox\">\n <input\n aria-hidden=\"true\"\n type=\"checkbox\"\n [id]=\"nodeId + '-check'\"\n class=\"clr-checkbox\"\n [disabled]=\"disabled\"\n [checked]=\"_model.selected.value === STATES.SELECTED\"\n [indeterminate]=\"_model.selected.value === STATES.INDETERMINATE\"\n (change)=\"_model.toggleSelection(featuresService.eager)\"\n (focus)=\"focusTreeNode()\"\n tabindex=\"-1\"\n />\n <label [for]=\"nodeId + '-check'\" class=\"clr-control-label\">\n <ng-container [ngTemplateOutlet]=\"treenodeContent\"></ng-container>\n </label>\n </div>\n } @if (!featuresService.selectable) {\n <div class=\"clr-treenode-content\" (mouseup)=\"focusTreeNode()\">\n <ng-container [ngTemplateOutlet]=\"treenodeContent\"></ng-container>\n </div>\n }\n\n <ng-template #treenodeContent>\n <ng-content></ng-content>\n @if (featuresService.selectable || ariaSelected) {\n <div class=\"clr-sr-only\">\n <span> {{ariaSelected ? commonStrings.keys.selectedTreeNode : commonStrings.keys.unselectedTreeNode}}</span>\n </div>\n }\n </ng-template>\n</div>\n<div\n class=\"clr-treenode-children\"\n [@toggleChildrenAnim]=\"expandService.expanded ? 'expanded' : 'collapsed'\"\n [attr.role]=\"isExpandable() && !featuresService.recursion ? 'group' : null\"\n [attr.inert]=\"expandService.expanded ? null : ''\"\n>\n <ng-content select=\"clr-tree-node\"></ng-content>\n <ng-content select=\"[clrIfExpanded]\"></ng-content>\n <clr-recursive-children [parent]=\"_model\"></clr-recursive-children>\n</div>\n" }]
993
+ }, standalone: false, template: "<!--\n ~ Copyright (c) 2016-2026 Broadcom. All Rights Reserved.\n ~ The term \"Broadcom\" refers to Broadcom Inc. and/or its subsidiaries.\n ~ This software is released under MIT license.\n ~ The full license information can be found in LICENSE in the root directory of this project.\n -->\n\n<div\n #contentContainer\n role=\"treeitem\"\n class=\"clr-tree-node-content-container\"\n tabindex=\"-1\"\n [class.clr-form-control-disabled]=\"disabled\"\n [attr.aria-disabled]=\"disabled\"\n [attr.aria-expanded]=\"isExpandable() ? expanded : null\"\n [attr.aria-selected]=\"ariaSelected\"\n (keydown)=\"onKeyDown($event)\"\n (focus)=\"broadcastFocusOnContainer()\"\n>\n @if (isExpandable() && !isModelLoading && !expandService.loading) {\n <button\n aria-hidden=\"true\"\n type=\"button\"\n tabindex=\"-1\"\n class=\"clr-treenode-caret\"\n (click)=\"expandService.toggle();\"\n (focus)=\"focusTreeNode()\"\n [disabled]=\"disabled\"\n >\n <cds-icon\n class=\"clr-treenode-caret-icon\"\n shape=\"angle\"\n [direction]=\"expandService.expanded ? 'down' : 'right'\"\n ></cds-icon>\n </button>\n } @if (expandService.loading || isModelLoading) {\n <div class=\"clr-treenode-spinner-container\">\n <span class=\"clr-treenode-spinner spinner\"></span>\n </div>\n } @if (featuresService.selectable) {\n <div class=\"clr-checkbox-wrapper clr-treenode-checkbox\">\n <input\n aria-hidden=\"true\"\n type=\"checkbox\"\n [id]=\"nodeId + '-check'\"\n class=\"clr-checkbox\"\n [disabled]=\"disabled\"\n [checked]=\"_model.selected.value === STATES.SELECTED\"\n [indeterminate]=\"_model.selected.value === STATES.INDETERMINATE\"\n (change)=\"_model.toggleSelection(featuresService.eager)\"\n (focus)=\"focusTreeNode()\"\n tabindex=\"-1\"\n />\n <label [for]=\"nodeId + '-check'\" class=\"clr-control-label\">\n <ng-container [ngTemplateOutlet]=\"treenodeContent\"></ng-container>\n </label>\n </div>\n } @if (!featuresService.selectable) {\n <div class=\"clr-treenode-content\" (mouseup)=\"focusTreeNode()\">\n <ng-container [ngTemplateOutlet]=\"treenodeContent\"></ng-container>\n </div>\n }\n\n <ng-template #treenodeContent>\n <ng-content></ng-content>\n @if (featuresService.selectable || ariaSelected) {\n <div class=\"clr-sr-only\">\n <span> {{ariaSelected ? commonStrings.keys.selectedTreeNode : commonStrings.keys.unselectedTreeNode}}</span>\n </div>\n }\n </ng-template>\n</div>\n<div\n class=\"clr-treenode-children\"\n [@toggleChildrenAnim]=\"childrenAnimationState\"\n [attr.role]=\"isExpandable() && !featuresService.recursion ? 'group' : null\"\n [attr.inert]=\"expandService.expanded ? null : ''\"\n>\n <ng-content select=\"clr-tree-node\"></ng-content>\n <ng-content select=\"[clrIfExpanded]\"></ng-content>\n <clr-recursive-children [parent]=\"_model\"></clr-recursive-children>\n</div>\n" }]
802
994
  }], ctorParameters: () => [{ type: undefined, decorators: [{
803
995
  type: Inject,
804
996
  args: [PLATFORM_ID]
@@ -850,6 +1042,7 @@ class ClrTree {
850
1042
  this.focusManagerService = focusManagerService;
851
1043
  this.renderer = renderer;
852
1044
  this.el = el;
1045
+ this.rootModels = [];
853
1046
  this.subscriptions = [];
854
1047
  this._isMultiSelectable = false;
855
1048
  const subscription = ngZone.runOutsideAngular(() => fromEvent(el.nativeElement, 'focusin').subscribe((event) => {
@@ -880,6 +1073,40 @@ class ClrTree {
880
1073
  ngOnDestroy() {
881
1074
  this.subscriptions.forEach(sub => sub.unsubscribe());
882
1075
  }
1076
+ /**
1077
+ * Expands every expandable node of the tree, without animation. Disabled nodes are left untouched.
1078
+ * Nodes added to the tree afterwards, including lazy-loaded children, come in expanded until any node gets collapsed.
1079
+ */
1080
+ expandAll() {
1081
+ this.setAllExpanded(true);
1082
+ }
1083
+ /**
1084
+ * Collapses every node of the tree, without animation. Disabled nodes are left untouched.
1085
+ */
1086
+ collapseAll() {
1087
+ this.setAllExpanded(false);
1088
+ }
1089
+ setAllExpanded(expanded) {
1090
+ this.featuresService._allExpanded = expanded;
1091
+ this.rootModels.forEach(model => model._setExpandedRecursive(expanded));
1092
+ if (!expanded) {
1093
+ this.reclaimTabStop();
1094
+ }
1095
+ }
1096
+ /*
1097
+ * A collapsed subtree is made inert, so the tree's single tab stop must not be left inside one: the host
1098
+ * gives up its own tabindex the first time it is focused, which would leave the whole tree unreachable by
1099
+ * keyboard. Only the roots stay visible after a collapse, so the first one takes the tab stop over.
1100
+ */
1101
+ reclaimTabStop() {
1102
+ const stranded = this.el.nativeElement.querySelector('.clr-treenode-children .clr-tree-node-content-container[tabindex="0"]');
1103
+ if (stranded) {
1104
+ const firstRoot = this.rootNodes.find(node => !node._model.parent);
1105
+ if (firstRoot) {
1106
+ firstRoot._takeTabStop();
1107
+ }
1108
+ }
1109
+ }
883
1110
  setMultiSelectable() {
884
1111
  if (this.featuresService.selectable && this.rootNodes.length > 0) {
885
1112
  this._isMultiSelectable = true;
@@ -894,17 +1121,18 @@ class ClrTree {
894
1121
  // if node has no parent, it's a root node
895
1122
  // for recursive tree, this.rootNodes registers also nested children
896
1123
  // so we have to use filter to extract the ones that are truly root nodes
897
- this.focusManagerService.rootNodeModels = this.rootNodes.map(node => node._model).filter(node => !node.parent);
1124
+ this.rootModels = this.rootNodes.map(node => node._model).filter(node => !node.parent);
1125
+ this.focusManagerService.rootNodeModels = this.rootModels;
898
1126
  }
899
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrTree, deps: [{ token: TreeFeaturesService }, { token: TreeFocusManagerService }, { token: i0.Renderer2 }, { token: i0.ElementRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component }); }
900
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: ClrTree, isStandalone: false, selector: "clr-tree", inputs: { lazy: ["clrLazy", "lazy"] }, host: { attributes: { "tabindex": "0" }, properties: { "attr.role": "\"tree\"" } }, providers: [TREE_FEATURES_PROVIDER, TreeFocusManagerService], queries: [{ propertyName: "rootNodes", predicate: ClrTreeNode }], ngImport: i0, template: `
1127
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: ClrTree, deps: [{ token: TreeFeaturesService }, { token: TreeFocusManagerService }, { token: i0.Renderer2 }, { token: i0.ElementRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component }); }
1128
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.25", type: ClrTree, isStandalone: false, selector: "clr-tree", inputs: { lazy: ["clrLazy", "lazy"] }, host: { attributes: { "tabindex": "0" }, properties: { "attr.role": "\"tree\"" } }, providers: [TREE_FEATURES_PROVIDER, TreeFocusManagerService], queries: [{ propertyName: "rootNodes", predicate: ClrTreeNode }], ngImport: i0, template: `
901
1129
  <ng-content></ng-content>
902
1130
  @if (featuresService.recursion) {
903
1131
  <clr-recursive-children [children]="featuresService.recursion.root"></clr-recursive-children>
904
1132
  }
905
1133
  `, isInline: true, dependencies: [{ kind: "component", type: RecursiveChildren, selector: "clr-recursive-children", inputs: ["parent", "children"] }] }); }
906
1134
  }
907
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrTree, decorators: [{
1135
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: ClrTree, decorators: [{
908
1136
  type: Component,
909
1137
  args: [{
910
1138
  selector: 'clr-tree',
@@ -967,6 +1195,9 @@ class RecursiveTreeNodeModel extends TreeNodeModel {
967
1195
  set children(value) {
968
1196
  this._children = value;
969
1197
  }
1198
+ get _loadedChildren() {
1199
+ return this._children || [];
1200
+ }
970
1201
  destroy() {
971
1202
  if (this.subscription) {
972
1203
  this.subscription.unsubscribe();
@@ -1010,7 +1241,7 @@ class RecursiveTreeNodeModel extends TreeNodeModel {
1010
1241
  }
1011
1242
  }
1012
1243
  wrapChildren(rawModels) {
1013
- return rawModels.map(m => new RecursiveTreeNodeModel(m, this, this.getChildren, this.featuresService));
1244
+ return (rawModels || []).map(m => new RecursiveTreeNodeModel(m, this, this.getChildren, this.featuresService));
1014
1245
  }
1015
1246
  }
1016
1247
 
@@ -1036,7 +1267,11 @@ class ClrRecursiveForOf {
1036
1267
  wrapped = [new RecursiveTreeNodeModel(this.nodes, null, this.getChildren, this.featuresService)];
1037
1268
  }
1038
1269
  if (!this.childrenFetchSubscription) {
1039
- this.childrenFetchSubscription = this.featuresService.childrenFetched.pipe(debounceTime(0)).subscribe(() => {
1270
+ // Children fetched synchronously while the tree renders are picked up once, in a microtask,
1271
+ // instead of one macrotask timer (and its own change detection pass) per node.
1272
+ this.childrenFetchSubscription = this.featuresService.childrenFetched
1273
+ .pipe(debounceTime(0, asapScheduler))
1274
+ .subscribe(() => {
1040
1275
  this.cdr.detectChanges();
1041
1276
  });
1042
1277
  }
@@ -1050,10 +1285,10 @@ class ClrRecursiveForOf {
1050
1285
  this.childrenFetchSubscription.unsubscribe();
1051
1286
  }
1052
1287
  }
1053
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrRecursiveForOf, deps: [{ token: i0.TemplateRef }, { token: TreeFeaturesService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Directive }); }
1054
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: ClrRecursiveForOf, isStandalone: false, selector: "[clrRecursiveFor][clrRecursiveForOf]", inputs: { nodes: ["clrRecursiveForOf", "nodes"], getChildren: ["clrRecursiveForGetChildren", "getChildren"] }, usesOnChanges: true, ngImport: i0 }); }
1288
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: ClrRecursiveForOf, deps: [{ token: i0.TemplateRef }, { token: TreeFeaturesService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Directive }); }
1289
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.25", type: ClrRecursiveForOf, isStandalone: false, selector: "[clrRecursiveFor][clrRecursiveForOf]", inputs: { nodes: ["clrRecursiveForOf", "nodes"], getChildren: ["clrRecursiveForGetChildren", "getChildren"] }, usesOnChanges: true, ngImport: i0 }); }
1055
1290
  }
1056
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrRecursiveForOf, decorators: [{
1291
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: ClrRecursiveForOf, decorators: [{
1057
1292
  type: Directive,
1058
1293
  args: [{
1059
1294
  selector: '[clrRecursiveFor][clrRecursiveForOf]',
@@ -1078,11 +1313,11 @@ class ClrTreeViewModule {
1078
1313
  constructor() {
1079
1314
  ClarityIcons.addIcons(angleIcon);
1080
1315
  }
1081
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrTreeViewModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
1082
- static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: ClrTreeViewModule, declarations: [ClrTree, ClrTreeNode, ClrRecursiveForOf, ClrTreeNodeLink, RecursiveChildren], imports: [CommonModule, ClrIcon, ClrLoadingModule], exports: [ClrTree, ClrTreeNode, ClrRecursiveForOf, ClrTreeNodeLink] }); }
1083
- static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrTreeViewModule, imports: [CommonModule, ClrIcon, ClrLoadingModule] }); }
1316
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: ClrTreeViewModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
1317
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.25", ngImport: i0, type: ClrTreeViewModule, declarations: [ClrTree, ClrTreeNode, ClrRecursiveForOf, ClrTreeNodeLink, RecursiveChildren], imports: [CommonModule, ClrIcon, ClrLoadingModule], exports: [ClrTree, ClrTreeNode, ClrRecursiveForOf, ClrTreeNodeLink] }); }
1318
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: ClrTreeViewModule, imports: [CommonModule, ClrIcon, ClrLoadingModule] }); }
1084
1319
  }
1085
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrTreeViewModule, decorators: [{
1320
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: ClrTreeViewModule, decorators: [{
1086
1321
  type: NgModule,
1087
1322
  args: [{
1088
1323
  imports: [CommonModule, ClrIcon, ClrLoadingModule],