@truenas/ui-components 0.3.19 → 0.3.21
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.
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { Overlay, OverlayPositionBuilder, OverlayModule } from '@angular/cdk/overlay';
|
|
2
2
|
import { TemplatePortal, ComponentPortal, PortalModule } from '@angular/cdk/portal';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
|
-
import { input, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, inject, Injector, InjectionToken, Renderer2, ElementRef, effect, Directive, ViewContainerRef, output, viewChild, signal, untracked, isDevMode, forwardRef, model, afterNextRender, Injectable, contentChildren, Pipe, HostListener, linkedSignal, contentChild, TemplateRef, ChangeDetectorRef, DestroyRef, isSignal, IterableDiffers, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID } from '@angular/core';
|
|
4
|
+
import { input, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, inject, Injector, InjectionToken, Renderer2, ElementRef, effect, Directive, ViewContainerRef, output, viewChild, signal, untracked, isDevMode, forwardRef, model, afterNextRender, Injectable, contentChildren, Pipe, HostListener, linkedSignal, contentChild, TemplateRef, ChangeDetectorRef, DestroyRef, isSignal, IterableDiffers, booleanAttribute, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID } from '@angular/core';
|
|
5
5
|
import * as i1$3 from '@angular/forms';
|
|
6
6
|
import { NgControl, NG_VALUE_ACCESSOR, FormsModule, Validators } from '@angular/forms';
|
|
7
7
|
import { ComponentHarness, HarnessPredicate, TestKey } from '@angular/cdk/testing';
|
|
8
8
|
import * as i1 from '@angular/cdk/a11y';
|
|
9
9
|
import { A11yModule, FocusMonitor } from '@angular/cdk/a11y';
|
|
10
10
|
import * as i1$2 from '@angular/common';
|
|
11
|
-
import { DOCUMENT, NgTemplateOutlet, CommonModule, isPlatformBrowser } from '@angular/common';
|
|
11
|
+
import { DOCUMENT, NgTemplateOutlet, CommonModule, AsyncPipe, isPlatformBrowser } from '@angular/common';
|
|
12
12
|
import { mdiCheckCircle, mdiAlertCircle, mdiAlert, mdiInformation, mdiOpenInNew, mdiHelpCircle, mdiDotsVertical, mdiClose, mdiFolderOpen, mdiLock, mdiLoading, mdiFolderPlus, mdiFolderNetwork, mdiHarddisk, mdiDatabase, mdiFile, mdiFolder } from '@mdi/js';
|
|
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, merge, Subject, 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';
|
|
@@ -23,11 +23,11 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
|
23
23
|
import { trigger, state, transition, style, animate } from '@angular/animations';
|
|
24
24
|
import { SelectionModel, DataSource } from '@angular/cdk/collections';
|
|
25
25
|
import * as i2 from '@angular/cdk/tree';
|
|
26
|
-
import { CdkTree, CdkTreeModule, CdkTreeNode, CDK_TREE_NODE_OUTLET_NODE, CdkTreeNodeOutlet, CdkNestedTreeNode } from '@angular/cdk/tree';
|
|
26
|
+
import { CdkTree, CdkTreeModule, CdkTreeNode, CDK_TREE_NODE_OUTLET_NODE, CdkTreeNodeOutlet, CdkNestedTreeNode, CdkTreeNodeOutletContext } from '@angular/cdk/tree';
|
|
27
27
|
export { FlatTreeControl } from '@angular/cdk/tree';
|
|
28
|
-
import { map } from 'rxjs/operators';
|
|
28
|
+
import { map, auditTime } from 'rxjs/operators';
|
|
29
|
+
import { CdkVirtualScrollViewport, CdkVirtualScrollableWindow, CdkFixedSizeVirtualScroll, CdkVirtualForOf, ScrollingModule } from '@angular/cdk/scrolling';
|
|
29
30
|
import { DialogRef, DIALOG_DATA, Dialog } from '@angular/cdk/dialog';
|
|
30
|
-
import { ScrollingModule } from '@angular/cdk/scrolling';
|
|
31
31
|
|
|
32
32
|
class TnSpinnerComponent {
|
|
33
33
|
mode = input('indeterminate', ...(ngDevMode ? [{ debugName: "mode" }] : []));
|
|
@@ -2199,7 +2199,11 @@ class TnBannerHarness extends ComponentHarness {
|
|
|
2199
2199
|
*/
|
|
2200
2200
|
static with(options = {}) {
|
|
2201
2201
|
return new HarnessPredicate(TnBannerHarness, options)
|
|
2202
|
-
.addOption('textContains', options.textContains, (harness, text) => HarnessPredicate.stringMatches(harness.getText(),
|
|
2202
|
+
.addOption('textContains', options.textContains, (harness, text) => HarnessPredicate.stringMatches(harness.getText(),
|
|
2203
|
+
// strings trigger exact matching in `stringMatches`, but since we call the option
|
|
2204
|
+
// `textContains`, this would be misleading. here, we convert strings to a Regex
|
|
2205
|
+
// to trigger partial matching behavior on `stringMatches`.
|
|
2206
|
+
typeof text === 'string' ? new RegExp(helperEscapeRegex(text)) : text));
|
|
2203
2207
|
}
|
|
2204
2208
|
/**
|
|
2205
2209
|
* Gets all text content from the banner (heading + message combined).
|
|
@@ -2218,6 +2222,15 @@ class TnBannerHarness extends ComponentHarness {
|
|
|
2218
2222
|
return (await host.text()).trim();
|
|
2219
2223
|
}
|
|
2220
2224
|
}
|
|
2225
|
+
/**
|
|
2226
|
+
* helper function to stand-in for `RegExp.escape`, since that doesn't
|
|
2227
|
+
* exist in our deployment target of ES2023.
|
|
2228
|
+
* @param text a string to escape.
|
|
2229
|
+
* @returns an escaped string, safe for using in the `RegExp` constructor.
|
|
2230
|
+
*/
|
|
2231
|
+
function helperEscapeRegex(text) {
|
|
2232
|
+
return text.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
2233
|
+
}
|
|
2221
2234
|
|
|
2222
2235
|
const escapableChars = new Set(['*', '`', '\\']);
|
|
2223
2236
|
function isWhitespace(char) {
|
|
@@ -8716,6 +8729,16 @@ class TnSelectComponent {
|
|
|
8716
8729
|
/** Label of the empty option rendered when `allowEmpty` is set. */
|
|
8717
8730
|
emptyLabel = input('--', ...(ngDevMode ? [{ debugName: "emptyLabel" }] : []));
|
|
8718
8731
|
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
8732
|
+
/**
|
|
8733
|
+
* When `true` (multiple mode only), renders a "select all" row at the top of
|
|
8734
|
+
* the dropdown that toggles every selectable (non-disabled) option on/off in
|
|
8735
|
+
* one click. Mirrors webui ix-select's `[showSelectAll]`. Ignored in single
|
|
8736
|
+
* mode. Its checkbox reflects the aggregate state: checked when all are
|
|
8737
|
+
* selected, indeterminate when only some are.
|
|
8738
|
+
*/
|
|
8739
|
+
showSelectAll = input(false, ...(ngDevMode ? [{ debugName: "showSelectAll" }] : []));
|
|
8740
|
+
/** Label of the select-all row rendered when `showSelectAll` is set. */
|
|
8741
|
+
selectAllLabel = input('Select All', ...(ngDevMode ? [{ debugName: "selectAllLabel" }] : []));
|
|
8719
8742
|
testId = input(undefined, ...(ngDevMode ? [{ debugName: "testId" }] : []));
|
|
8720
8743
|
/** Test-id base, falling back to the bound control name when `testId` is unset. */
|
|
8721
8744
|
resolvedTestId = controlTestId(this.testId);
|
|
@@ -8803,18 +8826,23 @@ class TnSelectComponent {
|
|
|
8803
8826
|
*/
|
|
8804
8827
|
navigableOptions = computed(() => {
|
|
8805
8828
|
const result = [];
|
|
8829
|
+
// The select-all row is the first navigable entry when shown, so
|
|
8830
|
+
// ArrowDown from the closed trigger lands on it before the options.
|
|
8831
|
+
if (this.showSelectAllRow()) {
|
|
8832
|
+
result.push({ kind: 'select-all', id: this.selectAllId() });
|
|
8833
|
+
}
|
|
8806
8834
|
const baseId = `tn-select-opt-${this.idNamespace()}`;
|
|
8807
8835
|
let i = 0;
|
|
8808
8836
|
for (const opt of this.displayOptions()) {
|
|
8809
8837
|
if (!opt.disabled) {
|
|
8810
|
-
result.push({ option: opt, id: `${baseId}-${i}` });
|
|
8838
|
+
result.push({ kind: 'option', option: opt, id: `${baseId}-${i}` });
|
|
8811
8839
|
}
|
|
8812
8840
|
i++;
|
|
8813
8841
|
}
|
|
8814
8842
|
for (const group of this.optionGroups()) {
|
|
8815
8843
|
for (const opt of group.options) {
|
|
8816
8844
|
if (!opt.disabled && !group.disabled) {
|
|
8817
|
-
result.push({ option: opt, id: `${baseId}-${i}` });
|
|
8845
|
+
result.push({ kind: 'option', option: opt, id: `${baseId}-${i}` });
|
|
8818
8846
|
}
|
|
8819
8847
|
i++;
|
|
8820
8848
|
}
|
|
@@ -8829,14 +8857,15 @@ class TnSelectComponent {
|
|
|
8829
8857
|
}, ...(ngDevMode ? [{ debugName: "focusedOptionId" }] : []));
|
|
8830
8858
|
/** Stable DOM id for an option; matches what navigableOptions() assigns. */
|
|
8831
8859
|
optionId(option) {
|
|
8832
|
-
const entry = this.navigableOptions().find((x) => x.option === option);
|
|
8860
|
+
const entry = this.navigableOptions().find((x) => x.kind === 'option' && x.option === option);
|
|
8833
8861
|
return entry?.id ?? null;
|
|
8834
8862
|
}
|
|
8835
8863
|
/** Whether `option` is the keyboard-highlighted item. */
|
|
8836
8864
|
isOptionFocused(option) {
|
|
8837
8865
|
const idx = this.focusedIndex();
|
|
8838
8866
|
const nav = this.navigableOptions();
|
|
8839
|
-
|
|
8867
|
+
const entry = idx >= 0 && idx < nav.length ? nav[idx] : null;
|
|
8868
|
+
return entry?.kind === 'option' && entry.option === option;
|
|
8840
8869
|
}
|
|
8841
8870
|
/**
|
|
8842
8871
|
* Test-id segments for an option row, consumed by `[tnTestId]` with
|
|
@@ -8921,7 +8950,7 @@ class TnSelectComponent {
|
|
|
8921
8950
|
// otherwise leave it unset so the next ArrowDown lands on the first item.
|
|
8922
8951
|
const selected = this.selectedValue();
|
|
8923
8952
|
if (selected !== null && selected !== undefined) {
|
|
8924
|
-
const idx = this.navigableOptions().findIndex((x) => this.compareValues(x.option.value, selected));
|
|
8953
|
+
const idx = this.navigableOptions().findIndex((x) => x.kind === 'option' && this.compareValues(x.option.value, selected));
|
|
8925
8954
|
this.focusedIndex.set(idx);
|
|
8926
8955
|
}
|
|
8927
8956
|
else if (this.emptyOption()) {
|
|
@@ -9118,6 +9147,92 @@ class TnSelectComponent {
|
|
|
9118
9147
|
hasAnyOptions = computed(() => {
|
|
9119
9148
|
return this.options().length > 0 || this.optionGroups().length > 0;
|
|
9120
9149
|
}, ...(ngDevMode ? [{ debugName: "hasAnyOptions" }] : []));
|
|
9150
|
+
/**
|
|
9151
|
+
* Values of every selectable (non-disabled) option, across ungrouped options
|
|
9152
|
+
* and enabled groups. This is the set the select-all row operates on —
|
|
9153
|
+
* disabled options and options in disabled groups are excluded because they
|
|
9154
|
+
* can't be toggled individually either.
|
|
9155
|
+
*
|
|
9156
|
+
* Values are deduped with `compareValues` so a value that appears both
|
|
9157
|
+
* ungrouped and inside a group isn't pushed twice — that would make
|
|
9158
|
+
* select-all diverge from `toggleOption()`, which never produces duplicates.
|
|
9159
|
+
*/
|
|
9160
|
+
selectableValues = computed(() => {
|
|
9161
|
+
const values = [];
|
|
9162
|
+
const push = (value) => {
|
|
9163
|
+
if (!values.some((v) => this.compareValues(v, value))) {
|
|
9164
|
+
values.push(value);
|
|
9165
|
+
}
|
|
9166
|
+
};
|
|
9167
|
+
for (const opt of this.options()) {
|
|
9168
|
+
if (!opt.disabled) {
|
|
9169
|
+
push(opt.value);
|
|
9170
|
+
}
|
|
9171
|
+
}
|
|
9172
|
+
for (const group of this.optionGroups()) {
|
|
9173
|
+
if (group.disabled) {
|
|
9174
|
+
continue;
|
|
9175
|
+
}
|
|
9176
|
+
for (const opt of group.options) {
|
|
9177
|
+
if (!opt.disabled) {
|
|
9178
|
+
push(opt.value);
|
|
9179
|
+
}
|
|
9180
|
+
}
|
|
9181
|
+
}
|
|
9182
|
+
return values;
|
|
9183
|
+
}, ...(ngDevMode ? [{ debugName: "selectableValues" }] : []));
|
|
9184
|
+
/** Whether the select-all row is shown (multiple mode, opted in, with options). */
|
|
9185
|
+
showSelectAllRow = computed(() => this.multiple() && this.showSelectAll() && this.selectableValues().length > 0, ...(ngDevMode ? [{ debugName: "showSelectAllRow" }] : []));
|
|
9186
|
+
/** Stable DOM id of the select-all row, for aria-activedescendant. */
|
|
9187
|
+
selectAllId = computed(() => `tn-select-selectall-${this.idNamespace()}`, ...(ngDevMode ? [{ debugName: "selectAllId" }] : []));
|
|
9188
|
+
/** True when every selectable option is currently selected. */
|
|
9189
|
+
allSelected = computed(() => {
|
|
9190
|
+
const selectable = this.selectableValues();
|
|
9191
|
+
if (selectable.length === 0) {
|
|
9192
|
+
return false;
|
|
9193
|
+
}
|
|
9194
|
+
const selected = this.selectedValues();
|
|
9195
|
+
return selectable.every((v) => selected.some((s) => this.compareValues(s, v)));
|
|
9196
|
+
}, ...(ngDevMode ? [{ debugName: "allSelected" }] : []));
|
|
9197
|
+
/** True when some — but not all — selectable options are selected. */
|
|
9198
|
+
selectAllIndeterminate = computed(() => {
|
|
9199
|
+
if (this.allSelected()) {
|
|
9200
|
+
return false;
|
|
9201
|
+
}
|
|
9202
|
+
const selected = this.selectedValues();
|
|
9203
|
+
return this.selectableValues().some((v) => selected.some((s) => this.compareValues(s, v)));
|
|
9204
|
+
}, ...(ngDevMode ? [{ debugName: "selectAllIndeterminate" }] : []));
|
|
9205
|
+
/** Test-id segments for the select-all row; mirrors ix-select's `[name, 'select-all']`. */
|
|
9206
|
+
selectAllTestIdParts() {
|
|
9207
|
+
return scopeTestId(this.resolvedTestId(), 'select-all');
|
|
9208
|
+
}
|
|
9209
|
+
/**
|
|
9210
|
+
* Toggles every selectable option: clears them all when they're all already
|
|
9211
|
+
* selected, otherwise selects them all. Preserves the multi-select "open"
|
|
9212
|
+
* behaviour — the dropdown stays open so the user can keep adjusting.
|
|
9213
|
+
*
|
|
9214
|
+
* Disabled-but-selected values (e.g. a disabled option pre-selected via
|
|
9215
|
+
* `writeValue`) are preserved on both paths: the user can't toggle those
|
|
9216
|
+
* rows individually, so select-all must not silently discard them either.
|
|
9217
|
+
*/
|
|
9218
|
+
toggleSelectAll() {
|
|
9219
|
+
if (this.isDisabled()) {
|
|
9220
|
+
return;
|
|
9221
|
+
}
|
|
9222
|
+
const selectable = this.selectableValues();
|
|
9223
|
+
const preserved = this.selectedValues().filter((v) => !selectable.some((s) => this.compareValues(s, v)));
|
|
9224
|
+
const updated = this.allSelected() ? preserved : [...preserved, ...selectable];
|
|
9225
|
+
this.selectedValues.set(updated);
|
|
9226
|
+
this.onChange(updated);
|
|
9227
|
+
this.multiSelectionChange.emit(updated);
|
|
9228
|
+
this.cdr.markForCheck();
|
|
9229
|
+
}
|
|
9230
|
+
/** Whether the select-all row is the keyboard-highlighted item. */
|
|
9231
|
+
isSelectAllFocused() {
|
|
9232
|
+
const idx = this.focusedIndex();
|
|
9233
|
+
const nav = this.navigableOptions();
|
|
9234
|
+
return idx >= 0 && idx < nav.length && nav[idx].kind === 'select-all';
|
|
9235
|
+
}
|
|
9121
9236
|
/** One-shot guard so the object-compare warning fires at most once per instance. */
|
|
9122
9237
|
warnedAboutObjectCompare = false;
|
|
9123
9238
|
/**
|
|
@@ -9261,7 +9376,13 @@ class TnSelectComponent {
|
|
|
9261
9376
|
if (idx < 0 || idx >= nav.length) {
|
|
9262
9377
|
return;
|
|
9263
9378
|
}
|
|
9264
|
-
|
|
9379
|
+
const entry = nav[idx];
|
|
9380
|
+
if (entry.kind === 'select-all') {
|
|
9381
|
+
this.toggleSelectAll();
|
|
9382
|
+
}
|
|
9383
|
+
else {
|
|
9384
|
+
this.onOptionClick(entry.option);
|
|
9385
|
+
}
|
|
9265
9386
|
}
|
|
9266
9387
|
scrollFocusedIntoView() {
|
|
9267
9388
|
const id = this.focusedOptionId();
|
|
@@ -9284,13 +9405,13 @@ class TnSelectComponent {
|
|
|
9284
9405
|
});
|
|
9285
9406
|
}
|
|
9286
9407
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
9287
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnSelectComponent, isStandalone: true, selector: "tn-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, optionGroups: { classPropertyName: "optionGroups", publicName: "optionGroups", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, noOptionsLabel: { classPropertyName: "noOptionsLabel", publicName: "noOptionsLabel", isSignal: true, isRequired: false, transformFunction: null }, allowEmpty: { classPropertyName: "allowEmpty", publicName: "allowEmpty", isSignal: true, isRequired: false, transformFunction: null }, emptyLabel: { classPropertyName: "emptyLabel", publicName: "emptyLabel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, optionTestIdKey: { classPropertyName: "optionTestIdKey", publicName: "optionTestIdKey", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange", multiSelectionChange: "multiSelectionChange" }, providers: [
|
|
9408
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnSelectComponent, isStandalone: true, selector: "tn-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, optionGroups: { classPropertyName: "optionGroups", publicName: "optionGroups", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, noOptionsLabel: { classPropertyName: "noOptionsLabel", publicName: "noOptionsLabel", isSignal: true, isRequired: false, transformFunction: null }, allowEmpty: { classPropertyName: "allowEmpty", publicName: "allowEmpty", isSignal: true, isRequired: false, transformFunction: null }, emptyLabel: { classPropertyName: "emptyLabel", publicName: "emptyLabel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, showSelectAll: { classPropertyName: "showSelectAll", publicName: "showSelectAll", isSignal: true, isRequired: false, transformFunction: null }, selectAllLabel: { classPropertyName: "selectAllLabel", publicName: "selectAllLabel", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, optionTestIdKey: { classPropertyName: "optionTestIdKey", publicName: "optionTestIdKey", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange", multiSelectionChange: "multiSelectionChange" }, providers: [
|
|
9288
9409
|
{
|
|
9289
9410
|
provide: NG_VALUE_ACCESSOR,
|
|
9290
9411
|
useExisting: forwardRef(() => TnSelectComponent),
|
|
9291
9412
|
multi: true
|
|
9292
9413
|
}
|
|
9293
|
-
], viewQueries: [{ propertyName: "triggerEl", first: true, predicate: ["triggerEl"], descendants: true, isSignal: true }, { propertyName: "dropdownTemplate", first: true, predicate: ["dropdownTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-select-container\">\n <!-- Select Trigger -->\n <div\n #triggerEl\n class=\"tn-select-trigger\"\n role=\"combobox\"\n tnTestIdType=\"select\"\n [tnTestId]=\"resolvedTestId()\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-controls]=\"isOpen() ? 'tn-select-dropdown-' + idNamespace() : null\"\n [attr.aria-label]=\"ariaLabel() ?? placeholder()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.aria-activedescendant]=\"isOpen() ? focusedOptionId() : null\"\n (click)=\"toggleDropdown()\"\n (keydown)=\"onKeydown($event)\">\n\n <!-- Display Text -->\n <span\n class=\"tn-select-text\"\n [class.placeholder]=\"multiple() ? selectedValues().length === 0 : (selectedValue() === null || selectedValue() === undefined)\">\n {{ displayText() }}\n </span>\n\n <!-- Dropdown Arrow -->\n <div class=\"tn-select-arrow\" [class.open]=\"isOpen()\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <polyline points=\"6,9 12,15 18,9\" />\n </svg>\n </div>\n </div>\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. The trigger keeps DOM focus throughout (combobox\n pattern); options use aria-activedescendant for virtual focus. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-select-dropdown\"\n role=\"listbox\"\n [attr.aria-multiselectable]=\"multiple()\"\n [attr.id]=\"'tn-select-dropdown-' + idNamespace()\">\n\n <!-- Options List -->\n <div class=\"tn-select-options\">\n <!-- Regular Options (preceded by the synthetic empty option when allowEmpty is set) -->\n @for (option of displayOptions(); track $index) {\n <!-- Option activation lives on the trigger via aria-activedescendant; options have tabindex=\"-1\" and never receive DOM focus. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [class.tn-select-empty-option]=\"isEmptyOption(option)\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [attr.id]=\"optionId(option)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.focused]=\"isOptionFocused(option)\"\n [class.disabled]=\"option.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n\n <!-- Option Groups -->\n @for (group of optionGroups(); track $index; let isFirst = $first) {\n <!-- Group Separator (not shown before first group if we have regular options) -->\n @if (!isFirst || displayOptions().length > 0) {\n <div\n class=\"tn-select-separator\"\n role=\"separator\">\n </div>\n }\n\n <div role=\"group\" [attr.aria-labelledby]=\"'tn-select-group-' + idNamespace() + '-' + $index\">\n <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\n [attr.id]=\"'tn-select-group-' + idNamespace() + '-' + $index\"\n [class.disabled]=\"group.disabled\">\n {{ group.label }}\n </div>\n\n <!-- Group Options -->\n @for (option of group.options; track $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [attr.id]=\"optionId(option)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.focused]=\"isOptionFocused(option)\"\n [class.disabled]=\"option.disabled || group.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled || group.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option, !!group.disabled)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n </div>\n }\n\n <!-- No Options Message -->\n @if (!hasAnyOptions()) {\n <div class=\"tn-select-no-options\">\n {{ noOptionsLabel() }}\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: [".tn-select-container{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-select-label{display:block;margin-bottom:.5rem;font-size:1rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-select-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-select-trigger{position:relative;display:flex;align-items:center;min-height:2.5rem;padding:.5rem 2.5rem .5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:pointer;transition:all .15s ease-in-out;outline:none;box-sizing:border-box}.tn-select-trigger:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-select-trigger:focus-visible{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-select-trigger.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-select-trigger.error{border-color:var(--tn-error, #dc3545)}.tn-select-trigger.error:focus-visible,.tn-select-trigger.error.open{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-error, #dc3545) 25%,transparent)}.tn-select-trigger.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-text{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--tn-fg1, #212529)}.tn-select-text.placeholder{color:var(--tn-alt-fg1, #999)}.tn-select-arrow{position:absolute;right:.75rem;top:50%;transform:translateY(-50%);color:var(--tn-fg2, #6c757d);transition:transform .15s ease-in-out;pointer-events:none}.tn-select-arrow.open{transform:translateY(-50%) rotate(180deg)}.tn-select-arrow svg{display:block}.tn-select-dropdown{--tn-select-dropdown-max-height: 350px;background-color:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;max-height:var(--tn-select-dropdown-max-height)}.tn-select-options{overflow-y:auto;padding:.25rem 0;max-height:var(--tn-select-dropdown-max-height)}.tn-select-option{display:flex;align-items:center;padding:.5rem .75rem;overflow-wrap:anywhere;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out;pointer-events:auto;position:relative;z-index:1001}.tn-select-option.selected{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg1, #212529)}.tn-select-option:hover:not(.disabled):not(.focused){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-select-option.focused{background-color:var(--tn-alt-bg2, #e8f4fd);box-shadow:inset 2px 0 0 var(--tn-primary, #007bff)}.tn-select-option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-option.tn-select-empty-option{color:var(--tn-fg2, #6c757d)}.tn-select-check{margin-right:.5rem;flex-shrink:0;pointer-events:none}.tn-select-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:.25rem 0}.tn-select-group-label{padding:.375rem .75rem;font-size:.75rem;font-weight:600;color:var(--tn-alt-fg1, #9ca3af);text-transform:uppercase;letter-spacing:.05em;cursor:default;-webkit-user-select:none;user-select:none}.tn-select-group-label.disabled{opacity:.6}.tn-select-no-options{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}.tn-select-error{margin-top:.25rem;font-size:.75rem;color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-select-trigger,.tn-select-option,.tn-select-arrow{transition:none}}@media(prefers-contrast:high){.tn-select-trigger{border-width:2px}.tn-select-option.selected{outline:2px solid var(--tn-fg1, #000);outline-offset:-2px}}\n"], dependencies: [{ kind: "component", type: TnCheckboxComponent, selector: "tn-checkbox", inputs: ["label", "hideLabel", "disabled", "required", "indeterminate", "testId", "error", "checked"], outputs: ["change"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9414
|
+
], viewQueries: [{ propertyName: "triggerEl", first: true, predicate: ["triggerEl"], descendants: true, isSignal: true }, { propertyName: "dropdownTemplate", first: true, predicate: ["dropdownTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-select-container\">\n <!-- Select Trigger -->\n <div\n #triggerEl\n class=\"tn-select-trigger\"\n role=\"combobox\"\n tnTestIdType=\"select\"\n [tnTestId]=\"resolvedTestId()\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-controls]=\"isOpen() ? 'tn-select-dropdown-' + idNamespace() : null\"\n [attr.aria-label]=\"ariaLabel() ?? placeholder()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.aria-activedescendant]=\"isOpen() ? focusedOptionId() : null\"\n (click)=\"toggleDropdown()\"\n (keydown)=\"onKeydown($event)\">\n\n <!-- Display Text -->\n <span\n class=\"tn-select-text\"\n [class.placeholder]=\"multiple() ? selectedValues().length === 0 : (selectedValue() === null || selectedValue() === undefined)\">\n {{ displayText() }}\n </span>\n\n <!-- Dropdown Arrow -->\n <div class=\"tn-select-arrow\" [class.open]=\"isOpen()\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <polyline points=\"6,9 12,15 18,9\" />\n </svg>\n </div>\n </div>\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. The trigger keeps DOM focus throughout (combobox\n pattern); options use aria-activedescendant for virtual focus. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-select-dropdown\"\n role=\"listbox\"\n [attr.aria-multiselectable]=\"multiple()\"\n [attr.id]=\"'tn-select-dropdown-' + idNamespace()\">\n\n <!-- Options List -->\n <div class=\"tn-select-options\">\n <!-- Select-all row (multiple mode with showSelectAll). Toggles every\n selectable option; its checkbox reflects all/some/none. -->\n @if (showSelectAllRow()) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option tn-select-select-all\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"selectAllTestIdParts()\"\n [attr.id]=\"selectAllId()\"\n [class.selected]=\"allSelected()\"\n [class.focused]=\"isSelectAllFocused()\"\n [attr.aria-selected]=\"allSelected()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"toggleSelectAll()\">\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"allSelected()\"\n [indeterminate]=\"selectAllIndeterminate()\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n {{ selectAllLabel() }}\n </div>\n <div class=\"tn-select-separator\" role=\"separator\"></div>\n }\n\n <!-- Regular Options (preceded by the synthetic empty option when allowEmpty is set) -->\n @for (option of displayOptions(); track $index) {\n <!-- Option activation lives on the trigger via aria-activedescendant; options have tabindex=\"-1\" and never receive DOM focus. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [class.tn-select-empty-option]=\"isEmptyOption(option)\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [attr.id]=\"optionId(option)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.focused]=\"isOptionFocused(option)\"\n [class.disabled]=\"option.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n\n <!-- Option Groups -->\n @for (group of optionGroups(); track $index; let isFirst = $first) {\n <!-- Group Separator (not shown before first group if we have regular options) -->\n @if (!isFirst || displayOptions().length > 0) {\n <div\n class=\"tn-select-separator\"\n role=\"separator\">\n </div>\n }\n\n <div role=\"group\" [attr.aria-labelledby]=\"'tn-select-group-' + idNamespace() + '-' + $index\">\n <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\n [attr.id]=\"'tn-select-group-' + idNamespace() + '-' + $index\"\n [class.disabled]=\"group.disabled\">\n {{ group.label }}\n </div>\n\n <!-- Group Options -->\n @for (option of group.options; track $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [attr.id]=\"optionId(option)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.focused]=\"isOptionFocused(option)\"\n [class.disabled]=\"option.disabled || group.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled || group.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option, !!group.disabled)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n </div>\n }\n\n <!-- No Options Message -->\n @if (!hasAnyOptions()) {\n <div class=\"tn-select-no-options\">\n {{ noOptionsLabel() }}\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: [".tn-select-container{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-select-label{display:block;margin-bottom:.5rem;font-size:1rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-select-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-select-trigger{position:relative;display:flex;align-items:center;min-height:2.5rem;padding:.5rem 2.5rem .5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:pointer;transition:all .15s ease-in-out;outline:none;box-sizing:border-box}.tn-select-trigger:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-select-trigger:focus-visible{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-select-trigger.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-select-trigger.error{border-color:var(--tn-error, #dc3545)}.tn-select-trigger.error:focus-visible,.tn-select-trigger.error.open{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-error, #dc3545) 25%,transparent)}.tn-select-trigger.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-text{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--tn-fg1, #212529)}.tn-select-text.placeholder{color:var(--tn-alt-fg1, #999)}.tn-select-arrow{position:absolute;right:.75rem;top:50%;transform:translateY(-50%);color:var(--tn-fg2, #6c757d);transition:transform .15s ease-in-out;pointer-events:none}.tn-select-arrow.open{transform:translateY(-50%) rotate(180deg)}.tn-select-arrow svg{display:block}.tn-select-dropdown{--tn-select-dropdown-max-height: 350px;background-color:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;max-height:var(--tn-select-dropdown-max-height)}.tn-select-options{overflow-y:auto;padding:.25rem 0;max-height:var(--tn-select-dropdown-max-height)}.tn-select-option{display:flex;align-items:center;padding:.5rem .75rem;overflow-wrap:anywhere;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out;pointer-events:auto;position:relative;z-index:1001}.tn-select-option.selected{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg1, #212529)}.tn-select-option:hover:not(.disabled):not(.focused){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-select-option.focused{background-color:var(--tn-alt-bg2, #e8f4fd);box-shadow:inset 2px 0 0 var(--tn-primary, #007bff)}.tn-select-option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-option.tn-select-empty-option{color:var(--tn-fg2, #6c757d)}.tn-select-select-all{font-weight:600}.tn-select-check{margin-right:.5rem;flex-shrink:0;pointer-events:none}.tn-select-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:.25rem 0}.tn-select-group-label{padding:.375rem .75rem;font-size:.75rem;font-weight:600;color:var(--tn-alt-fg1, #9ca3af);text-transform:uppercase;letter-spacing:.05em;cursor:default;-webkit-user-select:none;user-select:none}.tn-select-group-label.disabled{opacity:.6}.tn-select-no-options{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}.tn-select-error{margin-top:.25rem;font-size:.75rem;color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-select-trigger,.tn-select-option,.tn-select-arrow{transition:none}}@media(prefers-contrast:high){.tn-select-trigger{border-width:2px}.tn-select-option.selected{outline:2px solid var(--tn-fg1, #000);outline-offset:-2px}}\n"], dependencies: [{ kind: "component", type: TnCheckboxComponent, selector: "tn-checkbox", inputs: ["label", "hideLabel", "disabled", "required", "indeterminate", "testId", "error", "checked"], outputs: ["change"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9294
9415
|
}
|
|
9295
9416
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSelectComponent, decorators: [{
|
|
9296
9417
|
type: Component,
|
|
@@ -9300,8 +9421,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
9300
9421
|
useExisting: forwardRef(() => TnSelectComponent),
|
|
9301
9422
|
multi: true
|
|
9302
9423
|
}
|
|
9303
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"tn-select-container\">\n <!-- Select Trigger -->\n <div\n #triggerEl\n class=\"tn-select-trigger\"\n role=\"combobox\"\n tnTestIdType=\"select\"\n [tnTestId]=\"resolvedTestId()\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-controls]=\"isOpen() ? 'tn-select-dropdown-' + idNamespace() : null\"\n [attr.aria-label]=\"ariaLabel() ?? placeholder()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.aria-activedescendant]=\"isOpen() ? focusedOptionId() : null\"\n (click)=\"toggleDropdown()\"\n (keydown)=\"onKeydown($event)\">\n\n <!-- Display Text -->\n <span\n class=\"tn-select-text\"\n [class.placeholder]=\"multiple() ? selectedValues().length === 0 : (selectedValue() === null || selectedValue() === undefined)\">\n {{ displayText() }}\n </span>\n\n <!-- Dropdown Arrow -->\n <div class=\"tn-select-arrow\" [class.open]=\"isOpen()\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <polyline points=\"6,9 12,15 18,9\" />\n </svg>\n </div>\n </div>\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. The trigger keeps DOM focus throughout (combobox\n pattern); options use aria-activedescendant for virtual focus. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-select-dropdown\"\n role=\"listbox\"\n [attr.aria-multiselectable]=\"multiple()\"\n [attr.id]=\"'tn-select-dropdown-' + idNamespace()\">\n\n <!-- Options List -->\n <div class=\"tn-select-options\">\n <!-- Regular Options (preceded by the synthetic empty option when allowEmpty is set) -->\n @for (option of displayOptions(); track $index) {\n <!-- Option activation lives on the trigger via aria-activedescendant; options have tabindex=\"-1\" and never receive DOM focus. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [class.tn-select-empty-option]=\"isEmptyOption(option)\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [attr.id]=\"optionId(option)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.focused]=\"isOptionFocused(option)\"\n [class.disabled]=\"option.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n\n <!-- Option Groups -->\n @for (group of optionGroups(); track $index; let isFirst = $first) {\n <!-- Group Separator (not shown before first group if we have regular options) -->\n @if (!isFirst || displayOptions().length > 0) {\n <div\n class=\"tn-select-separator\"\n role=\"separator\">\n </div>\n }\n\n <div role=\"group\" [attr.aria-labelledby]=\"'tn-select-group-' + idNamespace() + '-' + $index\">\n <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\n [attr.id]=\"'tn-select-group-' + idNamespace() + '-' + $index\"\n [class.disabled]=\"group.disabled\">\n {{ group.label }}\n </div>\n\n <!-- Group Options -->\n @for (option of group.options; track $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [attr.id]=\"optionId(option)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.focused]=\"isOptionFocused(option)\"\n [class.disabled]=\"option.disabled || group.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled || group.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option, !!group.disabled)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n </div>\n }\n\n <!-- No Options Message -->\n @if (!hasAnyOptions()) {\n <div class=\"tn-select-no-options\">\n {{ noOptionsLabel() }}\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: [".tn-select-container{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-select-label{display:block;margin-bottom:.5rem;font-size:1rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-select-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-select-trigger{position:relative;display:flex;align-items:center;min-height:2.5rem;padding:.5rem 2.5rem .5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:pointer;transition:all .15s ease-in-out;outline:none;box-sizing:border-box}.tn-select-trigger:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-select-trigger:focus-visible{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-select-trigger.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-select-trigger.error{border-color:var(--tn-error, #dc3545)}.tn-select-trigger.error:focus-visible,.tn-select-trigger.error.open{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-error, #dc3545) 25%,transparent)}.tn-select-trigger.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-text{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--tn-fg1, #212529)}.tn-select-text.placeholder{color:var(--tn-alt-fg1, #999)}.tn-select-arrow{position:absolute;right:.75rem;top:50%;transform:translateY(-50%);color:var(--tn-fg2, #6c757d);transition:transform .15s ease-in-out;pointer-events:none}.tn-select-arrow.open{transform:translateY(-50%) rotate(180deg)}.tn-select-arrow svg{display:block}.tn-select-dropdown{--tn-select-dropdown-max-height: 350px;background-color:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;max-height:var(--tn-select-dropdown-max-height)}.tn-select-options{overflow-y:auto;padding:.25rem 0;max-height:var(--tn-select-dropdown-max-height)}.tn-select-option{display:flex;align-items:center;padding:.5rem .75rem;overflow-wrap:anywhere;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out;pointer-events:auto;position:relative;z-index:1001}.tn-select-option.selected{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg1, #212529)}.tn-select-option:hover:not(.disabled):not(.focused){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-select-option.focused{background-color:var(--tn-alt-bg2, #e8f4fd);box-shadow:inset 2px 0 0 var(--tn-primary, #007bff)}.tn-select-option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-option.tn-select-empty-option{color:var(--tn-fg2, #6c757d)}.tn-select-check{margin-right:.5rem;flex-shrink:0;pointer-events:none}.tn-select-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:.25rem 0}.tn-select-group-label{padding:.375rem .75rem;font-size:.75rem;font-weight:600;color:var(--tn-alt-fg1, #9ca3af);text-transform:uppercase;letter-spacing:.05em;cursor:default;-webkit-user-select:none;user-select:none}.tn-select-group-label.disabled{opacity:.6}.tn-select-no-options{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}.tn-select-error{margin-top:.25rem;font-size:.75rem;color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-select-trigger,.tn-select-option,.tn-select-arrow{transition:none}}@media(prefers-contrast:high){.tn-select-trigger{border-width:2px}.tn-select-option.selected{outline:2px solid var(--tn-fg1, #000);outline-offset:-2px}}\n"] }]
|
|
9304
|
-
}], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionGroups: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionGroups", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], noOptionsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "noOptionsLabel", required: false }] }], allowEmpty: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowEmpty", required: false }] }], emptyLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], optionTestIdKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionTestIdKey", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], multiSelectionChange: [{ type: i0.Output, args: ["multiSelectionChange"] }], triggerEl: [{ type: i0.ViewChild, args: ['triggerEl', { isSignal: true }] }], dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdownTemplate', { isSignal: true }] }] } });
|
|
9424
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"tn-select-container\">\n <!-- Select Trigger -->\n <div\n #triggerEl\n class=\"tn-select-trigger\"\n role=\"combobox\"\n tnTestIdType=\"select\"\n [tnTestId]=\"resolvedTestId()\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-controls]=\"isOpen() ? 'tn-select-dropdown-' + idNamespace() : null\"\n [attr.aria-label]=\"ariaLabel() ?? placeholder()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.aria-activedescendant]=\"isOpen() ? focusedOptionId() : null\"\n (click)=\"toggleDropdown()\"\n (keydown)=\"onKeydown($event)\">\n\n <!-- Display Text -->\n <span\n class=\"tn-select-text\"\n [class.placeholder]=\"multiple() ? selectedValues().length === 0 : (selectedValue() === null || selectedValue() === undefined)\">\n {{ displayText() }}\n </span>\n\n <!-- Dropdown Arrow -->\n <div class=\"tn-select-arrow\" [class.open]=\"isOpen()\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <polyline points=\"6,9 12,15 18,9\" />\n </svg>\n </div>\n </div>\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. The trigger keeps DOM focus throughout (combobox\n pattern); options use aria-activedescendant for virtual focus. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-select-dropdown\"\n role=\"listbox\"\n [attr.aria-multiselectable]=\"multiple()\"\n [attr.id]=\"'tn-select-dropdown-' + idNamespace()\">\n\n <!-- Options List -->\n <div class=\"tn-select-options\">\n <!-- Select-all row (multiple mode with showSelectAll). Toggles every\n selectable option; its checkbox reflects all/some/none. -->\n @if (showSelectAllRow()) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option tn-select-select-all\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"selectAllTestIdParts()\"\n [attr.id]=\"selectAllId()\"\n [class.selected]=\"allSelected()\"\n [class.focused]=\"isSelectAllFocused()\"\n [attr.aria-selected]=\"allSelected()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"toggleSelectAll()\">\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"allSelected()\"\n [indeterminate]=\"selectAllIndeterminate()\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n {{ selectAllLabel() }}\n </div>\n <div class=\"tn-select-separator\" role=\"separator\"></div>\n }\n\n <!-- Regular Options (preceded by the synthetic empty option when allowEmpty is set) -->\n @for (option of displayOptions(); track $index) {\n <!-- Option activation lives on the trigger via aria-activedescendant; options have tabindex=\"-1\" and never receive DOM focus. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [class.tn-select-empty-option]=\"isEmptyOption(option)\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [attr.id]=\"optionId(option)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.focused]=\"isOptionFocused(option)\"\n [class.disabled]=\"option.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n\n <!-- Option Groups -->\n @for (group of optionGroups(); track $index; let isFirst = $first) {\n <!-- Group Separator (not shown before first group if we have regular options) -->\n @if (!isFirst || displayOptions().length > 0) {\n <div\n class=\"tn-select-separator\"\n role=\"separator\">\n </div>\n }\n\n <div role=\"group\" [attr.aria-labelledby]=\"'tn-select-group-' + idNamespace() + '-' + $index\">\n <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\n [attr.id]=\"'tn-select-group-' + idNamespace() + '-' + $index\"\n [class.disabled]=\"group.disabled\">\n {{ group.label }}\n </div>\n\n <!-- Group Options -->\n @for (option of group.options; track $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [attr.id]=\"optionId(option)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.focused]=\"isOptionFocused(option)\"\n [class.disabled]=\"option.disabled || group.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled || group.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option, !!group.disabled)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n </div>\n }\n\n <!-- No Options Message -->\n @if (!hasAnyOptions()) {\n <div class=\"tn-select-no-options\">\n {{ noOptionsLabel() }}\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: [".tn-select-container{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-select-label{display:block;margin-bottom:.5rem;font-size:1rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-select-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-select-trigger{position:relative;display:flex;align-items:center;min-height:2.5rem;padding:.5rem 2.5rem .5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:pointer;transition:all .15s ease-in-out;outline:none;box-sizing:border-box}.tn-select-trigger:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-select-trigger:focus-visible{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-select-trigger.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-select-trigger.error{border-color:var(--tn-error, #dc3545)}.tn-select-trigger.error:focus-visible,.tn-select-trigger.error.open{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-error, #dc3545) 25%,transparent)}.tn-select-trigger.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-text{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--tn-fg1, #212529)}.tn-select-text.placeholder{color:var(--tn-alt-fg1, #999)}.tn-select-arrow{position:absolute;right:.75rem;top:50%;transform:translateY(-50%);color:var(--tn-fg2, #6c757d);transition:transform .15s ease-in-out;pointer-events:none}.tn-select-arrow.open{transform:translateY(-50%) rotate(180deg)}.tn-select-arrow svg{display:block}.tn-select-dropdown{--tn-select-dropdown-max-height: 350px;background-color:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;max-height:var(--tn-select-dropdown-max-height)}.tn-select-options{overflow-y:auto;padding:.25rem 0;max-height:var(--tn-select-dropdown-max-height)}.tn-select-option{display:flex;align-items:center;padding:.5rem .75rem;overflow-wrap:anywhere;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out;pointer-events:auto;position:relative;z-index:1001}.tn-select-option.selected{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg1, #212529)}.tn-select-option:hover:not(.disabled):not(.focused){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-select-option.focused{background-color:var(--tn-alt-bg2, #e8f4fd);box-shadow:inset 2px 0 0 var(--tn-primary, #007bff)}.tn-select-option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-option.tn-select-empty-option{color:var(--tn-fg2, #6c757d)}.tn-select-select-all{font-weight:600}.tn-select-check{margin-right:.5rem;flex-shrink:0;pointer-events:none}.tn-select-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:.25rem 0}.tn-select-group-label{padding:.375rem .75rem;font-size:.75rem;font-weight:600;color:var(--tn-alt-fg1, #9ca3af);text-transform:uppercase;letter-spacing:.05em;cursor:default;-webkit-user-select:none;user-select:none}.tn-select-group-label.disabled{opacity:.6}.tn-select-no-options{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}.tn-select-error{margin-top:.25rem;font-size:.75rem;color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-select-trigger,.tn-select-option,.tn-select-arrow{transition:none}}@media(prefers-contrast:high){.tn-select-trigger{border-width:2px}.tn-select-option.selected{outline:2px solid var(--tn-fg1, #000);outline-offset:-2px}}\n"] }]
|
|
9425
|
+
}], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionGroups: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionGroups", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], noOptionsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "noOptionsLabel", required: false }] }], allowEmpty: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowEmpty", required: false }] }], emptyLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], showSelectAll: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSelectAll", required: false }] }], selectAllLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectAllLabel", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], optionTestIdKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionTestIdKey", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], multiSelectionChange: [{ type: i0.Output, args: ["multiSelectionChange"] }], triggerEl: [{ type: i0.ViewChild, args: ['triggerEl', { isSignal: true }] }], dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdownTemplate', { isSignal: true }] }] } });
|
|
9305
9426
|
|
|
9306
9427
|
/**
|
|
9307
9428
|
* Harness for interacting with tn-select in tests.
|
|
@@ -9459,7 +9580,10 @@ class TnSelectHarness extends ComponentHarness {
|
|
|
9459
9580
|
await this.open();
|
|
9460
9581
|
// Dropdown panel is rendered in a CDK overlay (outside the host element),
|
|
9461
9582
|
// so we search the document root rather than the harness-local subtree.
|
|
9462
|
-
|
|
9583
|
+
// Exclude the select-all row (`.tn-select-select-all`) — it's a bulk
|
|
9584
|
+
// action, not a choice; use `toggleSelectAll()` for it.
|
|
9585
|
+
const options = await this.documentRootLocatorFactory()
|
|
9586
|
+
.locatorForAll('.tn-select-option:not(.tn-select-select-all)')();
|
|
9463
9587
|
for (const option of options) {
|
|
9464
9588
|
const text = (await option.text()).trim();
|
|
9465
9589
|
const matches = filter instanceof RegExp
|
|
@@ -9519,13 +9643,55 @@ class TnSelectHarness extends ComponentHarness {
|
|
|
9519
9643
|
await this.open();
|
|
9520
9644
|
// Dropdown panel is rendered in a CDK overlay (outside the host element),
|
|
9521
9645
|
// so we search the document root rather than the harness-local subtree.
|
|
9522
|
-
|
|
9646
|
+
// The select-all row is excluded — it's a bulk action, not an option.
|
|
9647
|
+
const options = await this.documentRootLocatorFactory()
|
|
9648
|
+
.locatorForAll('.tn-select-option:not(.tn-select-select-all)')();
|
|
9523
9649
|
const labels = [];
|
|
9524
9650
|
for (const option of options) {
|
|
9525
9651
|
labels.push((await option.text()).trim());
|
|
9526
9652
|
}
|
|
9527
9653
|
return labels;
|
|
9528
9654
|
}
|
|
9655
|
+
/**
|
|
9656
|
+
* Toggles the select-all row (multiple mode with `showSelectAll`). Opens the
|
|
9657
|
+
* dropdown if needed. Throws when the select-all row isn't present.
|
|
9658
|
+
*
|
|
9659
|
+
* @returns Promise that resolves once the row has been clicked.
|
|
9660
|
+
*
|
|
9661
|
+
* @example
|
|
9662
|
+
* ```typescript
|
|
9663
|
+
* const select = await loader.getHarness(TnSelectHarness);
|
|
9664
|
+
* await select.toggleSelectAll(); // selects every option
|
|
9665
|
+
* await select.toggleSelectAll(); // clears them again
|
|
9666
|
+
* ```
|
|
9667
|
+
*/
|
|
9668
|
+
async toggleSelectAll() {
|
|
9669
|
+
await this.open();
|
|
9670
|
+
// Rendered in the CDK overlay, outside the host subtree.
|
|
9671
|
+
const row = await this.documentRootLocatorFactory()
|
|
9672
|
+
.locatorForOptional('.tn-select-select-all')();
|
|
9673
|
+
if (!row) {
|
|
9674
|
+
await this.close();
|
|
9675
|
+
throw new Error('Select has no select-all row — set `multiple` and `showSelectAll`.');
|
|
9676
|
+
}
|
|
9677
|
+
await row.click();
|
|
9678
|
+
}
|
|
9679
|
+
/**
|
|
9680
|
+
* Whether the select-all checkbox reads as fully checked (all options
|
|
9681
|
+
* selected). Opens the dropdown if needed. Throws when the row isn't present.
|
|
9682
|
+
*
|
|
9683
|
+
* @returns Promise resolving to true when every option is selected.
|
|
9684
|
+
*/
|
|
9685
|
+
async isSelectAllChecked() {
|
|
9686
|
+
await this.open();
|
|
9687
|
+
const row = await this.documentRootLocatorFactory()
|
|
9688
|
+
.locatorForOptional('.tn-select-select-all')();
|
|
9689
|
+
if (!row) {
|
|
9690
|
+
await this.close();
|
|
9691
|
+
throw new Error('Select has no select-all row — set `multiple` and `showSelectAll`.');
|
|
9692
|
+
}
|
|
9693
|
+
return row.hasClass('selected');
|
|
9694
|
+
}
|
|
9529
9695
|
}
|
|
9530
9696
|
|
|
9531
9697
|
/// <reference types="jest" />
|
|
@@ -11238,7 +11404,7 @@ class TnTablePagerComponent {
|
|
|
11238
11404
|
provider.setPagination(pagination);
|
|
11239
11405
|
}
|
|
11240
11406
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTablePagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11241
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnTablePagerComponent, isStandalone: true, selector: "tn-table-pager", inputs: { testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, currentPage: { classPropertyName: "currentPage", publicName: "currentPage", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, totalItems: { classPropertyName: "totalItems", publicName: "totalItems", isSignal: true, isRequired: false, transformFunction: null }, dataProvider: { classPropertyName: "dataProvider", publicName: "dataProvider", isSignal: true, isRequired: false, transformFunction: null }, itemsPerPageLabel: { classPropertyName: "itemsPerPageLabel", publicName: "itemsPerPageLabel", isSignal: true, isRequired: false, transformFunction: null }, ofLabel: { classPropertyName: "ofLabel", publicName: "ofLabel", isSignal: true, isRequired: false, transformFunction: null }, firstPageLabel: { classPropertyName: "firstPageLabel", publicName: "firstPageLabel", isSignal: true, isRequired: false, transformFunction: null }, previousPageLabel: { classPropertyName: "previousPageLabel", publicName: "previousPageLabel", isSignal: true, isRequired: false, transformFunction: null }, nextPageLabel: { classPropertyName: "nextPageLabel", publicName: "nextPageLabel", isSignal: true, isRequired: false, transformFunction: null }, lastPageLabel: { classPropertyName: "lastPageLabel", publicName: "lastPageLabel", isSignal: true, isRequired: false, transformFunction: null }, tablePaginationLabel: { classPropertyName: "tablePaginationLabel", publicName: "tablePaginationLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { currentPage: "currentPageChange", pageSize: "pageSizeChange", pageChange: "pageChange", pageSizeChange: "pageSizeChange" }, host: { attributes: { "role": "navigation" }, properties: { "attr.aria-label": "resolvedTablePaginationLabel()" }, classAttribute: "tn-table-pager" }, ngImport: i0, template: "<div class=\"tn-table-pager__page-size\">\n <span class=\"tn-table-pager__label\">{{ resolvedItemsPerPageLabel() }}:</span>\n <tn-select\n class=\"tn-table-pager__size-select\"\n [testId]=\"childTestId('page-size')\"\n [options]=\"pageSizeSelectOptions()\"\n [ariaLabel]=\"resolvedItemsPerPageLabel()\"\n [ngModel]=\"pageSize()\"\n (ngModelChange)=\"onPageSizeChange($event)\" />\n</div>\n\n<span class=\"tn-table-pager__range\">\n @if (effectiveTotalItems() === 0) {\n 0 {{ resolvedOfLabel() }} 0\n } @else if (lastItemOnPage() > firstItemOnPage()) {\n {{ firstItemOnPage() }} \u2013 {{ lastItemOnPage() }} {{ resolvedOfLabel() }} {{ effectiveTotalItems() }}\n } @else {\n {{ lastItemOnPage() }} {{ resolvedOfLabel() }} {{ effectiveTotalItems() }}\n }\n</span>\n\n<div class=\"tn-table-pager__buttons\">\n <tn-icon-button\n name=\"page-first\"\n library=\"mdi\"\n [testId]=\"childTestId('first-page')\"\n [ariaLabel]=\"resolvedFirstPageLabel()\"\n [disabled]=\"isFirstPageDisabled()\"\n (onClick)=\"goToPage(1)\" />\n <tn-icon-button\n name=\"chevron-left\"\n library=\"mdi\"\n [testId]=\"childTestId('previous-page')\"\n [ariaLabel]=\"resolvedPreviousPageLabel()\"\n [disabled]=\"isFirstPageDisabled()\"\n (onClick)=\"previousPage()\" />\n <tn-icon-button\n name=\"chevron-right\"\n library=\"mdi\"\n [testId]=\"childTestId('next-page')\"\n [ariaLabel]=\"resolvedNextPageLabel()\"\n [disabled]=\"isLastPageDisabled()\"\n (onClick)=\"nextPage()\" />\n <tn-icon-button\n name=\"page-last\"\n library=\"mdi\"\n [testId]=\"childTestId('last-page')\"\n [ariaLabel]=\"resolvedLastPageLabel()\"\n [disabled]=\"isLastPageDisabled()\"\n (onClick)=\"goToPage(totalPages())\" />\n</div>\n", styles: [":host{display:flex;align-items:center;justify-content:flex-end;gap:16px;padding:10px;background-color:var(--tn-bg2);border:1px solid var(--tn-lines);color:var(--tn-fg2)}.tn-table-pager__page-size{display:flex;align-items:center;flex-wrap:wrap;gap:8px}.tn-table-pager__label{white-space:nowrap}.tn-table-pager__size-select{min-width:84px}.tn-table-pager__range{white-space:nowrap}.tn-table-pager__buttons{display:flex;align-items:center;gap:4px}@media(max-width:600px){:host{gap:8px}.tn-table-pager__page-size{gap:4px}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: TnIconButtonComponent, selector: "tn-icon-button", inputs: ["disabled", "dense", "ariaLabel", "ariaExpanded", "testId", "name", "size", "color", "tooltip", "tooltipPosition", "library", "iconClass"], outputs: ["onClick"] }, { kind: "component", type: TnSelectComponent, selector: "tn-select", inputs: ["options", "optionGroups", "placeholder", "ariaLabel", "noOptionsLabel", "allowEmpty", "emptyLabel", "disabled", "testId", "multiple", "optionTestIdKey", "compareWith"], outputs: ["selectionChange", "multiSelectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11407
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnTablePagerComponent, isStandalone: true, selector: "tn-table-pager", inputs: { testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, currentPage: { classPropertyName: "currentPage", publicName: "currentPage", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, totalItems: { classPropertyName: "totalItems", publicName: "totalItems", isSignal: true, isRequired: false, transformFunction: null }, dataProvider: { classPropertyName: "dataProvider", publicName: "dataProvider", isSignal: true, isRequired: false, transformFunction: null }, itemsPerPageLabel: { classPropertyName: "itemsPerPageLabel", publicName: "itemsPerPageLabel", isSignal: true, isRequired: false, transformFunction: null }, ofLabel: { classPropertyName: "ofLabel", publicName: "ofLabel", isSignal: true, isRequired: false, transformFunction: null }, firstPageLabel: { classPropertyName: "firstPageLabel", publicName: "firstPageLabel", isSignal: true, isRequired: false, transformFunction: null }, previousPageLabel: { classPropertyName: "previousPageLabel", publicName: "previousPageLabel", isSignal: true, isRequired: false, transformFunction: null }, nextPageLabel: { classPropertyName: "nextPageLabel", publicName: "nextPageLabel", isSignal: true, isRequired: false, transformFunction: null }, lastPageLabel: { classPropertyName: "lastPageLabel", publicName: "lastPageLabel", isSignal: true, isRequired: false, transformFunction: null }, tablePaginationLabel: { classPropertyName: "tablePaginationLabel", publicName: "tablePaginationLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { currentPage: "currentPageChange", pageSize: "pageSizeChange", pageChange: "pageChange", pageSizeChange: "pageSizeChange" }, host: { attributes: { "role": "navigation" }, properties: { "attr.aria-label": "resolvedTablePaginationLabel()" }, classAttribute: "tn-table-pager" }, ngImport: i0, template: "<div class=\"tn-table-pager__page-size\">\n <span class=\"tn-table-pager__label\">{{ resolvedItemsPerPageLabel() }}:</span>\n <tn-select\n class=\"tn-table-pager__size-select\"\n [testId]=\"childTestId('page-size')\"\n [options]=\"pageSizeSelectOptions()\"\n [ariaLabel]=\"resolvedItemsPerPageLabel()\"\n [ngModel]=\"pageSize()\"\n (ngModelChange)=\"onPageSizeChange($event)\" />\n</div>\n\n<span class=\"tn-table-pager__range\">\n @if (effectiveTotalItems() === 0) {\n 0 {{ resolvedOfLabel() }} 0\n } @else if (lastItemOnPage() > firstItemOnPage()) {\n {{ firstItemOnPage() }} \u2013 {{ lastItemOnPage() }} {{ resolvedOfLabel() }} {{ effectiveTotalItems() }}\n } @else {\n {{ lastItemOnPage() }} {{ resolvedOfLabel() }} {{ effectiveTotalItems() }}\n }\n</span>\n\n<div class=\"tn-table-pager__buttons\">\n <tn-icon-button\n name=\"page-first\"\n library=\"mdi\"\n [testId]=\"childTestId('first-page')\"\n [ariaLabel]=\"resolvedFirstPageLabel()\"\n [disabled]=\"isFirstPageDisabled()\"\n (onClick)=\"goToPage(1)\" />\n <tn-icon-button\n name=\"chevron-left\"\n library=\"mdi\"\n [testId]=\"childTestId('previous-page')\"\n [ariaLabel]=\"resolvedPreviousPageLabel()\"\n [disabled]=\"isFirstPageDisabled()\"\n (onClick)=\"previousPage()\" />\n <tn-icon-button\n name=\"chevron-right\"\n library=\"mdi\"\n [testId]=\"childTestId('next-page')\"\n [ariaLabel]=\"resolvedNextPageLabel()\"\n [disabled]=\"isLastPageDisabled()\"\n (onClick)=\"nextPage()\" />\n <tn-icon-button\n name=\"page-last\"\n library=\"mdi\"\n [testId]=\"childTestId('last-page')\"\n [ariaLabel]=\"resolvedLastPageLabel()\"\n [disabled]=\"isLastPageDisabled()\"\n (onClick)=\"goToPage(totalPages())\" />\n</div>\n", styles: [":host{display:flex;align-items:center;justify-content:flex-end;gap:16px;padding:10px;background-color:var(--tn-bg2);border:1px solid var(--tn-lines);color:var(--tn-fg2)}.tn-table-pager__page-size{display:flex;align-items:center;flex-wrap:wrap;gap:8px}.tn-table-pager__label{white-space:nowrap}.tn-table-pager__size-select{min-width:84px}.tn-table-pager__range{white-space:nowrap}.tn-table-pager__buttons{display:flex;align-items:center;gap:4px}@media(max-width:600px){:host{gap:8px}.tn-table-pager__page-size{gap:4px}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: TnIconButtonComponent, selector: "tn-icon-button", inputs: ["disabled", "dense", "ariaLabel", "ariaExpanded", "testId", "name", "size", "color", "tooltip", "tooltipPosition", "library", "iconClass"], outputs: ["onClick"] }, { kind: "component", type: TnSelectComponent, selector: "tn-select", inputs: ["options", "optionGroups", "placeholder", "ariaLabel", "noOptionsLabel", "allowEmpty", "emptyLabel", "disabled", "showSelectAll", "selectAllLabel", "testId", "multiple", "optionTestIdKey", "compareWith"], outputs: ["selectionChange", "multiSelectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11242
11408
|
}
|
|
11243
11409
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTablePagerComponent, decorators: [{
|
|
11244
11410
|
type: Component,
|
|
@@ -11433,7 +11599,7 @@ class TnTreeNodeComponent extends CdkTreeNode {
|
|
|
11433
11599
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11434
11600
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnTreeNodeComponent, isStandalone: true, selector: "tn-tree-node", host: { attributes: { "role": "treeitem" }, properties: { "attr.aria-level": "level + 1", "attr.aria-expanded": "isExpandable ? isExpanded : null" }, classAttribute: "tn-tree-node-wrapper" }, providers: [
|
|
11435
11601
|
{ provide: CdkTreeNode, useExisting: TnTreeNodeComponent }
|
|
11436
|
-
], exportAs: ["tnTreeNode"], usesInheritance: true, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "
|
|
11602
|
+
], exportAs: ["tnTreeNode"], usesInheritance: true, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<!-- The `role=\"treeitem\"` (plus aria-level/aria-expanded) lives on the host\n `.tn-tree-node-wrapper`, so this inner div is a presentational container \u2014\n no role or aria-* here, to avoid exposing a second, nested treeitem to AT. -->\n<div class=\"tn-tree-node\"\n cdkTreeNodeToggle\n [class.tn-tree-node--expandable]=\"isExpandable\"\n [style.cursor]=\"isExpandable ? 'pointer' : 'default'\">\n \n <div class=\"tn-tree-node__content\">\n <!-- Arrow icon for expandable nodes -->\n @if (isExpandable) {\n <div\n class=\"tn-tree-node__toggle\"\n [class.tn-tree-node__toggle--expanded]=\"isExpanded\">\n <tn-icon\n library=\"mdi\"\n size=\"sm\"\n style=\"transition: transform 0.2s ease;\"\n [name]=\"isExpanded ? 'chevron-down' : 'chevron-right'\" />\n </div>\n }\n\n <!-- Spacer for non-expandable nodes -->\n @if (!isExpandable) {\n <div class=\"tn-tree-node__spacer\"></div>\n }\n \n <!-- Node content -->\n <div class=\"tn-tree-node__text\">\n <ng-content />\n </div>\n </div>\n</div>", styles: [":host{display:block}.tn-tree-node{border-bottom:1px solid var(--tn-lines);transition:background-color .2s ease}.tn-tree-node:hover{background-color:var(--tn-alt-bg2)}.tn-tree-node:last-child{border-bottom:none}.tn-tree-node--expandable{cursor:pointer}.tn-tree-node--expandable:hover{background-color:var(--tn-alt-bg2)}.tn-tree-node--expandable:active{background-color:var(--tn-alt-bg1)}.tn-tree-node__content{display:flex;align-items:center;gap:8px;min-height:48px;padding:12px 16px}.tn-tree-node__toggle{display:flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:none;background:none;color:var(--tn-fg2);cursor:pointer;border-radius:3px;transition:all .2s ease;flex-shrink:0}.tn-tree-node__toggle:hover{background-color:var(--tn-alt-bg2);color:var(--tn-fg1)}.tn-tree-node__toggle:focus{outline:2px solid var(--tn-primary);outline-offset:1px}.tn-tree-node__toggle svg{transition:transform .2s ease;transform:rotate(0)}.tn-tree-node__toggle--expanded svg{transform:rotate(90deg)}.tn-tree-node__spacer{width:24px;height:24px;flex-shrink:0}.tn-tree-node__text{flex:1;min-width:0;color:var(--tn-fg1)}.tn-tree-node__children{padding-left:24px}.tn-tree-invisible{display:none}\n"], dependencies: [{ kind: "ngmodule", type: CdkTreeModule }, { kind: "directive", type: i2.CdkTreeNodeToggle, selector: "[cdkTreeNodeToggle]", inputs: ["cdkTreeNodeToggleRecursive"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11437
11603
|
}
|
|
11438
11604
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeNodeComponent, decorators: [{
|
|
11439
11605
|
type: Component,
|
|
@@ -11444,7 +11610,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
11444
11610
|
'[attr.aria-level]': 'level + 1',
|
|
11445
11611
|
'[attr.aria-expanded]': 'isExpandable ? isExpanded : null',
|
|
11446
11612
|
'role': 'treeitem'
|
|
11447
|
-
}, encapsulation: ViewEncapsulation.Emulated, changeDetection: ChangeDetectionStrategy.OnPush, template: "
|
|
11613
|
+
}, encapsulation: ViewEncapsulation.Emulated, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- The `role=\"treeitem\"` (plus aria-level/aria-expanded) lives on the host\n `.tn-tree-node-wrapper`, so this inner div is a presentational container \u2014\n no role or aria-* here, to avoid exposing a second, nested treeitem to AT. -->\n<div class=\"tn-tree-node\"\n cdkTreeNodeToggle\n [class.tn-tree-node--expandable]=\"isExpandable\"\n [style.cursor]=\"isExpandable ? 'pointer' : 'default'\">\n \n <div class=\"tn-tree-node__content\">\n <!-- Arrow icon for expandable nodes -->\n @if (isExpandable) {\n <div\n class=\"tn-tree-node__toggle\"\n [class.tn-tree-node__toggle--expanded]=\"isExpanded\">\n <tn-icon\n library=\"mdi\"\n size=\"sm\"\n style=\"transition: transform 0.2s ease;\"\n [name]=\"isExpanded ? 'chevron-down' : 'chevron-right'\" />\n </div>\n }\n\n <!-- Spacer for non-expandable nodes -->\n @if (!isExpandable) {\n <div class=\"tn-tree-node__spacer\"></div>\n }\n \n <!-- Node content -->\n <div class=\"tn-tree-node__text\">\n <ng-content />\n </div>\n </div>\n</div>", styles: [":host{display:block}.tn-tree-node{border-bottom:1px solid var(--tn-lines);transition:background-color .2s ease}.tn-tree-node:hover{background-color:var(--tn-alt-bg2)}.tn-tree-node:last-child{border-bottom:none}.tn-tree-node--expandable{cursor:pointer}.tn-tree-node--expandable:hover{background-color:var(--tn-alt-bg2)}.tn-tree-node--expandable:active{background-color:var(--tn-alt-bg1)}.tn-tree-node__content{display:flex;align-items:center;gap:8px;min-height:48px;padding:12px 16px}.tn-tree-node__toggle{display:flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:none;background:none;color:var(--tn-fg2);cursor:pointer;border-radius:3px;transition:all .2s ease;flex-shrink:0}.tn-tree-node__toggle:hover{background-color:var(--tn-alt-bg2);color:var(--tn-fg1)}.tn-tree-node__toggle:focus{outline:2px solid var(--tn-primary);outline-offset:1px}.tn-tree-node__toggle svg{transition:transform .2s ease;transform:rotate(0)}.tn-tree-node__toggle--expanded svg{transform:rotate(90deg)}.tn-tree-node__spacer{width:24px;height:24px;flex-shrink:0}.tn-tree-node__text{flex:1;min-width:0;color:var(--tn-fg1)}.tn-tree-node__children{padding-left:24px}.tn-tree-invisible{display:none}\n"] }]
|
|
11448
11614
|
}], ctorParameters: () => [] });
|
|
11449
11615
|
|
|
11450
11616
|
class TnTreeNodeOutletDirective {
|
|
@@ -11529,6 +11695,679 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
11529
11695
|
}, encapsulation: ViewEncapsulation.Emulated, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"tn-nested-tree-node__content\">\n <!-- Toggle button for expandable nodes (provided by component) -->\n @if (isExpandable) {\n <button\n class=\"tn-nested-tree-node__toggle\"\n cdkTreeNodeToggle\n type=\"button\"\n [class.tn-nested-tree-node__toggle--expanded]=\"isExpanded\"\n [attr.aria-label]=\"'Toggle node'\">\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 <!-- 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}", 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"] }]
|
|
11530
11696
|
}], ctorParameters: () => [] });
|
|
11531
11697
|
|
|
11698
|
+
/**
|
|
11699
|
+
* Renders a single virtual tree row.
|
|
11700
|
+
*
|
|
11701
|
+
* `cdk-virtual-scroll-viewport` materialises this outlet for each row that scrolls
|
|
11702
|
+
* into view; the outlet instantiates the node's template with its context and
|
|
11703
|
+
* (re)binds `CdkTreeNode.mostRecentTreeNode.data`, so the row behaves exactly as a
|
|
11704
|
+
* node rendered directly by the CDK tree. When the same DOM view is recycled for a
|
|
11705
|
+
* different node (virtual scrolling reuses views), only the context is patched.
|
|
11706
|
+
*/
|
|
11707
|
+
class TnTreeVirtualScrollNodeOutletDirective {
|
|
11708
|
+
_viewContainerRef = inject(ViewContainerRef);
|
|
11709
|
+
_viewRef = null;
|
|
11710
|
+
data = input.required(...(ngDevMode ? [{ debugName: "data" }] : []));
|
|
11711
|
+
/**
|
|
11712
|
+
* Re-assert the row's aria position every check. The viewport's recycling view
|
|
11713
|
+
* repeater can swap the underlying DOM view for a different row without our
|
|
11714
|
+
* `ngOnChanges` writing the new `aria-setsize`/`aria-posinset` in a way that
|
|
11715
|
+
* survives the swap, so we reconcile the current element against the current data
|
|
11716
|
+
* on each change-detection pass (two cheap attribute writes) — self-healing.
|
|
11717
|
+
*/
|
|
11718
|
+
ngDoCheck() {
|
|
11719
|
+
this.applyAriaPosition();
|
|
11720
|
+
this.applyKeyboardAffordance();
|
|
11721
|
+
}
|
|
11722
|
+
ngOnChanges(changes) {
|
|
11723
|
+
const dataChange = changes['data'];
|
|
11724
|
+
const recreateView = dataChange ? this.shouldRecreateView(dataChange) : false;
|
|
11725
|
+
if (recreateView) {
|
|
11726
|
+
const viewContainerRef = this._viewContainerRef;
|
|
11727
|
+
if (this._viewRef) {
|
|
11728
|
+
viewContainerRef.remove(viewContainerRef.indexOf(this._viewRef));
|
|
11729
|
+
}
|
|
11730
|
+
this._viewRef = this.data()
|
|
11731
|
+
? viewContainerRef.createEmbeddedView(this.data().nodeDef.template, this.data().context)
|
|
11732
|
+
: null;
|
|
11733
|
+
// Bind the freshly-created CdkTreeNode to this row's data. `mostRecentTreeNode`
|
|
11734
|
+
// is a global CDK static (verified against @angular/cdk 21.x) set synchronously
|
|
11735
|
+
// while `createEmbeddedView` above instantiates the node template. This mirrors
|
|
11736
|
+
// CDK's own internal wiring and assumes the node def's template contains exactly
|
|
11737
|
+
// one CdkTreeNode; it is fragile under view recycling, hence the shape check that
|
|
11738
|
+
// gates recreation. A CDK upgrade that changes this static would stop rows from
|
|
11739
|
+
// binding their data — covered by the outlet's rendering tests.
|
|
11740
|
+
if (CdkTreeNode.mostRecentTreeNode && this._viewRef) {
|
|
11741
|
+
CdkTreeNode.mostRecentTreeNode.data = this.data().data;
|
|
11742
|
+
}
|
|
11743
|
+
}
|
|
11744
|
+
else if (this._viewRef && this.data().context) {
|
|
11745
|
+
this.updateExistingContext(this.data().context);
|
|
11746
|
+
}
|
|
11747
|
+
}
|
|
11748
|
+
/**
|
|
11749
|
+
* Whether the row must be re-instantiated (vs. patched in place). Every wrapper
|
|
11750
|
+
* carries the same fixed key set (`data`/`context`/`nodeDef`/`posInSet`/`setSize`),
|
|
11751
|
+
* so recreation comes down to whether the raw data node is a different reference —
|
|
11752
|
+
* that is what marks a genuinely different row (a missing wrapper also recreates).
|
|
11753
|
+
*/
|
|
11754
|
+
shouldRecreateView(dataChange) {
|
|
11755
|
+
const prevValue = dataChange.previousValue;
|
|
11756
|
+
const currValue = dataChange.currentValue;
|
|
11757
|
+
return prevValue?.data !== currValue?.data;
|
|
11758
|
+
}
|
|
11759
|
+
/**
|
|
11760
|
+
* Reflect posinset/setsize onto the row element so screen readers can announce
|
|
11761
|
+
* "item N of M". Runs on every `ngDoCheck` (the hot CD path), so only write when the
|
|
11762
|
+
* attribute actually differs — skipping the redundant `setAttribute` on the common
|
|
11763
|
+
* pass where nothing changed.
|
|
11764
|
+
*/
|
|
11765
|
+
applyAriaPosition() {
|
|
11766
|
+
const root = this._viewRef?.rootNodes?.[0];
|
|
11767
|
+
const data = this.data();
|
|
11768
|
+
if (!root?.setAttribute || !data) {
|
|
11769
|
+
return;
|
|
11770
|
+
}
|
|
11771
|
+
const setSize = String(data.setSize);
|
|
11772
|
+
const posInSet = String(data.posInSet);
|
|
11773
|
+
if (root.getAttribute('aria-setsize') !== setSize) {
|
|
11774
|
+
root.setAttribute('aria-setsize', setSize);
|
|
11775
|
+
}
|
|
11776
|
+
if (root.getAttribute('aria-posinset') !== posInSet) {
|
|
11777
|
+
root.setAttribute('aria-posinset', posInSet);
|
|
11778
|
+
}
|
|
11779
|
+
}
|
|
11780
|
+
/**
|
|
11781
|
+
* Make expandable rows keyboard-focusable so a user can Tab to a pool/branch and
|
|
11782
|
+
* expand it with Enter/Space (the host `TnTreeVirtualScrollViewComponent` handles the
|
|
11783
|
+
* key). The virtual viewport recycles a row's view between expandable and leaf nodes,
|
|
11784
|
+
* so this reconciles every check; leaf rows are left to the consumer's own interactive
|
|
11785
|
+
* content (e.g. a `routerLink` anchor). Expandability is read from `aria-expanded`,
|
|
11786
|
+
* which `tn-tree-node` sets only on expandable nodes. Only touch the DOM on change —
|
|
11787
|
+
* this runs on the hot CD path.
|
|
11788
|
+
*/
|
|
11789
|
+
applyKeyboardAffordance() {
|
|
11790
|
+
const root = this._viewRef?.rootNodes?.[0];
|
|
11791
|
+
if (!root?.setAttribute) {
|
|
11792
|
+
return;
|
|
11793
|
+
}
|
|
11794
|
+
const expandable = root.hasAttribute('aria-expanded');
|
|
11795
|
+
if (expandable) {
|
|
11796
|
+
if (root.getAttribute('tabindex') !== '0') {
|
|
11797
|
+
root.setAttribute('tabindex', '0');
|
|
11798
|
+
}
|
|
11799
|
+
}
|
|
11800
|
+
else if (root.getAttribute('tabindex') === '0') {
|
|
11801
|
+
root.removeAttribute('tabindex');
|
|
11802
|
+
}
|
|
11803
|
+
}
|
|
11804
|
+
updateExistingContext(ctx) {
|
|
11805
|
+
if (!this._viewRef) {
|
|
11806
|
+
return;
|
|
11807
|
+
}
|
|
11808
|
+
for (const propName of Object.keys(ctx)) {
|
|
11809
|
+
this._viewRef.context[propName]
|
|
11810
|
+
= this.data().context[propName];
|
|
11811
|
+
}
|
|
11812
|
+
}
|
|
11813
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeVirtualScrollNodeOutletDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
11814
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: TnTreeVirtualScrollNodeOutletDirective, isStandalone: true, selector: "[tnTreeVirtualScrollNodeOutlet]", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null } }, usesOnChanges: true, ngImport: i0 });
|
|
11815
|
+
}
|
|
11816
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeVirtualScrollNodeOutletDirective, decorators: [{
|
|
11817
|
+
type: Directive,
|
|
11818
|
+
args: [{
|
|
11819
|
+
selector: '[tnTreeVirtualScrollNodeOutlet]',
|
|
11820
|
+
standalone: true,
|
|
11821
|
+
}]
|
|
11822
|
+
}], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }] } });
|
|
11823
|
+
|
|
11824
|
+
/** Default fixed row height (px) used by the virtual scroll viewport. */
|
|
11825
|
+
const defaultTreeItemSize = 48;
|
|
11826
|
+
/**
|
|
11827
|
+
* Row counts for the default buffer sizes (how many extra rows of content the viewport
|
|
11828
|
+
* renders past the visible area) and for the distance the user must scroll before the
|
|
11829
|
+
* scroll-to-top button appears. Expressed in rows and multiplied by `itemSize()` at
|
|
11830
|
+
* runtime (see resolvedMin/MaxBufferPx) so the defaults scale with a custom row height.
|
|
11831
|
+
*/
|
|
11832
|
+
const defaultMinBufferRows = 4;
|
|
11833
|
+
const defaultBufferRows = 8;
|
|
11834
|
+
const scrollToTopThresholdRows = 8;
|
|
11835
|
+
const scrollFrameScheduler = typeof requestAnimationFrame !== 'undefined' ? animationFrameScheduler : asapScheduler;
|
|
11836
|
+
/**
|
|
11837
|
+
* A `tn-tree` that renders through a `cdk-virtual-scroll-viewport`, so only the
|
|
11838
|
+
* rows currently in view are materialised in the DOM. Drop-in for large trees
|
|
11839
|
+
* (thousands of nodes) where the plain `tn-tree` would render every node.
|
|
11840
|
+
*
|
|
11841
|
+
* Usage mirrors `tn-tree`: provide a `TnTreeFlatDataSource` + `FlatTreeControl`
|
|
11842
|
+
* and one `*cdkTreeNodeDef` template. Rows are a fixed height (`itemSize`).
|
|
11843
|
+
*
|
|
11844
|
+
* ```html
|
|
11845
|
+
* <tn-tree-virtual-scroll-view [dataSource]="dataSource" [treeControl]="treeControl" [itemSize]="52">
|
|
11846
|
+
* <tn-tree-node *cdkTreeNodeDef="let node" cdkTreeNodePadding [routerLink]="...">
|
|
11847
|
+
* {{ node.name }}
|
|
11848
|
+
* </tn-tree-node>
|
|
11849
|
+
* </tn-tree-virtual-scroll-view>
|
|
11850
|
+
* ```
|
|
11851
|
+
*
|
|
11852
|
+
* Accessibility: rows are exposed with `role="treeitem"` plus `aria-level`,
|
|
11853
|
+
* `aria-posinset` and `aria-setsize` (the latter two relative to the node's
|
|
11854
|
+
* siblings — nodes sharing the same parent/level — per the WAI-ARIA tree
|
|
11855
|
+
* pattern, not the flattened list of all visible rows), and the virtual-scroll
|
|
11856
|
+
* wrappers are marked `role="presentation"` so assistive tech still sees the
|
|
11857
|
+
* items as children of the `role="tree"` host.
|
|
11858
|
+
*
|
|
11859
|
+
* Keyboard: expandable rows are focusable and expand/collapse with Enter/Space (see
|
|
11860
|
+
* {@link onTreeKeydown}), so a keyboard/AT user can operate the `aria-expanded` that
|
|
11861
|
+
* each expandable `treeitem` advertises. Roving arrow-key / Home / End navigation
|
|
11862
|
+
* BETWEEN nodes is NOT supported here, though — because rows are materialised (and
|
|
11863
|
+
* recycled) by the virtual scroll viewport rather than registered with CdkTree's node
|
|
11864
|
+
* outlet, the CDK `TreeKeyManager` has no stable node set to drive. Use Tab to move
|
|
11865
|
+
* between rows, or the non-virtual `tn-tree` when full roving navigation is required.
|
|
11866
|
+
*/
|
|
11867
|
+
class TnTreeVirtualScrollViewComponent extends CdkTree {
|
|
11868
|
+
destroyRef = inject(DestroyRef);
|
|
11869
|
+
cdr = inject(ChangeDetectorRef);
|
|
11870
|
+
virtualScrollViewport = viewChild.required(CdkVirtualScrollViewport);
|
|
11871
|
+
/** Fixed row height in px. Must match the actual rendered node height. */
|
|
11872
|
+
itemSize = input(defaultTreeItemSize, ...(ngDevMode ? [{ debugName: "itemSize" }] : []));
|
|
11873
|
+
/**
|
|
11874
|
+
* Viewport buffer sizes (px). Leave at 0 (the default) to derive them from `itemSize()`
|
|
11875
|
+
* — {@link defaultMinBufferRows} / {@link defaultBufferRows} rows — so the buffers scale
|
|
11876
|
+
* with a custom row height; pass an explicit px value to override. (A fixed px default
|
|
11877
|
+
* would under-buffer a tall `itemSize`, even failing to buffer a single row when
|
|
11878
|
+
* `itemSize > maxBufferPx`.)
|
|
11879
|
+
*/
|
|
11880
|
+
minBufferPx = input(0, ...(ngDevMode ? [{ debugName: "minBufferPx" }] : []));
|
|
11881
|
+
maxBufferPx = input(0, ...(ngDevMode ? [{ debugName: "maxBufferPx" }] : []));
|
|
11882
|
+
/** `minBufferPx` resolved to px, falling back to a row-count default that tracks `itemSize()`. */
|
|
11883
|
+
resolvedMinBufferPx = computed(() => this.minBufferPx() || this.itemSize() * defaultMinBufferRows, ...(ngDevMode ? [{ debugName: "resolvedMinBufferPx" }] : []));
|
|
11884
|
+
/** `maxBufferPx` resolved to px, falling back to a row-count default that tracks `itemSize()`. */
|
|
11885
|
+
resolvedMaxBufferPx = computed(() => this.maxBufferPx() || this.itemSize() * defaultBufferRows, ...(ngDevMode ? [{ debugName: "resolvedMaxBufferPx" }] : []));
|
|
11886
|
+
/**
|
|
11887
|
+
* When true the viewport scrolls with the window instead of an internal scroll area.
|
|
11888
|
+
* `booleanAttribute` so the bare presence form (`<... scrollWindow>`) coerces to `true`
|
|
11889
|
+
* rather than the empty-string (falsy) an un-transformed input would receive.
|
|
11890
|
+
*
|
|
11891
|
+
* Set once at initialisation — NOT reactive. The scroll listener, presentational roles
|
|
11892
|
+
* and ResizeObserver are wired in `ngAfterViewInit` against the viewport that this input
|
|
11893
|
+
* selected; toggling it later re-renders the viewport but does not re-run that wiring.
|
|
11894
|
+
*/
|
|
11895
|
+
scrollWindow = input(false, { ...(ngDevMode ? { debugName: "scrollWindow" } : {}), transform: booleanAttribute });
|
|
11896
|
+
/** Whether to show the floating "scroll to top" button once scrolled down. */
|
|
11897
|
+
showScrollToTop = input(true, { ...(ngDevMode ? { debugName: "showScrollToTop" } : {}), transform: booleanAttribute });
|
|
11898
|
+
/** Accessible label / tooltip for the scroll-to-top button (i18n is the consumer's job). */
|
|
11899
|
+
scrollToTopLabel = input('Scroll to top', ...(ngDevMode ? [{ debugName: "scrollToTopLabel" }] : []));
|
|
11900
|
+
/**
|
|
11901
|
+
* Per-row trackBy over the ORIGINAL data node (not the internal wrapper). Strongly
|
|
11902
|
+
* recommended: without it rows track by index, so the recycling viewport reuses a
|
|
11903
|
+
* detached view for whatever node lands at that index on data changes. Pass a stable
|
|
11904
|
+
* key (e.g. `(_, node) => node.id`) as the stories do.
|
|
11905
|
+
*/
|
|
11906
|
+
nodeTrackBy = input(...(ngDevMode ? [undefined, { debugName: "nodeTrackBy" }] : []));
|
|
11907
|
+
/** Emits the viewport's horizontal `scrollLeft` (used to sync a sticky column header). */
|
|
11908
|
+
viewportScrolled = output();
|
|
11909
|
+
/**
|
|
11910
|
+
* Emits the observed content size whenever it changes (used to size a sticky header).
|
|
11911
|
+
* `width` is the full rendered content width — including horizontal overflow past the
|
|
11912
|
+
* viewport — which is what a horizontally-synced header needs. `height` is the observed
|
|
11913
|
+
* content wrapper's height: the visible viewport height in internal-scroll mode, but the
|
|
11914
|
+
* FULL content height (row count × `itemSize`) in `scrollWindow` mode, where the wrapper
|
|
11915
|
+
* grows with the document. Consumers wanting the visible viewport height should not rely
|
|
11916
|
+
* on this field in window mode.
|
|
11917
|
+
*/
|
|
11918
|
+
viewportResized = output();
|
|
11919
|
+
/** Last `scrollLeft` emitted via {@link viewportScrolled}; used to skip redundant emits. */
|
|
11920
|
+
lastEmittedScrollLeft = 0;
|
|
11921
|
+
nodes$ = new BehaviorSubject([]);
|
|
11922
|
+
innerTrackBy = computed(() => {
|
|
11923
|
+
const fn = this.nodeTrackBy();
|
|
11924
|
+
return fn
|
|
11925
|
+
? (index, node) => fn(index, node.data)
|
|
11926
|
+
: (index) => index;
|
|
11927
|
+
}, ...(ngDevMode ? [{ debugName: "innerTrackBy" }] : []));
|
|
11928
|
+
renderNodeChanges$ = new BehaviorSubject([]);
|
|
11929
|
+
resizeObserver = null;
|
|
11930
|
+
scrollViewportElement = null;
|
|
11931
|
+
scrollFrameSubscription = null;
|
|
11932
|
+
constructor() {
|
|
11933
|
+
super(inject(IterableDiffers), inject(ChangeDetectorRef), inject(ViewContainerRef));
|
|
11934
|
+
this.listenForNodeChanges();
|
|
11935
|
+
}
|
|
11936
|
+
ngAfterViewInit() {
|
|
11937
|
+
super.ngAfterViewInit?.();
|
|
11938
|
+
const element = this.virtualScrollViewport().elementRef.nativeElement;
|
|
11939
|
+
const scrollable = this.virtualScrollViewport().scrollable;
|
|
11940
|
+
// `getElementRef()` resolves to the viewport in the self-scrolling case and to the
|
|
11941
|
+
// external scroll element otherwise — in scrollWindow mode that is
|
|
11942
|
+
// `document.documentElement`, which is what we read `scrollLeft` from below.
|
|
11943
|
+
this.scrollViewportElement = scrollable.getElementRef().nativeElement;
|
|
11944
|
+
// React to scrolls via the scrollable's own stream rather than addEventListener on
|
|
11945
|
+
// that element ref. In scrollWindow mode the scroll EVENTS fire on `document` while
|
|
11946
|
+
// the element ref is `document.documentElement`, so a raw listener on the element
|
|
11947
|
+
// would never fire and the scroll-to-top button / `viewportScrolled` could not react.
|
|
11948
|
+
// `elementScrolled()` is wired to the correct target for both internal and window modes.
|
|
11949
|
+
scrollable.elementScrolled()
|
|
11950
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
11951
|
+
.subscribe(this.onViewportScroll);
|
|
11952
|
+
// Observe the rendered CONTENT wrapper rather than the viewport itself, so
|
|
11953
|
+
// `viewportResized` reports the full (possibly horizontally-overflowing) content
|
|
11954
|
+
// width — that is what a sticky column header needs to stay aligned with the rows.
|
|
11955
|
+
// Falls back to the viewport element if the wrapper is not present.
|
|
11956
|
+
const contentWrapper = element.querySelector('.cdk-virtual-scroll-content-wrapper');
|
|
11957
|
+
const observed = contentWrapper ?? element;
|
|
11958
|
+
// The CDK viewport + content wrapper sit between the host `role="tree"` and the
|
|
11959
|
+
// `role="treeitem"` rows. Mark them presentational so assistive tech collapses
|
|
11960
|
+
// them and still sees the rows as direct tree items (ARIA requires treeitems to
|
|
11961
|
+
// be children of the tree, optionally through a presentational container).
|
|
11962
|
+
element.setAttribute('role', 'presentation');
|
|
11963
|
+
contentWrapper?.setAttribute('role', 'presentation');
|
|
11964
|
+
// Guarded for non-browser environments (SSR, jsdom-based unit tests) where
|
|
11965
|
+
// ResizeObserver is absent; the resize output simply does not emit there.
|
|
11966
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
11967
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
11968
|
+
const rect = entries[0]?.contentRect;
|
|
11969
|
+
if (rect) {
|
|
11970
|
+
this.viewportResized.emit({ width: rect.width, height: rect.height });
|
|
11971
|
+
}
|
|
11972
|
+
});
|
|
11973
|
+
this.resizeObserver.observe(observed);
|
|
11974
|
+
}
|
|
11975
|
+
}
|
|
11976
|
+
ngOnDestroy() {
|
|
11977
|
+
super.ngOnDestroy();
|
|
11978
|
+
// The scroll subscription is torn down by takeUntilDestroyed; just drop refs here.
|
|
11979
|
+
this.scrollViewportElement = null;
|
|
11980
|
+
this.scrollFrameSubscription?.unsubscribe();
|
|
11981
|
+
this.scrollFrameSubscription = null;
|
|
11982
|
+
this.resizeObserver?.disconnect();
|
|
11983
|
+
this.resizeObserver = null;
|
|
11984
|
+
}
|
|
11985
|
+
/**
|
|
11986
|
+
* Cached visibility of the scroll-to-top button. Recomputed only on scroll (see
|
|
11987
|
+
* {@link onViewportScroll}) rather than read as a getter every change-detection
|
|
11988
|
+
* pass, so `measureScrollOffset` — a layout-forcing read — is not called each cycle.
|
|
11989
|
+
*/
|
|
11990
|
+
isScrollTopButtonVisible = false;
|
|
11991
|
+
scrollToTop() {
|
|
11992
|
+
// Use scrollToOffset (which delegates to the viewport's scrollable) rather than
|
|
11993
|
+
// scrollTo (which scrolls the viewport element directly) so this also scrolls an
|
|
11994
|
+
// external scroll container back to the top.
|
|
11995
|
+
this.virtualScrollViewport().scrollToOffset(0, 'smooth');
|
|
11996
|
+
this.cdr.markForCheck();
|
|
11997
|
+
}
|
|
11998
|
+
/**
|
|
11999
|
+
* Enter/Space on a focused expandable row toggles its expansion. The virtual viewport
|
|
12000
|
+
* recycles rows so CDK's `TreeKeyManager` can't drive them (see class docs); this keeps
|
|
12001
|
+
* the expand/collapse that each `aria-expanded` row advertises keyboard-operable, via
|
|
12002
|
+
* event delegation on the `role="tree"` host that reuses the row's `cdkTreeNodeToggle`
|
|
12003
|
+
* click handler. Ignored when focus is on an inner control (e.g. a `routerLink` anchor)
|
|
12004
|
+
* so that element keeps its own Enter/Space behaviour.
|
|
12005
|
+
*/
|
|
12006
|
+
onTreeKeydown(event) {
|
|
12007
|
+
if (event.key !== 'Enter' && event.key !== ' ' && event.key !== 'Spacebar') {
|
|
12008
|
+
return;
|
|
12009
|
+
}
|
|
12010
|
+
const target = event.target;
|
|
12011
|
+
const row = target?.closest?.('.tn-tree-node-wrapper[role="treeitem"]');
|
|
12012
|
+
if (!row || row !== target || !row.hasAttribute('aria-expanded')) {
|
|
12013
|
+
return;
|
|
12014
|
+
}
|
|
12015
|
+
const toggle = row.querySelector('[cdkTreeNodeToggle]');
|
|
12016
|
+
if (toggle) {
|
|
12017
|
+
// Prevent Space from scrolling the page; reuse the toggle's existing click handler.
|
|
12018
|
+
event.preventDefault();
|
|
12019
|
+
toggle.click();
|
|
12020
|
+
}
|
|
12021
|
+
}
|
|
12022
|
+
updateScrollTopButtonVisibility() {
|
|
12023
|
+
const visible = this.showScrollToTop()
|
|
12024
|
+
&& this.virtualScrollViewport().measureScrollOffset('top') > this.itemSize() * scrollToTopThresholdRows;
|
|
12025
|
+
if (visible !== this.isScrollTopButtonVisible) {
|
|
12026
|
+
this.isScrollTopButtonVisible = visible;
|
|
12027
|
+
this.cdr.markForCheck();
|
|
12028
|
+
}
|
|
12029
|
+
}
|
|
12030
|
+
// CdkTree calls this with the full set of currently-visible nodes. Instead of
|
|
12031
|
+
// rendering them into the outlet, feed them to the virtual scroll pipeline.
|
|
12032
|
+
renderNodeChanges(data) {
|
|
12033
|
+
this.renderNodeChanges$.next(data);
|
|
12034
|
+
}
|
|
12035
|
+
getNodeLevel(nodeData) {
|
|
12036
|
+
return this.treeControl?.getLevel ? this.treeControl.getLevel(nodeData) : 0;
|
|
12037
|
+
}
|
|
12038
|
+
/**
|
|
12039
|
+
* Compute per-row `aria-posinset` / `aria-setsize` scoped to each node's SIBLINGS
|
|
12040
|
+
* (same parent / same level), as the WAI-ARIA tree pattern requires — not the flat
|
|
12041
|
+
* list of all visible rows. The flat node set is depth-first ordered and each node
|
|
12042
|
+
* carries a level, so siblings at level L are the consecutive level-L nodes bounded
|
|
12043
|
+
* by any shallower node (level < L = a parent boundary); deeper nodes in between are
|
|
12044
|
+
* descendants of an earlier sibling and don't split the group. One pass, tracking the
|
|
12045
|
+
* open sibling group at each level.
|
|
12046
|
+
*/
|
|
12047
|
+
computeAriaPositions(levels) {
|
|
12048
|
+
const posInSet = new Array(levels.length);
|
|
12049
|
+
const setSize = new Array(levels.length);
|
|
12050
|
+
// groups[level] = indices of the currently-open sibling group at that depth.
|
|
12051
|
+
const groups = [];
|
|
12052
|
+
// Close (and stamp setSize on) every group deeper than `level`, because reaching a
|
|
12053
|
+
// node at `level` ends any sibling groups nested under the previous sibling.
|
|
12054
|
+
const finalizeDeeperThan = (level) => {
|
|
12055
|
+
for (let l = groups.length - 1; l > level; l--) {
|
|
12056
|
+
const group = groups[l];
|
|
12057
|
+
if (group?.length) {
|
|
12058
|
+
for (const idx of group) {
|
|
12059
|
+
setSize[idx] = group.length;
|
|
12060
|
+
}
|
|
12061
|
+
}
|
|
12062
|
+
}
|
|
12063
|
+
groups.length = level + 1;
|
|
12064
|
+
};
|
|
12065
|
+
for (let i = 0; i < levels.length; i++) {
|
|
12066
|
+
const level = levels[i];
|
|
12067
|
+
finalizeDeeperThan(level);
|
|
12068
|
+
(groups[level] ??= []).push(i);
|
|
12069
|
+
posInSet[i] = groups[level].length;
|
|
12070
|
+
}
|
|
12071
|
+
// Close the groups still open at the end of the list.
|
|
12072
|
+
finalizeDeeperThan(-1);
|
|
12073
|
+
return levels.map((_, i) => ({ posInSet: posInSet[i], setSize: setSize[i] }));
|
|
12074
|
+
}
|
|
12075
|
+
createNode(nodeData, index, level, posInSet, setSize) {
|
|
12076
|
+
// `_getNodeDef` is a private CdkTree API (verified against @angular/cdk 21.x). Assert
|
|
12077
|
+
// it resolves so a CDK upgrade that renames/removes it fails loudly here rather than
|
|
12078
|
+
// silently rendering a blank tree at runtime.
|
|
12079
|
+
const nodeDef = this._getNodeDef(nodeData, index);
|
|
12080
|
+
if (!nodeDef) {
|
|
12081
|
+
throw new Error('TnTreeVirtualScrollView: CdkTree._getNodeDef returned no node definition. '
|
|
12082
|
+
+ 'This private @angular/cdk API (expected ^21.1.0) may have changed — verify tree compatibility.');
|
|
12083
|
+
}
|
|
12084
|
+
const context = new CdkTreeNodeOutletContext(nodeData);
|
|
12085
|
+
context.level = level;
|
|
12086
|
+
// posInSet/setSize let a screen reader announce "item N of M" even though only a
|
|
12087
|
+
// slice of rows exists in the DOM at any time. They are scoped to this node's
|
|
12088
|
+
// siblings (see computeAriaPositions), per the WAI-ARIA tree pattern.
|
|
12089
|
+
return {
|
|
12090
|
+
data: nodeData, context, nodeDef, posInSet, setSize,
|
|
12091
|
+
};
|
|
12092
|
+
}
|
|
12093
|
+
buildVirtualNodes(data) {
|
|
12094
|
+
const levels = data.map((node) => this.getNodeLevel(node));
|
|
12095
|
+
const positions = this.computeAriaPositions(levels);
|
|
12096
|
+
return data.map((node, index) => this.createNode(node, index, levels[index], positions[index].posInSet, positions[index].setSize));
|
|
12097
|
+
}
|
|
12098
|
+
listenForNodeChanges() {
|
|
12099
|
+
this.renderNodeChanges$.pipe(auditTime(0, scrollFrameScheduler), map((data) => this.buildVirtualNodes(data)), takeUntilDestroyed(this.destroyRef)).subscribe((nodes) => {
|
|
12100
|
+
this.nodes$.next(nodes);
|
|
12101
|
+
this.cdr.markForCheck();
|
|
12102
|
+
// The viewport's recycling repeater may swap a row's DOM view during the render
|
|
12103
|
+
// triggered above, leaving the outlet's aria-posinset/aria-setsize written to a
|
|
12104
|
+
// now-detached element. Schedule one more check so the outlet's ngDoCheck
|
|
12105
|
+
// reconciles the aria attributes onto the settled elements.
|
|
12106
|
+
queueMicrotask(() => this.cdr.markForCheck());
|
|
12107
|
+
// The visible node set just changed (e.g. a large branch was collapsed), which can
|
|
12108
|
+
// shrink the scrollable content and clamp the scroll offset without firing a scroll
|
|
12109
|
+
// event. Re-evaluate the scroll-to-top button after layout settles so it can't linger
|
|
12110
|
+
// over content that is no longer scrollable. Guarded by the live element ref so a
|
|
12111
|
+
// frame scheduled just before destroy doesn't measure a torn-down viewport.
|
|
12112
|
+
scrollFrameScheduler.schedule(() => {
|
|
12113
|
+
if (this.scrollViewportElement) {
|
|
12114
|
+
this.updateScrollTopButtonVisibility();
|
|
12115
|
+
}
|
|
12116
|
+
});
|
|
12117
|
+
});
|
|
12118
|
+
}
|
|
12119
|
+
onViewportScroll = () => {
|
|
12120
|
+
// Scroll events can fire several times per frame. Coalesce the work — a
|
|
12121
|
+
// horizontal-offset emit and `measureScrollOffset` (a layout-forcing read) — into a
|
|
12122
|
+
// single animation frame so fast scrolls don't trigger repeated forced reflows.
|
|
12123
|
+
if (this.scrollFrameSubscription) {
|
|
12124
|
+
return;
|
|
12125
|
+
}
|
|
12126
|
+
this.scrollFrameSubscription = scrollFrameScheduler.schedule(() => {
|
|
12127
|
+
this.scrollFrameSubscription = null;
|
|
12128
|
+
// Read scrollLeft from the actual scroll source, not the viewport element: in
|
|
12129
|
+
// scrollWindow mode the viewport itself does not scroll (the window/external
|
|
12130
|
+
// element does), so the viewport's own scrollLeft would always be 0. Only emit when
|
|
12131
|
+
// it actually changed — this fires on vertical scrolls too, where scrollLeft is
|
|
12132
|
+
// unchanged, and a consumer syncing a sticky header would otherwise re-apply the
|
|
12133
|
+
// same translateX on every vertical scroll frame of a large tree.
|
|
12134
|
+
const left = this.scrollViewportElement?.scrollLeft ?? 0;
|
|
12135
|
+
if (left !== this.lastEmittedScrollLeft) {
|
|
12136
|
+
this.lastEmittedScrollLeft = left;
|
|
12137
|
+
this.viewportScrolled.emit(left);
|
|
12138
|
+
}
|
|
12139
|
+
// Re-evaluate the scroll-to-top button on every scroll (the scroll source may be an
|
|
12140
|
+
// external, OnPush-detached container, so nothing else marks this view dirty).
|
|
12141
|
+
this.updateScrollTopButtonVisibility();
|
|
12142
|
+
});
|
|
12143
|
+
};
|
|
12144
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeVirtualScrollViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
12145
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnTreeVirtualScrollViewComponent, isStandalone: true, selector: "tn-tree-virtual-scroll-view", inputs: { itemSize: { classPropertyName: "itemSize", publicName: "itemSize", isSignal: true, isRequired: false, transformFunction: null }, minBufferPx: { classPropertyName: "minBufferPx", publicName: "minBufferPx", isSignal: true, isRequired: false, transformFunction: null }, maxBufferPx: { classPropertyName: "maxBufferPx", publicName: "maxBufferPx", isSignal: true, isRequired: false, transformFunction: null }, scrollWindow: { classPropertyName: "scrollWindow", publicName: "scrollWindow", isSignal: true, isRequired: false, transformFunction: null }, showScrollToTop: { classPropertyName: "showScrollToTop", publicName: "showScrollToTop", isSignal: true, isRequired: false, transformFunction: null }, scrollToTopLabel: { classPropertyName: "scrollToTopLabel", publicName: "scrollToTopLabel", isSignal: true, isRequired: false, transformFunction: null }, nodeTrackBy: { classPropertyName: "nodeTrackBy", publicName: "nodeTrackBy", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { viewportScrolled: "viewportScrolled", viewportResized: "viewportResized" }, host: { attributes: { "role": "tree" }, listeners: { "keydown": "onTreeKeydown($event)" }, properties: { "style.--tn-tree-item-size.px": "itemSize()" }, classAttribute: "tn-tree tn-tree-virtual-scroll-view" }, providers: [
|
|
12146
|
+
{ provide: CdkTree, useExisting: TnTreeVirtualScrollViewComponent },
|
|
12147
|
+
], viewQueries: [{ propertyName: "virtualScrollViewport", first: true, predicate: CdkVirtualScrollViewport, descendants: true, isSignal: true }], exportAs: ["tnTreeVirtualScrollView"], usesInheritance: true, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<!-- The two branches are intentionally near-identical: `scrollWindow` is a structural\n directive (CdkVirtualScrollableWindow) selected by the bare attribute's presence, so\n it can't be toggled via a property binding on a single viewport. Don't DRY this into\n one element \u2014 dropping the attribute selection breaks window-scroll mode. -->\n@if (scrollWindow()) {\n <cdk-virtual-scroll-viewport\n scrollWindow\n [itemSize]=\"itemSize()\"\n [minBufferPx]=\"resolvedMinBufferPx()\"\n [maxBufferPx]=\"resolvedMaxBufferPx()\"\n >\n <ng-container *cdkVirtualFor=\"let item of nodes$ | async; trackBy: innerTrackBy()\">\n <ng-template tnTreeVirtualScrollNodeOutlet [data]=\"item\" />\n </ng-container>\n </cdk-virtual-scroll-viewport>\n} @else {\n <cdk-virtual-scroll-viewport\n class=\"tn-tree-virtual-scroll-view__viewport\"\n [itemSize]=\"itemSize()\"\n [minBufferPx]=\"resolvedMinBufferPx()\"\n [maxBufferPx]=\"resolvedMaxBufferPx()\"\n >\n <ng-container *cdkVirtualFor=\"let item of nodes$ | async; trackBy: innerTrackBy()\">\n <ng-template tnTreeVirtualScrollNodeOutlet [data]=\"item\" />\n </ng-container>\n </cdk-virtual-scroll-viewport>\n}\n\n<!-- Required so CdkTree finds a node outlet during init, but intentionally left\n empty: rows are rendered through the virtual scroll viewport above, not here. -->\n<ng-container cdkTreeNodeOutlet />\n\n@if (isScrollTopButtonVisible) {\n <tn-icon-button\n name=\"arrow-up\"\n library=\"mdi\"\n class=\"tn-tree-virtual-scroll-view__scroll-top\"\n testId=\"scroll-to-top\"\n [class.tn-tree-virtual-scroll-view__scroll-top--fixed]=\"scrollWindow()\"\n [ariaLabel]=\"scrollToTopLabel()\"\n [tooltip]=\"scrollToTopLabel()\"\n (onClick)=\"scrollToTop()\"\n />\n}\n", styles: [".tn-tree-virtual-scroll-view__viewport{height:100%;width:100%}.tn-tree-virtual-scroll-view{display:block;position:relative;width:100%;height:100%;--tn-tree-item-size: 48px}.tn-tree-virtual-scroll-view .tn-tree-node__content{height:var(--tn-tree-item-size);min-height:var(--tn-tree-item-size);padding-top:0;padding-bottom:0;box-sizing:border-box}.tn-tree-virtual-scroll-view__scroll-top{position:absolute;right:16px;bottom:16px;z-index:2}.tn-tree-virtual-scroll-view__scroll-top--fixed{position:fixed}\n"], dependencies: [{ kind: "ngmodule", type: CdkTreeModule }, { kind: "directive", type: i2.CdkTreeNodeOutlet, selector: "[cdkTreeNodeOutlet]" }, { kind: "component", type: CdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }, { kind: "directive", type: CdkVirtualScrollableWindow, selector: "cdk-virtual-scroll-viewport[scrollWindow]" }, { kind: "directive", type: CdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: CdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }, { kind: "directive", type: TnTreeVirtualScrollNodeOutletDirective, selector: "[tnTreeVirtualScrollNodeOutlet]", inputs: ["data"] }, { kind: "component", type: TnIconButtonComponent, selector: "tn-icon-button", inputs: ["disabled", "dense", "ariaLabel", "ariaExpanded", "testId", "name", "size", "color", "tooltip", "tooltipPosition", "library", "iconClass"], outputs: ["onClick"] }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
12148
|
+
}
|
|
12149
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeVirtualScrollViewComponent, decorators: [{
|
|
12150
|
+
type: Component,
|
|
12151
|
+
args: [{ selector: 'tn-tree-virtual-scroll-view', standalone: true, exportAs: 'tnTreeVirtualScrollView', providers: [
|
|
12152
|
+
{ provide: CdkTree, useExisting: TnTreeVirtualScrollViewComponent },
|
|
12153
|
+
], imports: [
|
|
12154
|
+
CdkTreeModule,
|
|
12155
|
+
CdkVirtualScrollViewport,
|
|
12156
|
+
CdkVirtualScrollableWindow,
|
|
12157
|
+
CdkFixedSizeVirtualScroll,
|
|
12158
|
+
CdkVirtualForOf,
|
|
12159
|
+
TnTreeVirtualScrollNodeOutletDirective,
|
|
12160
|
+
TnIconButtonComponent,
|
|
12161
|
+
AsyncPipe,
|
|
12162
|
+
], hostDirectives: [{ directive: TnTestIdDirective, inputs: ['tnTestId: testId'] }], host: {
|
|
12163
|
+
'class': 'tn-tree tn-tree-virtual-scroll-view',
|
|
12164
|
+
'role': 'tree',
|
|
12165
|
+
// Expose itemSize to CSS so each row can be pinned to exactly this height
|
|
12166
|
+
// (see the .scss) — virtual scrolling needs the painted row height to match
|
|
12167
|
+
// the height the viewport assumes per item, otherwise hover/selection
|
|
12168
|
+
// backgrounds bleed across neighbouring rows.
|
|
12169
|
+
'[style.--tn-tree-item-size.px]': 'itemSize()',
|
|
12170
|
+
'(keydown)': 'onTreeKeydown($event)',
|
|
12171
|
+
}, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- The two branches are intentionally near-identical: `scrollWindow` is a structural\n directive (CdkVirtualScrollableWindow) selected by the bare attribute's presence, so\n it can't be toggled via a property binding on a single viewport. Don't DRY this into\n one element \u2014 dropping the attribute selection breaks window-scroll mode. -->\n@if (scrollWindow()) {\n <cdk-virtual-scroll-viewport\n scrollWindow\n [itemSize]=\"itemSize()\"\n [minBufferPx]=\"resolvedMinBufferPx()\"\n [maxBufferPx]=\"resolvedMaxBufferPx()\"\n >\n <ng-container *cdkVirtualFor=\"let item of nodes$ | async; trackBy: innerTrackBy()\">\n <ng-template tnTreeVirtualScrollNodeOutlet [data]=\"item\" />\n </ng-container>\n </cdk-virtual-scroll-viewport>\n} @else {\n <cdk-virtual-scroll-viewport\n class=\"tn-tree-virtual-scroll-view__viewport\"\n [itemSize]=\"itemSize()\"\n [minBufferPx]=\"resolvedMinBufferPx()\"\n [maxBufferPx]=\"resolvedMaxBufferPx()\"\n >\n <ng-container *cdkVirtualFor=\"let item of nodes$ | async; trackBy: innerTrackBy()\">\n <ng-template tnTreeVirtualScrollNodeOutlet [data]=\"item\" />\n </ng-container>\n </cdk-virtual-scroll-viewport>\n}\n\n<!-- Required so CdkTree finds a node outlet during init, but intentionally left\n empty: rows are rendered through the virtual scroll viewport above, not here. -->\n<ng-container cdkTreeNodeOutlet />\n\n@if (isScrollTopButtonVisible) {\n <tn-icon-button\n name=\"arrow-up\"\n library=\"mdi\"\n class=\"tn-tree-virtual-scroll-view__scroll-top\"\n testId=\"scroll-to-top\"\n [class.tn-tree-virtual-scroll-view__scroll-top--fixed]=\"scrollWindow()\"\n [ariaLabel]=\"scrollToTopLabel()\"\n [tooltip]=\"scrollToTopLabel()\"\n (onClick)=\"scrollToTop()\"\n />\n}\n", styles: [".tn-tree-virtual-scroll-view__viewport{height:100%;width:100%}.tn-tree-virtual-scroll-view{display:block;position:relative;width:100%;height:100%;--tn-tree-item-size: 48px}.tn-tree-virtual-scroll-view .tn-tree-node__content{height:var(--tn-tree-item-size);min-height:var(--tn-tree-item-size);padding-top:0;padding-bottom:0;box-sizing:border-box}.tn-tree-virtual-scroll-view__scroll-top{position:absolute;right:16px;bottom:16px;z-index:2}.tn-tree-virtual-scroll-view__scroll-top--fixed{position:fixed}\n"] }]
|
|
12172
|
+
}], ctorParameters: () => [], propDecorators: { virtualScrollViewport: [{ type: i0.ViewChild, args: [i0.forwardRef(() => CdkVirtualScrollViewport), { isSignal: true }] }], itemSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemSize", required: false }] }], minBufferPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "minBufferPx", required: false }] }], maxBufferPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxBufferPx", required: false }] }], scrollWindow: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollWindow", required: false }] }], showScrollToTop: [{ type: i0.Input, args: [{ isSignal: true, alias: "showScrollToTop", required: false }] }], scrollToTopLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollToTopLabel", required: false }] }], nodeTrackBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodeTrackBy", required: false }] }], viewportScrolled: [{ type: i0.Output, args: ["viewportScrolled"] }], viewportResized: [{ type: i0.Output, args: ["viewportResized"] }] } });
|
|
12173
|
+
|
|
12174
|
+
/**
|
|
12175
|
+
* Harness for interacting with `tn-tree-virtual-scroll-view` in tests.
|
|
12176
|
+
*
|
|
12177
|
+
* Because the component virtualises its rows, every query below operates over the
|
|
12178
|
+
* currently-MATERIALISED (visible + buffered) rows only — nodes scrolled far out of view
|
|
12179
|
+
* are not in the DOM. Indexes are positions within that visible set, not the full data.
|
|
12180
|
+
*
|
|
12181
|
+
* @example
|
|
12182
|
+
* ```typescript
|
|
12183
|
+
* const tree = await loader.getHarness(TnTreeVirtualScrollViewHarness);
|
|
12184
|
+
* expect(await tree.getRowTexts()).toEqual(['pool', 'other']);
|
|
12185
|
+
*
|
|
12186
|
+
* await tree.expand(0);
|
|
12187
|
+
* expect(await tree.isExpanded(0)).toBe(true);
|
|
12188
|
+
* expect(await tree.getAriaLevel(1)).toBe(2);
|
|
12189
|
+
* ```
|
|
12190
|
+
*/
|
|
12191
|
+
class TnTreeVirtualScrollViewHarness extends ComponentHarness {
|
|
12192
|
+
static hostSelector = 'tn-tree-virtual-scroll-view';
|
|
12193
|
+
/**
|
|
12194
|
+
* Gets a `HarnessPredicate` that can be used to search for a virtual-scroll tree
|
|
12195
|
+
* with specific attributes.
|
|
12196
|
+
*
|
|
12197
|
+
* @param options Options for filtering which instances are considered a match.
|
|
12198
|
+
* @returns A `HarnessPredicate` configured with the given options.
|
|
12199
|
+
*/
|
|
12200
|
+
static with(options = {}) {
|
|
12201
|
+
return new HarnessPredicate(TnTreeVirtualScrollViewHarness, options);
|
|
12202
|
+
}
|
|
12203
|
+
rows = this.locatorForAll('.tn-tree-node-wrapper[role="treeitem"]');
|
|
12204
|
+
scrollTopButton = this.locatorForOptional('.tn-tree-virtual-scroll-view__scroll-top');
|
|
12205
|
+
// --- Row queries ---
|
|
12206
|
+
/**
|
|
12207
|
+
* Gets the number of currently-materialised tree rows.
|
|
12208
|
+
*
|
|
12209
|
+
* @returns Promise resolving to the visible row count.
|
|
12210
|
+
*/
|
|
12211
|
+
async getRowCount() {
|
|
12212
|
+
return (await this.rows()).length;
|
|
12213
|
+
}
|
|
12214
|
+
/**
|
|
12215
|
+
* Gets the text label of every materialised row (the node's text cell only, excluding
|
|
12216
|
+
* the expand chevron glyph).
|
|
12217
|
+
*
|
|
12218
|
+
* @returns Promise resolving to an array of row label strings.
|
|
12219
|
+
*/
|
|
12220
|
+
async getRowTexts() {
|
|
12221
|
+
const cells = await this.locatorForAll('.tn-tree-node-wrapper[role="treeitem"] .tn-tree-node__text')();
|
|
12222
|
+
const texts = [];
|
|
12223
|
+
for (const cell of cells) {
|
|
12224
|
+
texts.push((await cell.text()).trim());
|
|
12225
|
+
}
|
|
12226
|
+
return texts;
|
|
12227
|
+
}
|
|
12228
|
+
/**
|
|
12229
|
+
* Gets the text label of a single materialised row.
|
|
12230
|
+
*
|
|
12231
|
+
* @param index Zero-based index within the visible rows.
|
|
12232
|
+
* @returns Promise resolving to the row's label.
|
|
12233
|
+
*/
|
|
12234
|
+
async getRowText(index) {
|
|
12235
|
+
const cell = await this.locatorFor(`.tn-tree-node-wrapper[role="treeitem"]:nth-of-type(${index + 1}) .tn-tree-node__text`)();
|
|
12236
|
+
return (await cell.text()).trim();
|
|
12237
|
+
}
|
|
12238
|
+
/**
|
|
12239
|
+
* Gets the 1-based `aria-level` of a row (root nodes are 1).
|
|
12240
|
+
*
|
|
12241
|
+
* @param index Zero-based index within the visible rows.
|
|
12242
|
+
* @returns Promise resolving to the aria-level, or null if unset.
|
|
12243
|
+
*/
|
|
12244
|
+
async getAriaLevel(index) {
|
|
12245
|
+
return this.getRowNumberAttribute(index, 'aria-level');
|
|
12246
|
+
}
|
|
12247
|
+
/**
|
|
12248
|
+
* Gets a row's `aria-posinset` — its 1-based position within its SIBLING set.
|
|
12249
|
+
*
|
|
12250
|
+
* @param index Zero-based index within the visible rows.
|
|
12251
|
+
* @returns Promise resolving to the aria-posinset, or null if unset.
|
|
12252
|
+
*/
|
|
12253
|
+
async getAriaPosInSet(index) {
|
|
12254
|
+
return this.getRowNumberAttribute(index, 'aria-posinset');
|
|
12255
|
+
}
|
|
12256
|
+
/**
|
|
12257
|
+
* Gets a row's `aria-setsize` — the size of its SIBLING set.
|
|
12258
|
+
*
|
|
12259
|
+
* @param index Zero-based index within the visible rows.
|
|
12260
|
+
* @returns Promise resolving to the aria-setsize, or null if unset.
|
|
12261
|
+
*/
|
|
12262
|
+
async getAriaSetSize(index) {
|
|
12263
|
+
return this.getRowNumberAttribute(index, 'aria-setsize');
|
|
12264
|
+
}
|
|
12265
|
+
/**
|
|
12266
|
+
* Whether a row is expandable (advertises `aria-expanded`).
|
|
12267
|
+
*
|
|
12268
|
+
* @param index Zero-based index within the visible rows.
|
|
12269
|
+
* @returns Promise resolving to true if the row is expandable.
|
|
12270
|
+
*/
|
|
12271
|
+
async isExpandable(index) {
|
|
12272
|
+
const row = await this.getRow(index);
|
|
12273
|
+
return (await row.getAttribute('aria-expanded')) !== null;
|
|
12274
|
+
}
|
|
12275
|
+
/**
|
|
12276
|
+
* Whether an expandable row is currently expanded.
|
|
12277
|
+
*
|
|
12278
|
+
* @param index Zero-based index within the visible rows.
|
|
12279
|
+
* @returns Promise resolving to true if `aria-expanded="true"`.
|
|
12280
|
+
*/
|
|
12281
|
+
async isExpanded(index) {
|
|
12282
|
+
const row = await this.getRow(index);
|
|
12283
|
+
return (await row.getAttribute('aria-expanded')) === 'true';
|
|
12284
|
+
}
|
|
12285
|
+
/**
|
|
12286
|
+
* Whether a row is keyboard tab-focusable (`tabindex="0"`). Only expandable rows are.
|
|
12287
|
+
*
|
|
12288
|
+
* @param index Zero-based index within the visible rows.
|
|
12289
|
+
* @returns Promise resolving to true if the row is tab-focusable.
|
|
12290
|
+
*/
|
|
12291
|
+
async isFocusable(index) {
|
|
12292
|
+
const row = await this.getRow(index);
|
|
12293
|
+
return (await row.getAttribute('tabindex')) === '0';
|
|
12294
|
+
}
|
|
12295
|
+
// --- Expansion ---
|
|
12296
|
+
/**
|
|
12297
|
+
* Toggles a row's expansion by clicking its toggle. No-op semantics are the tree's
|
|
12298
|
+
* (clicking a leaf does nothing).
|
|
12299
|
+
*
|
|
12300
|
+
* @param index Zero-based index within the visible rows.
|
|
12301
|
+
*/
|
|
12302
|
+
async toggle(index) {
|
|
12303
|
+
const toggle = await this.locatorFor(`.tn-tree-node-wrapper[role="treeitem"]:nth-of-type(${index + 1}) .tn-tree-node`)();
|
|
12304
|
+
await toggle.click();
|
|
12305
|
+
}
|
|
12306
|
+
/**
|
|
12307
|
+
* Expands a row if it is expandable and currently collapsed.
|
|
12308
|
+
*
|
|
12309
|
+
* @param index Zero-based index within the visible rows.
|
|
12310
|
+
*/
|
|
12311
|
+
async expand(index) {
|
|
12312
|
+
if ((await this.isExpandable(index)) && !(await this.isExpanded(index))) {
|
|
12313
|
+
await this.toggle(index);
|
|
12314
|
+
}
|
|
12315
|
+
}
|
|
12316
|
+
/**
|
|
12317
|
+
* Collapses a row if it is currently expanded.
|
|
12318
|
+
*
|
|
12319
|
+
* @param index Zero-based index within the visible rows.
|
|
12320
|
+
*/
|
|
12321
|
+
async collapse(index) {
|
|
12322
|
+
if (await this.isExpanded(index)) {
|
|
12323
|
+
await this.toggle(index);
|
|
12324
|
+
}
|
|
12325
|
+
}
|
|
12326
|
+
/**
|
|
12327
|
+
* Focuses a row and presses a key on it (Enter/Space toggle an expandable row).
|
|
12328
|
+
*
|
|
12329
|
+
* @param index Zero-based index within the visible rows.
|
|
12330
|
+
* @param key Which key to press — Enter or Space.
|
|
12331
|
+
*/
|
|
12332
|
+
async pressKeyOnRow(index, key) {
|
|
12333
|
+
const row = await this.getRow(index);
|
|
12334
|
+
await row.focus();
|
|
12335
|
+
await row.sendKeys(key === 'enter' ? TestKey.ENTER : ' ');
|
|
12336
|
+
}
|
|
12337
|
+
// --- Scroll-to-top button ---
|
|
12338
|
+
/**
|
|
12339
|
+
* Whether the floating scroll-to-top button is currently visible.
|
|
12340
|
+
*
|
|
12341
|
+
* @returns Promise resolving to true if the button is rendered.
|
|
12342
|
+
*/
|
|
12343
|
+
async isScrollToTopVisible() {
|
|
12344
|
+
return (await this.scrollTopButton()) !== null;
|
|
12345
|
+
}
|
|
12346
|
+
/**
|
|
12347
|
+
* Clicks the scroll-to-top button. Throws if it is not currently visible.
|
|
12348
|
+
*/
|
|
12349
|
+
async clickScrollToTop() {
|
|
12350
|
+
const button = await this.scrollTopButton();
|
|
12351
|
+
if (!button) {
|
|
12352
|
+
throw new Error('Scroll-to-top button is not currently visible');
|
|
12353
|
+
}
|
|
12354
|
+
await button.click();
|
|
12355
|
+
}
|
|
12356
|
+
// --- Internal helpers ---
|
|
12357
|
+
async getRow(index) {
|
|
12358
|
+
const rows = await this.rows();
|
|
12359
|
+
if (index >= rows.length) {
|
|
12360
|
+
throw new Error(`Row index ${index} out of bounds (${rows.length} materialised rows)`);
|
|
12361
|
+
}
|
|
12362
|
+
return rows[index];
|
|
12363
|
+
}
|
|
12364
|
+
async getRowNumberAttribute(index, attr) {
|
|
12365
|
+
const row = await this.getRow(index);
|
|
12366
|
+
const value = await row.getAttribute(attr);
|
|
12367
|
+
return value === null ? null : Number(value);
|
|
12368
|
+
}
|
|
12369
|
+
}
|
|
12370
|
+
|
|
11532
12371
|
var ModifierKeys;
|
|
11533
12372
|
(function (ModifierKeys) {
|
|
11534
12373
|
ModifierKeys["COMMAND"] = "\u2318";
|
|
@@ -14015,7 +14854,7 @@ class TnTimeInputComponent {
|
|
|
14015
14854
|
useExisting: forwardRef(() => TnTimeInputComponent),
|
|
14016
14855
|
multi: true
|
|
14017
14856
|
}
|
|
14018
|
-
], ngImport: i0, template: "<tn-select\n [options]=\"timeSelectOptions()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [testId]=\"testId()\"\n [ngModel]=\"_value\"\n (selectionChange)=\"onSelectionChange($event)\" />\n", styles: [":host{display:block;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: TnSelectComponent, selector: "tn-select", inputs: ["options", "optionGroups", "placeholder", "ariaLabel", "noOptionsLabel", "allowEmpty", "emptyLabel", "disabled", "testId", "multiple", "optionTestIdKey", "compareWith"], outputs: ["selectionChange", "multiSelectionChange"] }] });
|
|
14857
|
+
], ngImport: i0, template: "<tn-select\n [options]=\"timeSelectOptions()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [testId]=\"testId()\"\n [ngModel]=\"_value\"\n (selectionChange)=\"onSelectionChange($event)\" />\n", styles: [":host{display:block;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: TnSelectComponent, selector: "tn-select", inputs: ["options", "optionGroups", "placeholder", "ariaLabel", "noOptionsLabel", "allowEmpty", "emptyLabel", "disabled", "showSelectAll", "selectAllLabel", "testId", "multiple", "optionTestIdKey", "compareWith"], outputs: ["selectionChange", "multiSelectionChange"] }] });
|
|
14019
14858
|
}
|
|
14020
14859
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTimeInputComponent, decorators: [{
|
|
14021
14860
|
type: Component,
|
|
@@ -17859,5 +18698,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
17859
18698
|
* Generated bundle index. Do not edit.
|
|
17860
18699
|
*/
|
|
17861
18700
|
|
|
17862
|
-
export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LabelMarkupPipe, LabelTextPipe, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, StripMntPrefixPipe, THEME_MAP, THEME_STORAGE_KEY, TN_FORM_FIELD_ERRORS, TN_TABLE_PAGER_DEFAULT_LABELS, TN_TABLE_PAGER_LABELS, TN_TEST_ATTR, TN_THEME_DEFINITIONS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnButtonToggleGroupHarness, TnButtonToggleHarness, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCardFooterActionsDirective, TnCardHeaderActionsDirective, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnChipHarness, TnChipInputComponent, TnChipInputHarness, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFileInputComponent, TnFileInputHarness, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, TnFormSectionComponent, TnFormSectionHarness, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnInputHarness, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuActivateHoverDirective, TnMenuComponent, TnMenuHarness, TnMenuItemComponent, TnMenuTesting, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnRadioHarness, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSlideToggleHarness, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnStepperHarness, TnStepperNextDirective, TnStepperPreviousDirective, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTableHarness, TnTablePagerComponent, TnTablePagerHarness, TnTabsComponent, TnTabsHarness, TnTestIdDirective, TnTheme, TnThemeService, TnTimeInputComponent, TnToastComponent, TnToastMock, TnToastPosition, TnToastRef, TnToastService, TnToastTesting, TnToastType, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeNodeComponent, TnTreeNodeOutletDirective, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, composeTestId, controlTestId, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, formatSize, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, parseLabelMarkup, parseSize, registerLucideIcons, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
|
|
18701
|
+
export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LabelMarkupPipe, LabelTextPipe, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, StripMntPrefixPipe, THEME_MAP, THEME_STORAGE_KEY, TN_FORM_FIELD_ERRORS, TN_TABLE_PAGER_DEFAULT_LABELS, TN_TABLE_PAGER_LABELS, TN_TEST_ATTR, TN_THEME_DEFINITIONS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnButtonToggleGroupHarness, TnButtonToggleHarness, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCardFooterActionsDirective, TnCardHeaderActionsDirective, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnChipHarness, TnChipInputComponent, TnChipInputHarness, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFileInputComponent, TnFileInputHarness, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, TnFormSectionComponent, TnFormSectionHarness, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnInputHarness, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuActivateHoverDirective, TnMenuComponent, TnMenuHarness, TnMenuItemComponent, TnMenuTesting, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnRadioHarness, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSlideToggleHarness, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnStepperHarness, TnStepperNextDirective, TnStepperPreviousDirective, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTableHarness, TnTablePagerComponent, TnTablePagerHarness, TnTabsComponent, TnTabsHarness, TnTestIdDirective, TnTheme, TnThemeService, TnTimeInputComponent, TnToastComponent, TnToastMock, TnToastPosition, TnToastRef, TnToastService, TnToastTesting, TnToastType, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeNodeComponent, TnTreeNodeOutletDirective, TnTreeVirtualScrollNodeOutletDirective, TnTreeVirtualScrollViewComponent, TnTreeVirtualScrollViewHarness, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, composeTestId, controlTestId, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, defaultTreeItemSize, formatSize, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, parseLabelMarkup, parseSize, registerLucideIcons, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
|
|
17863
18702
|
//# sourceMappingURL=truenas-ui-components.mjs.map
|