@mozaic-ds/angular 2.0.33 → 2.0.35

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.
@@ -2935,20 +2935,26 @@ class MozSegmentedControlComponent {
2935
2935
  items = input([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
2936
2936
  size = input('s', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
2937
2937
  full = input(false, { ...(ngDevMode ? { debugName: "full" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2938
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2938
2939
  ariaLabel = input('Segmented control', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
2939
2940
  selectedIndex = model(0, ...(ngDevMode ? [{ debugName: "selectedIndex" }] : /* istanbul ignore next */ []));
2940
2941
  change = output();
2941
2942
  classes = computed(() => ({
2942
2943
  'segmented-control': true,
2944
+ 'segmented-control--disabled': this.disabled(),
2943
2945
  'segmented-control--m': this.size() === 'm',
2944
2946
  'segmented-control--full': this.full(),
2945
2947
  }), ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2946
2948
  segmentClasses = (index) => computed(() => ({
2949
+ 'segmented-control__segment--disabled': this.disabled() || !!this.items()[index]?.disabled,
2947
2950
  'segmented-control__segment': true,
2948
2951
  'segmented-control__segment--selected': index === this.selectedIndex(),
2949
2952
  }));
2950
2953
  buttons = viewChildren('segBtn', ...(ngDevMode ? [{ debugName: "buttons" }] : /* istanbul ignore next */ []));
2951
2954
  onSelect(index) {
2955
+ if (this.disabled() || this.items()[index]?.disabled) {
2956
+ return;
2957
+ }
2952
2958
  const item = this.items()[index];
2953
2959
  this.selectedIndex.set(index);
2954
2960
  this.change.emit({ index, value: item.value });
@@ -2960,12 +2966,12 @@ class MozSegmentedControlComponent {
2960
2966
  btn?.focus();
2961
2967
  }
2962
2968
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: MozSegmentedControlComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2963
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.6", type: MozSegmentedControlComponent, isStandalone: true, selector: "moz-segmented-control", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, full: { classPropertyName: "full", publicName: "full", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, selectedIndex: { classPropertyName: "selectedIndex", publicName: "selectedIndex", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedIndex: "selectedIndexChange", change: "change" }, viewQueries: [{ propertyName: "buttons", predicate: ["segBtn"], descendants: true, isSignal: true }], ngImport: i0, template: "<div [class]=\"classes()\" role=\"tablist\" [attr.aria-label]=\"ariaLabel()\">\n @for (item of items(); track item.value; let i = $index) {\n <button\n #segBtn\n type=\"button\"\n [class]=\"segmentClasses(i)()\"\n role=\"tab\"\n [attr.aria-selected]=\"i === selectedIndex()\"\n [attr.tabindex]=\"i === selectedIndex() ? 0 : -1\"\n (click)=\"onSelect(i)\"\n >\n {{ item.label }}\n </button>\n }\n</div>\n", styles: [".mc-segmented-control,.segmented-control{display:inline-flex;background-color:var(--segmented-control-color-background-default, #ffffff);border:1px solid var(--segmented-control-color-border, #cccccc);border-radius:3rem;height:2rem;padding:.25rem;box-sizing:border-box;gap:.25rem}.mc-segmented-control__segment,.segmented-control__segment{display:flex;align-items:center;justify-content:center;background-color:transparent;border:none;color:var(--segmented-control-color-text-default, #666666);cursor:pointer;font-family:inherit;font-weight:var(--font-weight-semi-bold, 600);font-size:var(--font-size-100, .875rem);padding:0 1rem;border-radius:3rem;height:1.5rem;width:100%;transition:background-color .3s ease;white-space:nowrap}.mc-segmented-control__segment:not(.mc-segmented-control__segment--selected):not(.segmented-control__segment--selected):hover,.segmented-control__segment:not(.mc-segmented-control__segment--selected):not(.segmented-control__segment--selected):hover{background-color:var(--segmented-control-color-background-hover, rgba(0, 0, 0, .05))}.mc-segmented-control__segment--selected,.segmented-control__segment--selected{color:var(--segmented-control-color-text-selected, #ffffff);background-color:var(--segmented-control-color-background-selected, #464e63)}.mc-segmented-control__segment:focus-visible,.segmented-control__segment:focus-visible{box-shadow:0 0 0 .125rem var(--focus-color-mid, var(--focus-color-outline-mid, #ffffff)),0 0 0 .25rem var(--focus-color-outer, var(--focus-color-outline-outer, #000000));outline:.125rem solid transparent;outline-offset:.125rem}.mc-segmented-control--m,.segmented-control--m{height:3rem;padding:.5rem}.mc-segmented-control--m .mc-segmented-control__segment,.segmented-control--m .mc-segmented-control__segment,.mc-segmented-control--m .segmented-control__segment,.segmented-control--m .segmented-control__segment{height:2rem}.mc-segmented-control--full,.segmented-control--full{display:flex}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2969
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.6", type: MozSegmentedControlComponent, isStandalone: true, selector: "moz-segmented-control", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, full: { classPropertyName: "full", publicName: "full", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, selectedIndex: { classPropertyName: "selectedIndex", publicName: "selectedIndex", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedIndex: "selectedIndexChange", change: "change" }, viewQueries: [{ propertyName: "buttons", predicate: ["segBtn"], descendants: true, isSignal: true }], ngImport: i0, template: "<div [class]=\"classes()\" role=\"tablist\" [attr.aria-label]=\"ariaLabel()\">\n @for (item of items(); track item.value; let i = $index) {\n <button\n #segBtn\n type=\"button\"\n [class]=\"segmentClasses(i)()\"\n role=\"tab\"\n [attr.aria-selected]=\"i === selectedIndex()\"\n [attr.aria-disabled]=\"disabled() || !!item.disabled\"\n [attr.tabindex]=\"i === selectedIndex() ? 0 : -1\"\n [disabled]=\"disabled() || !!item.disabled\"\n (click)=\"onSelect(i)\"\n >\n {{ item.label }}\n </button>\n }\n</div>\n", styles: [".mc-segmented-control,.segmented-control{display:inline-flex;background-color:var(--segmented-control-color-background-default, #ffffff);border:1px solid var(--segmented-control-color-border, #cccccc);border-radius:3rem;height:2rem;padding:.25rem;box-sizing:border-box;gap:.25rem}.mc-segmented-control__segment,.segmented-control__segment{display:flex;align-items:center;justify-content:center;background-color:transparent;border:none;color:var(--segmented-control-color-text-default, #666666);cursor:pointer;font-family:inherit;font-weight:var(--font-weight-semi-bold, 600);font-size:var(--font-size-100, .875rem);padding:0 1rem;border-radius:3rem;height:1.5rem;width:100%;transition:background-color .3s ease;white-space:nowrap}.mc-segmented-control__segment:not(.mc-segmented-control__segment--selected):not(.segmented-control__segment--selected):hover,.segmented-control__segment:not(.mc-segmented-control__segment--selected):not(.segmented-control__segment--selected):hover{background-color:var(--segmented-control-color-background-hover, rgba(0, 0, 0, .05))}.mc-segmented-control__segment--selected,.segmented-control__segment--selected{color:var(--segmented-control-color-text-selected, #ffffff);background-color:var(--segmented-control-color-background-selected, #464e63)}.mc-segmented-control__segment:focus-visible,.segmented-control__segment:focus-visible{box-shadow:0 0 0 .125rem var(--focus-color-mid, var(--focus-color-outline-mid, #ffffff)),0 0 0 .25rem var(--focus-color-outer, var(--focus-color-outline-outer, #000000));outline:.125rem solid transparent;outline-offset:.125rem}.mc-segmented-control--m,.segmented-control--m{height:3rem;padding:.5rem}.mc-segmented-control--m .mc-segmented-control__segment,.segmented-control--m .mc-segmented-control__segment,.mc-segmented-control--m .segmented-control__segment,.segmented-control--m .segmented-control__segment{height:2rem}.mc-segmented-control--full,.segmented-control--full{display:flex}.segmented-control--disabled{cursor:not-allowed}.segmented-control__segment:disabled{color:var(--button-state-disabled-color, #737373);cursor:not-allowed}.segmented-control__segment:disabled:not(.segmented-control__segment--selected):hover{background-color:transparent}.segmented-control__segment:disabled.segmented-control__segment--selected{background-color:var(--button-state-disabled-background, #d9d9d9);color:var(--button-state-disabled-color, #737373)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2964
2970
  }
2965
2971
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: MozSegmentedControlComponent, decorators: [{
2966
2972
  type: Component,
2967
- args: [{ selector: 'moz-segmented-control', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div [class]=\"classes()\" role=\"tablist\" [attr.aria-label]=\"ariaLabel()\">\n @for (item of items(); track item.value; let i = $index) {\n <button\n #segBtn\n type=\"button\"\n [class]=\"segmentClasses(i)()\"\n role=\"tab\"\n [attr.aria-selected]=\"i === selectedIndex()\"\n [attr.tabindex]=\"i === selectedIndex() ? 0 : -1\"\n (click)=\"onSelect(i)\"\n >\n {{ item.label }}\n </button>\n }\n</div>\n", styles: [".mc-segmented-control,.segmented-control{display:inline-flex;background-color:var(--segmented-control-color-background-default, #ffffff);border:1px solid var(--segmented-control-color-border, #cccccc);border-radius:3rem;height:2rem;padding:.25rem;box-sizing:border-box;gap:.25rem}.mc-segmented-control__segment,.segmented-control__segment{display:flex;align-items:center;justify-content:center;background-color:transparent;border:none;color:var(--segmented-control-color-text-default, #666666);cursor:pointer;font-family:inherit;font-weight:var(--font-weight-semi-bold, 600);font-size:var(--font-size-100, .875rem);padding:0 1rem;border-radius:3rem;height:1.5rem;width:100%;transition:background-color .3s ease;white-space:nowrap}.mc-segmented-control__segment:not(.mc-segmented-control__segment--selected):not(.segmented-control__segment--selected):hover,.segmented-control__segment:not(.mc-segmented-control__segment--selected):not(.segmented-control__segment--selected):hover{background-color:var(--segmented-control-color-background-hover, rgba(0, 0, 0, .05))}.mc-segmented-control__segment--selected,.segmented-control__segment--selected{color:var(--segmented-control-color-text-selected, #ffffff);background-color:var(--segmented-control-color-background-selected, #464e63)}.mc-segmented-control__segment:focus-visible,.segmented-control__segment:focus-visible{box-shadow:0 0 0 .125rem var(--focus-color-mid, var(--focus-color-outline-mid, #ffffff)),0 0 0 .25rem var(--focus-color-outer, var(--focus-color-outline-outer, #000000));outline:.125rem solid transparent;outline-offset:.125rem}.mc-segmented-control--m,.segmented-control--m{height:3rem;padding:.5rem}.mc-segmented-control--m .mc-segmented-control__segment,.segmented-control--m .mc-segmented-control__segment,.mc-segmented-control--m .segmented-control__segment,.segmented-control--m .segmented-control__segment{height:2rem}.mc-segmented-control--full,.segmented-control--full{display:flex}\n"] }]
2968
- }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], full: [{ type: i0.Input, args: [{ isSignal: true, alias: "full", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], selectedIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedIndex", required: false }] }, { type: i0.Output, args: ["selectedIndexChange"] }], change: [{ type: i0.Output, args: ["change"] }], buttons: [{ type: i0.ViewChildren, args: ['segBtn', { isSignal: true }] }] } });
2973
+ args: [{ selector: 'moz-segmented-control', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div [class]=\"classes()\" role=\"tablist\" [attr.aria-label]=\"ariaLabel()\">\n @for (item of items(); track item.value; let i = $index) {\n <button\n #segBtn\n type=\"button\"\n [class]=\"segmentClasses(i)()\"\n role=\"tab\"\n [attr.aria-selected]=\"i === selectedIndex()\"\n [attr.aria-disabled]=\"disabled() || !!item.disabled\"\n [attr.tabindex]=\"i === selectedIndex() ? 0 : -1\"\n [disabled]=\"disabled() || !!item.disabled\"\n (click)=\"onSelect(i)\"\n >\n {{ item.label }}\n </button>\n }\n</div>\n", styles: [".mc-segmented-control,.segmented-control{display:inline-flex;background-color:var(--segmented-control-color-background-default, #ffffff);border:1px solid var(--segmented-control-color-border, #cccccc);border-radius:3rem;height:2rem;padding:.25rem;box-sizing:border-box;gap:.25rem}.mc-segmented-control__segment,.segmented-control__segment{display:flex;align-items:center;justify-content:center;background-color:transparent;border:none;color:var(--segmented-control-color-text-default, #666666);cursor:pointer;font-family:inherit;font-weight:var(--font-weight-semi-bold, 600);font-size:var(--font-size-100, .875rem);padding:0 1rem;border-radius:3rem;height:1.5rem;width:100%;transition:background-color .3s ease;white-space:nowrap}.mc-segmented-control__segment:not(.mc-segmented-control__segment--selected):not(.segmented-control__segment--selected):hover,.segmented-control__segment:not(.mc-segmented-control__segment--selected):not(.segmented-control__segment--selected):hover{background-color:var(--segmented-control-color-background-hover, rgba(0, 0, 0, .05))}.mc-segmented-control__segment--selected,.segmented-control__segment--selected{color:var(--segmented-control-color-text-selected, #ffffff);background-color:var(--segmented-control-color-background-selected, #464e63)}.mc-segmented-control__segment:focus-visible,.segmented-control__segment:focus-visible{box-shadow:0 0 0 .125rem var(--focus-color-mid, var(--focus-color-outline-mid, #ffffff)),0 0 0 .25rem var(--focus-color-outer, var(--focus-color-outline-outer, #000000));outline:.125rem solid transparent;outline-offset:.125rem}.mc-segmented-control--m,.segmented-control--m{height:3rem;padding:.5rem}.mc-segmented-control--m .mc-segmented-control__segment,.segmented-control--m .mc-segmented-control__segment,.mc-segmented-control--m .segmented-control__segment,.segmented-control--m .segmented-control__segment{height:2rem}.mc-segmented-control--full,.segmented-control--full{display:flex}.segmented-control--disabled{cursor:not-allowed}.segmented-control__segment:disabled{color:var(--button-state-disabled-color, #737373);cursor:not-allowed}.segmented-control__segment:disabled:not(.segmented-control__segment--selected):hover{background-color:transparent}.segmented-control__segment:disabled.segmented-control__segment--selected{background-color:var(--button-state-disabled-background, #d9d9d9);color:var(--button-state-disabled-color, #737373)}\n"] }]
2974
+ }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], full: [{ type: i0.Input, args: [{ isSignal: true, alias: "full", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], selectedIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedIndex", required: false }] }, { type: i0.Output, args: ["selectedIndexChange"] }], change: [{ type: i0.Output, args: ["change"] }], buttons: [{ type: i0.ViewChildren, args: ['segBtn', { isSignal: true }] }] } });
2969
2975
 
2970
2976
  class MozAccordionHeaderComponent {
2971
2977
  tpl = viewChild('tpl', { ...(ngDevMode ? { debugName: "tpl" } : /* istanbul ignore next */ {}), read: TemplateRef });
@@ -11433,7 +11439,8 @@ class TreeStateService {
11433
11439
  this.toggleExpanded(node.id);
11434
11440
  if (!wasExpanded) {
11435
11441
  const loadFn = this.loadChildrenFn();
11436
- if (loadFn && node.children === undefined) {
11442
+ const resolved = this.findNode(node.id) ?? node;
11443
+ if (loadFn && resolved.children === undefined) {
11437
11444
  this.addLoading(node.id);
11438
11445
  return loadFn(node).pipe(take(1), tap((children) => {
11439
11446
  this.patchChildren(node.id, children);
@@ -11495,6 +11502,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImpor
11495
11502
  }] });
11496
11503
 
11497
11504
  class TreeSelectionService {
11505
+ stateService = inject(TreeStateService);
11498
11506
  selectedIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "selectedIds" }] : /* istanbul ignore next */ []));
11499
11507
  selectionMode = signal('none', ...(ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []));
11500
11508
  rootNodes = signal([], ...(ngDevMode ? [{ debugName: "rootNodes" }] : /* istanbul ignore next */ []));
@@ -11515,40 +11523,62 @@ class TreeSelectionService {
11515
11523
  return true;
11516
11524
  return ancestors.some((a) => a.disabled);
11517
11525
  }
11526
+ /**
11527
+ * A node is indeterminate when it has loaded children AND
11528
+ * some (but not all) of its leaf-level descendants are selected.
11529
+ */
11518
11530
  isIndeterminate(node) {
11519
- if (node.children === undefined)
11531
+ const resolved = this._resolveNode(node);
11532
+ const leaves = this._collectLeafIds(resolved);
11533
+ if (leaves.length === 0)
11520
11534
  return false;
11521
- if (node.children.length === 0)
11522
- return false;
11523
- const enabledIds = this._collectEnabledDescendantIds(node);
11524
- if (enabledIds.length === 0)
11525
- return false;
11526
- const selectedCount = enabledIds.filter((id) => this.selectedIds().has(id)).length;
11527
- return selectedCount > 0 && selectedCount < enabledIds.length;
11535
+ const selectedCount = leaves.filter((id) => this.selectedIds().has(id)).length;
11536
+ return selectedCount > 0 && selectedCount < leaves.length;
11528
11537
  }
11538
+ /**
11539
+ * A node is checked when:
11540
+ * - Leaf node (no children or children not loaded): its ID is in selectedIds
11541
+ * - Parent with loaded children: ALL leaf descendants are selected
11542
+ */
11529
11543
  isCheckedComputed(node) {
11530
11544
  if (this.selectionMode() === 'radio') {
11531
11545
  return this.selectedIds().has(node.id);
11532
11546
  }
11533
- if (node.children === undefined || node.children.length === 0) {
11547
+ const resolved = this._resolveNode(node);
11548
+ const leaves = this._collectLeafIds(resolved);
11549
+ // Node is a leaf or has no loaded children
11550
+ if (leaves.length === 0) {
11534
11551
  return this.selectedIds().has(node.id);
11535
11552
  }
11536
- const enabledIds = this._collectEnabledDescendantIds(node);
11537
- if (enabledIds.length === 0)
11538
- return this.selectedIds().has(node.id);
11539
- return enabledIds.every((id) => this.selectedIds().has(id));
11553
+ return leaves.every((id) => this.selectedIds().has(id));
11540
11554
  }
11555
+ /**
11556
+ * Select a node: add all leaf descendant IDs (or the node's own ID if leaf).
11557
+ */
11541
11558
  selectNode(node) {
11542
11559
  if (this.selectionMode() === 'radio') {
11543
11560
  this.selectedIds.set(new Set([node.id]));
11544
11561
  return;
11545
11562
  }
11563
+ const resolved = this._resolveNode(node);
11546
11564
  this.selectedIds.update((current) => {
11547
11565
  const next = new Set(current);
11548
- this._collectEnabledIds(node).forEach((id) => next.add(id));
11566
+ const leaves = this._collectLeafIds(resolved);
11567
+ if (leaves.length === 0) {
11568
+ // Node is a leaf
11569
+ if (!node.disabled)
11570
+ next.add(node.id);
11571
+ }
11572
+ else {
11573
+ leaves.forEach((id) => next.add(id));
11574
+ }
11549
11575
  return next;
11550
11576
  });
11551
11577
  }
11578
+ /**
11579
+ * Deselect a node: remove all leaf descendant IDs (or the node's own ID if leaf).
11580
+ * Also remove the node's own ID in case it's in the set from external sources.
11581
+ */
11552
11582
  deselectNode(node) {
11553
11583
  if (this.selectionMode() === 'radio') {
11554
11584
  this.selectedIds.update((current) => {
@@ -11558,9 +11588,13 @@ class TreeSelectionService {
11558
11588
  });
11559
11589
  return;
11560
11590
  }
11591
+ const resolved = this._resolveNode(node);
11561
11592
  this.selectedIds.update((current) => {
11562
11593
  const next = new Set(current);
11563
- this._collectEnabledIds(node).forEach((id) => next.delete(id));
11594
+ // Always remove the node itself
11595
+ next.delete(node.id);
11596
+ // Remove all known descendants (leaf + parents)
11597
+ this._collectAllDescendantIds(resolved).forEach((id) => next.delete(id));
11564
11598
  return next;
11565
11599
  });
11566
11600
  }
@@ -11572,30 +11606,83 @@ class TreeSelectionService {
11572
11606
  this.selectNode(node);
11573
11607
  }
11574
11608
  }
11575
- _collectEnabledIds(node) {
11576
- if (node.disabled)
11609
+ /**
11610
+ * Called when children are loaded for a node.
11611
+ * - If parent ID was in selectedIds: replace it with children's leaf IDs (propagate down).
11612
+ * - If parent ID was NOT in selectedIds: remove any orphan children IDs that
11613
+ * may have been left from a prior bulk operation (e.g. Select All → deselect parent).
11614
+ */
11615
+ propagateOnChildrenLoaded(parentId) {
11616
+ const parent = this._resolveNode({ id: parentId });
11617
+ if (!parent || !parent.children || parent.children.length === 0)
11618
+ return;
11619
+ const selected = this.selectedIds();
11620
+ const next = new Set(selected);
11621
+ if (selected.has(parentId)) {
11622
+ // Parent was selected → replace with leaf children
11623
+ next.delete(parentId);
11624
+ for (const child of parent.children) {
11625
+ if (!child.disabled) {
11626
+ const childLeaves = this._collectLeafIds(this._resolveNode(child));
11627
+ if (childLeaves.length === 0) {
11628
+ next.add(child.id);
11629
+ }
11630
+ else {
11631
+ childLeaves.forEach((id) => next.add(id));
11632
+ }
11633
+ }
11634
+ }
11635
+ }
11636
+ else {
11637
+ // Parent was NOT selected → clean up any orphan descendant IDs
11638
+ this._collectAllDescendantIds(parent).forEach((id) => next.delete(id));
11639
+ }
11640
+ this.selectedIds.set(next);
11641
+ }
11642
+ /**
11643
+ * Collect leaf-level IDs (terminal nodes that have no loaded children).
11644
+ * Returns empty array if the node itself is a leaf.
11645
+ */
11646
+ _collectLeafIds(node) {
11647
+ const resolved = this._resolveNode(node);
11648
+ if (!resolved.children || resolved.children.length === 0) {
11577
11649
  return [];
11578
- const ids = [node.id];
11579
- if (node.children) {
11580
- for (const child of node.children) {
11581
- ids.push(...this._collectEnabledIds(child));
11650
+ }
11651
+ const ids = [];
11652
+ for (const child of resolved.children) {
11653
+ if (child.disabled)
11654
+ continue;
11655
+ const resolvedChild = this._resolveNode(child);
11656
+ const childLeaves = this._collectLeafIds(resolvedChild);
11657
+ if (childLeaves.length === 0) {
11658
+ // This child is a leaf
11659
+ ids.push(resolvedChild.id);
11660
+ }
11661
+ else {
11662
+ ids.push(...childLeaves);
11582
11663
  }
11583
11664
  }
11584
11665
  return ids;
11585
11666
  }
11586
- _collectEnabledDescendantIds(node) {
11667
+ /**
11668
+ * Collect ALL descendant IDs (both parents and leaves) for thorough cleanup on deselect.
11669
+ */
11670
+ _collectAllDescendantIds(node) {
11671
+ const resolved = this._resolveNode(node);
11587
11672
  const ids = [];
11588
- if (!node.children)
11673
+ if (!resolved.children)
11589
11674
  return ids;
11590
- for (const child of node.children) {
11591
- if (!child.disabled) {
11592
- ids.push(child.id);
11593
- ids.push(...this._collectEnabledDescendantIds(child));
11594
- }
11675
+ for (const child of resolved.children) {
11676
+ const resolvedChild = this._resolveNode(child);
11677
+ ids.push(resolvedChild.id);
11678
+ ids.push(...this._collectAllDescendantIds(resolvedChild));
11595
11679
  }
11596
11680
  return ids;
11597
11681
  }
11598
11682
  allSelectedIds = computed(() => new Set(this.selectedIds()), ...(ngDevMode ? [{ debugName: "allSelectedIds" }] : /* istanbul ignore next */ []));
11683
+ _resolveNode(node) {
11684
+ return this.stateService.findNode(node.id) ?? node;
11685
+ }
11599
11686
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: TreeSelectionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
11600
11687
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: TreeSelectionService });
11601
11688
  }
@@ -11826,7 +11913,7 @@ class MozTreeNodeComponent {
11826
11913
  return null;
11827
11914
  }
11828
11915
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: MozTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
11829
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.6", type: MozTreeNodeComponent, isStandalone: true, selector: "moz-tree-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, depth: { classPropertyName: "depth", publicName: "depth", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, indentSize: { classPropertyName: "indentSize", publicName: "indentSize", isSignal: true, isRequired: false, transformFunction: null }, nodeTemplate: { classPropertyName: "nodeTemplate", publicName: "nodeTemplate", isSignal: true, isRequired: false, transformFunction: null }, nodeTemplates: { classPropertyName: "nodeTemplates", publicName: "nodeTemplates", isSignal: true, isRequired: false, transformFunction: null }, loadChildren: { classPropertyName: "loadChildren", publicName: "loadChildren", isSignal: true, isRequired: false, transformFunction: null }, ancestors: { classPropertyName: "ancestors", publicName: "ancestors", isSignal: true, isRequired: false, transformFunction: null }, flat: { classPropertyName: "flat", publicName: "flat", isSignal: true, isRequired: false, transformFunction: null }, noResultText: { classPropertyName: "noResultText", publicName: "noResultText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { expandChange: "expandChange", selectionChange: "selectionChange" }, host: { styleAttribute: "display: block" }, ngImport: i0, template: "<li\n class=\"tree-node\"\n [class.tree-node--selected]=\"isSelected()\"\n [class.tree-node--disabled]=\"isDisabled()\"\n [class.tree-node--focused]=\"isFocused()\"\n role=\"treeitem\"\n [id]=\"'tree-node-' + node().id\"\n [attr.aria-level]=\"depth() + 1\"\n [attr.aria-expanded]=\"hasChildren() ? isExpanded() : null\"\n [attr.aria-selected]=\"selectionMode() !== 'none' ? isSelected() : null\"\n [attr.aria-disabled]=\"isDisabled() || null\"\n [attr.data-tree-node-id]=\"node().id\"\n [tabindex]=\"isFocused() ? 0 : -1\"\n>\n <div\n class=\"tree-node__header\"\n [class.tree-node__header--expandable]=\"hasChildren()\"\n (click)=\"onHeaderClick()\"\n >\n <div class=\"tree-node__indent\" [style.width.px]=\"indentPx()\"></div>\n\n <span class=\"tree-node__chevron\" [class.tree-node__chevron--leaf]=\"!hasChildren()\">\n @if (hasChildren()) { @if (isExpanded()) {\n <ChevronDown20 />\n } @else {\n <ChevronRight20 />\n } }\n </span>\n\n <div class=\"tree-node__content\">\n @if (resolvedTemplate()) {\n <ng-container\n [ngTemplateOutlet]=\"resolvedTemplate()!\"\n [ngTemplateOutletContext]=\"templateContext()\"\n />\n } @else {\n <span class=\"tree-node__label\">{{ node().id }}</span>\n }\n </div>\n\n @if (selectionMode() === 'checkbox') {\n <moz-checkbox\n class=\"tree-node__selection\"\n [id]=\"'tree-checkbox-' + node().id\"\n [indeterminate]=\"isIndeterminate()\"\n [disabled]=\"isDisabled()\"\n [ngModel]=\"isSelected()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onCheckboxChange($event)\"\n />\n } @else if (selectionMode() === 'radio') {\n <moz-radio\n class=\"tree-node__selection\"\n [id]=\"'tree-radio-' + node().id\"\n [name]=\"radioName\"\n [disabled]=\"isDisabled()\"\n [ngModel]=\"isSelected()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onRadioChange($event)\"\n />\n }\n </div>\n\n @if (isExpanded() && !flat()) {\n <ul class=\"tree-node__children\" role=\"group\">\n @if (isLoading()) {\n <li class=\"tree-node__loading\" role=\"presentation\">\n <moz-loader size=\"s\" [appearance]=\"'accent'\" />\n </li>\n } @else { @if (resolvedChildren().length === 0) {\n <li class=\"tree-node__empty\" role=\"presentation\">\n <span>{{ noResultText() }}</span>\n </li>\n } @for (child of resolvedChildren(); track child.id) {\n <moz-tree-node\n [node]=\"child\"\n [depth]=\"depth() + 1\"\n [selectionMode]=\"selectionMode()\"\n [indentSize]=\"indentSize()\"\n [nodeTemplate]=\"nodeTemplate()\"\n [nodeTemplates]=\"nodeTemplates()\"\n [loadChildren]=\"loadChildren()\"\n [ancestors]=\"ancestorsWithSelf()\"\n (expandChange)=\"expandChange.emit($event)\"\n (selectionChange)=\"selectionChange.emit($event)\"\n />\n } }\n </ul>\n }\n</li>\n", styles: [".tree-node{list-style:none;margin:0;padding:0;outline:none;display:flex;flex-direction:column;gap:4px}.tree-node--selected>.tree-node__header{background:var(--color-background-accent);border-radius:var(--border-radius-m, 8px)}.tree-node--disabled{pointer-events:none;color:var(--color-text-disabled)}.tree-node--disabled>.tree-node__header{opacity:.5}.tree-node--focused>.tree-node__header,.tree-node:focus-visible>.tree-node__header{outline:2px solid var(--color-background-accent-inverse);outline-offset:-2px;border-radius:var(--border-radius-m, 8px)}.tree-node__header{display:flex;align-items:center;width:100%;min-height:57px;padding:4px 0;border-radius:var(--border-radius-m, 8px);transition:background .15s ease;-webkit-user-select:none;user-select:none}.tree-node__header--expandable{cursor:pointer}.tree-node__header:hover{background:var(--color-background-secondary)}.tree-node--selected>.tree-node__header:hover{background:var(--color-background-accent)}.tree-node__indent{flex-shrink:0}.tree-node__chevron{display:flex;align-items:center;justify-content:center;flex-shrink:0;padding-left:16px;color:var(--color-text-primary)}.tree-node__chevron--leaf{width:28px;visibility:hidden}.tree-node__content{flex:1;min-width:0;display:flex;align-items:center;padding:4px 8px}.tree-node__selection{flex-shrink:0;margin-left:auto;margin-right:8px}.tree-node__label{font-size:var(--font-size-m, 16px);color:var(--color-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-node__children{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:4px}.tree-node__loading{display:flex;align-items:center;justify-content:center;padding:8px 16px}.tree-node__empty{display:flex;align-items:center;padding:8px 16px;font-size:var(--font-size-s, 14px);color:var(--color-text-secondary)}\n"], dependencies: [{ kind: "component", type: MozTreeNodeComponent, selector: "moz-tree-node", inputs: ["node", "depth", "selectionMode", "indentSize", "nodeTemplate", "nodeTemplates", "loadChildren", "ancestors", "flat", "noResultText"], outputs: ["expandChange", "selectionChange"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MozCheckboxComponent, selector: "moz-checkbox", inputs: ["id", "name", "label", "indeterminate", "isInvalid", "disabled", "indented"] }, { kind: "component", type: MozRadioComponent, selector: "moz-radio", inputs: ["id", "name", "label", "isInvalid", "disabled"] }, { kind: "component", type: MozLoaderComponent, selector: "moz-loader", inputs: ["appearance", "size", "text"] }, { kind: "component", type: ChevronDown20, selector: "ChevronDown20", inputs: ["hostClass"] }, { kind: "component", type: ChevronRight20, selector: "ChevronRight20", inputs: ["hostClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11916
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.6", type: MozTreeNodeComponent, isStandalone: true, selector: "moz-tree-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, depth: { classPropertyName: "depth", publicName: "depth", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, indentSize: { classPropertyName: "indentSize", publicName: "indentSize", isSignal: true, isRequired: false, transformFunction: null }, nodeTemplate: { classPropertyName: "nodeTemplate", publicName: "nodeTemplate", isSignal: true, isRequired: false, transformFunction: null }, nodeTemplates: { classPropertyName: "nodeTemplates", publicName: "nodeTemplates", isSignal: true, isRequired: false, transformFunction: null }, loadChildren: { classPropertyName: "loadChildren", publicName: "loadChildren", isSignal: true, isRequired: false, transformFunction: null }, ancestors: { classPropertyName: "ancestors", publicName: "ancestors", isSignal: true, isRequired: false, transformFunction: null }, flat: { classPropertyName: "flat", publicName: "flat", isSignal: true, isRequired: false, transformFunction: null }, noResultText: { classPropertyName: "noResultText", publicName: "noResultText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { expandChange: "expandChange", selectionChange: "selectionChange" }, host: { styleAttribute: "display: block" }, ngImport: i0, template: "<li\n class=\"tree-node\"\n [class.tree-node--selected]=\"isSelected()\"\n [class.tree-node--disabled]=\"isDisabled()\"\n [class.tree-node--focused]=\"isFocused()\"\n role=\"treeitem\"\n [id]=\"'tree-node-' + node().id\"\n [attr.aria-level]=\"depth() + 1\"\n [attr.aria-expanded]=\"hasChildren() ? isExpanded() : null\"\n [attr.aria-selected]=\"selectionMode() !== 'none' ? isSelected() : null\"\n [attr.aria-disabled]=\"isDisabled() || null\"\n [attr.data-tree-node-id]=\"node().id\"\n [tabindex]=\"isFocused() ? 0 : -1\"\n>\n <div\n class=\"tree-node__header\"\n [class.tree-node__header--expandable]=\"hasChildren()\"\n (click)=\"onHeaderClick()\"\n >\n <div class=\"tree-node__indent\" [style.width.px]=\"indentPx()\"></div>\n\n <div class=\"tree-node__row\">\n <span class=\"tree-node__chevron\" [class.tree-node__chevron--leaf]=\"!hasChildren()\">\n @if (hasChildren()) { @if (isExpanded()) {\n <ChevronDown20 />\n } @else {\n <ChevronRight20 />\n } }\n </span>\n\n <div class=\"tree-node__content\">\n @if (resolvedTemplate()) {\n <ng-container\n [ngTemplateOutlet]=\"resolvedTemplate()!\"\n [ngTemplateOutletContext]=\"templateContext()\"\n />\n } @else {\n <span class=\"tree-node__label\">{{ node().id }}</span>\n }\n </div>\n\n @if (selectionMode() === 'checkbox') {\n <moz-checkbox\n class=\"tree-node__selection\"\n [id]=\"'tree-checkbox-' + node().id\"\n [indeterminate]=\"isIndeterminate()\"\n [disabled]=\"isDisabled()\"\n [ngModel]=\"isSelected()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onCheckboxChange($event)\"\n />\n } @else if (selectionMode() === 'radio') {\n <moz-radio\n class=\"tree-node__selection\"\n [id]=\"'tree-radio-' + node().id\"\n [name]=\"radioName\"\n [disabled]=\"isDisabled()\"\n [ngModel]=\"isSelected()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onRadioChange($event)\"\n />\n }\n </div>\n </div>\n\n @if (isExpanded() && !flat()) {\n <ul class=\"tree-node__children\" role=\"group\">\n @if (isLoading()) {\n <li class=\"tree-node__loading\" role=\"presentation\">\n <moz-loader size=\"s\" [appearance]=\"'accent'\" />\n </li>\n } @else { @if (resolvedChildren().length === 0) {\n <li class=\"tree-node__empty\" role=\"presentation\">\n <span>{{ noResultText() }}</span>\n </li>\n } @for (child of resolvedChildren(); track child.id) {\n <moz-tree-node\n [node]=\"child\"\n [depth]=\"depth() + 1\"\n [selectionMode]=\"selectionMode()\"\n [indentSize]=\"indentSize()\"\n [nodeTemplate]=\"nodeTemplate()\"\n [nodeTemplates]=\"nodeTemplates()\"\n [loadChildren]=\"loadChildren()\"\n [ancestors]=\"ancestorsWithSelf()\"\n (expandChange)=\"expandChange.emit($event)\"\n (selectionChange)=\"selectionChange.emit($event)\"\n />\n } }\n </ul>\n }\n</li>\n", styles: [".tree-node{list-style:none;margin:0;padding:0;outline:none;display:flex;flex-direction:column;gap:4px}.tree-node--selected>.tree-node__header>.tree-node__row{background:var(--color-background-accent);border-radius:var(--border-radius-m, 8px)}.tree-node--disabled{pointer-events:none;color:var(--color-text-disabled)}.tree-node--disabled>.tree-node__header{opacity:.5}.tree-node--focused>.tree-node__header>.tree-node__row,.tree-node:focus-visible>.tree-node__header>.tree-node__row{outline:2px solid var(--color-background-accent-inverse);outline-offset:-2px;border-radius:var(--border-radius-m, 8px)}.tree-node__header{display:flex;align-items:center;width:100%;min-height:57px;-webkit-user-select:none;user-select:none}.tree-node__header--expandable{cursor:pointer}.tree-node__row{display:flex;align-items:center;height:57px;flex:1;min-width:0;border-radius:var(--border-radius-m, 8px);transition:background .15s ease}.tree-node__header:hover>.tree-node__row{background:var(--color-background-secondary)}.tree-node--selected>.tree-node__header:hover>.tree-node__row{background:var(--color-background-accent)}.tree-node__indent{flex-shrink:0}.tree-node__chevron{display:flex;align-items:center;justify-content:center;flex-shrink:0;padding-left:16px;color:var(--color-text-primary)}.tree-node__chevron--leaf{width:0;padding-left:0;overflow:hidden}.tree-node__content{flex:1;min-width:0;display:flex;align-items:center;padding:4px 8px}.tree-node__selection{flex-shrink:0;margin-left:auto;margin-right:8px}.tree-node__label{font-size:var(--font-size-m, 16px);color:var(--color-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-node__children{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:4px}.tree-node__loading{display:flex;align-items:center;justify-content:center;padding:8px 16px}.tree-node__empty{display:flex;align-items:center;padding:8px 16px;font-size:var(--font-size-s, 14px);color:var(--color-text-secondary)}\n"], dependencies: [{ kind: "component", type: MozTreeNodeComponent, selector: "moz-tree-node", inputs: ["node", "depth", "selectionMode", "indentSize", "nodeTemplate", "nodeTemplates", "loadChildren", "ancestors", "flat", "noResultText"], outputs: ["expandChange", "selectionChange"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MozCheckboxComponent, selector: "moz-checkbox", inputs: ["id", "name", "label", "indeterminate", "isInvalid", "disabled", "indented"] }, { kind: "component", type: MozRadioComponent, selector: "moz-radio", inputs: ["id", "name", "label", "isInvalid", "disabled"] }, { kind: "component", type: MozLoaderComponent, selector: "moz-loader", inputs: ["appearance", "size", "text"] }, { kind: "component", type: ChevronDown20, selector: "ChevronDown20", inputs: ["hostClass"] }, { kind: "component", type: ChevronRight20, selector: "ChevronRight20", inputs: ["hostClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11830
11917
  }
11831
11918
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: MozTreeNodeComponent, decorators: [{
11832
11919
  type: Component,
@@ -11838,7 +11925,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImpor
11838
11925
  MozLoaderComponent,
11839
11926
  ChevronDown20,
11840
11927
  ChevronRight20,
11841
- ], template: "<li\n class=\"tree-node\"\n [class.tree-node--selected]=\"isSelected()\"\n [class.tree-node--disabled]=\"isDisabled()\"\n [class.tree-node--focused]=\"isFocused()\"\n role=\"treeitem\"\n [id]=\"'tree-node-' + node().id\"\n [attr.aria-level]=\"depth() + 1\"\n [attr.aria-expanded]=\"hasChildren() ? isExpanded() : null\"\n [attr.aria-selected]=\"selectionMode() !== 'none' ? isSelected() : null\"\n [attr.aria-disabled]=\"isDisabled() || null\"\n [attr.data-tree-node-id]=\"node().id\"\n [tabindex]=\"isFocused() ? 0 : -1\"\n>\n <div\n class=\"tree-node__header\"\n [class.tree-node__header--expandable]=\"hasChildren()\"\n (click)=\"onHeaderClick()\"\n >\n <div class=\"tree-node__indent\" [style.width.px]=\"indentPx()\"></div>\n\n <span class=\"tree-node__chevron\" [class.tree-node__chevron--leaf]=\"!hasChildren()\">\n @if (hasChildren()) { @if (isExpanded()) {\n <ChevronDown20 />\n } @else {\n <ChevronRight20 />\n } }\n </span>\n\n <div class=\"tree-node__content\">\n @if (resolvedTemplate()) {\n <ng-container\n [ngTemplateOutlet]=\"resolvedTemplate()!\"\n [ngTemplateOutletContext]=\"templateContext()\"\n />\n } @else {\n <span class=\"tree-node__label\">{{ node().id }}</span>\n }\n </div>\n\n @if (selectionMode() === 'checkbox') {\n <moz-checkbox\n class=\"tree-node__selection\"\n [id]=\"'tree-checkbox-' + node().id\"\n [indeterminate]=\"isIndeterminate()\"\n [disabled]=\"isDisabled()\"\n [ngModel]=\"isSelected()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onCheckboxChange($event)\"\n />\n } @else if (selectionMode() === 'radio') {\n <moz-radio\n class=\"tree-node__selection\"\n [id]=\"'tree-radio-' + node().id\"\n [name]=\"radioName\"\n [disabled]=\"isDisabled()\"\n [ngModel]=\"isSelected()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onRadioChange($event)\"\n />\n }\n </div>\n\n @if (isExpanded() && !flat()) {\n <ul class=\"tree-node__children\" role=\"group\">\n @if (isLoading()) {\n <li class=\"tree-node__loading\" role=\"presentation\">\n <moz-loader size=\"s\" [appearance]=\"'accent'\" />\n </li>\n } @else { @if (resolvedChildren().length === 0) {\n <li class=\"tree-node__empty\" role=\"presentation\">\n <span>{{ noResultText() }}</span>\n </li>\n } @for (child of resolvedChildren(); track child.id) {\n <moz-tree-node\n [node]=\"child\"\n [depth]=\"depth() + 1\"\n [selectionMode]=\"selectionMode()\"\n [indentSize]=\"indentSize()\"\n [nodeTemplate]=\"nodeTemplate()\"\n [nodeTemplates]=\"nodeTemplates()\"\n [loadChildren]=\"loadChildren()\"\n [ancestors]=\"ancestorsWithSelf()\"\n (expandChange)=\"expandChange.emit($event)\"\n (selectionChange)=\"selectionChange.emit($event)\"\n />\n } }\n </ul>\n }\n</li>\n", styles: [".tree-node{list-style:none;margin:0;padding:0;outline:none;display:flex;flex-direction:column;gap:4px}.tree-node--selected>.tree-node__header{background:var(--color-background-accent);border-radius:var(--border-radius-m, 8px)}.tree-node--disabled{pointer-events:none;color:var(--color-text-disabled)}.tree-node--disabled>.tree-node__header{opacity:.5}.tree-node--focused>.tree-node__header,.tree-node:focus-visible>.tree-node__header{outline:2px solid var(--color-background-accent-inverse);outline-offset:-2px;border-radius:var(--border-radius-m, 8px)}.tree-node__header{display:flex;align-items:center;width:100%;min-height:57px;padding:4px 0;border-radius:var(--border-radius-m, 8px);transition:background .15s ease;-webkit-user-select:none;user-select:none}.tree-node__header--expandable{cursor:pointer}.tree-node__header:hover{background:var(--color-background-secondary)}.tree-node--selected>.tree-node__header:hover{background:var(--color-background-accent)}.tree-node__indent{flex-shrink:0}.tree-node__chevron{display:flex;align-items:center;justify-content:center;flex-shrink:0;padding-left:16px;color:var(--color-text-primary)}.tree-node__chevron--leaf{width:28px;visibility:hidden}.tree-node__content{flex:1;min-width:0;display:flex;align-items:center;padding:4px 8px}.tree-node__selection{flex-shrink:0;margin-left:auto;margin-right:8px}.tree-node__label{font-size:var(--font-size-m, 16px);color:var(--color-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-node__children{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:4px}.tree-node__loading{display:flex;align-items:center;justify-content:center;padding:8px 16px}.tree-node__empty{display:flex;align-items:center;padding:8px 16px;font-size:var(--font-size-s, 14px);color:var(--color-text-secondary)}\n"] }]
11928
+ ], template: "<li\n class=\"tree-node\"\n [class.tree-node--selected]=\"isSelected()\"\n [class.tree-node--disabled]=\"isDisabled()\"\n [class.tree-node--focused]=\"isFocused()\"\n role=\"treeitem\"\n [id]=\"'tree-node-' + node().id\"\n [attr.aria-level]=\"depth() + 1\"\n [attr.aria-expanded]=\"hasChildren() ? isExpanded() : null\"\n [attr.aria-selected]=\"selectionMode() !== 'none' ? isSelected() : null\"\n [attr.aria-disabled]=\"isDisabled() || null\"\n [attr.data-tree-node-id]=\"node().id\"\n [tabindex]=\"isFocused() ? 0 : -1\"\n>\n <div\n class=\"tree-node__header\"\n [class.tree-node__header--expandable]=\"hasChildren()\"\n (click)=\"onHeaderClick()\"\n >\n <div class=\"tree-node__indent\" [style.width.px]=\"indentPx()\"></div>\n\n <div class=\"tree-node__row\">\n <span class=\"tree-node__chevron\" [class.tree-node__chevron--leaf]=\"!hasChildren()\">\n @if (hasChildren()) { @if (isExpanded()) {\n <ChevronDown20 />\n } @else {\n <ChevronRight20 />\n } }\n </span>\n\n <div class=\"tree-node__content\">\n @if (resolvedTemplate()) {\n <ng-container\n [ngTemplateOutlet]=\"resolvedTemplate()!\"\n [ngTemplateOutletContext]=\"templateContext()\"\n />\n } @else {\n <span class=\"tree-node__label\">{{ node().id }}</span>\n }\n </div>\n\n @if (selectionMode() === 'checkbox') {\n <moz-checkbox\n class=\"tree-node__selection\"\n [id]=\"'tree-checkbox-' + node().id\"\n [indeterminate]=\"isIndeterminate()\"\n [disabled]=\"isDisabled()\"\n [ngModel]=\"isSelected()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onCheckboxChange($event)\"\n />\n } @else if (selectionMode() === 'radio') {\n <moz-radio\n class=\"tree-node__selection\"\n [id]=\"'tree-radio-' + node().id\"\n [name]=\"radioName\"\n [disabled]=\"isDisabled()\"\n [ngModel]=\"isSelected()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onRadioChange($event)\"\n />\n }\n </div>\n </div>\n\n @if (isExpanded() && !flat()) {\n <ul class=\"tree-node__children\" role=\"group\">\n @if (isLoading()) {\n <li class=\"tree-node__loading\" role=\"presentation\">\n <moz-loader size=\"s\" [appearance]=\"'accent'\" />\n </li>\n } @else { @if (resolvedChildren().length === 0) {\n <li class=\"tree-node__empty\" role=\"presentation\">\n <span>{{ noResultText() }}</span>\n </li>\n } @for (child of resolvedChildren(); track child.id) {\n <moz-tree-node\n [node]=\"child\"\n [depth]=\"depth() + 1\"\n [selectionMode]=\"selectionMode()\"\n [indentSize]=\"indentSize()\"\n [nodeTemplate]=\"nodeTemplate()\"\n [nodeTemplates]=\"nodeTemplates()\"\n [loadChildren]=\"loadChildren()\"\n [ancestors]=\"ancestorsWithSelf()\"\n (expandChange)=\"expandChange.emit($event)\"\n (selectionChange)=\"selectionChange.emit($event)\"\n />\n } }\n </ul>\n }\n</li>\n", styles: [".tree-node{list-style:none;margin:0;padding:0;outline:none;display:flex;flex-direction:column;gap:4px}.tree-node--selected>.tree-node__header>.tree-node__row{background:var(--color-background-accent);border-radius:var(--border-radius-m, 8px)}.tree-node--disabled{pointer-events:none;color:var(--color-text-disabled)}.tree-node--disabled>.tree-node__header{opacity:.5}.tree-node--focused>.tree-node__header>.tree-node__row,.tree-node:focus-visible>.tree-node__header>.tree-node__row{outline:2px solid var(--color-background-accent-inverse);outline-offset:-2px;border-radius:var(--border-radius-m, 8px)}.tree-node__header{display:flex;align-items:center;width:100%;min-height:57px;-webkit-user-select:none;user-select:none}.tree-node__header--expandable{cursor:pointer}.tree-node__row{display:flex;align-items:center;height:57px;flex:1;min-width:0;border-radius:var(--border-radius-m, 8px);transition:background .15s ease}.tree-node__header:hover>.tree-node__row{background:var(--color-background-secondary)}.tree-node--selected>.tree-node__header:hover>.tree-node__row{background:var(--color-background-accent)}.tree-node__indent{flex-shrink:0}.tree-node__chevron{display:flex;align-items:center;justify-content:center;flex-shrink:0;padding-left:16px;color:var(--color-text-primary)}.tree-node__chevron--leaf{width:0;padding-left:0;overflow:hidden}.tree-node__content{flex:1;min-width:0;display:flex;align-items:center;padding:4px 8px}.tree-node__selection{flex-shrink:0;margin-left:auto;margin-right:8px}.tree-node__label{font-size:var(--font-size-m, 16px);color:var(--color-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-node__children{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:4px}.tree-node__loading{display:flex;align-items:center;justify-content:center;padding:8px 16px}.tree-node__empty{display:flex;align-items:center;padding:8px 16px;font-size:var(--font-size-s, 14px);color:var(--color-text-secondary)}\n"] }]
11842
11929
  }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }], depth: [{ type: i0.Input, args: [{ isSignal: true, alias: "depth", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], indentSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "indentSize", required: false }] }], nodeTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodeTemplate", required: false }] }], nodeTemplates: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodeTemplates", required: false }] }], loadChildren: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadChildren", required: false }] }], ancestors: [{ type: i0.Input, args: [{ isSignal: true, alias: "ancestors", required: false }] }], flat: [{ type: i0.Input, args: [{ isSignal: true, alias: "flat", required: false }] }], noResultText: [{ type: i0.Input, args: [{ isSignal: true, alias: "noResultText", required: false }] }], expandChange: [{ type: i0.Output, args: ["expandChange"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }] } });
11843
11930
 
11844
11931
  class MozTreeNodeTemplateDirective {
@@ -11907,9 +11994,44 @@ class MozTreeComponent {
11907
11994
  effect(() => {
11908
11995
  this.stateService.loadChildrenFn.set(this.loadChildren());
11909
11996
  });
11997
+ // When children load for a node whose ID is in selectedIds,
11998
+ // replace the parent ID with its children's leaf IDs.
11999
+ let prevChildMap = new Map();
12000
+ effect(() => {
12001
+ const nodes = this.stateService.internalNodes();
12002
+ if (this.selectionService.selectionMode() !== 'checkbox')
12003
+ return;
12004
+ const newChildMap = new Map();
12005
+ const newlyLoadedParentIds = [];
12006
+ const visit = (list) => {
12007
+ for (const n of list) {
12008
+ const hadChildren = prevChildMap.get(n.id) ?? false;
12009
+ const hasChildrenNow = !!(n.children && n.children.length > 0);
12010
+ newChildMap.set(n.id, hasChildrenNow);
12011
+ if (!hadChildren && hasChildrenNow) {
12012
+ newlyLoadedParentIds.push(n.id);
12013
+ }
12014
+ if (n.children)
12015
+ visit(n.children);
12016
+ }
12017
+ };
12018
+ visit(nodes);
12019
+ prevChildMap = newChildMap;
12020
+ for (const parentId of newlyLoadedParentIds) {
12021
+ this.selectionService.propagateOnChildrenLoaded(parentId);
12022
+ }
12023
+ if (newlyLoadedParentIds.length > 0) {
12024
+ this.selectionChange.emit(this.selectionService.allSelectedIds());
12025
+ }
12026
+ });
11910
12027
  }
11911
12028
  onTreeKeydown(event) {
12029
+ const prevSelection = this.selectionService.allSelectedIds();
11912
12030
  this.keyboardService.handleKeydown(event);
12031
+ const newSelection = this.selectionService.allSelectedIds();
12032
+ if (prevSelection !== newSelection) {
12033
+ this.selectionChange.emit(newSelection);
12034
+ }
11913
12035
  }
11914
12036
  onTreeFocus() {
11915
12037
  this.keyboardService.initFocus();