@truenas/ui-components 0.3.24 → 0.3.26

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.
@@ -13,7 +13,7 @@ import { mdiCheckCircle, mdiAlertCircle, mdiAlert, mdiInformation, mdiOpenInNew,
13
13
  import * as i1$1 from '@angular/platform-browser';
14
14
  import { DomSanitizer } from '@angular/platform-browser';
15
15
  import { HttpClient } from '@angular/common/http';
16
- import { firstValueFrom, BehaviorSubject, Subject, merge, animationFrameScheduler, asapScheduler, take } from 'rxjs';
16
+ import { firstValueFrom, BehaviorSubject, merge, animationFrameScheduler, asapScheduler, Subject, take } from 'rxjs';
17
17
  import { RouterLink } from '@angular/router';
18
18
  import { filesize } from 'filesize';
19
19
  import { CdkMenu, CdkMenuItem, CdkMenuTrigger } from '@angular/cdk/menu';
@@ -25,7 +25,7 @@ import { SelectionModel, DataSource } from '@angular/cdk/collections';
25
25
  import * as i2 from '@angular/cdk/tree';
26
26
  import { CdkTree, CdkTreeModule, CdkTreeNode, CDK_TREE_NODE_OUTLET_NODE, CdkNestedTreeNode, CdkTreeNodeOutlet, FlatTreeControl, NestedTreeControl, CdkTreeNodeOutletContext } from '@angular/cdk/tree';
27
27
  export { FlatTreeControl } from '@angular/cdk/tree';
28
- import { map, filter, debounceTime, distinctUntilChanged, takeUntil, auditTime } from 'rxjs/operators';
28
+ import { map, filter, debounceTime, distinctUntilChanged, auditTime } from 'rxjs/operators';
29
29
  import { CdkVirtualScrollViewport, CdkVirtualScrollableWindow, CdkFixedSizeVirtualScroll, CdkVirtualForOf, ScrollingModule } from '@angular/cdk/scrolling';
30
30
  import { DialogRef, DIALOG_DATA, Dialog } from '@angular/cdk/dialog';
31
31
 
@@ -11694,7 +11694,6 @@ class TnTreeFlatDataSource extends DataSource {
11694
11694
  _filteredData = new BehaviorSubject([]);
11695
11695
  filterValue = '';
11696
11696
  filterChanged$ = new BehaviorSubject('');
11697
- disconnect$ = new Subject();
11698
11697
  constructor(_treeControl, _treeFlattener) {
11699
11698
  super();
11700
11699
  this._treeControl = _treeControl;
@@ -11709,9 +11708,9 @@ class TnTreeFlatDataSource extends DataSource {
11709
11708
  }
11710
11709
  this.flattenData();
11711
11710
  }
11712
- /** The currently filtered (unflattened) data; equals `data` until `filter()` is used. */
11711
+ /** The currently filtered (unflattened) data; equals `data` while no filter query is active. */
11713
11712
  get filteredData() {
11714
- return this._filteredData.value;
11713
+ return this.filterValue ? this._filteredData.value : this.data;
11715
11714
  }
11716
11715
  connect() {
11717
11716
  return merge(this._treeControl.expansionModel.changed, this._flattenedData).pipe(map(() => {
@@ -11719,10 +11718,7 @@ class TnTreeFlatDataSource extends DataSource {
11719
11718
  return this._expandedData.value;
11720
11719
  }));
11721
11720
  }
11722
- disconnect() {
11723
- this.disconnect$.next();
11724
- this.disconnect$.complete();
11725
- }
11721
+ disconnect() { }
11726
11722
  /** Filters the tree using `filterPredicate` (debounced). No-op until `filterPredicate` is set. */
11727
11723
  filter(query) {
11728
11724
  this.filterChanged$.next(query);
@@ -11731,8 +11727,15 @@ class TnTreeFlatDataSource extends DataSource {
11731
11727
  this._flattenedData.next(this._treeFlattener.flattenNodes(this.filterValue ? this._filteredData.value : this.data));
11732
11728
  this._treeControl.dataNodes = this._flattenedData.value;
11733
11729
  }
11730
+ /**
11731
+ * Deliberately NOT torn down in `disconnect()`: CdkTree disconnects the
11732
+ * datasource when the tree is destroyed, but the same instance may be
11733
+ * reconnected (e.g. a tree behind an `@if`), and filtering must survive that.
11734
+ * The pipeline only references the datasource's own subjects, so it holds no
11735
+ * external resources and is collected with the instance.
11736
+ */
11734
11737
  detectFilterChanges() {
11735
- this.filterChanged$.pipe(filter(() => !!this.filterPredicate), debounceTime(200), distinctUntilChanged(), takeUntil(this.disconnect$)).subscribe((changedValue) => {
11738
+ this.filterChanged$.pipe(filter(() => !!this.filterPredicate), debounceTime(200), distinctUntilChanged()).subscribe((changedValue) => {
11736
11739
  if (this.filterValue === changedValue || !this.filterPredicate) {
11737
11740
  return;
11738
11741
  }
@@ -11767,7 +11770,7 @@ class TnTreeComponent extends CdkTree {
11767
11770
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
11768
11771
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.0", type: TnTreeComponent, isStandalone: true, selector: "tn-tree", host: { attributes: { "role": "tree" }, classAttribute: "tn-tree" }, providers: [
11769
11772
  { provide: CdkTree, useExisting: TnTreeComponent }
11770
- ], exportAs: ["tnTree"], usesInheritance: true, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<ng-container cdkTreeNodeOutlet />", styles: [":host{display:block;width:100%}.tn-tree{width:100%;background-color:var(--tn-bg1);border:1px solid var(--tn-lines);border-radius:6px;overflow:hidden}\n"], dependencies: [{ kind: "ngmodule", type: CdkTreeModule }, { kind: "directive", type: i2.CdkTreeNodeOutlet, selector: "[cdkTreeNodeOutlet]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
11773
+ ], exportAs: ["tnTree"], usesInheritance: true, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<ng-container cdkTreeNodeOutlet />", styles: [".tn-tree{display:block;width:100%;background-color:var(--tn-bg1);border:1px solid var(--tn-lines);border-radius:6px;overflow:hidden}tn-tree .tn-tree-node-wrapper>.tn-tree-node,tn-tree .tn-nested-tree-node-wrapper>.tn-nested-tree-node__content{border-bottom:none;border-top:1px solid var(--tn-lines)}tn-tree>.tn-tree-node-wrapper:first-child>.tn-tree-node,tn-tree>.tn-nested-tree-node-wrapper:first-child>.tn-nested-tree-node__content{border-top:none}\n"], dependencies: [{ kind: "ngmodule", type: CdkTreeModule }, { kind: "directive", type: i2.CdkTreeNodeOutlet, selector: "[cdkTreeNodeOutlet]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
11771
11774
  }
11772
11775
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeComponent, decorators: [{
11773
11776
  type: Component,
@@ -11776,7 +11779,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
11776
11779
  ], hostDirectives: [{ directive: TnTestIdDirective, inputs: ['tnTestId: testId'] }], host: {
11777
11780
  'class': 'tn-tree',
11778
11781
  'role': 'tree'
11779
- }, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container cdkTreeNodeOutlet />", styles: [":host{display:block;width:100%}.tn-tree{width:100%;background-color:var(--tn-bg1);border:1px solid var(--tn-lines);border-radius:6px;overflow:hidden}\n"] }]
11782
+ }, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container cdkTreeNodeOutlet />", styles: [".tn-tree{display:block;width:100%;background-color:var(--tn-bg1);border:1px solid var(--tn-lines);border-radius:6px;overflow:hidden}tn-tree .tn-tree-node-wrapper>.tn-tree-node,tn-tree .tn-nested-tree-node-wrapper>.tn-nested-tree-node__content{border-bottom:none;border-top:1px solid var(--tn-lines)}tn-tree>.tn-tree-node-wrapper:first-child>.tn-tree-node,tn-tree>.tn-nested-tree-node-wrapper:first-child>.tn-nested-tree-node__content{border-top:none}\n"] }]
11780
11783
  }], ctorParameters: () => [] });
11781
11784
 
11782
11785
  class TnTreeNodeComponent extends CdkTreeNode {
@@ -11818,15 +11821,39 @@ class TnNestedTreeNodeComponent extends CdkNestedTreeNode {
11818
11821
  * defaults to the previous hardcoded value.
11819
11822
  */
11820
11823
  toggleAriaLabel = input('Toggle node', ...(ngDevMode ? [{ debugName: "toggleAriaLabel" }] : []));
11821
- /** Semantic test-id base applied to the built-in toggle button (`button-` prefixed). */
11824
+ /**
11825
+ * Semantic test-id base applied to the built-in toggle button (`button-`
11826
+ * prefixed). When unset, derives from the node's own `testId` — a row with
11827
+ * `[testId]="['vdev', guid]"` gets `button-toggle-vdev-<guid>` — so every
11828
+ * identified row has an addressable expand control without repeating the id.
11829
+ */
11822
11830
  toggleTestId = input(...(ngDevMode ? [undefined, { debugName: "toggleTestId" }] : []));
11823
11831
  /**
11824
11832
  * Suppresses the built-in toggle button (and its alignment spacer) so the
11825
11833
  * consumer can render a fully custom toggle in the projected content.
11834
+ *
11835
+ * The built-in button is the only keyboard/AT-operable expand control this
11836
+ * component guarantees — when hiding it, the projected replacement must be
11837
+ * an accessible control (a real button with an aria-label, not a bare icon).
11826
11838
  */
11827
11839
  hideToggle = input(false, { ...(ngDevMode ? { debugName: "hideToggle" } : {}), transform: booleanAttribute });
11828
11840
  changeDetectorRef = inject(ChangeDetectorRef);
11841
+ hostTestId = inject(TnTestIdDirective, { self: true });
11829
11842
  destroyed = false;
11843
+ /** Explicit `toggleTestId` wins; otherwise `toggle`-scoped from the node's `testId`. */
11844
+ resolvedToggleTestId = computed(() => {
11845
+ const explicit = this.toggleTestId();
11846
+ if (explicit !== undefined && explicit !== null) {
11847
+ return explicit;
11848
+ }
11849
+ const nodeTestId = this.hostTestId.testId();
11850
+ if (nodeTestId === undefined || nodeTestId === null || nodeTestId === '') {
11851
+ return undefined;
11852
+ }
11853
+ // Role leads (button-toggle-<node id>), mirroring tn-dialog-shell's
11854
+ // button-close-<base> convention for fixed chrome.
11855
+ return scopeTestId('toggle', ...(Array.isArray(nodeTestId) ? nodeTestId : [nodeTestId]));
11856
+ }, ...(ngDevMode ? [{ debugName: "resolvedToggleTestId" }] : []));
11830
11857
  constructor() {
11831
11858
  super(inject((ElementRef)), inject(CdkTree, { optional: true }), inject(CDK_TREE_NODE_OUTLET_NODE, { optional: true }), inject(ChangeDetectorRef, { optional: true }));
11832
11859
  inject(DestroyRef).onDestroy(() => this.destroyed = true);
@@ -11911,13 +11938,16 @@ class TnNestedTreeNodeComponent extends CdkNestedTreeNode {
11911
11938
  else {
11912
11939
  this._tree.toggle(this.data);
11913
11940
  }
11941
+ // Mirrors CdkTreeNodeToggle's focus handling. `_keyManager` is a CDK
11942
+ // underscore member, so a CDK upgrade may rename it — the optional call
11943
+ // degrades to "focus not moved" rather than breaking the toggle.
11914
11944
  this._tree._keyManager?.focusItem(this);
11915
11945
  }
11916
11946
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnNestedTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
11917
11947
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnNestedTreeNodeComponent, isStandalone: true, selector: "tn-nested-tree-node", inputs: { toggleAriaLabel: { classPropertyName: "toggleAriaLabel", publicName: "toggleAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, toggleTestId: { classPropertyName: "toggleTestId", publicName: "toggleTestId", isSignal: true, isRequired: false, transformFunction: null }, hideToggle: { classPropertyName: "hideToggle", publicName: "hideToggle", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "treeitem" }, properties: { "attr.aria-level": "level + 1", "attr.aria-expanded": "isExpandable ? isExpanded : null" }, classAttribute: "tn-nested-tree-node-wrapper" }, providers: [
11918
11948
  { provide: CdkNestedTreeNode, useExisting: TnNestedTreeNodeComponent },
11919
11949
  { provide: CdkTreeNode, useExisting: TnNestedTreeNodeComponent }
11920
- ], exportAs: ["tnNestedTreeNode"], usesInheritance: true, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<div class=\"tn-nested-tree-node__content\">\n @if (!hideToggle()) {\n <!-- Toggle button for expandable nodes (provided by component).\n Alt+click / Alt+Enter toggles the node together with all descendants.\n tabindex=\"-1\" keeps it out of the tab order \u2014 the tree's key manager\n drives focus at the treeitem level, matching cdkTreeNodeToggle. -->\n @if (isExpandable) {\n <button\n class=\"tn-nested-tree-node__toggle\"\n type=\"button\"\n tabindex=\"-1\"\n tnTestIdType=\"button\"\n [tnTestId]=\"toggleTestId()\"\n [class.tn-nested-tree-node__toggle--expanded]=\"isExpanded\"\n [attr.aria-label]=\"toggleAriaLabel()\"\n (click)=\"toggleNode($event); $event.stopPropagation()\"\n (keydown.enter)=\"toggleNode($event); $event.preventDefault()\"\n (keydown.space)=\"toggleNode($event); $event.preventDefault()\">\n <tn-icon\n library=\"mdi\"\n size=\"sm\"\n style=\"transition: transform 0.2s ease;\"\n [name]=\"isExpanded ? 'chevron-down' : 'chevron-right'\" />\n </button>\n }\n\n <!-- Spacer for non-expandable nodes to maintain alignment -->\n @if (!isExpandable) {\n <div class=\"tn-nested-tree-node__spacer\"></div>\n }\n }\n\n <!-- Consumer content -->\n <ng-content />\n</div>\n\n<!-- Children container -->\n@if (isExpandable) {\n <div class=\"tn-nested-tree-node-container\" role=\"group\" [class.tn-tree-invisible]=\"!isExpanded\">\n <ng-content select=\"[slot=children]\" />\n </div>\n}\n", styles: [".tn-nested-tree-node-wrapper{display:block;width:100%}.tn-nested-tree-node{display:block;width:100%;font-family:var(--tn-font-family-body);font-size:1rem;line-height:1.4;color:var(--tn-fg1)}.tn-nested-tree-node--expandable .tn-nested-tree-node__content{cursor:pointer}.tn-nested-tree-node__content{display:flex;align-items:center;gap:8px;min-height:48px;padding:12px 16px;border-bottom:1px solid var(--tn-lines);transition:background-color .2s ease}.tn-nested-tree-node__content:hover{background-color:var(--tn-alt-bg2)}.tn-nested-tree-node__content:focus-within{background-color:var(--tn-alt-bg2);outline:2px solid var(--tn-primary);outline-offset:-2px}.tn-tree-invisible{display:none}.tn-nested-tree-node__toggle{display:flex;align-items:center;justify-content:center;width:24px;height:24px;margin-right:8px;padding:0;border:none;background:transparent;border-radius:4px;cursor:pointer;color:var(--tn-fg2);transition:background-color .2s ease,color .2s ease}.tn-nested-tree-node__toggle:hover{background-color:var(--tn-bg3);color:var(--tn-fg1)}.tn-nested-tree-node__toggle:focus{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-nested-tree-node__toggle svg{transition:transform .2s ease}.tn-nested-tree-node__toggle--expanded svg{transform:rotate(90deg)}.tn-nested-tree-node__spacer{width:24px;height:24px;flex-shrink:0}.tn-nested-tree-node__text{flex:1;display:flex;align-items:center;gap:8px;min-width:0;color:var(--tn-fg1)}div.tn-nested-tree-node-container{padding-left:40px}@media(prefers-reduced-motion:reduce){.tn-nested-tree-node__toggle svg,.tn-nested-tree-node__content,.tn-nested-tree-node__children{transition:none}}@media(prefers-contrast:high){.tn-nested-tree-node__content{border:1px solid transparent}.tn-nested-tree-node__content:hover,.tn-nested-tree-node__content:focus-within{border-color:var(--tn-fg1)}.tn-nested-tree-node__toggle{border:1px solid var(--tn-fg2)}.tn-nested-tree-node__toggle:hover,.tn-nested-tree-node__toggle:focus{border-color:var(--tn-fg1)}}\n"], dependencies: [{ kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11950
+ ], exportAs: ["tnNestedTreeNode"], usesInheritance: true, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<div class=\"tn-nested-tree-node__content\">\n @if (!hideToggle()) {\n <!-- Toggle button for expandable nodes (provided by component).\n Alt+click / Alt+Enter toggles the node together with all descendants.\n tabindex=\"-1\" keeps it out of the tab order \u2014 the tree's key manager\n drives focus at the treeitem level, matching cdkTreeNodeToggle. -->\n @if (isExpandable) {\n <button\n class=\"tn-nested-tree-node__toggle\"\n type=\"button\"\n tabindex=\"-1\"\n tnTestIdType=\"button\"\n [tnTestId]=\"resolvedToggleTestId()\"\n [class.tn-nested-tree-node__toggle--expanded]=\"isExpanded\"\n [attr.aria-label]=\"toggleAriaLabel()\"\n (click)=\"toggleNode($event); $event.stopPropagation()\"\n (keydown.enter)=\"toggleNode($event); $event.preventDefault()\"\n (keydown.space)=\"toggleNode($event); $event.preventDefault()\">\n <tn-icon\n library=\"mdi\"\n size=\"sm\"\n style=\"transition: transform 0.2s ease;\"\n [name]=\"isExpanded ? 'chevron-down' : 'chevron-right'\" />\n </button>\n }\n\n <!-- Spacer for non-expandable nodes to maintain alignment -->\n @if (!isExpandable) {\n <div class=\"tn-nested-tree-node__spacer\"></div>\n }\n }\n\n <!-- Consumer content -->\n <ng-content />\n</div>\n\n<!-- Children container -->\n@if (isExpandable) {\n <div class=\"tn-nested-tree-node-container\" role=\"group\" [class.tn-tree-invisible]=\"!isExpanded\">\n <ng-content select=\"[slot=children]\" />\n </div>\n}\n", styles: [":host{display:block;width:100%}.tn-nested-tree-node{display:block;width:100%;font-family:var(--tn-font-family-body);font-size:1rem;line-height:1.4;color:var(--tn-fg1)}.tn-nested-tree-node--expandable .tn-nested-tree-node__content{cursor:pointer}.tn-nested-tree-node__content{display:flex;align-items:center;gap:8px;min-height:48px;padding:0 8px;border-bottom:1px solid var(--tn-lines);transition:background-color .2s ease}.tn-nested-tree-node__content:hover{background-color:var(--tn-alt-bg2)}.tn-nested-tree-node__content:focus-within{background-color:var(--tn-alt-bg2);outline:2px solid var(--tn-primary);outline-offset:-2px}.tn-tree-invisible{display:none}.tn-nested-tree-node__toggle{display:flex;align-items:center;justify-content:center;width:24px;height:24px;margin-right:8px;padding:0;border:none;background:transparent;border-radius:4px;cursor:pointer;color:var(--tn-fg2);transition:background-color .2s ease,color .2s ease}.tn-nested-tree-node__toggle:hover{background-color:var(--tn-bg3);color:var(--tn-fg1)}.tn-nested-tree-node__toggle:focus{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-nested-tree-node__toggle svg{transition:transform .2s ease}.tn-nested-tree-node__toggle--expanded svg{transform:rotate(90deg)}.tn-nested-tree-node__spacer{width:24px;height:24px;flex-shrink:0}.tn-nested-tree-node__text{flex:1;display:flex;align-items:center;gap:8px;min-width:0;color:var(--tn-fg1)}div.tn-nested-tree-node-container{padding-left:40px}@media(prefers-reduced-motion:reduce){.tn-nested-tree-node__toggle svg,.tn-nested-tree-node__content,.tn-nested-tree-node__children{transition:none}}@media(prefers-contrast:high){.tn-nested-tree-node__content{border:1px solid transparent}.tn-nested-tree-node__content:hover,.tn-nested-tree-node__content:focus-within{border-color:var(--tn-fg1)}.tn-nested-tree-node__toggle{border:1px solid var(--tn-fg2)}.tn-nested-tree-node__toggle:hover,.tn-nested-tree-node__toggle:focus{border-color:var(--tn-fg1)}}\n"], dependencies: [{ kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11921
11951
  }
11922
11952
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnNestedTreeNodeComponent, decorators: [{
11923
11953
  type: Component,
@@ -11929,7 +11959,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
11929
11959
  '[attr.aria-level]': 'level + 1',
11930
11960
  '[attr.aria-expanded]': 'isExpandable ? isExpanded : null',
11931
11961
  'role': 'treeitem'
11932
- }, encapsulation: ViewEncapsulation.Emulated, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"tn-nested-tree-node__content\">\n @if (!hideToggle()) {\n <!-- Toggle button for expandable nodes (provided by component).\n Alt+click / Alt+Enter toggles the node together with all descendants.\n tabindex=\"-1\" keeps it out of the tab order \u2014 the tree's key manager\n drives focus at the treeitem level, matching cdkTreeNodeToggle. -->\n @if (isExpandable) {\n <button\n class=\"tn-nested-tree-node__toggle\"\n type=\"button\"\n tabindex=\"-1\"\n tnTestIdType=\"button\"\n [tnTestId]=\"toggleTestId()\"\n [class.tn-nested-tree-node__toggle--expanded]=\"isExpanded\"\n [attr.aria-label]=\"toggleAriaLabel()\"\n (click)=\"toggleNode($event); $event.stopPropagation()\"\n (keydown.enter)=\"toggleNode($event); $event.preventDefault()\"\n (keydown.space)=\"toggleNode($event); $event.preventDefault()\">\n <tn-icon\n library=\"mdi\"\n size=\"sm\"\n style=\"transition: transform 0.2s ease;\"\n [name]=\"isExpanded ? 'chevron-down' : 'chevron-right'\" />\n </button>\n }\n\n <!-- Spacer for non-expandable nodes to maintain alignment -->\n @if (!isExpandable) {\n <div class=\"tn-nested-tree-node__spacer\"></div>\n }\n }\n\n <!-- Consumer content -->\n <ng-content />\n</div>\n\n<!-- Children container -->\n@if (isExpandable) {\n <div class=\"tn-nested-tree-node-container\" role=\"group\" [class.tn-tree-invisible]=\"!isExpanded\">\n <ng-content select=\"[slot=children]\" />\n </div>\n}\n", styles: [".tn-nested-tree-node-wrapper{display:block;width:100%}.tn-nested-tree-node{display:block;width:100%;font-family:var(--tn-font-family-body);font-size:1rem;line-height:1.4;color:var(--tn-fg1)}.tn-nested-tree-node--expandable .tn-nested-tree-node__content{cursor:pointer}.tn-nested-tree-node__content{display:flex;align-items:center;gap:8px;min-height:48px;padding:12px 16px;border-bottom:1px solid var(--tn-lines);transition:background-color .2s ease}.tn-nested-tree-node__content:hover{background-color:var(--tn-alt-bg2)}.tn-nested-tree-node__content:focus-within{background-color:var(--tn-alt-bg2);outline:2px solid var(--tn-primary);outline-offset:-2px}.tn-tree-invisible{display:none}.tn-nested-tree-node__toggle{display:flex;align-items:center;justify-content:center;width:24px;height:24px;margin-right:8px;padding:0;border:none;background:transparent;border-radius:4px;cursor:pointer;color:var(--tn-fg2);transition:background-color .2s ease,color .2s ease}.tn-nested-tree-node__toggle:hover{background-color:var(--tn-bg3);color:var(--tn-fg1)}.tn-nested-tree-node__toggle:focus{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-nested-tree-node__toggle svg{transition:transform .2s ease}.tn-nested-tree-node__toggle--expanded svg{transform:rotate(90deg)}.tn-nested-tree-node__spacer{width:24px;height:24px;flex-shrink:0}.tn-nested-tree-node__text{flex:1;display:flex;align-items:center;gap:8px;min-width:0;color:var(--tn-fg1)}div.tn-nested-tree-node-container{padding-left:40px}@media(prefers-reduced-motion:reduce){.tn-nested-tree-node__toggle svg,.tn-nested-tree-node__content,.tn-nested-tree-node__children{transition:none}}@media(prefers-contrast:high){.tn-nested-tree-node__content{border:1px solid transparent}.tn-nested-tree-node__content:hover,.tn-nested-tree-node__content:focus-within{border-color:var(--tn-fg1)}.tn-nested-tree-node__toggle{border:1px solid var(--tn-fg2)}.tn-nested-tree-node__toggle:hover,.tn-nested-tree-node__toggle:focus{border-color:var(--tn-fg1)}}\n"] }]
11962
+ }, encapsulation: ViewEncapsulation.Emulated, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"tn-nested-tree-node__content\">\n @if (!hideToggle()) {\n <!-- Toggle button for expandable nodes (provided by component).\n Alt+click / Alt+Enter toggles the node together with all descendants.\n tabindex=\"-1\" keeps it out of the tab order \u2014 the tree's key manager\n drives focus at the treeitem level, matching cdkTreeNodeToggle. -->\n @if (isExpandable) {\n <button\n class=\"tn-nested-tree-node__toggle\"\n type=\"button\"\n tabindex=\"-1\"\n tnTestIdType=\"button\"\n [tnTestId]=\"resolvedToggleTestId()\"\n [class.tn-nested-tree-node__toggle--expanded]=\"isExpanded\"\n [attr.aria-label]=\"toggleAriaLabel()\"\n (click)=\"toggleNode($event); $event.stopPropagation()\"\n (keydown.enter)=\"toggleNode($event); $event.preventDefault()\"\n (keydown.space)=\"toggleNode($event); $event.preventDefault()\">\n <tn-icon\n library=\"mdi\"\n size=\"sm\"\n style=\"transition: transform 0.2s ease;\"\n [name]=\"isExpanded ? 'chevron-down' : 'chevron-right'\" />\n </button>\n }\n\n <!-- Spacer for non-expandable nodes to maintain alignment -->\n @if (!isExpandable) {\n <div class=\"tn-nested-tree-node__spacer\"></div>\n }\n }\n\n <!-- Consumer content -->\n <ng-content />\n</div>\n\n<!-- Children container -->\n@if (isExpandable) {\n <div class=\"tn-nested-tree-node-container\" role=\"group\" [class.tn-tree-invisible]=\"!isExpanded\">\n <ng-content select=\"[slot=children]\" />\n </div>\n}\n", styles: [":host{display:block;width:100%}.tn-nested-tree-node{display:block;width:100%;font-family:var(--tn-font-family-body);font-size:1rem;line-height:1.4;color:var(--tn-fg1)}.tn-nested-tree-node--expandable .tn-nested-tree-node__content{cursor:pointer}.tn-nested-tree-node__content{display:flex;align-items:center;gap:8px;min-height:48px;padding:0 8px;border-bottom:1px solid var(--tn-lines);transition:background-color .2s ease}.tn-nested-tree-node__content:hover{background-color:var(--tn-alt-bg2)}.tn-nested-tree-node__content:focus-within{background-color:var(--tn-alt-bg2);outline:2px solid var(--tn-primary);outline-offset:-2px}.tn-tree-invisible{display:none}.tn-nested-tree-node__toggle{display:flex;align-items:center;justify-content:center;width:24px;height:24px;margin-right:8px;padding:0;border:none;background:transparent;border-radius:4px;cursor:pointer;color:var(--tn-fg2);transition:background-color .2s ease,color .2s ease}.tn-nested-tree-node__toggle:hover{background-color:var(--tn-bg3);color:var(--tn-fg1)}.tn-nested-tree-node__toggle:focus{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-nested-tree-node__toggle svg{transition:transform .2s ease}.tn-nested-tree-node__toggle--expanded svg{transform:rotate(90deg)}.tn-nested-tree-node__spacer{width:24px;height:24px;flex-shrink:0}.tn-nested-tree-node__text{flex:1;display:flex;align-items:center;gap:8px;min-width:0;color:var(--tn-fg1)}div.tn-nested-tree-node-container{padding-left:40px}@media(prefers-reduced-motion:reduce){.tn-nested-tree-node__toggle svg,.tn-nested-tree-node__content,.tn-nested-tree-node__children{transition:none}}@media(prefers-contrast:high){.tn-nested-tree-node__content{border:1px solid transparent}.tn-nested-tree-node__content:hover,.tn-nested-tree-node__content:focus-within{border-color:var(--tn-fg1)}.tn-nested-tree-node__toggle{border:1px solid var(--tn-fg2)}.tn-nested-tree-node__toggle:hover,.tn-nested-tree-node__toggle:focus{border-color:var(--tn-fg1)}}\n"] }]
11933
11963
  }], ctorParameters: () => [], propDecorators: { toggleAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "toggleAriaLabel", required: false }] }], toggleTestId: [{ type: i0.Input, args: [{ isSignal: true, alias: "toggleTestId", required: false }] }], hideToggle: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideToggle", required: false }] }] } });
11934
11964
 
11935
11965
  class TnTreeNodeOutletDirective {
@@ -11959,13 +11989,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
11959
11989
  class TnNestedTreeDataSource extends DataSource {
11960
11990
  /** When set, `filter(query)` re-renders the tree from `filterPredicate(data, query)`. */
11961
11991
  filterPredicate;
11962
- /** When set, incoming `data` is sorted recursively before rendering. */
11992
+ /**
11993
+ * When set, incoming `data` is sorted recursively before rendering.
11994
+ *
11995
+ * Note: sorting REWRITES `children` arrays on the caller's node objects
11996
+ * (a new sorted array is assigned per node). Node object identity is
11997
+ * preserved — deliberately, since tree controls track expansion state by
11998
+ * node identity — but don't pass an object graph that is shared with code
11999
+ * relying on the original child order.
12000
+ */
11963
12001
  sortComparer;
11964
12002
  filterValue = '';
11965
12003
  filterChanged$ = new BehaviorSubject('');
11966
12004
  _data = new BehaviorSubject([]);
11967
12005
  _filteredData = new BehaviorSubject([]);
11968
- disconnect$ = new Subject();
11969
12006
  get data() {
11970
12007
  return this._data.value;
11971
12008
  }
@@ -11982,16 +12019,20 @@ class TnNestedTreeDataSource extends DataSource {
11982
12019
  connect(collectionViewer) {
11983
12020
  return merge(collectionViewer.viewChange, this._data, this._filteredData).pipe(map(() => (this.filterValue ? this._filteredData.value : this.data)));
11984
12021
  }
11985
- disconnect() {
11986
- this.disconnect$.next();
11987
- this.disconnect$.complete();
11988
- }
12022
+ disconnect() { }
11989
12023
  /** Filters the tree using `filterPredicate` (debounced). No-op until `filterPredicate` is set. */
11990
12024
  filter(query) {
11991
12025
  this.filterChanged$.next(query);
11992
12026
  }
12027
+ /**
12028
+ * Deliberately NOT torn down in `disconnect()`: CdkTree disconnects the
12029
+ * datasource when the tree is destroyed, but the same instance may be
12030
+ * reconnected (e.g. a tree behind an `@if`), and filtering must survive that.
12031
+ * The pipeline only references the datasource's own subjects, so it holds no
12032
+ * external resources and is collected with the instance.
12033
+ */
11993
12034
  detectFilterChanges() {
11994
- this.filterChanged$.pipe(filter(() => !!this.filterPredicate), debounceTime(200), distinctUntilChanged(), takeUntil(this.disconnect$)).subscribe((changedValue) => {
12035
+ this.filterChanged$.pipe(filter(() => !!this.filterPredicate), debounceTime(200), distinctUntilChanged()).subscribe((changedValue) => {
11995
12036
  if (this.filterValue === changedValue || !this.filterPredicate) {
11996
12037
  return;
11997
12038
  }
@@ -12128,6 +12169,10 @@ class TnTreeNodeHarness extends ComponentHarness {
12128
12169
  /**
12129
12170
  * Toggles the node together with all of its descendants (Alt+click on the
12130
12171
  * built-in nested-node toggle).
12172
+ *
12173
+ * Only nested nodes honor the Alt modifier — on a flat node the row's
12174
+ * `cdkTreeNodeToggle` is not Alt-aware, so this degrades to a plain
12175
+ * single-level toggle.
12131
12176
  */
12132
12177
  async toggleWithDescendants() {
12133
12178
  await (await this.getToggle()).click({ alt: true });
@@ -12156,6 +12201,7 @@ class TnTreeNodeHarness extends ComponentHarness {
12156
12201
  return toggle;
12157
12202
  }
12158
12203
  }
12204
+
12159
12205
  /**
12160
12206
  * Harness for interacting with `tn-tree` in tests.
12161
12207
  *