@truenas/ui-components 0.3.20 → 0.3.22
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 {
|
|
4
|
+
import { InjectionToken, inject, computed, input, ViewEncapsulation, ChangeDetectionStrategy, Component, Injector, 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,34 @@ 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
|
-
|
|
31
|
+
|
|
32
|
+
/** DI token under which `tn-form-field` exposes its {@link TnFormFieldContext}. */
|
|
33
|
+
const TN_FORM_FIELD_CONTEXT = new InjectionToken('TN_FORM_FIELD_CONTEXT');
|
|
34
|
+
/**
|
|
35
|
+
* Wires a form control to an enclosing `tn-form-field`'s accessibility context.
|
|
36
|
+
* Must be called in an injection context (field initializer or constructor).
|
|
37
|
+
*
|
|
38
|
+
* Standalone usage (no enclosing field) yields all-`null` signals, so the
|
|
39
|
+
* attribute bindings simply render nothing.
|
|
40
|
+
*
|
|
41
|
+
* @param ariaLabel The control's own explicit accessible-name input, when it has
|
|
42
|
+
* one. While set, it wins: `labelledby` goes `null` so the explicit
|
|
43
|
+
* `aria-label` is what assistive tech announces.
|
|
44
|
+
*/
|
|
45
|
+
function injectTnFormFieldAria(ariaLabel) {
|
|
46
|
+
const field = inject(TN_FORM_FIELD_CONTEXT, { optional: true });
|
|
47
|
+
return {
|
|
48
|
+
labelledby: computed(() => (ariaLabel?.() ? null : field?.labelId() ?? null)),
|
|
49
|
+
describedby: computed(() => field?.describedBy() ?? null),
|
|
50
|
+
invalid: computed(() => (field?.errorState() ? true : null)),
|
|
51
|
+
required: computed(() => (field?.requiredState() ? true : null)),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
31
54
|
|
|
32
55
|
class TnSpinnerComponent {
|
|
33
56
|
mode = input('indeterminate', ...(ngDevMode ? [{ debugName: "mode" }] : []));
|
|
@@ -318,13 +341,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
318
341
|
}]
|
|
319
342
|
}], ctorParameters: () => [], propDecorators: { testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnTestId", required: false }] }], tnTestIdType: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnTestIdType", required: false }] }] } });
|
|
320
343
|
|
|
321
|
-
let nextId$
|
|
344
|
+
let nextId$3 = 0;
|
|
322
345
|
class TnAutocompleteComponent {
|
|
323
346
|
elementRef = inject(ElementRef);
|
|
324
347
|
overlay = inject(Overlay);
|
|
325
348
|
viewContainerRef = inject(ViewContainerRef);
|
|
326
349
|
/** Unique instance ID for ARIA linkage */
|
|
327
|
-
uid = `tn-autocomplete-${nextId$
|
|
350
|
+
uid = `tn-autocomplete-${nextId$3++}`;
|
|
328
351
|
/**
|
|
329
352
|
* All available options. The `label` is displayed; the `value` is committed
|
|
330
353
|
* to the form control. A written value is resolved back to its option's
|
|
@@ -379,8 +402,21 @@ class TnAutocompleteComponent {
|
|
|
379
402
|
* custom property so it can also be themed in CSS.
|
|
380
403
|
*/
|
|
381
404
|
panelMaxHeight = input('320px', ...(ngDevMode ? [{ debugName: "panelMaxHeight" }] : []));
|
|
405
|
+
/**
|
|
406
|
+
* Explicit accessible name for the text field. Inside a `tn-form-field` the
|
|
407
|
+
* field's label is associated automatically (via `aria-labelledby`), so leave
|
|
408
|
+
* this unset there unless the announced name must differ from the visible
|
|
409
|
+
* label — when set, it wins. Standalone without either, the input has no
|
|
410
|
+
* accessible name (the placeholder is not one).
|
|
411
|
+
*/
|
|
412
|
+
ariaLabel = input(undefined, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
382
413
|
/** Test ID attribute */
|
|
383
414
|
testId = input(undefined, ...(ngDevMode ? [{ debugName: "testId" }] : []));
|
|
415
|
+
/**
|
|
416
|
+
* ARIA wiring from an enclosing `tn-form-field` (label, error/hint,
|
|
417
|
+
* invalid, required). All-null when standalone or when `ariaLabel` overrides.
|
|
418
|
+
*/
|
|
419
|
+
fieldAria = injectTnFormFieldAria(this.ariaLabel);
|
|
384
420
|
/** Emits the full option (label + value) when one is selected */
|
|
385
421
|
optionSelected = output();
|
|
386
422
|
/**
|
|
@@ -885,13 +921,13 @@ class TnAutocompleteComponent {
|
|
|
885
921
|
}
|
|
886
922
|
}
|
|
887
923
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnAutocompleteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
888
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnAutocompleteComponent, isStandalone: true, selector: "tn-autocomplete", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, requireSelection: { classPropertyName: "requireSelection", publicName: "requireSelection", isSignal: true, isRequired: false, transformFunction: null }, allowCustomValue: { classPropertyName: "allowCustomValue", publicName: "allowCustomValue", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingText: { classPropertyName: "loadingText", publicName: "loadingText", isSignal: true, isRequired: false, transformFunction: null }, filterFn: { classPropertyName: "filterFn", publicName: "filterFn", isSignal: true, isRequired: false, transformFunction: null }, noResultsText: { classPropertyName: "noResultsText", publicName: "noResultsText", isSignal: true, isRequired: false, transformFunction: null }, maxResults: { classPropertyName: "maxResults", publicName: "maxResults", isSignal: true, isRequired: false, transformFunction: null }, panelMaxHeight: { classPropertyName: "panelMaxHeight", publicName: "panelMaxHeight", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { optionSelected: "optionSelected", searchChange: "searchChange", loadMore: "loadMore", opened: "opened" }, providers: [
|
|
924
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnAutocompleteComponent, isStandalone: true, selector: "tn-autocomplete", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, requireSelection: { classPropertyName: "requireSelection", publicName: "requireSelection", isSignal: true, isRequired: false, transformFunction: null }, allowCustomValue: { classPropertyName: "allowCustomValue", publicName: "allowCustomValue", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingText: { classPropertyName: "loadingText", publicName: "loadingText", isSignal: true, isRequired: false, transformFunction: null }, filterFn: { classPropertyName: "filterFn", publicName: "filterFn", isSignal: true, isRequired: false, transformFunction: null }, noResultsText: { classPropertyName: "noResultsText", publicName: "noResultsText", isSignal: true, isRequired: false, transformFunction: null }, maxResults: { classPropertyName: "maxResults", publicName: "maxResults", isSignal: true, isRequired: false, transformFunction: null }, panelMaxHeight: { classPropertyName: "panelMaxHeight", publicName: "panelMaxHeight", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { optionSelected: "optionSelected", searchChange: "searchChange", loadMore: "loadMore", opened: "opened" }, providers: [
|
|
889
925
|
{
|
|
890
926
|
provide: NG_VALUE_ACCESSOR,
|
|
891
927
|
useExisting: forwardRef(() => TnAutocompleteComponent),
|
|
892
928
|
multi: true,
|
|
893
929
|
},
|
|
894
|
-
], viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }, { propertyName: "dropdownTemplate", first: true, predicate: ["dropdownTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-autocomplete\">\n <input\n #inputEl\n type=\"text\"\n class=\"tn-autocomplete__input\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n tnTestIdType=\"autocomplete\"\n autocomplete=\"off\"\n [tnTestId]=\"testId()\"\n [class.open]=\"isOpen()\"\n [class.disabled]=\"isDisabled()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [value]=\"searchTerm()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-controls]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-owns]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\" />\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. Attach/detach (driven by open()/close()) controls\n visibility; the input keeps DOM focus throughout. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-autocomplete__dropdown\"\n [style.--tn-autocomplete-panel-max-height]=\"panelMaxHeightValue()\"\n (scroll)=\"onPanelScroll($event)\">\n\n <!-- Per ARIA, a listbox may only contain option/group children, so the\n loading and no-results rows live OUTSIDE it as live-region siblings.\n The listbox itself stays rendered (possibly empty) the whole time the\n panel is open, so the input's aria-controls always points at a real\n element \u2014 including the loading-with-no-results-yet state. -->\n <div role=\"listbox\" [attr.id]=\"uid + '-dropdown'\" [attr.aria-busy]=\"loading() ? true : null\">\n @if (hasResults()) {\n @for (option of filteredOptions(); track $index) {\n <div\n class=\"tn-autocomplete__option\"\n role=\"option\"\n tabindex=\"-1\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [class.disabled]=\"option.disabled\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled ? true : null\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\">\n {{ option.label }}\n </div>\n }\n }\n </div>\n\n <div role=\"status\">\n @if (loading()) {\n <div class=\"tn-autocomplete__loading\">\n <tn-spinner [diameter]=\"16\" [strokeWidth]=\"2\" [ariaLabel]=\"loadingText()\" />\n <span>{{ loadingText() }}</span>\n </div>\n } @else if (!hasResults()) {\n <div class=\"tn-autocomplete__no-results\">\n {{ noResultsText() }}\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: [".tn-autocomplete{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-autocomplete__input{display:block;width:100%;min-height:2.5rem;padding:.5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;color:var(--tn-fg1, #212529);font-size:1rem;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-autocomplete__input::placeholder{color:var(--tn-alt-fg1, #999)}.tn-autocomplete__input:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-autocomplete__input:focus-visible,.tn-autocomplete__input.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-autocomplete__input.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__dropdown{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-autocomplete-panel-max-height, 320px);overflow-y:auto;padding:.25rem 0}.tn-autocomplete__option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out}.tn-autocomplete__option:hover:not(.disabled){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-autocomplete__option.highlighted{background-color:var(--tn-alt-bg1, #f8f9fa)}.tn-autocomplete__option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__no-results{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}@media(prefers-reduced-motion:reduce){.tn-autocomplete__input,.tn-autocomplete__option{transition:none}}.tn-autocomplete__loading{align-items:center;color:var(--tn-fg2, #6c757d);display:flex;font-size:.875rem;gap:.5rem;padding:.5rem .75rem}\n"], dependencies: [{ kind: "component", type: TnSpinnerComponent, selector: "tn-spinner", inputs: ["mode", "value", "diameter", "strokeWidth", "ariaLabel", "ariaLabelledby"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
|
|
930
|
+
], viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }, { propertyName: "dropdownTemplate", first: true, predicate: ["dropdownTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-autocomplete\">\n <input\n #inputEl\n type=\"text\"\n class=\"tn-autocomplete__input\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n tnTestIdType=\"autocomplete\"\n autocomplete=\"off\"\n [tnTestId]=\"testId()\"\n [class.open]=\"isOpen()\"\n [class.disabled]=\"isDisabled()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [value]=\"searchTerm()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-labelledby]=\"fieldAria.labelledby()\"\n [attr.aria-describedby]=\"fieldAria.describedby()\"\n [attr.aria-invalid]=\"fieldAria.invalid()\"\n [attr.aria-required]=\"fieldAria.required()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-controls]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-owns]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\" />\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. Attach/detach (driven by open()/close()) controls\n visibility; the input keeps DOM focus throughout. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-autocomplete__dropdown\"\n [style.--tn-autocomplete-panel-max-height]=\"panelMaxHeightValue()\"\n (scroll)=\"onPanelScroll($event)\">\n\n <!-- Per ARIA, a listbox may only contain option/group children, so the\n loading and no-results rows live OUTSIDE it as live-region siblings.\n The listbox itself stays rendered (possibly empty) the whole time the\n panel is open, so the input's aria-controls always points at a real\n element \u2014 including the loading-with-no-results-yet state. -->\n <div role=\"listbox\" [attr.id]=\"uid + '-dropdown'\" [attr.aria-busy]=\"loading() ? true : null\">\n @if (hasResults()) {\n @for (option of filteredOptions(); track $index) {\n <div\n class=\"tn-autocomplete__option\"\n role=\"option\"\n tabindex=\"-1\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [class.disabled]=\"option.disabled\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled ? true : null\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\">\n {{ option.label }}\n </div>\n }\n }\n </div>\n\n <div role=\"status\">\n @if (loading()) {\n <div class=\"tn-autocomplete__loading\">\n <tn-spinner [diameter]=\"16\" [strokeWidth]=\"2\" [ariaLabel]=\"loadingText()\" />\n <span>{{ loadingText() }}</span>\n </div>\n } @else if (!hasResults()) {\n <div class=\"tn-autocomplete__no-results\">\n {{ noResultsText() }}\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: [".tn-autocomplete{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-autocomplete__input{display:block;width:100%;min-height:2.5rem;padding:.5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;color:var(--tn-fg1, #212529);font-size:1rem;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-autocomplete__input::placeholder{color:var(--tn-alt-fg1, #999)}.tn-autocomplete__input:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-autocomplete__input:focus-visible,.tn-autocomplete__input.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-autocomplete__input.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__dropdown{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-autocomplete-panel-max-height, 320px);overflow-y:auto;padding:.25rem 0}.tn-autocomplete__option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out}.tn-autocomplete__option:hover:not(.disabled){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-autocomplete__option.highlighted{background-color:var(--tn-alt-bg1, #f8f9fa)}.tn-autocomplete__option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__no-results{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}@media(prefers-reduced-motion:reduce){.tn-autocomplete__input,.tn-autocomplete__option{transition:none}}.tn-autocomplete__loading{align-items:center;color:var(--tn-fg2, #6c757d);display:flex;font-size:.875rem;gap:.5rem;padding:.5rem .75rem}\n"], dependencies: [{ kind: "component", type: TnSpinnerComponent, selector: "tn-spinner", inputs: ["mode", "value", "diameter", "strokeWidth", "ariaLabel", "ariaLabelledby"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
|
|
895
931
|
}
|
|
896
932
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnAutocompleteComponent, decorators: [{
|
|
897
933
|
type: Component,
|
|
@@ -901,8 +937,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
901
937
|
useExisting: forwardRef(() => TnAutocompleteComponent),
|
|
902
938
|
multi: true,
|
|
903
939
|
},
|
|
904
|
-
], template: "<div class=\"tn-autocomplete\">\n <input\n #inputEl\n type=\"text\"\n class=\"tn-autocomplete__input\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n tnTestIdType=\"autocomplete\"\n autocomplete=\"off\"\n [tnTestId]=\"testId()\"\n [class.open]=\"isOpen()\"\n [class.disabled]=\"isDisabled()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [value]=\"searchTerm()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-controls]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-owns]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\" />\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. Attach/detach (driven by open()/close()) controls\n visibility; the input keeps DOM focus throughout. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-autocomplete__dropdown\"\n [style.--tn-autocomplete-panel-max-height]=\"panelMaxHeightValue()\"\n (scroll)=\"onPanelScroll($event)\">\n\n <!-- Per ARIA, a listbox may only contain option/group children, so the\n loading and no-results rows live OUTSIDE it as live-region siblings.\n The listbox itself stays rendered (possibly empty) the whole time the\n panel is open, so the input's aria-controls always points at a real\n element \u2014 including the loading-with-no-results-yet state. -->\n <div role=\"listbox\" [attr.id]=\"uid + '-dropdown'\" [attr.aria-busy]=\"loading() ? true : null\">\n @if (hasResults()) {\n @for (option of filteredOptions(); track $index) {\n <div\n class=\"tn-autocomplete__option\"\n role=\"option\"\n tabindex=\"-1\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [class.disabled]=\"option.disabled\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled ? true : null\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\">\n {{ option.label }}\n </div>\n }\n }\n </div>\n\n <div role=\"status\">\n @if (loading()) {\n <div class=\"tn-autocomplete__loading\">\n <tn-spinner [diameter]=\"16\" [strokeWidth]=\"2\" [ariaLabel]=\"loadingText()\" />\n <span>{{ loadingText() }}</span>\n </div>\n } @else if (!hasResults()) {\n <div class=\"tn-autocomplete__no-results\">\n {{ noResultsText() }}\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: [".tn-autocomplete{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-autocomplete__input{display:block;width:100%;min-height:2.5rem;padding:.5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;color:var(--tn-fg1, #212529);font-size:1rem;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-autocomplete__input::placeholder{color:var(--tn-alt-fg1, #999)}.tn-autocomplete__input:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-autocomplete__input:focus-visible,.tn-autocomplete__input.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-autocomplete__input.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__dropdown{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-autocomplete-panel-max-height, 320px);overflow-y:auto;padding:.25rem 0}.tn-autocomplete__option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out}.tn-autocomplete__option:hover:not(.disabled){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-autocomplete__option.highlighted{background-color:var(--tn-alt-bg1, #f8f9fa)}.tn-autocomplete__option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__no-results{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}@media(prefers-reduced-motion:reduce){.tn-autocomplete__input,.tn-autocomplete__option{transition:none}}.tn-autocomplete__loading{align-items:center;color:var(--tn-fg2, #6c757d);display:flex;font-size:.875rem;gap:.5rem;padding:.5rem .75rem}\n"] }]
|
|
905
|
-
}], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], requireSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "requireSelection", required: false }] }], allowCustomValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCustomValue", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingText", required: false }] }], filterFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterFn", required: false }] }], noResultsText: [{ type: i0.Input, args: [{ isSignal: true, alias: "noResultsText", required: false }] }], maxResults: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxResults", required: false }] }], panelMaxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelMaxHeight", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], optionSelected: [{ type: i0.Output, args: ["optionSelected"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], loadMore: [{ type: i0.Output, args: ["loadMore"] }], opened: [{ type: i0.Output, args: ["opened"] }], inputEl: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdownTemplate', { isSignal: true }] }] } });
|
|
940
|
+
], template: "<div class=\"tn-autocomplete\">\n <input\n #inputEl\n type=\"text\"\n class=\"tn-autocomplete__input\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n tnTestIdType=\"autocomplete\"\n autocomplete=\"off\"\n [tnTestId]=\"testId()\"\n [class.open]=\"isOpen()\"\n [class.disabled]=\"isDisabled()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [value]=\"searchTerm()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-labelledby]=\"fieldAria.labelledby()\"\n [attr.aria-describedby]=\"fieldAria.describedby()\"\n [attr.aria-invalid]=\"fieldAria.invalid()\"\n [attr.aria-required]=\"fieldAria.required()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-controls]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-owns]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\" />\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. Attach/detach (driven by open()/close()) controls\n visibility; the input keeps DOM focus throughout. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-autocomplete__dropdown\"\n [style.--tn-autocomplete-panel-max-height]=\"panelMaxHeightValue()\"\n (scroll)=\"onPanelScroll($event)\">\n\n <!-- Per ARIA, a listbox may only contain option/group children, so the\n loading and no-results rows live OUTSIDE it as live-region siblings.\n The listbox itself stays rendered (possibly empty) the whole time the\n panel is open, so the input's aria-controls always points at a real\n element \u2014 including the loading-with-no-results-yet state. -->\n <div role=\"listbox\" [attr.id]=\"uid + '-dropdown'\" [attr.aria-busy]=\"loading() ? true : null\">\n @if (hasResults()) {\n @for (option of filteredOptions(); track $index) {\n <div\n class=\"tn-autocomplete__option\"\n role=\"option\"\n tabindex=\"-1\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [class.disabled]=\"option.disabled\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled ? true : null\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\">\n {{ option.label }}\n </div>\n }\n }\n </div>\n\n <div role=\"status\">\n @if (loading()) {\n <div class=\"tn-autocomplete__loading\">\n <tn-spinner [diameter]=\"16\" [strokeWidth]=\"2\" [ariaLabel]=\"loadingText()\" />\n <span>{{ loadingText() }}</span>\n </div>\n } @else if (!hasResults()) {\n <div class=\"tn-autocomplete__no-results\">\n {{ noResultsText() }}\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: [".tn-autocomplete{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-autocomplete__input{display:block;width:100%;min-height:2.5rem;padding:.5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;color:var(--tn-fg1, #212529);font-size:1rem;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-autocomplete__input::placeholder{color:var(--tn-alt-fg1, #999)}.tn-autocomplete__input:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-autocomplete__input:focus-visible,.tn-autocomplete__input.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-autocomplete__input.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__dropdown{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-autocomplete-panel-max-height, 320px);overflow-y:auto;padding:.25rem 0}.tn-autocomplete__option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out}.tn-autocomplete__option:hover:not(.disabled){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-autocomplete__option.highlighted{background-color:var(--tn-alt-bg1, #f8f9fa)}.tn-autocomplete__option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__no-results{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}@media(prefers-reduced-motion:reduce){.tn-autocomplete__input,.tn-autocomplete__option{transition:none}}.tn-autocomplete__loading{align-items:center;color:var(--tn-fg2, #6c757d);display:flex;font-size:.875rem;gap:.5rem;padding:.5rem .75rem}\n"] }]
|
|
941
|
+
}], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], requireSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "requireSelection", required: false }] }], allowCustomValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCustomValue", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingText", required: false }] }], filterFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterFn", required: false }] }], noResultsText: [{ type: i0.Input, args: [{ isSignal: true, alias: "noResultsText", required: false }] }], maxResults: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxResults", required: false }] }], panelMaxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelMaxHeight", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], optionSelected: [{ type: i0.Output, args: ["optionSelected"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], loadMore: [{ type: i0.Output, args: ["loadMore"] }], opened: [{ type: i0.Output, args: ["opened"] }], inputEl: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdownTemplate', { isSignal: true }] }] } });
|
|
906
942
|
|
|
907
943
|
/**
|
|
908
944
|
* Harness for interacting with tn-autocomplete in tests.
|
|
@@ -3356,7 +3392,7 @@ function unitExponent(unit) {
|
|
|
3356
3392
|
// Module-level counter for deterministic, unique instance ids (matches the
|
|
3357
3393
|
// tn-autocomplete convention). Deterministic ids are SSR/hydration-safe and
|
|
3358
3394
|
// stable across snapshots, unlike a Math.random() seed.
|
|
3359
|
-
let nextId$
|
|
3395
|
+
let nextId$2 = 0;
|
|
3360
3396
|
class TnInputComponent {
|
|
3361
3397
|
inputEl = viewChild.required('inputEl');
|
|
3362
3398
|
inputType = input(InputType.PlainText, ...(ngDevMode ? [{ debugName: "inputType" }] : []));
|
|
@@ -3368,13 +3404,18 @@ class TnInputComponent {
|
|
|
3368
3404
|
multiline = input(false, ...(ngDevMode ? [{ debugName: "multiline" }] : []));
|
|
3369
3405
|
rows = input(3, ...(ngDevMode ? [{ debugName: "rows" }] : []));
|
|
3370
3406
|
/**
|
|
3371
|
-
*
|
|
3372
|
-
*
|
|
3373
|
-
*
|
|
3374
|
-
*
|
|
3375
|
-
* the a11y tree.
|
|
3407
|
+
* Explicit accessible name for the control. Rendered as `aria-label` on the
|
|
3408
|
+
* input/textarea. Inside a `tn-form-field` the field's label is associated
|
|
3409
|
+
* automatically (via `aria-labelledby`), so leave this unset there unless the
|
|
3410
|
+
* announced name must differ from the visible label — when set, it wins. Set
|
|
3411
|
+
* it for standalone usage so the control isn't unnamed in the a11y tree.
|
|
3376
3412
|
*/
|
|
3377
3413
|
ariaLabel = input(undefined, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
3414
|
+
/**
|
|
3415
|
+
* ARIA wiring from an enclosing `tn-form-field` (label, error/hint,
|
|
3416
|
+
* invalid, required). All-null when standalone or when `ariaLabel` overrides.
|
|
3417
|
+
*/
|
|
3418
|
+
fieldAria = injectTnFormFieldAria(this.ariaLabel);
|
|
3378
3419
|
/**
|
|
3379
3420
|
* Native `autocomplete` hint rendered on the input/textarea. Pass the standard
|
|
3380
3421
|
* autofill tokens (`'username'`, `'current-password'`, `'new-password'`,
|
|
@@ -3522,7 +3563,7 @@ class TnInputComponent {
|
|
|
3522
3563
|
// Unique per instance: a hard-coded id produced duplicate ids when multiple
|
|
3523
3564
|
// tn-inputs share a page (invalid HTML, and it breaks any future label `for=`,
|
|
3524
3565
|
// aria-describedby, or getElementById targeting this input).
|
|
3525
|
-
id = `tn-input-${nextId$
|
|
3566
|
+
id = `tn-input-${nextId$2++}`;
|
|
3526
3567
|
// Protected: the display string is owned by the component and driven through the
|
|
3527
3568
|
// CVA flow (writeValue / onValueChange). External writes would bypass that flow.
|
|
3528
3569
|
// A signal so the `[value]` binding reflects writeValue reactively — a plain field
|
|
@@ -3666,7 +3707,7 @@ class TnInputComponent {
|
|
|
3666
3707
|
useExisting: forwardRef(() => TnInputComponent),
|
|
3667
3708
|
multi: true
|
|
3668
3709
|
}
|
|
3669
|
-
], viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"tn-input-container\"\n [class.tn-input-container--has-prefix]=\"hasPrefixIcon()\"\n [class.tn-input-container--has-suffix]=\"hasSuffixIcon() || hasPasswordToggle()\"\n>\n <!-- Prefix icon -->\n @if (hasPrefixIcon()) {\n <tn-icon\n class=\"tn-input__prefix-icon\"\n size=\"sm\"\n aria-hidden=\"true\"\n [name]=\"prefixIcon()!\"\n [library]=\"prefixIconLibrary()\"\n />\n }\n\n <!-- Regular input field -->\n @if (!showTextarea()) {\n <input\n #inputEl\n class=\"tn-input\"\n tnTestIdType=\"input\"\n [id]=\"id\"\n [value]=\"value()\"\n [type]=\"resolvedType()\"\n [attr.inputmode]=\"numericInputMode()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.name]=\"name()\"\n [tnTestId]=\"resolvedTestId()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [required]=\"required()\"\n (input)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n />\n }\n\n <!-- Textarea field -->\n @if (showTextarea()) {\n <textarea\n #inputEl\n class=\"tn-input tn-textarea\"\n tnTestIdType=\"textarea\"\n [id]=\"id\"\n [value]=\"value()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.name]=\"name()\"\n [tnTestId]=\"resolvedTestId()\"\n [rows]=\"rows()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [required]=\"required()\"\n (input)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n ></textarea>\n }\n\n <!-- Suffix icon action button -->\n @if (hasSuffixIcon()) {\n <button\n type=\"button\"\n class=\"tn-input__suffix-action\"\n tnTestIdType=\"button\"\n [tnTestId]=\"suffixActionTestId()\"\n [attr.aria-label]=\"suffixIconAriaLabel()\"\n [disabled]=\"isDisabled()\"\n (click)=\"onSuffixAction.emit($event)\"\n >\n <tn-icon\n size=\"sm\"\n [name]=\"suffixIcon()!\"\n [library]=\"suffixIconLibrary()\"\n />\n </button>\n }\n\n <!-- Password visibility toggle. State is conveyed via the swapping\n aria-label alone \u2014 no aria-pressed, which would contradict a label that\n already names the action (\"Hide password, pressed\" is ambiguous). -->\n @if (hasPasswordToggle()) {\n <button\n type=\"button\"\n class=\"tn-input__visibility-toggle\"\n tnTestIdType=\"button\"\n [tnTestId]=\"passwordToggleTestId()\"\n [attr.aria-label]=\"passwordVisible() ? 'Hide password' : 'Show password'\"\n [disabled]=\"isDisabled()\"\n (click)=\"togglePasswordVisibility()\"\n >\n <tn-icon size=\"sm\" aria-hidden=\"true\" [name]=\"passwordToggleIcon()\" />\n </button>\n }\n</div>\n", styles: [".tn-input-container{position:relative;display:flex;align-items:center;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-input-container:focus-within{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-input-container:has(.tn-input:disabled){background-color:var(--tn-alt-bg1, #f8f9fa);opacity:.6;cursor:not-allowed}.tn-input-container:has(.tn-input.error){border-color:var(--tn-error, #dc3545)}.tn-input-container:has(.tn-input.error):focus-within{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px #dc354540}.tn-input{display:block;flex:1;min-width:0;min-height:2.5rem;padding:.5rem .75rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background:transparent;border:none;border-radius:.375rem;outline:none;box-sizing:border-box}.tn-input::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-input:disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed}.tn-input-container--has-prefix .tn-input{padding-left:.75rem;border-left:1px solid var(--tn-lines, #d1d5db);border-top-left-radius:0;border-bottom-left-radius:0}.tn-input-container--has-suffix .tn-input{padding-right:.25rem}.tn-input__prefix-icon{flex-shrink:0;margin-left:.75rem;margin-right:.75rem;color:var(--tn-fg2, #6c757d);pointer-events:none}.tn-input__suffix-action,.tn-input__visibility-toggle{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-right:.375rem;padding:.25rem;border:none;border-radius:.25rem;background:transparent;color:var(--tn-fg2, #6c757d);cursor:pointer;transition:background-color .15s ease-in-out,color .15s ease-in-out}.tn-input__suffix-action:hover:not(:disabled),.tn-input__visibility-toggle:hover:not(:disabled){background-color:var(--tn-bg3, #f3f4f6);color:var(--tn-fg1, #1f2937)}.tn-input__suffix-action:focus-visible,.tn-input__visibility-toggle:focus-visible{outline:2px solid var(--tn-primary, #2563eb);outline-offset:1px}.tn-input__suffix-action:disabled,.tn-input__visibility-toggle:disabled{cursor:not-allowed;pointer-events:none}.tn-textarea{min-height:6rem;resize:vertical;line-height:1.4}@media(prefers-reduced-motion:reduce){.tn-input-container,.tn-input__suffix-action,.tn-input__visibility-toggle{transition:none}}@media(prefers-contrast:high){.tn-input-container{border-width:2px}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: A11yModule }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
|
|
3710
|
+
], viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"tn-input-container\"\n [class.tn-input-container--has-prefix]=\"hasPrefixIcon()\"\n [class.tn-input-container--has-suffix]=\"hasSuffixIcon() || hasPasswordToggle()\"\n>\n <!-- Prefix icon -->\n @if (hasPrefixIcon()) {\n <tn-icon\n class=\"tn-input__prefix-icon\"\n size=\"sm\"\n aria-hidden=\"true\"\n [name]=\"prefixIcon()!\"\n [library]=\"prefixIconLibrary()\"\n />\n }\n\n <!-- Regular input field -->\n @if (!showTextarea()) {\n <input\n #inputEl\n class=\"tn-input\"\n tnTestIdType=\"input\"\n [id]=\"id\"\n [value]=\"value()\"\n [type]=\"resolvedType()\"\n [attr.inputmode]=\"numericInputMode()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-labelledby]=\"fieldAria.labelledby()\"\n [attr.aria-describedby]=\"fieldAria.describedby()\"\n [attr.aria-invalid]=\"fieldAria.invalid()\"\n [attr.aria-required]=\"fieldAria.required()\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.name]=\"name()\"\n [tnTestId]=\"resolvedTestId()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [required]=\"required()\"\n (input)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n />\n }\n\n <!-- Textarea field -->\n @if (showTextarea()) {\n <textarea\n #inputEl\n class=\"tn-input tn-textarea\"\n tnTestIdType=\"textarea\"\n [id]=\"id\"\n [value]=\"value()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-labelledby]=\"fieldAria.labelledby()\"\n [attr.aria-describedby]=\"fieldAria.describedby()\"\n [attr.aria-invalid]=\"fieldAria.invalid()\"\n [attr.aria-required]=\"fieldAria.required()\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.name]=\"name()\"\n [tnTestId]=\"resolvedTestId()\"\n [rows]=\"rows()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [required]=\"required()\"\n (input)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n ></textarea>\n }\n\n <!-- Suffix icon action button -->\n @if (hasSuffixIcon()) {\n <button\n type=\"button\"\n class=\"tn-input__suffix-action\"\n tnTestIdType=\"button\"\n [tnTestId]=\"suffixActionTestId()\"\n [attr.aria-label]=\"suffixIconAriaLabel()\"\n [disabled]=\"isDisabled()\"\n (click)=\"onSuffixAction.emit($event)\"\n >\n <tn-icon\n size=\"sm\"\n [name]=\"suffixIcon()!\"\n [library]=\"suffixIconLibrary()\"\n />\n </button>\n }\n\n <!-- Password visibility toggle. State is conveyed via the swapping\n aria-label alone \u2014 no aria-pressed, which would contradict a label that\n already names the action (\"Hide password, pressed\" is ambiguous). -->\n @if (hasPasswordToggle()) {\n <button\n type=\"button\"\n class=\"tn-input__visibility-toggle\"\n tnTestIdType=\"button\"\n [tnTestId]=\"passwordToggleTestId()\"\n [attr.aria-label]=\"passwordVisible() ? 'Hide password' : 'Show password'\"\n [disabled]=\"isDisabled()\"\n (click)=\"togglePasswordVisibility()\"\n >\n <tn-icon size=\"sm\" aria-hidden=\"true\" [name]=\"passwordToggleIcon()\" />\n </button>\n }\n</div>\n", styles: [".tn-input-container{position:relative;display:flex;align-items:center;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-input-container:focus-within{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-input-container:has(.tn-input:disabled){background-color:var(--tn-alt-bg1, #f8f9fa);opacity:.6;cursor:not-allowed}.tn-input-container:has(.tn-input.error){border-color:var(--tn-error, #dc3545)}.tn-input-container:has(.tn-input.error):focus-within{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px #dc354540}.tn-input{display:block;flex:1;min-width:0;min-height:2.5rem;padding:.5rem .75rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background:transparent;border:none;border-radius:.375rem;outline:none;box-sizing:border-box}.tn-input::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-input:disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed}.tn-input-container--has-prefix .tn-input{padding-left:.75rem;border-left:1px solid var(--tn-lines, #d1d5db);border-top-left-radius:0;border-bottom-left-radius:0}.tn-input-container--has-suffix .tn-input{padding-right:.25rem}.tn-input__prefix-icon{flex-shrink:0;margin-left:.75rem;margin-right:.75rem;color:var(--tn-fg2, #6c757d);pointer-events:none}.tn-input__suffix-action,.tn-input__visibility-toggle{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-right:.375rem;padding:.25rem;border:none;border-radius:.25rem;background:transparent;color:var(--tn-fg2, #6c757d);cursor:pointer;transition:background-color .15s ease-in-out,color .15s ease-in-out}.tn-input__suffix-action:hover:not(:disabled),.tn-input__visibility-toggle:hover:not(:disabled){background-color:var(--tn-bg3, #f3f4f6);color:var(--tn-fg1, #1f2937)}.tn-input__suffix-action:focus-visible,.tn-input__visibility-toggle:focus-visible{outline:2px solid var(--tn-primary, #2563eb);outline-offset:1px}.tn-input__suffix-action:disabled,.tn-input__visibility-toggle:disabled{cursor:not-allowed;pointer-events:none}.tn-textarea{min-height:6rem;resize:vertical;line-height:1.4}@media(prefers-reduced-motion:reduce){.tn-input-container,.tn-input__suffix-action,.tn-input__visibility-toggle{transition:none}}@media(prefers-contrast:high){.tn-input-container{border-width:2px}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: A11yModule }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
|
|
3670
3711
|
}
|
|
3671
3712
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnInputComponent, decorators: [{
|
|
3672
3713
|
type: Component,
|
|
@@ -3676,7 +3717,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
3676
3717
|
useExisting: forwardRef(() => TnInputComponent),
|
|
3677
3718
|
multi: true
|
|
3678
3719
|
}
|
|
3679
|
-
], template: "<div\n class=\"tn-input-container\"\n [class.tn-input-container--has-prefix]=\"hasPrefixIcon()\"\n [class.tn-input-container--has-suffix]=\"hasSuffixIcon() || hasPasswordToggle()\"\n>\n <!-- Prefix icon -->\n @if (hasPrefixIcon()) {\n <tn-icon\n class=\"tn-input__prefix-icon\"\n size=\"sm\"\n aria-hidden=\"true\"\n [name]=\"prefixIcon()!\"\n [library]=\"prefixIconLibrary()\"\n />\n }\n\n <!-- Regular input field -->\n @if (!showTextarea()) {\n <input\n #inputEl\n class=\"tn-input\"\n tnTestIdType=\"input\"\n [id]=\"id\"\n [value]=\"value()\"\n [type]=\"resolvedType()\"\n [attr.inputmode]=\"numericInputMode()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.name]=\"name()\"\n [tnTestId]=\"resolvedTestId()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [required]=\"required()\"\n (input)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n />\n }\n\n <!-- Textarea field -->\n @if (showTextarea()) {\n <textarea\n #inputEl\n class=\"tn-input tn-textarea\"\n tnTestIdType=\"textarea\"\n [id]=\"id\"\n [value]=\"value()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.name]=\"name()\"\n [tnTestId]=\"resolvedTestId()\"\n [rows]=\"rows()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [required]=\"required()\"\n (input)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n ></textarea>\n }\n\n <!-- Suffix icon action button -->\n @if (hasSuffixIcon()) {\n <button\n type=\"button\"\n class=\"tn-input__suffix-action\"\n tnTestIdType=\"button\"\n [tnTestId]=\"suffixActionTestId()\"\n [attr.aria-label]=\"suffixIconAriaLabel()\"\n [disabled]=\"isDisabled()\"\n (click)=\"onSuffixAction.emit($event)\"\n >\n <tn-icon\n size=\"sm\"\n [name]=\"suffixIcon()!\"\n [library]=\"suffixIconLibrary()\"\n />\n </button>\n }\n\n <!-- Password visibility toggle. State is conveyed via the swapping\n aria-label alone \u2014 no aria-pressed, which would contradict a label that\n already names the action (\"Hide password, pressed\" is ambiguous). -->\n @if (hasPasswordToggle()) {\n <button\n type=\"button\"\n class=\"tn-input__visibility-toggle\"\n tnTestIdType=\"button\"\n [tnTestId]=\"passwordToggleTestId()\"\n [attr.aria-label]=\"passwordVisible() ? 'Hide password' : 'Show password'\"\n [disabled]=\"isDisabled()\"\n (click)=\"togglePasswordVisibility()\"\n >\n <tn-icon size=\"sm\" aria-hidden=\"true\" [name]=\"passwordToggleIcon()\" />\n </button>\n }\n</div>\n", styles: [".tn-input-container{position:relative;display:flex;align-items:center;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-input-container:focus-within{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-input-container:has(.tn-input:disabled){background-color:var(--tn-alt-bg1, #f8f9fa);opacity:.6;cursor:not-allowed}.tn-input-container:has(.tn-input.error){border-color:var(--tn-error, #dc3545)}.tn-input-container:has(.tn-input.error):focus-within{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px #dc354540}.tn-input{display:block;flex:1;min-width:0;min-height:2.5rem;padding:.5rem .75rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background:transparent;border:none;border-radius:.375rem;outline:none;box-sizing:border-box}.tn-input::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-input:disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed}.tn-input-container--has-prefix .tn-input{padding-left:.75rem;border-left:1px solid var(--tn-lines, #d1d5db);border-top-left-radius:0;border-bottom-left-radius:0}.tn-input-container--has-suffix .tn-input{padding-right:.25rem}.tn-input__prefix-icon{flex-shrink:0;margin-left:.75rem;margin-right:.75rem;color:var(--tn-fg2, #6c757d);pointer-events:none}.tn-input__suffix-action,.tn-input__visibility-toggle{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-right:.375rem;padding:.25rem;border:none;border-radius:.25rem;background:transparent;color:var(--tn-fg2, #6c757d);cursor:pointer;transition:background-color .15s ease-in-out,color .15s ease-in-out}.tn-input__suffix-action:hover:not(:disabled),.tn-input__visibility-toggle:hover:not(:disabled){background-color:var(--tn-bg3, #f3f4f6);color:var(--tn-fg1, #1f2937)}.tn-input__suffix-action:focus-visible,.tn-input__visibility-toggle:focus-visible{outline:2px solid var(--tn-primary, #2563eb);outline-offset:1px}.tn-input__suffix-action:disabled,.tn-input__visibility-toggle:disabled{cursor:not-allowed;pointer-events:none}.tn-textarea{min-height:6rem;resize:vertical;line-height:1.4}@media(prefers-reduced-motion:reduce){.tn-input-container,.tn-input__suffix-action,.tn-input__visibility-toggle{transition:none}}@media(prefers-contrast:high){.tn-input-container{border-width:2px}}\n"] }]
|
|
3720
|
+
], template: "<div\n class=\"tn-input-container\"\n [class.tn-input-container--has-prefix]=\"hasPrefixIcon()\"\n [class.tn-input-container--has-suffix]=\"hasSuffixIcon() || hasPasswordToggle()\"\n>\n <!-- Prefix icon -->\n @if (hasPrefixIcon()) {\n <tn-icon\n class=\"tn-input__prefix-icon\"\n size=\"sm\"\n aria-hidden=\"true\"\n [name]=\"prefixIcon()!\"\n [library]=\"prefixIconLibrary()\"\n />\n }\n\n <!-- Regular input field -->\n @if (!showTextarea()) {\n <input\n #inputEl\n class=\"tn-input\"\n tnTestIdType=\"input\"\n [id]=\"id\"\n [value]=\"value()\"\n [type]=\"resolvedType()\"\n [attr.inputmode]=\"numericInputMode()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-labelledby]=\"fieldAria.labelledby()\"\n [attr.aria-describedby]=\"fieldAria.describedby()\"\n [attr.aria-invalid]=\"fieldAria.invalid()\"\n [attr.aria-required]=\"fieldAria.required()\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.name]=\"name()\"\n [tnTestId]=\"resolvedTestId()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [required]=\"required()\"\n (input)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n />\n }\n\n <!-- Textarea field -->\n @if (showTextarea()) {\n <textarea\n #inputEl\n class=\"tn-input tn-textarea\"\n tnTestIdType=\"textarea\"\n [id]=\"id\"\n [value]=\"value()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-labelledby]=\"fieldAria.labelledby()\"\n [attr.aria-describedby]=\"fieldAria.describedby()\"\n [attr.aria-invalid]=\"fieldAria.invalid()\"\n [attr.aria-required]=\"fieldAria.required()\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.name]=\"name()\"\n [tnTestId]=\"resolvedTestId()\"\n [rows]=\"rows()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [required]=\"required()\"\n (input)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n ></textarea>\n }\n\n <!-- Suffix icon action button -->\n @if (hasSuffixIcon()) {\n <button\n type=\"button\"\n class=\"tn-input__suffix-action\"\n tnTestIdType=\"button\"\n [tnTestId]=\"suffixActionTestId()\"\n [attr.aria-label]=\"suffixIconAriaLabel()\"\n [disabled]=\"isDisabled()\"\n (click)=\"onSuffixAction.emit($event)\"\n >\n <tn-icon\n size=\"sm\"\n [name]=\"suffixIcon()!\"\n [library]=\"suffixIconLibrary()\"\n />\n </button>\n }\n\n <!-- Password visibility toggle. State is conveyed via the swapping\n aria-label alone \u2014 no aria-pressed, which would contradict a label that\n already names the action (\"Hide password, pressed\" is ambiguous). -->\n @if (hasPasswordToggle()) {\n <button\n type=\"button\"\n class=\"tn-input__visibility-toggle\"\n tnTestIdType=\"button\"\n [tnTestId]=\"passwordToggleTestId()\"\n [attr.aria-label]=\"passwordVisible() ? 'Hide password' : 'Show password'\"\n [disabled]=\"isDisabled()\"\n (click)=\"togglePasswordVisibility()\"\n >\n <tn-icon size=\"sm\" aria-hidden=\"true\" [name]=\"passwordToggleIcon()\" />\n </button>\n }\n</div>\n", styles: [".tn-input-container{position:relative;display:flex;align-items:center;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-input-container:focus-within{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-input-container:has(.tn-input:disabled){background-color:var(--tn-alt-bg1, #f8f9fa);opacity:.6;cursor:not-allowed}.tn-input-container:has(.tn-input.error){border-color:var(--tn-error, #dc3545)}.tn-input-container:has(.tn-input.error):focus-within{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px #dc354540}.tn-input{display:block;flex:1;min-width:0;min-height:2.5rem;padding:.5rem .75rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background:transparent;border:none;border-radius:.375rem;outline:none;box-sizing:border-box}.tn-input::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-input:disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed}.tn-input-container--has-prefix .tn-input{padding-left:.75rem;border-left:1px solid var(--tn-lines, #d1d5db);border-top-left-radius:0;border-bottom-left-radius:0}.tn-input-container--has-suffix .tn-input{padding-right:.25rem}.tn-input__prefix-icon{flex-shrink:0;margin-left:.75rem;margin-right:.75rem;color:var(--tn-fg2, #6c757d);pointer-events:none}.tn-input__suffix-action,.tn-input__visibility-toggle{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-right:.375rem;padding:.25rem;border:none;border-radius:.25rem;background:transparent;color:var(--tn-fg2, #6c757d);cursor:pointer;transition:background-color .15s ease-in-out,color .15s ease-in-out}.tn-input__suffix-action:hover:not(:disabled),.tn-input__visibility-toggle:hover:not(:disabled){background-color:var(--tn-bg3, #f3f4f6);color:var(--tn-fg1, #1f2937)}.tn-input__suffix-action:focus-visible,.tn-input__visibility-toggle:focus-visible{outline:2px solid var(--tn-primary, #2563eb);outline-offset:1px}.tn-input__suffix-action:disabled,.tn-input__visibility-toggle:disabled{cursor:not-allowed;pointer-events:none}.tn-textarea{min-height:6rem;resize:vertical;line-height:1.4}@media(prefers-reduced-motion:reduce){.tn-input-container,.tn-input__suffix-action,.tn-input__visibility-toggle{transition:none}}@media(prefers-contrast:high){.tn-input-container{border-width:2px}}\n"] }]
|
|
3680
3721
|
}], propDecorators: { inputEl: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], inputType: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputType", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], multiline: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiline", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], autocomplete: [{ type: i0.Input, args: [{ isSignal: true, alias: "autocomplete", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], allowDecimals: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowDecimals", required: false }] }], sizeStandard: [{ type: i0.Input, args: [{ isSignal: true, alias: "sizeStandard", required: false }] }], sizeDefaultUnit: [{ type: i0.Input, args: [{ isSignal: true, alias: "sizeDefaultUnit", required: false }] }], sizeRound: [{ type: i0.Input, args: [{ isSignal: true, alias: "sizeRound", required: false }] }], showPasswordToggle: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPasswordToggle", required: false }] }], prefixIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixIcon", required: false }] }], prefixIconLibrary: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixIconLibrary", required: false }] }], suffixIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixIcon", required: false }] }], suffixIconLibrary: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixIconLibrary", required: false }] }], suffixIconAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixIconAriaLabel", required: false }] }], suffixActionTestId: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixActionTestId", required: false }] }], onSuffixAction: [{ type: i0.Output, args: ["onSuffixAction"] }] } });
|
|
3681
3722
|
|
|
3682
3723
|
/**
|
|
@@ -4774,7 +4815,7 @@ class TnChipHarness extends ComponentHarness {
|
|
|
4774
4815
|
}
|
|
4775
4816
|
}
|
|
4776
4817
|
|
|
4777
|
-
let nextId = 0;
|
|
4818
|
+
let nextId$1 = 0;
|
|
4778
4819
|
/**
|
|
4779
4820
|
* An editable, multi-value chip input — tokenized entry where typed text
|
|
4780
4821
|
* becomes removable `tn-chip`s alongside an inline text field. Text is
|
|
@@ -4819,7 +4860,7 @@ class TnChipInputComponent {
|
|
|
4819
4860
|
overlay = inject(Overlay);
|
|
4820
4861
|
viewContainerRef = inject(ViewContainerRef);
|
|
4821
4862
|
/** Unique instance id for ARIA linkage between the input and its dropdown. */
|
|
4822
|
-
uid = `tn-chip-input-${nextId++}`;
|
|
4863
|
+
uid = `tn-chip-input-${nextId$1++}`;
|
|
4823
4864
|
/** Placeholder shown in the text field when it is empty. */
|
|
4824
4865
|
placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
|
|
4825
4866
|
/** Disables the whole control — chips become non-removable and the field read-only. */
|
|
@@ -4869,8 +4910,18 @@ class TnChipInputComponent {
|
|
|
4869
4910
|
* provide this when values are objects (e.g. `(a, b) => a?.id === b?.id`).
|
|
4870
4911
|
*/
|
|
4871
4912
|
compareWith = input(undefined, ...(ngDevMode ? [{ debugName: "compareWith" }] : []));
|
|
4872
|
-
/**
|
|
4913
|
+
/**
|
|
4914
|
+
* Explicit accessible name for the text field. Inside a `tn-form-field` the
|
|
4915
|
+
* field's label is associated automatically (via `aria-labelledby`), so leave
|
|
4916
|
+
* this unset there unless the announced name must differ from the visible
|
|
4917
|
+
* label — when set, it wins.
|
|
4918
|
+
*/
|
|
4873
4919
|
ariaLabel = input(undefined, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
4920
|
+
/**
|
|
4921
|
+
* ARIA wiring from an enclosing `tn-form-field` (label, error/hint,
|
|
4922
|
+
* invalid, required). All-null when standalone or when `ariaLabel` overrides.
|
|
4923
|
+
*/
|
|
4924
|
+
fieldAria = injectTnFormFieldAria(this.ariaLabel);
|
|
4874
4925
|
/**
|
|
4875
4926
|
* Semantic test-id base. The library prepends the `chip-input` element type
|
|
4876
4927
|
* (e.g. `testId="tags"` → `chip-input-tags`); each chip and suggestion is
|
|
@@ -5209,7 +5260,7 @@ class TnChipInputComponent {
|
|
|
5209
5260
|
useExisting: forwardRef(() => TnChipInputComponent),
|
|
5210
5261
|
multi: true,
|
|
5211
5262
|
},
|
|
5212
|
-
], viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, isSignal: true }, { propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }, { propertyName: "dropdownTemplate", first: true, predicate: ["dropdownTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Click-to-focus is a pointer affordance only; keyboard users Tab straight to\n the real <input> inside, so the container itself is not a focus target. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<div\n #container\n class=\"tn-chip-input\"\n [class.tn-chip-input--disabled]=\"isDisabled()\"\n (click)=\"focusInput()\"\n>\n @for (value of values(); track $index) {\n <tn-chip\n class=\"tn-chip-input__chip\"\n color=\"secondary\"\n [label]=\"displayLabel(value)\"\n [closable]=\"!isDisabled()\"\n [disabled]=\"isDisabled()\"\n [testId]=\"chipTestId(value)\"\n (onClose)=\"removeChip($index)\"\n />\n }\n\n <input\n #inputEl\n type=\"text\"\n class=\"tn-chip-input__field\"\n role=\"combobox\"\n autocomplete=\"off\"\n tnTestIdType=\"chip-input\"\n [tnTestId]=\"testId()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"isOpen() ? uid + '-listbox' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\"\n />\n</div>\n\n<!-- Suggestion dropdown \u2014 portaled via CDK overlay so it escapes ancestor\n overflow/clipping. Attach/detach is driven by open()/close(). -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-chip-input__dropdown\"\n role=\"listbox\"\n [attr.id]=\"uid + '-listbox'\"\n >\n @for (suggestion of filteredSuggestions(); track $index) {\n <!-- Options are tabindex=\"-1\" and never focused: keyboard selection goes\n through the input's aria-activedescendant + the Enter branch in\n onKeydown, so a key handler here would be dead code. Click is a\n pointer-only affordance. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-chip-input__option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"chipTestId(suggestion.value)\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"highlightedIndex() === $index\"\n (mousedown)=\"onSuggestionMousedown($event)\"\n (click)=\"onSuggestionClick(suggestion)\"\n >\n {{ suggestion.label }}\n </div>\n }\n </div>\n</ng-template>\n", styles: [".tn-chip-input{display:flex;flex-wrap:wrap;align-items:center;gap:.375rem;min-height:2.5rem;padding:.375rem .5rem;box-sizing:border-box;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:text;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-chip-input:focus-within{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-chip-input--disabled{background-color:var(--tn-alt-bg1, #f8f9fa);opacity:.6;cursor:not-allowed}.tn-chip-input__chip{flex-shrink:0}.tn-chip-input__field{flex:1;min-width:120px;padding:.25rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background:transparent;border:none;outline:none}.tn-chip-input__field::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-chip-input__field:disabled{cursor:not-allowed}.tn-chip-input__dropdown{max-height:var(--tn-chip-input-panel-max-height, 320px);overflow-y:auto;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.tn-chip-input__option{padding:.5rem .75rem;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;font-size:1rem;color:var(--tn-fg1, #212529);cursor:pointer}.tn-chip-input__option:hover,.tn-chip-input__option.highlighted{background-color:var(--tn-bg3, #f3f4f6)}@media(prefers-reduced-motion:reduce){.tn-chip-input{transition:none}}@media(prefers-contrast:high){.tn-chip-input{border-width:2px}}\n"], dependencies: [{ kind: "component", type: TnChipComponent, selector: "tn-chip", inputs: ["label", "icon", "closable", "disabled", "color", "testId"], outputs: ["onClose", "onClick"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
|
|
5263
|
+
], viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, isSignal: true }, { propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }, { propertyName: "dropdownTemplate", first: true, predicate: ["dropdownTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Click-to-focus is a pointer affordance only; keyboard users Tab straight to\n the real <input> inside, so the container itself is not a focus target. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<div\n #container\n class=\"tn-chip-input\"\n [class.tn-chip-input--disabled]=\"isDisabled()\"\n (click)=\"focusInput()\"\n>\n @for (value of values(); track $index) {\n <tn-chip\n class=\"tn-chip-input__chip\"\n color=\"secondary\"\n [label]=\"displayLabel(value)\"\n [closable]=\"!isDisabled()\"\n [disabled]=\"isDisabled()\"\n [testId]=\"chipTestId(value)\"\n (onClose)=\"removeChip($index)\"\n />\n }\n\n <input\n #inputEl\n type=\"text\"\n class=\"tn-chip-input__field\"\n role=\"combobox\"\n autocomplete=\"off\"\n tnTestIdType=\"chip-input\"\n [tnTestId]=\"testId()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-labelledby]=\"fieldAria.labelledby()\"\n [attr.aria-describedby]=\"fieldAria.describedby()\"\n [attr.aria-invalid]=\"fieldAria.invalid()\"\n [attr.aria-required]=\"fieldAria.required()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"isOpen() ? uid + '-listbox' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\"\n />\n</div>\n\n<!-- Suggestion dropdown \u2014 portaled via CDK overlay so it escapes ancestor\n overflow/clipping. Attach/detach is driven by open()/close(). -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-chip-input__dropdown\"\n role=\"listbox\"\n [attr.id]=\"uid + '-listbox'\"\n >\n @for (suggestion of filteredSuggestions(); track $index) {\n <!-- Options are tabindex=\"-1\" and never focused: keyboard selection goes\n through the input's aria-activedescendant + the Enter branch in\n onKeydown, so a key handler here would be dead code. Click is a\n pointer-only affordance. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-chip-input__option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"chipTestId(suggestion.value)\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"highlightedIndex() === $index\"\n (mousedown)=\"onSuggestionMousedown($event)\"\n (click)=\"onSuggestionClick(suggestion)\"\n >\n {{ suggestion.label }}\n </div>\n }\n </div>\n</ng-template>\n", styles: [".tn-chip-input{display:flex;flex-wrap:wrap;align-items:center;gap:.375rem;min-height:2.5rem;padding:.375rem .5rem;box-sizing:border-box;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:text;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-chip-input:focus-within{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-chip-input--disabled{background-color:var(--tn-alt-bg1, #f8f9fa);opacity:.6;cursor:not-allowed}.tn-chip-input__chip{flex-shrink:0}.tn-chip-input__field{flex:1;min-width:120px;padding:.25rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background:transparent;border:none;outline:none}.tn-chip-input__field::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-chip-input__field:disabled{cursor:not-allowed}.tn-chip-input__dropdown{max-height:var(--tn-chip-input-panel-max-height, 320px);overflow-y:auto;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.tn-chip-input__option{padding:.5rem .75rem;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;font-size:1rem;color:var(--tn-fg1, #212529);cursor:pointer}.tn-chip-input__option:hover,.tn-chip-input__option.highlighted{background-color:var(--tn-bg3, #f3f4f6)}@media(prefers-reduced-motion:reduce){.tn-chip-input{transition:none}}@media(prefers-contrast:high){.tn-chip-input{border-width:2px}}\n"], dependencies: [{ kind: "component", type: TnChipComponent, selector: "tn-chip", inputs: ["label", "icon", "closable", "disabled", "color", "testId"], outputs: ["onClose", "onClick"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
|
|
5213
5264
|
}
|
|
5214
5265
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnChipInputComponent, decorators: [{
|
|
5215
5266
|
type: Component,
|
|
@@ -5219,7 +5270,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
5219
5270
|
useExisting: forwardRef(() => TnChipInputComponent),
|
|
5220
5271
|
multi: true,
|
|
5221
5272
|
},
|
|
5222
|
-
], template: "<!-- Click-to-focus is a pointer affordance only; keyboard users Tab straight to\n the real <input> inside, so the container itself is not a focus target. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<div\n #container\n class=\"tn-chip-input\"\n [class.tn-chip-input--disabled]=\"isDisabled()\"\n (click)=\"focusInput()\"\n>\n @for (value of values(); track $index) {\n <tn-chip\n class=\"tn-chip-input__chip\"\n color=\"secondary\"\n [label]=\"displayLabel(value)\"\n [closable]=\"!isDisabled()\"\n [disabled]=\"isDisabled()\"\n [testId]=\"chipTestId(value)\"\n (onClose)=\"removeChip($index)\"\n />\n }\n\n <input\n #inputEl\n type=\"text\"\n class=\"tn-chip-input__field\"\n role=\"combobox\"\n autocomplete=\"off\"\n tnTestIdType=\"chip-input\"\n [tnTestId]=\"testId()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"isOpen() ? uid + '-listbox' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\"\n />\n</div>\n\n<!-- Suggestion dropdown \u2014 portaled via CDK overlay so it escapes ancestor\n overflow/clipping. Attach/detach is driven by open()/close(). -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-chip-input__dropdown\"\n role=\"listbox\"\n [attr.id]=\"uid + '-listbox'\"\n >\n @for (suggestion of filteredSuggestions(); track $index) {\n <!-- Options are tabindex=\"-1\" and never focused: keyboard selection goes\n through the input's aria-activedescendant + the Enter branch in\n onKeydown, so a key handler here would be dead code. Click is a\n pointer-only affordance. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-chip-input__option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"chipTestId(suggestion.value)\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"highlightedIndex() === $index\"\n (mousedown)=\"onSuggestionMousedown($event)\"\n (click)=\"onSuggestionClick(suggestion)\"\n >\n {{ suggestion.label }}\n </div>\n }\n </div>\n</ng-template>\n", styles: [".tn-chip-input{display:flex;flex-wrap:wrap;align-items:center;gap:.375rem;min-height:2.5rem;padding:.375rem .5rem;box-sizing:border-box;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:text;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-chip-input:focus-within{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-chip-input--disabled{background-color:var(--tn-alt-bg1, #f8f9fa);opacity:.6;cursor:not-allowed}.tn-chip-input__chip{flex-shrink:0}.tn-chip-input__field{flex:1;min-width:120px;padding:.25rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background:transparent;border:none;outline:none}.tn-chip-input__field::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-chip-input__field:disabled{cursor:not-allowed}.tn-chip-input__dropdown{max-height:var(--tn-chip-input-panel-max-height, 320px);overflow-y:auto;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.tn-chip-input__option{padding:.5rem .75rem;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;font-size:1rem;color:var(--tn-fg1, #212529);cursor:pointer}.tn-chip-input__option:hover,.tn-chip-input__option.highlighted{background-color:var(--tn-bg3, #f3f4f6)}@media(prefers-reduced-motion:reduce){.tn-chip-input{transition:none}}@media(prefers-contrast:high){.tn-chip-input{border-width:2px}}\n"] }]
|
|
5273
|
+
], template: "<!-- Click-to-focus is a pointer affordance only; keyboard users Tab straight to\n the real <input> inside, so the container itself is not a focus target. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<div\n #container\n class=\"tn-chip-input\"\n [class.tn-chip-input--disabled]=\"isDisabled()\"\n (click)=\"focusInput()\"\n>\n @for (value of values(); track $index) {\n <tn-chip\n class=\"tn-chip-input__chip\"\n color=\"secondary\"\n [label]=\"displayLabel(value)\"\n [closable]=\"!isDisabled()\"\n [disabled]=\"isDisabled()\"\n [testId]=\"chipTestId(value)\"\n (onClose)=\"removeChip($index)\"\n />\n }\n\n <input\n #inputEl\n type=\"text\"\n class=\"tn-chip-input__field\"\n role=\"combobox\"\n autocomplete=\"off\"\n tnTestIdType=\"chip-input\"\n [tnTestId]=\"testId()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-labelledby]=\"fieldAria.labelledby()\"\n [attr.aria-describedby]=\"fieldAria.describedby()\"\n [attr.aria-invalid]=\"fieldAria.invalid()\"\n [attr.aria-required]=\"fieldAria.required()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"isOpen() ? uid + '-listbox' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\"\n />\n</div>\n\n<!-- Suggestion dropdown \u2014 portaled via CDK overlay so it escapes ancestor\n overflow/clipping. Attach/detach is driven by open()/close(). -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-chip-input__dropdown\"\n role=\"listbox\"\n [attr.id]=\"uid + '-listbox'\"\n >\n @for (suggestion of filteredSuggestions(); track $index) {\n <!-- Options are tabindex=\"-1\" and never focused: keyboard selection goes\n through the input's aria-activedescendant + the Enter branch in\n onKeydown, so a key handler here would be dead code. Click is a\n pointer-only affordance. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-chip-input__option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"chipTestId(suggestion.value)\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"highlightedIndex() === $index\"\n (mousedown)=\"onSuggestionMousedown($event)\"\n (click)=\"onSuggestionClick(suggestion)\"\n >\n {{ suggestion.label }}\n </div>\n }\n </div>\n</ng-template>\n", styles: [".tn-chip-input{display:flex;flex-wrap:wrap;align-items:center;gap:.375rem;min-height:2.5rem;padding:.375rem .5rem;box-sizing:border-box;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:text;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-chip-input:focus-within{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-chip-input--disabled{background-color:var(--tn-alt-bg1, #f8f9fa);opacity:.6;cursor:not-allowed}.tn-chip-input__chip{flex-shrink:0}.tn-chip-input__field{flex:1;min-width:120px;padding:.25rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background:transparent;border:none;outline:none}.tn-chip-input__field::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-chip-input__field:disabled{cursor:not-allowed}.tn-chip-input__dropdown{max-height:var(--tn-chip-input-panel-max-height, 320px);overflow-y:auto;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.tn-chip-input__option{padding:.5rem .75rem;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;font-size:1rem;color:var(--tn-fg1, #212529);cursor:pointer}.tn-chip-input__option:hover,.tn-chip-input__option.highlighted{background-color:var(--tn-bg3, #f3f4f6)}@media(prefers-reduced-motion:reduce){.tn-chip-input{transition:none}}@media(prefers-contrast:high){.tn-chip-input{border-width:2px}}\n"] }]
|
|
5223
5274
|
}], ctorParameters: () => [], propDecorators: { placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], separatorKeys: [{ type: i0.Input, args: [{ isSignal: true, alias: "separatorKeys", required: false }] }], addOnBlur: [{ type: i0.Input, args: [{ isSignal: true, alias: "addOnBlur", required: false }] }], allowCustomValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCustomValue", required: false }] }], allowDuplicates: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowDuplicates", required: false }] }], maxChips: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxChips", required: false }] }], suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], chipAdded: [{ type: i0.Output, args: ["chipAdded"] }], chipRemoved: [{ type: i0.Output, args: ["chipRemoved"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], container: [{ type: i0.ViewChild, args: ['container', { isSignal: true }] }], inputEl: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdownTemplate', { isSignal: true }] }] } });
|
|
5224
5275
|
|
|
5225
5276
|
/**
|
|
@@ -8182,7 +8233,14 @@ function activeErrorKey(errors) {
|
|
|
8182
8233
|
return Object.keys(errors)[0] ?? null;
|
|
8183
8234
|
}
|
|
8184
8235
|
|
|
8236
|
+
let nextId = 0;
|
|
8185
8237
|
class TnFormFieldComponent {
|
|
8238
|
+
/** Unique instance id namespacing the label/error/hint ids for ARIA linkage. */
|
|
8239
|
+
uid = `tn-form-field-${nextId++}`;
|
|
8240
|
+
/** Id carried by the error message element (only meaningful while it renders). */
|
|
8241
|
+
errorId = `${this.uid}-error`;
|
|
8242
|
+
/** Id carried by the hint element (only meaningful while it renders). */
|
|
8243
|
+
hintId = `${this.uid}-hint`;
|
|
8186
8244
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
8187
8245
|
hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : []));
|
|
8188
8246
|
/**
|
|
@@ -8192,9 +8250,10 @@ class TnFormFieldComponent {
|
|
|
8192
8250
|
* see the requirement — e.g. a validator wrapped in `Validators.compose(...)`
|
|
8193
8251
|
* or a custom validator that emits a `required`-style error.
|
|
8194
8252
|
*
|
|
8195
|
-
*
|
|
8196
|
-
*
|
|
8197
|
-
* the native
|
|
8253
|
+
* Library form controls surface this state as `aria-required` automatically
|
|
8254
|
+
* (via {@link TnFormFieldContext}); pairing it with the projected control's
|
|
8255
|
+
* own `required` input (e.g. `tn-input`'s, which renders the native
|
|
8256
|
+
* attribute) additionally blocks native form submission.
|
|
8198
8257
|
*/
|
|
8199
8258
|
required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : []));
|
|
8200
8259
|
testId = input(undefined, ...(ngDevMode ? [{ debugName: "testId" }] : []));
|
|
@@ -8272,12 +8331,14 @@ class TnFormFieldComponent {
|
|
|
8272
8331
|
ngAfterContentInit() {
|
|
8273
8332
|
const control = this.control();
|
|
8274
8333
|
if (control) {
|
|
8275
|
-
//
|
|
8276
|
-
//
|
|
8277
|
-
//
|
|
8278
|
-
//
|
|
8279
|
-
//
|
|
8280
|
-
|
|
8334
|
+
// Prefer the unified `events` stream: unlike `statusChanges`, it also
|
|
8335
|
+
// emits on touched/pristine-only transitions (`markAsTouched()` on blur,
|
|
8336
|
+
// `markAllAsTouched()` on submit), so the error — visual and ARIA —
|
|
8337
|
+
// surfaces the moment the user leaves a required field empty. Fall back
|
|
8338
|
+
// to `statusChanges` for NgControl implementations whose underlying
|
|
8339
|
+
// AbstractControl isn't reachable yet.
|
|
8340
|
+
const changes = control.control?.events ?? control.statusChanges;
|
|
8341
|
+
changes
|
|
8281
8342
|
?.pipe(takeUntilDestroyed(this.destroyRef))
|
|
8282
8343
|
.subscribe(() => {
|
|
8283
8344
|
this.syncControlState();
|
|
@@ -8365,12 +8426,49 @@ class TnFormFieldComponent {
|
|
|
8365
8426
|
showSubscript = computed(() => {
|
|
8366
8427
|
return this.subscriptSizing() === 'fixed' || this.showError() || this.showHint();
|
|
8367
8428
|
}, ...(ngDevMode ? [{ debugName: "showSubscript" }] : []));
|
|
8429
|
+
// ── TnFormFieldContext (consumed by the projected control via DI) ──
|
|
8430
|
+
/**
|
|
8431
|
+
* Id of the label *text* span — deliberately not the whole `<label>`, so an
|
|
8432
|
+
* `aria-labelledby` pointing here never picks up the required asterisk's
|
|
8433
|
+
* "required" into the accessible name (that state travels as `aria-required`).
|
|
8434
|
+
*/
|
|
8435
|
+
labelId = computed(() => (this.label() ? `${this.uid}-label` : null), ...(ngDevMode ? [{ debugName: "labelId" }] : []));
|
|
8436
|
+
/** Id of the currently shown error or hint (they are mutually exclusive), or null. */
|
|
8437
|
+
describedBy = computed(() => {
|
|
8438
|
+
if (this.showError()) {
|
|
8439
|
+
return this.errorId;
|
|
8440
|
+
}
|
|
8441
|
+
if (this.showHint()) {
|
|
8442
|
+
return this.hintId;
|
|
8443
|
+
}
|
|
8444
|
+
return null;
|
|
8445
|
+
}, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
|
|
8446
|
+
/** Mirrors the visual error state (invalid AND interacted) for `aria-invalid`. */
|
|
8447
|
+
errorState = computed(() => this.hasError(), ...(ngDevMode ? [{ debugName: "errorState" }] : []));
|
|
8448
|
+
/** Forced or validator-inferred required state, for `aria-required`. */
|
|
8449
|
+
requiredState = computed(() => this.showRequired(), ...(ngDevMode ? [{ debugName: "requiredState" }] : []));
|
|
8368
8450
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFormFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8369
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnFormFieldComponent, isStandalone: true, selector: "tn-form-field", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, subscriptSizing: { classPropertyName: "subscriptSizing", publicName: "subscriptSizing", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, tooltipPosition: { classPropertyName: "tooltipPosition", publicName: "tooltipPosition", isSignal: true, isRequired: false, transformFunction: null }, errorMessages: { classPropertyName: "errorMessages", publicName: "errorMessages", isSignal: true, isRequired: false, transformFunction: null } },
|
|
8451
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnFormFieldComponent, isStandalone: true, selector: "tn-form-field", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, subscriptSizing: { classPropertyName: "subscriptSizing", publicName: "subscriptSizing", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, tooltipPosition: { classPropertyName: "tooltipPosition", publicName: "tooltipPosition", isSignal: true, isRequired: false, transformFunction: null }, errorMessages: { classPropertyName: "errorMessages", publicName: "errorMessages", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
|
|
8452
|
+
// Published to projected controls (their element injector chains through
|
|
8453
|
+
// this host), which bind aria-labelledby/-describedby/-invalid/-required
|
|
8454
|
+
// to the field's label and messages. See TnFormFieldContext.
|
|
8455
|
+
{
|
|
8456
|
+
provide: TN_FORM_FIELD_CONTEXT,
|
|
8457
|
+
useExisting: forwardRef(() => TnFormFieldComponent),
|
|
8458
|
+
},
|
|
8459
|
+
], queries: [{ propertyName: "control", first: true, predicate: NgControl, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-form-field\" tnTestIdType=\"form-field\" [tnTestId]=\"testId()\">\n <!-- Label -->\n @if (label()) {\n <div class=\"tn-form-field-label-row\">\n <label class=\"tn-form-field-label\" [class.required]=\"showRequired()\">\n <span [attr.id]=\"labelId()\" [innerHTML]=\"label() | tnLabelMarkup\"></span>\n @if (showRequired()) {\n <span class=\"required-asterisk\" aria-label=\"required\">*</span>\n }\n </label>\n @if (tooltip()) {\n <ng-container [ngTemplateOutlet]=\"tooltipButton\" />\n }\n </div>\n }\n\n <!-- Form Control Content -->\n <div\n class=\"tn-form-field-wrapper\"\n [class.tn-form-field-wrapper--inline]=\"showInlineExtras()\">\n <ng-content />\n @if (showInlineRequired()) {\n <span class=\"required-asterisk\" aria-label=\"required\">*</span>\n }\n @if (showInlineTooltip()) {\n <ng-container [ngTemplateOutlet]=\"tooltipButton\" />\n }\n </div>\n\n <!-- Hint or Error Message -->\n @if (showSubscript()) {\n <div class=\"tn-form-field-subscript\" [class.tn-form-field-subscript-dynamic]=\"subscriptSizing() === 'dynamic'\">\n @if (showError()) {\n <!-- role=\"alert\" is implicitly assertive+atomic, so no aria-live here.\n The id feeds the projected control's aria-describedby (see\n TnFormFieldContext). -->\n <div\n class=\"tn-form-field-error\"\n role=\"alert\"\n [attr.id]=\"errorId\">\n {{ errorMessage() }}\n </div>\n }\n @if (showHint()) {\n <div class=\"tn-form-field-hint\" [attr.id]=\"hintId\">\n {{ hint() }}\n </div>\n }\n </div>\n }\n</div>\n\n<ng-template #tooltipButton>\n <button\n type=\"button\"\n class=\"tn-form-field-tooltip\"\n [attr.aria-label]=\"tooltip()\"\n [tnTooltip]=\"tooltip()\"\n [tnTooltipPosition]=\"tooltipPosition()\">\n <tn-icon name=\"help-circle\" library=\"mdi\" size=\"sm\" />\n </button>\n</ng-template>\n", styles: [".tn-form-field{display:block;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-form-field-label-row{display:flex;align-items:center;gap:.375rem;margin-bottom:.5rem}.tn-form-field-label{display:block;font-size:1rem;font-weight:400;color:var(--tn-fg1, #333);line-height:1.4}.tn-form-field-label ::ng-deep code{font-family:var(--tn-font-family-monospace, monospace);font-size:.875em;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #ccc);border-radius:4px;padding:.125em .45em}.tn-form-field-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-form-field-tooltip{display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:var(--tn-fg2, #6c757d);cursor:help;line-height:0}.tn-form-field-tooltip:hover,.tn-form-field-tooltip:focus-visible{color:var(--tn-primary, #007bff)}.tn-form-field-tooltip:focus-visible{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;border-radius:50%}.tn-form-field-wrapper{position:relative;width:100%;overflow:visible}.tn-form-field-wrapper :ng-deep .tn-select-container,.tn-form-field-wrapper :ng-deep .tn-input-container{margin-bottom:0}.tn-form-field-wrapper :ng-deep .tn-select-label,.tn-form-field-wrapper :ng-deep .tn-input-label{display:none}.tn-form-field-wrapper :ng-deep .tn-select-error,.tn-form-field-wrapper :ng-deep .tn-input-error{display:none}.tn-form-field-wrapper :ng-deep .tn-select-dropdown{z-index:1000}.tn-form-field-wrapper--inline{align-items:center;display:flex;gap:.375rem}.tn-form-field-wrapper--inline .required-asterisk{color:var(--tn-error, #dc3545)}.tn-form-field-subscript{min-height:1.25rem;margin-top:.25rem;font-size:.75rem;line-height:1.4}.tn-form-field-subscript-dynamic{min-height:0}.tn-form-field-error{color:var(--tn-error, #dc3545);margin:0}.tn-form-field-hint{color:var(--tn-fg2, #6c757d);margin:0}.tn-form-field-wrapper:has(:focus-visible) .tn-form-field-label{color:var(--tn-primary, #007bff)}.tn-form-field-wrapper:has(.error) .tn-form-field-label{color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-form-field-label{transition:none}}@media(prefers-contrast:high){.tn-form-field-label,.tn-form-field-error{font-weight:600}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "directive", type: TnTooltipDirective, selector: "[tnTooltip]", inputs: ["tnTooltip", "tnTooltipPosition", "tnTooltipDisabled", "tnTooltipShowDelay", "tnTooltipHideDelay", "tnTooltipClass"] }, { kind: "pipe", type: LabelMarkupPipe, name: "tnLabelMarkup" }] });
|
|
8370
8460
|
}
|
|
8371
8461
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFormFieldComponent, decorators: [{
|
|
8372
8462
|
type: Component,
|
|
8373
|
-
args: [{ selector: 'tn-form-field', standalone: true, imports: [NgTemplateOutlet, TnTestIdDirective, TnIconComponent, TnTooltipDirective, LabelMarkupPipe],
|
|
8463
|
+
args: [{ selector: 'tn-form-field', standalone: true, imports: [NgTemplateOutlet, TnTestIdDirective, TnIconComponent, TnTooltipDirective, LabelMarkupPipe], providers: [
|
|
8464
|
+
// Published to projected controls (their element injector chains through
|
|
8465
|
+
// this host), which bind aria-labelledby/-describedby/-invalid/-required
|
|
8466
|
+
// to the field's label and messages. See TnFormFieldContext.
|
|
8467
|
+
{
|
|
8468
|
+
provide: TN_FORM_FIELD_CONTEXT,
|
|
8469
|
+
useExisting: forwardRef(() => TnFormFieldComponent),
|
|
8470
|
+
},
|
|
8471
|
+
], template: "<div class=\"tn-form-field\" tnTestIdType=\"form-field\" [tnTestId]=\"testId()\">\n <!-- Label -->\n @if (label()) {\n <div class=\"tn-form-field-label-row\">\n <label class=\"tn-form-field-label\" [class.required]=\"showRequired()\">\n <span [attr.id]=\"labelId()\" [innerHTML]=\"label() | tnLabelMarkup\"></span>\n @if (showRequired()) {\n <span class=\"required-asterisk\" aria-label=\"required\">*</span>\n }\n </label>\n @if (tooltip()) {\n <ng-container [ngTemplateOutlet]=\"tooltipButton\" />\n }\n </div>\n }\n\n <!-- Form Control Content -->\n <div\n class=\"tn-form-field-wrapper\"\n [class.tn-form-field-wrapper--inline]=\"showInlineExtras()\">\n <ng-content />\n @if (showInlineRequired()) {\n <span class=\"required-asterisk\" aria-label=\"required\">*</span>\n }\n @if (showInlineTooltip()) {\n <ng-container [ngTemplateOutlet]=\"tooltipButton\" />\n }\n </div>\n\n <!-- Hint or Error Message -->\n @if (showSubscript()) {\n <div class=\"tn-form-field-subscript\" [class.tn-form-field-subscript-dynamic]=\"subscriptSizing() === 'dynamic'\">\n @if (showError()) {\n <!-- role=\"alert\" is implicitly assertive+atomic, so no aria-live here.\n The id feeds the projected control's aria-describedby (see\n TnFormFieldContext). -->\n <div\n class=\"tn-form-field-error\"\n role=\"alert\"\n [attr.id]=\"errorId\">\n {{ errorMessage() }}\n </div>\n }\n @if (showHint()) {\n <div class=\"tn-form-field-hint\" [attr.id]=\"hintId\">\n {{ hint() }}\n </div>\n }\n </div>\n }\n</div>\n\n<ng-template #tooltipButton>\n <button\n type=\"button\"\n class=\"tn-form-field-tooltip\"\n [attr.aria-label]=\"tooltip()\"\n [tnTooltip]=\"tooltip()\"\n [tnTooltipPosition]=\"tooltipPosition()\">\n <tn-icon name=\"help-circle\" library=\"mdi\" size=\"sm\" />\n </button>\n</ng-template>\n", styles: [".tn-form-field{display:block;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-form-field-label-row{display:flex;align-items:center;gap:.375rem;margin-bottom:.5rem}.tn-form-field-label{display:block;font-size:1rem;font-weight:400;color:var(--tn-fg1, #333);line-height:1.4}.tn-form-field-label ::ng-deep code{font-family:var(--tn-font-family-monospace, monospace);font-size:.875em;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #ccc);border-radius:4px;padding:.125em .45em}.tn-form-field-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-form-field-tooltip{display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:var(--tn-fg2, #6c757d);cursor:help;line-height:0}.tn-form-field-tooltip:hover,.tn-form-field-tooltip:focus-visible{color:var(--tn-primary, #007bff)}.tn-form-field-tooltip:focus-visible{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;border-radius:50%}.tn-form-field-wrapper{position:relative;width:100%;overflow:visible}.tn-form-field-wrapper :ng-deep .tn-select-container,.tn-form-field-wrapper :ng-deep .tn-input-container{margin-bottom:0}.tn-form-field-wrapper :ng-deep .tn-select-label,.tn-form-field-wrapper :ng-deep .tn-input-label{display:none}.tn-form-field-wrapper :ng-deep .tn-select-error,.tn-form-field-wrapper :ng-deep .tn-input-error{display:none}.tn-form-field-wrapper :ng-deep .tn-select-dropdown{z-index:1000}.tn-form-field-wrapper--inline{align-items:center;display:flex;gap:.375rem}.tn-form-field-wrapper--inline .required-asterisk{color:var(--tn-error, #dc3545)}.tn-form-field-subscript{min-height:1.25rem;margin-top:.25rem;font-size:.75rem;line-height:1.4}.tn-form-field-subscript-dynamic{min-height:0}.tn-form-field-error{color:var(--tn-error, #dc3545);margin:0}.tn-form-field-hint{color:var(--tn-fg2, #6c757d);margin:0}.tn-form-field-wrapper:has(:focus-visible) .tn-form-field-label{color:var(--tn-primary, #007bff)}.tn-form-field-wrapper:has(.error) .tn-form-field-label{color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-form-field-label{transition:none}}@media(prefers-contrast:high){.tn-form-field-label,.tn-form-field-error{font-weight:600}}\n"] }]
|
|
8374
8472
|
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], subscriptSizing: [{ type: i0.Input, args: [{ isSignal: true, alias: "subscriptSizing", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], tooltipPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipPosition", required: false }] }], errorMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMessages", required: false }] }], control: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NgControl), { isSignal: true }] }] } });
|
|
8375
8473
|
|
|
8376
8474
|
/**
|
|
@@ -8705,12 +8803,26 @@ class TnSelectComponent {
|
|
|
8705
8803
|
optionGroups = input([], ...(ngDevMode ? [{ debugName: "optionGroups" }] : []));
|
|
8706
8804
|
placeholder = input('Select an option', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
|
|
8707
8805
|
/**
|
|
8708
|
-
*
|
|
8709
|
-
* trigger's `aria-label` instead of the visible `placeholder` — useful in
|
|
8806
|
+
* Explicit accessible label for the select trigger. When set, this is used as
|
|
8807
|
+
* the trigger's `aria-label` instead of the visible `placeholder` — useful in
|
|
8710
8808
|
* contexts (e.g. a pager's page-size dropdown) where the placeholder text
|
|
8711
|
-
* doesn't accurately describe the field's purpose to screen readers.
|
|
8809
|
+
* doesn't accurately describe the field's purpose to screen readers. Inside a
|
|
8810
|
+
* `tn-form-field` the field's label is associated automatically (via
|
|
8811
|
+
* `aria-labelledby`) and takes precedence over the placeholder fallback;
|
|
8812
|
+
* setting this overrides both.
|
|
8712
8813
|
*/
|
|
8713
8814
|
ariaLabel = input(undefined, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
8815
|
+
/**
|
|
8816
|
+
* ARIA wiring from an enclosing `tn-form-field` (label, error/hint,
|
|
8817
|
+
* invalid, required). All-null when standalone or when `ariaLabel` overrides.
|
|
8818
|
+
*/
|
|
8819
|
+
fieldAria = injectTnFormFieldAria(this.ariaLabel);
|
|
8820
|
+
/**
|
|
8821
|
+
* `aria-label` for the trigger. An explicit `ariaLabel` always wins; the
|
|
8822
|
+
* `placeholder` fallback only applies while no form-field label is wired via
|
|
8823
|
+
* `aria-labelledby`, so the trigger never advertises two names at once.
|
|
8824
|
+
*/
|
|
8825
|
+
triggerAriaLabel = computed(() => this.ariaLabel() ?? (this.fieldAria.labelledby() ? null : this.placeholder()), ...(ngDevMode ? [{ debugName: "triggerAriaLabel" }] : []));
|
|
8714
8826
|
/**
|
|
8715
8827
|
* Message shown inside the dropdown when no options (and no option groups)
|
|
8716
8828
|
* are available. Defaults to the English `'No options available'`; consumers
|
|
@@ -8729,6 +8841,16 @@ class TnSelectComponent {
|
|
|
8729
8841
|
/** Label of the empty option rendered when `allowEmpty` is set. */
|
|
8730
8842
|
emptyLabel = input('--', ...(ngDevMode ? [{ debugName: "emptyLabel" }] : []));
|
|
8731
8843
|
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
8844
|
+
/**
|
|
8845
|
+
* When `true` (multiple mode only), renders a "select all" row at the top of
|
|
8846
|
+
* the dropdown that toggles every selectable (non-disabled) option on/off in
|
|
8847
|
+
* one click. Mirrors webui ix-select's `[showSelectAll]`. Ignored in single
|
|
8848
|
+
* mode. Its checkbox reflects the aggregate state: checked when all are
|
|
8849
|
+
* selected, indeterminate when only some are.
|
|
8850
|
+
*/
|
|
8851
|
+
showSelectAll = input(false, ...(ngDevMode ? [{ debugName: "showSelectAll" }] : []));
|
|
8852
|
+
/** Label of the select-all row rendered when `showSelectAll` is set. */
|
|
8853
|
+
selectAllLabel = input('Select All', ...(ngDevMode ? [{ debugName: "selectAllLabel" }] : []));
|
|
8732
8854
|
testId = input(undefined, ...(ngDevMode ? [{ debugName: "testId" }] : []));
|
|
8733
8855
|
/** Test-id base, falling back to the bound control name when `testId` is unset. */
|
|
8734
8856
|
resolvedTestId = controlTestId(this.testId);
|
|
@@ -8816,18 +8938,23 @@ class TnSelectComponent {
|
|
|
8816
8938
|
*/
|
|
8817
8939
|
navigableOptions = computed(() => {
|
|
8818
8940
|
const result = [];
|
|
8941
|
+
// The select-all row is the first navigable entry when shown, so
|
|
8942
|
+
// ArrowDown from the closed trigger lands on it before the options.
|
|
8943
|
+
if (this.showSelectAllRow()) {
|
|
8944
|
+
result.push({ kind: 'select-all', id: this.selectAllId() });
|
|
8945
|
+
}
|
|
8819
8946
|
const baseId = `tn-select-opt-${this.idNamespace()}`;
|
|
8820
8947
|
let i = 0;
|
|
8821
8948
|
for (const opt of this.displayOptions()) {
|
|
8822
8949
|
if (!opt.disabled) {
|
|
8823
|
-
result.push({ option: opt, id: `${baseId}-${i}` });
|
|
8950
|
+
result.push({ kind: 'option', option: opt, id: `${baseId}-${i}` });
|
|
8824
8951
|
}
|
|
8825
8952
|
i++;
|
|
8826
8953
|
}
|
|
8827
8954
|
for (const group of this.optionGroups()) {
|
|
8828
8955
|
for (const opt of group.options) {
|
|
8829
8956
|
if (!opt.disabled && !group.disabled) {
|
|
8830
|
-
result.push({ option: opt, id: `${baseId}-${i}` });
|
|
8957
|
+
result.push({ kind: 'option', option: opt, id: `${baseId}-${i}` });
|
|
8831
8958
|
}
|
|
8832
8959
|
i++;
|
|
8833
8960
|
}
|
|
@@ -8842,14 +8969,15 @@ class TnSelectComponent {
|
|
|
8842
8969
|
}, ...(ngDevMode ? [{ debugName: "focusedOptionId" }] : []));
|
|
8843
8970
|
/** Stable DOM id for an option; matches what navigableOptions() assigns. */
|
|
8844
8971
|
optionId(option) {
|
|
8845
|
-
const entry = this.navigableOptions().find((x) => x.option === option);
|
|
8972
|
+
const entry = this.navigableOptions().find((x) => x.kind === 'option' && x.option === option);
|
|
8846
8973
|
return entry?.id ?? null;
|
|
8847
8974
|
}
|
|
8848
8975
|
/** Whether `option` is the keyboard-highlighted item. */
|
|
8849
8976
|
isOptionFocused(option) {
|
|
8850
8977
|
const idx = this.focusedIndex();
|
|
8851
8978
|
const nav = this.navigableOptions();
|
|
8852
|
-
|
|
8979
|
+
const entry = idx >= 0 && idx < nav.length ? nav[idx] : null;
|
|
8980
|
+
return entry?.kind === 'option' && entry.option === option;
|
|
8853
8981
|
}
|
|
8854
8982
|
/**
|
|
8855
8983
|
* Test-id segments for an option row, consumed by `[tnTestId]` with
|
|
@@ -8934,7 +9062,7 @@ class TnSelectComponent {
|
|
|
8934
9062
|
// otherwise leave it unset so the next ArrowDown lands on the first item.
|
|
8935
9063
|
const selected = this.selectedValue();
|
|
8936
9064
|
if (selected !== null && selected !== undefined) {
|
|
8937
|
-
const idx = this.navigableOptions().findIndex((x) => this.compareValues(x.option.value, selected));
|
|
9065
|
+
const idx = this.navigableOptions().findIndex((x) => x.kind === 'option' && this.compareValues(x.option.value, selected));
|
|
8938
9066
|
this.focusedIndex.set(idx);
|
|
8939
9067
|
}
|
|
8940
9068
|
else if (this.emptyOption()) {
|
|
@@ -9131,6 +9259,92 @@ class TnSelectComponent {
|
|
|
9131
9259
|
hasAnyOptions = computed(() => {
|
|
9132
9260
|
return this.options().length > 0 || this.optionGroups().length > 0;
|
|
9133
9261
|
}, ...(ngDevMode ? [{ debugName: "hasAnyOptions" }] : []));
|
|
9262
|
+
/**
|
|
9263
|
+
* Values of every selectable (non-disabled) option, across ungrouped options
|
|
9264
|
+
* and enabled groups. This is the set the select-all row operates on —
|
|
9265
|
+
* disabled options and options in disabled groups are excluded because they
|
|
9266
|
+
* can't be toggled individually either.
|
|
9267
|
+
*
|
|
9268
|
+
* Values are deduped with `compareValues` so a value that appears both
|
|
9269
|
+
* ungrouped and inside a group isn't pushed twice — that would make
|
|
9270
|
+
* select-all diverge from `toggleOption()`, which never produces duplicates.
|
|
9271
|
+
*/
|
|
9272
|
+
selectableValues = computed(() => {
|
|
9273
|
+
const values = [];
|
|
9274
|
+
const push = (value) => {
|
|
9275
|
+
if (!values.some((v) => this.compareValues(v, value))) {
|
|
9276
|
+
values.push(value);
|
|
9277
|
+
}
|
|
9278
|
+
};
|
|
9279
|
+
for (const opt of this.options()) {
|
|
9280
|
+
if (!opt.disabled) {
|
|
9281
|
+
push(opt.value);
|
|
9282
|
+
}
|
|
9283
|
+
}
|
|
9284
|
+
for (const group of this.optionGroups()) {
|
|
9285
|
+
if (group.disabled) {
|
|
9286
|
+
continue;
|
|
9287
|
+
}
|
|
9288
|
+
for (const opt of group.options) {
|
|
9289
|
+
if (!opt.disabled) {
|
|
9290
|
+
push(opt.value);
|
|
9291
|
+
}
|
|
9292
|
+
}
|
|
9293
|
+
}
|
|
9294
|
+
return values;
|
|
9295
|
+
}, ...(ngDevMode ? [{ debugName: "selectableValues" }] : []));
|
|
9296
|
+
/** Whether the select-all row is shown (multiple mode, opted in, with options). */
|
|
9297
|
+
showSelectAllRow = computed(() => this.multiple() && this.showSelectAll() && this.selectableValues().length > 0, ...(ngDevMode ? [{ debugName: "showSelectAllRow" }] : []));
|
|
9298
|
+
/** Stable DOM id of the select-all row, for aria-activedescendant. */
|
|
9299
|
+
selectAllId = computed(() => `tn-select-selectall-${this.idNamespace()}`, ...(ngDevMode ? [{ debugName: "selectAllId" }] : []));
|
|
9300
|
+
/** True when every selectable option is currently selected. */
|
|
9301
|
+
allSelected = computed(() => {
|
|
9302
|
+
const selectable = this.selectableValues();
|
|
9303
|
+
if (selectable.length === 0) {
|
|
9304
|
+
return false;
|
|
9305
|
+
}
|
|
9306
|
+
const selected = this.selectedValues();
|
|
9307
|
+
return selectable.every((v) => selected.some((s) => this.compareValues(s, v)));
|
|
9308
|
+
}, ...(ngDevMode ? [{ debugName: "allSelected" }] : []));
|
|
9309
|
+
/** True when some — but not all — selectable options are selected. */
|
|
9310
|
+
selectAllIndeterminate = computed(() => {
|
|
9311
|
+
if (this.allSelected()) {
|
|
9312
|
+
return false;
|
|
9313
|
+
}
|
|
9314
|
+
const selected = this.selectedValues();
|
|
9315
|
+
return this.selectableValues().some((v) => selected.some((s) => this.compareValues(s, v)));
|
|
9316
|
+
}, ...(ngDevMode ? [{ debugName: "selectAllIndeterminate" }] : []));
|
|
9317
|
+
/** Test-id segments for the select-all row; mirrors ix-select's `[name, 'select-all']`. */
|
|
9318
|
+
selectAllTestIdParts() {
|
|
9319
|
+
return scopeTestId(this.resolvedTestId(), 'select-all');
|
|
9320
|
+
}
|
|
9321
|
+
/**
|
|
9322
|
+
* Toggles every selectable option: clears them all when they're all already
|
|
9323
|
+
* selected, otherwise selects them all. Preserves the multi-select "open"
|
|
9324
|
+
* behaviour — the dropdown stays open so the user can keep adjusting.
|
|
9325
|
+
*
|
|
9326
|
+
* Disabled-but-selected values (e.g. a disabled option pre-selected via
|
|
9327
|
+
* `writeValue`) are preserved on both paths: the user can't toggle those
|
|
9328
|
+
* rows individually, so select-all must not silently discard them either.
|
|
9329
|
+
*/
|
|
9330
|
+
toggleSelectAll() {
|
|
9331
|
+
if (this.isDisabled()) {
|
|
9332
|
+
return;
|
|
9333
|
+
}
|
|
9334
|
+
const selectable = this.selectableValues();
|
|
9335
|
+
const preserved = this.selectedValues().filter((v) => !selectable.some((s) => this.compareValues(s, v)));
|
|
9336
|
+
const updated = this.allSelected() ? preserved : [...preserved, ...selectable];
|
|
9337
|
+
this.selectedValues.set(updated);
|
|
9338
|
+
this.onChange(updated);
|
|
9339
|
+
this.multiSelectionChange.emit(updated);
|
|
9340
|
+
this.cdr.markForCheck();
|
|
9341
|
+
}
|
|
9342
|
+
/** Whether the select-all row is the keyboard-highlighted item. */
|
|
9343
|
+
isSelectAllFocused() {
|
|
9344
|
+
const idx = this.focusedIndex();
|
|
9345
|
+
const nav = this.navigableOptions();
|
|
9346
|
+
return idx >= 0 && idx < nav.length && nav[idx].kind === 'select-all';
|
|
9347
|
+
}
|
|
9134
9348
|
/** One-shot guard so the object-compare warning fires at most once per instance. */
|
|
9135
9349
|
warnedAboutObjectCompare = false;
|
|
9136
9350
|
/**
|
|
@@ -9274,7 +9488,13 @@ class TnSelectComponent {
|
|
|
9274
9488
|
if (idx < 0 || idx >= nav.length) {
|
|
9275
9489
|
return;
|
|
9276
9490
|
}
|
|
9277
|
-
|
|
9491
|
+
const entry = nav[idx];
|
|
9492
|
+
if (entry.kind === 'select-all') {
|
|
9493
|
+
this.toggleSelectAll();
|
|
9494
|
+
}
|
|
9495
|
+
else {
|
|
9496
|
+
this.onOptionClick(entry.option);
|
|
9497
|
+
}
|
|
9278
9498
|
}
|
|
9279
9499
|
scrollFocusedIntoView() {
|
|
9280
9500
|
const id = this.focusedOptionId();
|
|
@@ -9297,13 +9517,13 @@ class TnSelectComponent {
|
|
|
9297
9517
|
});
|
|
9298
9518
|
}
|
|
9299
9519
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
9300
|
-
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: [
|
|
9520
|
+
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: [
|
|
9301
9521
|
{
|
|
9302
9522
|
provide: NG_VALUE_ACCESSOR,
|
|
9303
9523
|
useExisting: forwardRef(() => TnSelectComponent),
|
|
9304
9524
|
multi: true
|
|
9305
9525
|
}
|
|
9306
|
-
], 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 });
|
|
9526
|
+
], 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]=\"triggerAriaLabel()\"\n [attr.aria-labelledby]=\"fieldAria.labelledby()\"\n [attr.aria-describedby]=\"fieldAria.describedby()\"\n [attr.aria-invalid]=\"fieldAria.invalid()\"\n [attr.aria-required]=\"fieldAria.required()\"\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 });
|
|
9307
9527
|
}
|
|
9308
9528
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSelectComponent, decorators: [{
|
|
9309
9529
|
type: Component,
|
|
@@ -9313,8 +9533,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
9313
9533
|
useExisting: forwardRef(() => TnSelectComponent),
|
|
9314
9534
|
multi: true
|
|
9315
9535
|
}
|
|
9316
|
-
], 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"] }]
|
|
9317
|
-
}], 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 }] }] } });
|
|
9536
|
+
], 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]=\"triggerAriaLabel()\"\n [attr.aria-labelledby]=\"fieldAria.labelledby()\"\n [attr.aria-describedby]=\"fieldAria.describedby()\"\n [attr.aria-invalid]=\"fieldAria.invalid()\"\n [attr.aria-required]=\"fieldAria.required()\"\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"] }]
|
|
9537
|
+
}], 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 }] }] } });
|
|
9318
9538
|
|
|
9319
9539
|
/**
|
|
9320
9540
|
* Harness for interacting with tn-select in tests.
|
|
@@ -9472,7 +9692,10 @@ class TnSelectHarness extends ComponentHarness {
|
|
|
9472
9692
|
await this.open();
|
|
9473
9693
|
// Dropdown panel is rendered in a CDK overlay (outside the host element),
|
|
9474
9694
|
// so we search the document root rather than the harness-local subtree.
|
|
9475
|
-
|
|
9695
|
+
// Exclude the select-all row (`.tn-select-select-all`) — it's a bulk
|
|
9696
|
+
// action, not a choice; use `toggleSelectAll()` for it.
|
|
9697
|
+
const options = await this.documentRootLocatorFactory()
|
|
9698
|
+
.locatorForAll('.tn-select-option:not(.tn-select-select-all)')();
|
|
9476
9699
|
for (const option of options) {
|
|
9477
9700
|
const text = (await option.text()).trim();
|
|
9478
9701
|
const matches = filter instanceof RegExp
|
|
@@ -9532,13 +9755,55 @@ class TnSelectHarness extends ComponentHarness {
|
|
|
9532
9755
|
await this.open();
|
|
9533
9756
|
// Dropdown panel is rendered in a CDK overlay (outside the host element),
|
|
9534
9757
|
// so we search the document root rather than the harness-local subtree.
|
|
9535
|
-
|
|
9758
|
+
// The select-all row is excluded — it's a bulk action, not an option.
|
|
9759
|
+
const options = await this.documentRootLocatorFactory()
|
|
9760
|
+
.locatorForAll('.tn-select-option:not(.tn-select-select-all)')();
|
|
9536
9761
|
const labels = [];
|
|
9537
9762
|
for (const option of options) {
|
|
9538
9763
|
labels.push((await option.text()).trim());
|
|
9539
9764
|
}
|
|
9540
9765
|
return labels;
|
|
9541
9766
|
}
|
|
9767
|
+
/**
|
|
9768
|
+
* Toggles the select-all row (multiple mode with `showSelectAll`). Opens the
|
|
9769
|
+
* dropdown if needed. Throws when the select-all row isn't present.
|
|
9770
|
+
*
|
|
9771
|
+
* @returns Promise that resolves once the row has been clicked.
|
|
9772
|
+
*
|
|
9773
|
+
* @example
|
|
9774
|
+
* ```typescript
|
|
9775
|
+
* const select = await loader.getHarness(TnSelectHarness);
|
|
9776
|
+
* await select.toggleSelectAll(); // selects every option
|
|
9777
|
+
* await select.toggleSelectAll(); // clears them again
|
|
9778
|
+
* ```
|
|
9779
|
+
*/
|
|
9780
|
+
async toggleSelectAll() {
|
|
9781
|
+
await this.open();
|
|
9782
|
+
// Rendered in the CDK overlay, outside the host subtree.
|
|
9783
|
+
const row = await this.documentRootLocatorFactory()
|
|
9784
|
+
.locatorForOptional('.tn-select-select-all')();
|
|
9785
|
+
if (!row) {
|
|
9786
|
+
await this.close();
|
|
9787
|
+
throw new Error('Select has no select-all row — set `multiple` and `showSelectAll`.');
|
|
9788
|
+
}
|
|
9789
|
+
await row.click();
|
|
9790
|
+
}
|
|
9791
|
+
/**
|
|
9792
|
+
* Whether the select-all checkbox reads as fully checked (all options
|
|
9793
|
+
* selected). Opens the dropdown if needed. Throws when the row isn't present.
|
|
9794
|
+
*
|
|
9795
|
+
* @returns Promise resolving to true when every option is selected.
|
|
9796
|
+
*/
|
|
9797
|
+
async isSelectAllChecked() {
|
|
9798
|
+
await this.open();
|
|
9799
|
+
const row = await this.documentRootLocatorFactory()
|
|
9800
|
+
.locatorForOptional('.tn-select-select-all')();
|
|
9801
|
+
if (!row) {
|
|
9802
|
+
await this.close();
|
|
9803
|
+
throw new Error('Select has no select-all row — set `multiple` and `showSelectAll`.');
|
|
9804
|
+
}
|
|
9805
|
+
return row.hasClass('selected');
|
|
9806
|
+
}
|
|
9542
9807
|
}
|
|
9543
9808
|
|
|
9544
9809
|
/// <reference types="jest" />
|
|
@@ -11251,7 +11516,7 @@ class TnTablePagerComponent {
|
|
|
11251
11516
|
provider.setPagination(pagination);
|
|
11252
11517
|
}
|
|
11253
11518
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTablePagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11254
|
-
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 });
|
|
11519
|
+
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 });
|
|
11255
11520
|
}
|
|
11256
11521
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTablePagerComponent, decorators: [{
|
|
11257
11522
|
type: Component,
|
|
@@ -11446,7 +11711,7 @@ class TnTreeNodeComponent extends CdkTreeNode {
|
|
|
11446
11711
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11447
11712
|
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: [
|
|
11448
11713
|
{ provide: CdkTreeNode, useExisting: TnTreeNodeComponent }
|
|
11449
|
-
], exportAs: ["tnTreeNode"], usesInheritance: true, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "
|
|
11714
|
+
], 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 });
|
|
11450
11715
|
}
|
|
11451
11716
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeNodeComponent, decorators: [{
|
|
11452
11717
|
type: Component,
|
|
@@ -11457,7 +11722,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
11457
11722
|
'[attr.aria-level]': 'level + 1',
|
|
11458
11723
|
'[attr.aria-expanded]': 'isExpandable ? isExpanded : null',
|
|
11459
11724
|
'role': 'treeitem'
|
|
11460
|
-
}, encapsulation: ViewEncapsulation.Emulated, changeDetection: ChangeDetectionStrategy.OnPush, template: "
|
|
11725
|
+
}, 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"] }]
|
|
11461
11726
|
}], ctorParameters: () => [] });
|
|
11462
11727
|
|
|
11463
11728
|
class TnTreeNodeOutletDirective {
|
|
@@ -11542,6 +11807,679 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
11542
11807
|
}, 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"] }]
|
|
11543
11808
|
}], ctorParameters: () => [] });
|
|
11544
11809
|
|
|
11810
|
+
/**
|
|
11811
|
+
* Renders a single virtual tree row.
|
|
11812
|
+
*
|
|
11813
|
+
* `cdk-virtual-scroll-viewport` materialises this outlet for each row that scrolls
|
|
11814
|
+
* into view; the outlet instantiates the node's template with its context and
|
|
11815
|
+
* (re)binds `CdkTreeNode.mostRecentTreeNode.data`, so the row behaves exactly as a
|
|
11816
|
+
* node rendered directly by the CDK tree. When the same DOM view is recycled for a
|
|
11817
|
+
* different node (virtual scrolling reuses views), only the context is patched.
|
|
11818
|
+
*/
|
|
11819
|
+
class TnTreeVirtualScrollNodeOutletDirective {
|
|
11820
|
+
_viewContainerRef = inject(ViewContainerRef);
|
|
11821
|
+
_viewRef = null;
|
|
11822
|
+
data = input.required(...(ngDevMode ? [{ debugName: "data" }] : []));
|
|
11823
|
+
/**
|
|
11824
|
+
* Re-assert the row's aria position every check. The viewport's recycling view
|
|
11825
|
+
* repeater can swap the underlying DOM view for a different row without our
|
|
11826
|
+
* `ngOnChanges` writing the new `aria-setsize`/`aria-posinset` in a way that
|
|
11827
|
+
* survives the swap, so we reconcile the current element against the current data
|
|
11828
|
+
* on each change-detection pass (two cheap attribute writes) — self-healing.
|
|
11829
|
+
*/
|
|
11830
|
+
ngDoCheck() {
|
|
11831
|
+
this.applyAriaPosition();
|
|
11832
|
+
this.applyKeyboardAffordance();
|
|
11833
|
+
}
|
|
11834
|
+
ngOnChanges(changes) {
|
|
11835
|
+
const dataChange = changes['data'];
|
|
11836
|
+
const recreateView = dataChange ? this.shouldRecreateView(dataChange) : false;
|
|
11837
|
+
if (recreateView) {
|
|
11838
|
+
const viewContainerRef = this._viewContainerRef;
|
|
11839
|
+
if (this._viewRef) {
|
|
11840
|
+
viewContainerRef.remove(viewContainerRef.indexOf(this._viewRef));
|
|
11841
|
+
}
|
|
11842
|
+
this._viewRef = this.data()
|
|
11843
|
+
? viewContainerRef.createEmbeddedView(this.data().nodeDef.template, this.data().context)
|
|
11844
|
+
: null;
|
|
11845
|
+
// Bind the freshly-created CdkTreeNode to this row's data. `mostRecentTreeNode`
|
|
11846
|
+
// is a global CDK static (verified against @angular/cdk 21.x) set synchronously
|
|
11847
|
+
// while `createEmbeddedView` above instantiates the node template. This mirrors
|
|
11848
|
+
// CDK's own internal wiring and assumes the node def's template contains exactly
|
|
11849
|
+
// one CdkTreeNode; it is fragile under view recycling, hence the shape check that
|
|
11850
|
+
// gates recreation. A CDK upgrade that changes this static would stop rows from
|
|
11851
|
+
// binding their data — covered by the outlet's rendering tests.
|
|
11852
|
+
if (CdkTreeNode.mostRecentTreeNode && this._viewRef) {
|
|
11853
|
+
CdkTreeNode.mostRecentTreeNode.data = this.data().data;
|
|
11854
|
+
}
|
|
11855
|
+
}
|
|
11856
|
+
else if (this._viewRef && this.data().context) {
|
|
11857
|
+
this.updateExistingContext(this.data().context);
|
|
11858
|
+
}
|
|
11859
|
+
}
|
|
11860
|
+
/**
|
|
11861
|
+
* Whether the row must be re-instantiated (vs. patched in place). Every wrapper
|
|
11862
|
+
* carries the same fixed key set (`data`/`context`/`nodeDef`/`posInSet`/`setSize`),
|
|
11863
|
+
* so recreation comes down to whether the raw data node is a different reference —
|
|
11864
|
+
* that is what marks a genuinely different row (a missing wrapper also recreates).
|
|
11865
|
+
*/
|
|
11866
|
+
shouldRecreateView(dataChange) {
|
|
11867
|
+
const prevValue = dataChange.previousValue;
|
|
11868
|
+
const currValue = dataChange.currentValue;
|
|
11869
|
+
return prevValue?.data !== currValue?.data;
|
|
11870
|
+
}
|
|
11871
|
+
/**
|
|
11872
|
+
* Reflect posinset/setsize onto the row element so screen readers can announce
|
|
11873
|
+
* "item N of M". Runs on every `ngDoCheck` (the hot CD path), so only write when the
|
|
11874
|
+
* attribute actually differs — skipping the redundant `setAttribute` on the common
|
|
11875
|
+
* pass where nothing changed.
|
|
11876
|
+
*/
|
|
11877
|
+
applyAriaPosition() {
|
|
11878
|
+
const root = this._viewRef?.rootNodes?.[0];
|
|
11879
|
+
const data = this.data();
|
|
11880
|
+
if (!root?.setAttribute || !data) {
|
|
11881
|
+
return;
|
|
11882
|
+
}
|
|
11883
|
+
const setSize = String(data.setSize);
|
|
11884
|
+
const posInSet = String(data.posInSet);
|
|
11885
|
+
if (root.getAttribute('aria-setsize') !== setSize) {
|
|
11886
|
+
root.setAttribute('aria-setsize', setSize);
|
|
11887
|
+
}
|
|
11888
|
+
if (root.getAttribute('aria-posinset') !== posInSet) {
|
|
11889
|
+
root.setAttribute('aria-posinset', posInSet);
|
|
11890
|
+
}
|
|
11891
|
+
}
|
|
11892
|
+
/**
|
|
11893
|
+
* Make expandable rows keyboard-focusable so a user can Tab to a pool/branch and
|
|
11894
|
+
* expand it with Enter/Space (the host `TnTreeVirtualScrollViewComponent` handles the
|
|
11895
|
+
* key). The virtual viewport recycles a row's view between expandable and leaf nodes,
|
|
11896
|
+
* so this reconciles every check; leaf rows are left to the consumer's own interactive
|
|
11897
|
+
* content (e.g. a `routerLink` anchor). Expandability is read from `aria-expanded`,
|
|
11898
|
+
* which `tn-tree-node` sets only on expandable nodes. Only touch the DOM on change —
|
|
11899
|
+
* this runs on the hot CD path.
|
|
11900
|
+
*/
|
|
11901
|
+
applyKeyboardAffordance() {
|
|
11902
|
+
const root = this._viewRef?.rootNodes?.[0];
|
|
11903
|
+
if (!root?.setAttribute) {
|
|
11904
|
+
return;
|
|
11905
|
+
}
|
|
11906
|
+
const expandable = root.hasAttribute('aria-expanded');
|
|
11907
|
+
if (expandable) {
|
|
11908
|
+
if (root.getAttribute('tabindex') !== '0') {
|
|
11909
|
+
root.setAttribute('tabindex', '0');
|
|
11910
|
+
}
|
|
11911
|
+
}
|
|
11912
|
+
else if (root.getAttribute('tabindex') === '0') {
|
|
11913
|
+
root.removeAttribute('tabindex');
|
|
11914
|
+
}
|
|
11915
|
+
}
|
|
11916
|
+
updateExistingContext(ctx) {
|
|
11917
|
+
if (!this._viewRef) {
|
|
11918
|
+
return;
|
|
11919
|
+
}
|
|
11920
|
+
for (const propName of Object.keys(ctx)) {
|
|
11921
|
+
this._viewRef.context[propName]
|
|
11922
|
+
= this.data().context[propName];
|
|
11923
|
+
}
|
|
11924
|
+
}
|
|
11925
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeVirtualScrollNodeOutletDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
11926
|
+
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 });
|
|
11927
|
+
}
|
|
11928
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeVirtualScrollNodeOutletDirective, decorators: [{
|
|
11929
|
+
type: Directive,
|
|
11930
|
+
args: [{
|
|
11931
|
+
selector: '[tnTreeVirtualScrollNodeOutlet]',
|
|
11932
|
+
standalone: true,
|
|
11933
|
+
}]
|
|
11934
|
+
}], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }] } });
|
|
11935
|
+
|
|
11936
|
+
/** Default fixed row height (px) used by the virtual scroll viewport. */
|
|
11937
|
+
const defaultTreeItemSize = 48;
|
|
11938
|
+
/**
|
|
11939
|
+
* Row counts for the default buffer sizes (how many extra rows of content the viewport
|
|
11940
|
+
* renders past the visible area) and for the distance the user must scroll before the
|
|
11941
|
+
* scroll-to-top button appears. Expressed in rows and multiplied by `itemSize()` at
|
|
11942
|
+
* runtime (see resolvedMin/MaxBufferPx) so the defaults scale with a custom row height.
|
|
11943
|
+
*/
|
|
11944
|
+
const defaultMinBufferRows = 4;
|
|
11945
|
+
const defaultBufferRows = 8;
|
|
11946
|
+
const scrollToTopThresholdRows = 8;
|
|
11947
|
+
const scrollFrameScheduler = typeof requestAnimationFrame !== 'undefined' ? animationFrameScheduler : asapScheduler;
|
|
11948
|
+
/**
|
|
11949
|
+
* A `tn-tree` that renders through a `cdk-virtual-scroll-viewport`, so only the
|
|
11950
|
+
* rows currently in view are materialised in the DOM. Drop-in for large trees
|
|
11951
|
+
* (thousands of nodes) where the plain `tn-tree` would render every node.
|
|
11952
|
+
*
|
|
11953
|
+
* Usage mirrors `tn-tree`: provide a `TnTreeFlatDataSource` + `FlatTreeControl`
|
|
11954
|
+
* and one `*cdkTreeNodeDef` template. Rows are a fixed height (`itemSize`).
|
|
11955
|
+
*
|
|
11956
|
+
* ```html
|
|
11957
|
+
* <tn-tree-virtual-scroll-view [dataSource]="dataSource" [treeControl]="treeControl" [itemSize]="52">
|
|
11958
|
+
* <tn-tree-node *cdkTreeNodeDef="let node" cdkTreeNodePadding [routerLink]="...">
|
|
11959
|
+
* {{ node.name }}
|
|
11960
|
+
* </tn-tree-node>
|
|
11961
|
+
* </tn-tree-virtual-scroll-view>
|
|
11962
|
+
* ```
|
|
11963
|
+
*
|
|
11964
|
+
* Accessibility: rows are exposed with `role="treeitem"` plus `aria-level`,
|
|
11965
|
+
* `aria-posinset` and `aria-setsize` (the latter two relative to the node's
|
|
11966
|
+
* siblings — nodes sharing the same parent/level — per the WAI-ARIA tree
|
|
11967
|
+
* pattern, not the flattened list of all visible rows), and the virtual-scroll
|
|
11968
|
+
* wrappers are marked `role="presentation"` so assistive tech still sees the
|
|
11969
|
+
* items as children of the `role="tree"` host.
|
|
11970
|
+
*
|
|
11971
|
+
* Keyboard: expandable rows are focusable and expand/collapse with Enter/Space (see
|
|
11972
|
+
* {@link onTreeKeydown}), so a keyboard/AT user can operate the `aria-expanded` that
|
|
11973
|
+
* each expandable `treeitem` advertises. Roving arrow-key / Home / End navigation
|
|
11974
|
+
* BETWEEN nodes is NOT supported here, though — because rows are materialised (and
|
|
11975
|
+
* recycled) by the virtual scroll viewport rather than registered with CdkTree's node
|
|
11976
|
+
* outlet, the CDK `TreeKeyManager` has no stable node set to drive. Use Tab to move
|
|
11977
|
+
* between rows, or the non-virtual `tn-tree` when full roving navigation is required.
|
|
11978
|
+
*/
|
|
11979
|
+
class TnTreeVirtualScrollViewComponent extends CdkTree {
|
|
11980
|
+
destroyRef = inject(DestroyRef);
|
|
11981
|
+
cdr = inject(ChangeDetectorRef);
|
|
11982
|
+
virtualScrollViewport = viewChild.required(CdkVirtualScrollViewport);
|
|
11983
|
+
/** Fixed row height in px. Must match the actual rendered node height. */
|
|
11984
|
+
itemSize = input(defaultTreeItemSize, ...(ngDevMode ? [{ debugName: "itemSize" }] : []));
|
|
11985
|
+
/**
|
|
11986
|
+
* Viewport buffer sizes (px). Leave at 0 (the default) to derive them from `itemSize()`
|
|
11987
|
+
* — {@link defaultMinBufferRows} / {@link defaultBufferRows} rows — so the buffers scale
|
|
11988
|
+
* with a custom row height; pass an explicit px value to override. (A fixed px default
|
|
11989
|
+
* would under-buffer a tall `itemSize`, even failing to buffer a single row when
|
|
11990
|
+
* `itemSize > maxBufferPx`.)
|
|
11991
|
+
*/
|
|
11992
|
+
minBufferPx = input(0, ...(ngDevMode ? [{ debugName: "minBufferPx" }] : []));
|
|
11993
|
+
maxBufferPx = input(0, ...(ngDevMode ? [{ debugName: "maxBufferPx" }] : []));
|
|
11994
|
+
/** `minBufferPx` resolved to px, falling back to a row-count default that tracks `itemSize()`. */
|
|
11995
|
+
resolvedMinBufferPx = computed(() => this.minBufferPx() || this.itemSize() * defaultMinBufferRows, ...(ngDevMode ? [{ debugName: "resolvedMinBufferPx" }] : []));
|
|
11996
|
+
/** `maxBufferPx` resolved to px, falling back to a row-count default that tracks `itemSize()`. */
|
|
11997
|
+
resolvedMaxBufferPx = computed(() => this.maxBufferPx() || this.itemSize() * defaultBufferRows, ...(ngDevMode ? [{ debugName: "resolvedMaxBufferPx" }] : []));
|
|
11998
|
+
/**
|
|
11999
|
+
* When true the viewport scrolls with the window instead of an internal scroll area.
|
|
12000
|
+
* `booleanAttribute` so the bare presence form (`<... scrollWindow>`) coerces to `true`
|
|
12001
|
+
* rather than the empty-string (falsy) an un-transformed input would receive.
|
|
12002
|
+
*
|
|
12003
|
+
* Set once at initialisation — NOT reactive. The scroll listener, presentational roles
|
|
12004
|
+
* and ResizeObserver are wired in `ngAfterViewInit` against the viewport that this input
|
|
12005
|
+
* selected; toggling it later re-renders the viewport but does not re-run that wiring.
|
|
12006
|
+
*/
|
|
12007
|
+
scrollWindow = input(false, { ...(ngDevMode ? { debugName: "scrollWindow" } : {}), transform: booleanAttribute });
|
|
12008
|
+
/** Whether to show the floating "scroll to top" button once scrolled down. */
|
|
12009
|
+
showScrollToTop = input(true, { ...(ngDevMode ? { debugName: "showScrollToTop" } : {}), transform: booleanAttribute });
|
|
12010
|
+
/** Accessible label / tooltip for the scroll-to-top button (i18n is the consumer's job). */
|
|
12011
|
+
scrollToTopLabel = input('Scroll to top', ...(ngDevMode ? [{ debugName: "scrollToTopLabel" }] : []));
|
|
12012
|
+
/**
|
|
12013
|
+
* Per-row trackBy over the ORIGINAL data node (not the internal wrapper). Strongly
|
|
12014
|
+
* recommended: without it rows track by index, so the recycling viewport reuses a
|
|
12015
|
+
* detached view for whatever node lands at that index on data changes. Pass a stable
|
|
12016
|
+
* key (e.g. `(_, node) => node.id`) as the stories do.
|
|
12017
|
+
*/
|
|
12018
|
+
nodeTrackBy = input(...(ngDevMode ? [undefined, { debugName: "nodeTrackBy" }] : []));
|
|
12019
|
+
/** Emits the viewport's horizontal `scrollLeft` (used to sync a sticky column header). */
|
|
12020
|
+
viewportScrolled = output();
|
|
12021
|
+
/**
|
|
12022
|
+
* Emits the observed content size whenever it changes (used to size a sticky header).
|
|
12023
|
+
* `width` is the full rendered content width — including horizontal overflow past the
|
|
12024
|
+
* viewport — which is what a horizontally-synced header needs. `height` is the observed
|
|
12025
|
+
* content wrapper's height: the visible viewport height in internal-scroll mode, but the
|
|
12026
|
+
* FULL content height (row count × `itemSize`) in `scrollWindow` mode, where the wrapper
|
|
12027
|
+
* grows with the document. Consumers wanting the visible viewport height should not rely
|
|
12028
|
+
* on this field in window mode.
|
|
12029
|
+
*/
|
|
12030
|
+
viewportResized = output();
|
|
12031
|
+
/** Last `scrollLeft` emitted via {@link viewportScrolled}; used to skip redundant emits. */
|
|
12032
|
+
lastEmittedScrollLeft = 0;
|
|
12033
|
+
nodes$ = new BehaviorSubject([]);
|
|
12034
|
+
innerTrackBy = computed(() => {
|
|
12035
|
+
const fn = this.nodeTrackBy();
|
|
12036
|
+
return fn
|
|
12037
|
+
? (index, node) => fn(index, node.data)
|
|
12038
|
+
: (index) => index;
|
|
12039
|
+
}, ...(ngDevMode ? [{ debugName: "innerTrackBy" }] : []));
|
|
12040
|
+
renderNodeChanges$ = new BehaviorSubject([]);
|
|
12041
|
+
resizeObserver = null;
|
|
12042
|
+
scrollViewportElement = null;
|
|
12043
|
+
scrollFrameSubscription = null;
|
|
12044
|
+
constructor() {
|
|
12045
|
+
super(inject(IterableDiffers), inject(ChangeDetectorRef), inject(ViewContainerRef));
|
|
12046
|
+
this.listenForNodeChanges();
|
|
12047
|
+
}
|
|
12048
|
+
ngAfterViewInit() {
|
|
12049
|
+
super.ngAfterViewInit?.();
|
|
12050
|
+
const element = this.virtualScrollViewport().elementRef.nativeElement;
|
|
12051
|
+
const scrollable = this.virtualScrollViewport().scrollable;
|
|
12052
|
+
// `getElementRef()` resolves to the viewport in the self-scrolling case and to the
|
|
12053
|
+
// external scroll element otherwise — in scrollWindow mode that is
|
|
12054
|
+
// `document.documentElement`, which is what we read `scrollLeft` from below.
|
|
12055
|
+
this.scrollViewportElement = scrollable.getElementRef().nativeElement;
|
|
12056
|
+
// React to scrolls via the scrollable's own stream rather than addEventListener on
|
|
12057
|
+
// that element ref. In scrollWindow mode the scroll EVENTS fire on `document` while
|
|
12058
|
+
// the element ref is `document.documentElement`, so a raw listener on the element
|
|
12059
|
+
// would never fire and the scroll-to-top button / `viewportScrolled` could not react.
|
|
12060
|
+
// `elementScrolled()` is wired to the correct target for both internal and window modes.
|
|
12061
|
+
scrollable.elementScrolled()
|
|
12062
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
12063
|
+
.subscribe(this.onViewportScroll);
|
|
12064
|
+
// Observe the rendered CONTENT wrapper rather than the viewport itself, so
|
|
12065
|
+
// `viewportResized` reports the full (possibly horizontally-overflowing) content
|
|
12066
|
+
// width — that is what a sticky column header needs to stay aligned with the rows.
|
|
12067
|
+
// Falls back to the viewport element if the wrapper is not present.
|
|
12068
|
+
const contentWrapper = element.querySelector('.cdk-virtual-scroll-content-wrapper');
|
|
12069
|
+
const observed = contentWrapper ?? element;
|
|
12070
|
+
// The CDK viewport + content wrapper sit between the host `role="tree"` and the
|
|
12071
|
+
// `role="treeitem"` rows. Mark them presentational so assistive tech collapses
|
|
12072
|
+
// them and still sees the rows as direct tree items (ARIA requires treeitems to
|
|
12073
|
+
// be children of the tree, optionally through a presentational container).
|
|
12074
|
+
element.setAttribute('role', 'presentation');
|
|
12075
|
+
contentWrapper?.setAttribute('role', 'presentation');
|
|
12076
|
+
// Guarded for non-browser environments (SSR, jsdom-based unit tests) where
|
|
12077
|
+
// ResizeObserver is absent; the resize output simply does not emit there.
|
|
12078
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
12079
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
12080
|
+
const rect = entries[0]?.contentRect;
|
|
12081
|
+
if (rect) {
|
|
12082
|
+
this.viewportResized.emit({ width: rect.width, height: rect.height });
|
|
12083
|
+
}
|
|
12084
|
+
});
|
|
12085
|
+
this.resizeObserver.observe(observed);
|
|
12086
|
+
}
|
|
12087
|
+
}
|
|
12088
|
+
ngOnDestroy() {
|
|
12089
|
+
super.ngOnDestroy();
|
|
12090
|
+
// The scroll subscription is torn down by takeUntilDestroyed; just drop refs here.
|
|
12091
|
+
this.scrollViewportElement = null;
|
|
12092
|
+
this.scrollFrameSubscription?.unsubscribe();
|
|
12093
|
+
this.scrollFrameSubscription = null;
|
|
12094
|
+
this.resizeObserver?.disconnect();
|
|
12095
|
+
this.resizeObserver = null;
|
|
12096
|
+
}
|
|
12097
|
+
/**
|
|
12098
|
+
* Cached visibility of the scroll-to-top button. Recomputed only on scroll (see
|
|
12099
|
+
* {@link onViewportScroll}) rather than read as a getter every change-detection
|
|
12100
|
+
* pass, so `measureScrollOffset` — a layout-forcing read — is not called each cycle.
|
|
12101
|
+
*/
|
|
12102
|
+
isScrollTopButtonVisible = false;
|
|
12103
|
+
scrollToTop() {
|
|
12104
|
+
// Use scrollToOffset (which delegates to the viewport's scrollable) rather than
|
|
12105
|
+
// scrollTo (which scrolls the viewport element directly) so this also scrolls an
|
|
12106
|
+
// external scroll container back to the top.
|
|
12107
|
+
this.virtualScrollViewport().scrollToOffset(0, 'smooth');
|
|
12108
|
+
this.cdr.markForCheck();
|
|
12109
|
+
}
|
|
12110
|
+
/**
|
|
12111
|
+
* Enter/Space on a focused expandable row toggles its expansion. The virtual viewport
|
|
12112
|
+
* recycles rows so CDK's `TreeKeyManager` can't drive them (see class docs); this keeps
|
|
12113
|
+
* the expand/collapse that each `aria-expanded` row advertises keyboard-operable, via
|
|
12114
|
+
* event delegation on the `role="tree"` host that reuses the row's `cdkTreeNodeToggle`
|
|
12115
|
+
* click handler. Ignored when focus is on an inner control (e.g. a `routerLink` anchor)
|
|
12116
|
+
* so that element keeps its own Enter/Space behaviour.
|
|
12117
|
+
*/
|
|
12118
|
+
onTreeKeydown(event) {
|
|
12119
|
+
if (event.key !== 'Enter' && event.key !== ' ' && event.key !== 'Spacebar') {
|
|
12120
|
+
return;
|
|
12121
|
+
}
|
|
12122
|
+
const target = event.target;
|
|
12123
|
+
const row = target?.closest?.('.tn-tree-node-wrapper[role="treeitem"]');
|
|
12124
|
+
if (!row || row !== target || !row.hasAttribute('aria-expanded')) {
|
|
12125
|
+
return;
|
|
12126
|
+
}
|
|
12127
|
+
const toggle = row.querySelector('[cdkTreeNodeToggle]');
|
|
12128
|
+
if (toggle) {
|
|
12129
|
+
// Prevent Space from scrolling the page; reuse the toggle's existing click handler.
|
|
12130
|
+
event.preventDefault();
|
|
12131
|
+
toggle.click();
|
|
12132
|
+
}
|
|
12133
|
+
}
|
|
12134
|
+
updateScrollTopButtonVisibility() {
|
|
12135
|
+
const visible = this.showScrollToTop()
|
|
12136
|
+
&& this.virtualScrollViewport().measureScrollOffset('top') > this.itemSize() * scrollToTopThresholdRows;
|
|
12137
|
+
if (visible !== this.isScrollTopButtonVisible) {
|
|
12138
|
+
this.isScrollTopButtonVisible = visible;
|
|
12139
|
+
this.cdr.markForCheck();
|
|
12140
|
+
}
|
|
12141
|
+
}
|
|
12142
|
+
// CdkTree calls this with the full set of currently-visible nodes. Instead of
|
|
12143
|
+
// rendering them into the outlet, feed them to the virtual scroll pipeline.
|
|
12144
|
+
renderNodeChanges(data) {
|
|
12145
|
+
this.renderNodeChanges$.next(data);
|
|
12146
|
+
}
|
|
12147
|
+
getNodeLevel(nodeData) {
|
|
12148
|
+
return this.treeControl?.getLevel ? this.treeControl.getLevel(nodeData) : 0;
|
|
12149
|
+
}
|
|
12150
|
+
/**
|
|
12151
|
+
* Compute per-row `aria-posinset` / `aria-setsize` scoped to each node's SIBLINGS
|
|
12152
|
+
* (same parent / same level), as the WAI-ARIA tree pattern requires — not the flat
|
|
12153
|
+
* list of all visible rows. The flat node set is depth-first ordered and each node
|
|
12154
|
+
* carries a level, so siblings at level L are the consecutive level-L nodes bounded
|
|
12155
|
+
* by any shallower node (level < L = a parent boundary); deeper nodes in between are
|
|
12156
|
+
* descendants of an earlier sibling and don't split the group. One pass, tracking the
|
|
12157
|
+
* open sibling group at each level.
|
|
12158
|
+
*/
|
|
12159
|
+
computeAriaPositions(levels) {
|
|
12160
|
+
const posInSet = new Array(levels.length);
|
|
12161
|
+
const setSize = new Array(levels.length);
|
|
12162
|
+
// groups[level] = indices of the currently-open sibling group at that depth.
|
|
12163
|
+
const groups = [];
|
|
12164
|
+
// Close (and stamp setSize on) every group deeper than `level`, because reaching a
|
|
12165
|
+
// node at `level` ends any sibling groups nested under the previous sibling.
|
|
12166
|
+
const finalizeDeeperThan = (level) => {
|
|
12167
|
+
for (let l = groups.length - 1; l > level; l--) {
|
|
12168
|
+
const group = groups[l];
|
|
12169
|
+
if (group?.length) {
|
|
12170
|
+
for (const idx of group) {
|
|
12171
|
+
setSize[idx] = group.length;
|
|
12172
|
+
}
|
|
12173
|
+
}
|
|
12174
|
+
}
|
|
12175
|
+
groups.length = level + 1;
|
|
12176
|
+
};
|
|
12177
|
+
for (let i = 0; i < levels.length; i++) {
|
|
12178
|
+
const level = levels[i];
|
|
12179
|
+
finalizeDeeperThan(level);
|
|
12180
|
+
(groups[level] ??= []).push(i);
|
|
12181
|
+
posInSet[i] = groups[level].length;
|
|
12182
|
+
}
|
|
12183
|
+
// Close the groups still open at the end of the list.
|
|
12184
|
+
finalizeDeeperThan(-1);
|
|
12185
|
+
return levels.map((_, i) => ({ posInSet: posInSet[i], setSize: setSize[i] }));
|
|
12186
|
+
}
|
|
12187
|
+
createNode(nodeData, index, level, posInSet, setSize) {
|
|
12188
|
+
// `_getNodeDef` is a private CdkTree API (verified against @angular/cdk 21.x). Assert
|
|
12189
|
+
// it resolves so a CDK upgrade that renames/removes it fails loudly here rather than
|
|
12190
|
+
// silently rendering a blank tree at runtime.
|
|
12191
|
+
const nodeDef = this._getNodeDef(nodeData, index);
|
|
12192
|
+
if (!nodeDef) {
|
|
12193
|
+
throw new Error('TnTreeVirtualScrollView: CdkTree._getNodeDef returned no node definition. '
|
|
12194
|
+
+ 'This private @angular/cdk API (expected ^21.1.0) may have changed — verify tree compatibility.');
|
|
12195
|
+
}
|
|
12196
|
+
const context = new CdkTreeNodeOutletContext(nodeData);
|
|
12197
|
+
context.level = level;
|
|
12198
|
+
// posInSet/setSize let a screen reader announce "item N of M" even though only a
|
|
12199
|
+
// slice of rows exists in the DOM at any time. They are scoped to this node's
|
|
12200
|
+
// siblings (see computeAriaPositions), per the WAI-ARIA tree pattern.
|
|
12201
|
+
return {
|
|
12202
|
+
data: nodeData, context, nodeDef, posInSet, setSize,
|
|
12203
|
+
};
|
|
12204
|
+
}
|
|
12205
|
+
buildVirtualNodes(data) {
|
|
12206
|
+
const levels = data.map((node) => this.getNodeLevel(node));
|
|
12207
|
+
const positions = this.computeAriaPositions(levels);
|
|
12208
|
+
return data.map((node, index) => this.createNode(node, index, levels[index], positions[index].posInSet, positions[index].setSize));
|
|
12209
|
+
}
|
|
12210
|
+
listenForNodeChanges() {
|
|
12211
|
+
this.renderNodeChanges$.pipe(auditTime(0, scrollFrameScheduler), map((data) => this.buildVirtualNodes(data)), takeUntilDestroyed(this.destroyRef)).subscribe((nodes) => {
|
|
12212
|
+
this.nodes$.next(nodes);
|
|
12213
|
+
this.cdr.markForCheck();
|
|
12214
|
+
// The viewport's recycling repeater may swap a row's DOM view during the render
|
|
12215
|
+
// triggered above, leaving the outlet's aria-posinset/aria-setsize written to a
|
|
12216
|
+
// now-detached element. Schedule one more check so the outlet's ngDoCheck
|
|
12217
|
+
// reconciles the aria attributes onto the settled elements.
|
|
12218
|
+
queueMicrotask(() => this.cdr.markForCheck());
|
|
12219
|
+
// The visible node set just changed (e.g. a large branch was collapsed), which can
|
|
12220
|
+
// shrink the scrollable content and clamp the scroll offset without firing a scroll
|
|
12221
|
+
// event. Re-evaluate the scroll-to-top button after layout settles so it can't linger
|
|
12222
|
+
// over content that is no longer scrollable. Guarded by the live element ref so a
|
|
12223
|
+
// frame scheduled just before destroy doesn't measure a torn-down viewport.
|
|
12224
|
+
scrollFrameScheduler.schedule(() => {
|
|
12225
|
+
if (this.scrollViewportElement) {
|
|
12226
|
+
this.updateScrollTopButtonVisibility();
|
|
12227
|
+
}
|
|
12228
|
+
});
|
|
12229
|
+
});
|
|
12230
|
+
}
|
|
12231
|
+
onViewportScroll = () => {
|
|
12232
|
+
// Scroll events can fire several times per frame. Coalesce the work — a
|
|
12233
|
+
// horizontal-offset emit and `measureScrollOffset` (a layout-forcing read) — into a
|
|
12234
|
+
// single animation frame so fast scrolls don't trigger repeated forced reflows.
|
|
12235
|
+
if (this.scrollFrameSubscription) {
|
|
12236
|
+
return;
|
|
12237
|
+
}
|
|
12238
|
+
this.scrollFrameSubscription = scrollFrameScheduler.schedule(() => {
|
|
12239
|
+
this.scrollFrameSubscription = null;
|
|
12240
|
+
// Read scrollLeft from the actual scroll source, not the viewport element: in
|
|
12241
|
+
// scrollWindow mode the viewport itself does not scroll (the window/external
|
|
12242
|
+
// element does), so the viewport's own scrollLeft would always be 0. Only emit when
|
|
12243
|
+
// it actually changed — this fires on vertical scrolls too, where scrollLeft is
|
|
12244
|
+
// unchanged, and a consumer syncing a sticky header would otherwise re-apply the
|
|
12245
|
+
// same translateX on every vertical scroll frame of a large tree.
|
|
12246
|
+
const left = this.scrollViewportElement?.scrollLeft ?? 0;
|
|
12247
|
+
if (left !== this.lastEmittedScrollLeft) {
|
|
12248
|
+
this.lastEmittedScrollLeft = left;
|
|
12249
|
+
this.viewportScrolled.emit(left);
|
|
12250
|
+
}
|
|
12251
|
+
// Re-evaluate the scroll-to-top button on every scroll (the scroll source may be an
|
|
12252
|
+
// external, OnPush-detached container, so nothing else marks this view dirty).
|
|
12253
|
+
this.updateScrollTopButtonVisibility();
|
|
12254
|
+
});
|
|
12255
|
+
};
|
|
12256
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeVirtualScrollViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
12257
|
+
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: [
|
|
12258
|
+
{ provide: CdkTree, useExisting: TnTreeVirtualScrollViewComponent },
|
|
12259
|
+
], 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 });
|
|
12260
|
+
}
|
|
12261
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeVirtualScrollViewComponent, decorators: [{
|
|
12262
|
+
type: Component,
|
|
12263
|
+
args: [{ selector: 'tn-tree-virtual-scroll-view', standalone: true, exportAs: 'tnTreeVirtualScrollView', providers: [
|
|
12264
|
+
{ provide: CdkTree, useExisting: TnTreeVirtualScrollViewComponent },
|
|
12265
|
+
], imports: [
|
|
12266
|
+
CdkTreeModule,
|
|
12267
|
+
CdkVirtualScrollViewport,
|
|
12268
|
+
CdkVirtualScrollableWindow,
|
|
12269
|
+
CdkFixedSizeVirtualScroll,
|
|
12270
|
+
CdkVirtualForOf,
|
|
12271
|
+
TnTreeVirtualScrollNodeOutletDirective,
|
|
12272
|
+
TnIconButtonComponent,
|
|
12273
|
+
AsyncPipe,
|
|
12274
|
+
], hostDirectives: [{ directive: TnTestIdDirective, inputs: ['tnTestId: testId'] }], host: {
|
|
12275
|
+
'class': 'tn-tree tn-tree-virtual-scroll-view',
|
|
12276
|
+
'role': 'tree',
|
|
12277
|
+
// Expose itemSize to CSS so each row can be pinned to exactly this height
|
|
12278
|
+
// (see the .scss) — virtual scrolling needs the painted row height to match
|
|
12279
|
+
// the height the viewport assumes per item, otherwise hover/selection
|
|
12280
|
+
// backgrounds bleed across neighbouring rows.
|
|
12281
|
+
'[style.--tn-tree-item-size.px]': 'itemSize()',
|
|
12282
|
+
'(keydown)': 'onTreeKeydown($event)',
|
|
12283
|
+
}, 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"] }]
|
|
12284
|
+
}], 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"] }] } });
|
|
12285
|
+
|
|
12286
|
+
/**
|
|
12287
|
+
* Harness for interacting with `tn-tree-virtual-scroll-view` in tests.
|
|
12288
|
+
*
|
|
12289
|
+
* Because the component virtualises its rows, every query below operates over the
|
|
12290
|
+
* currently-MATERIALISED (visible + buffered) rows only — nodes scrolled far out of view
|
|
12291
|
+
* are not in the DOM. Indexes are positions within that visible set, not the full data.
|
|
12292
|
+
*
|
|
12293
|
+
* @example
|
|
12294
|
+
* ```typescript
|
|
12295
|
+
* const tree = await loader.getHarness(TnTreeVirtualScrollViewHarness);
|
|
12296
|
+
* expect(await tree.getRowTexts()).toEqual(['pool', 'other']);
|
|
12297
|
+
*
|
|
12298
|
+
* await tree.expand(0);
|
|
12299
|
+
* expect(await tree.isExpanded(0)).toBe(true);
|
|
12300
|
+
* expect(await tree.getAriaLevel(1)).toBe(2);
|
|
12301
|
+
* ```
|
|
12302
|
+
*/
|
|
12303
|
+
class TnTreeVirtualScrollViewHarness extends ComponentHarness {
|
|
12304
|
+
static hostSelector = 'tn-tree-virtual-scroll-view';
|
|
12305
|
+
/**
|
|
12306
|
+
* Gets a `HarnessPredicate` that can be used to search for a virtual-scroll tree
|
|
12307
|
+
* with specific attributes.
|
|
12308
|
+
*
|
|
12309
|
+
* @param options Options for filtering which instances are considered a match.
|
|
12310
|
+
* @returns A `HarnessPredicate` configured with the given options.
|
|
12311
|
+
*/
|
|
12312
|
+
static with(options = {}) {
|
|
12313
|
+
return new HarnessPredicate(TnTreeVirtualScrollViewHarness, options);
|
|
12314
|
+
}
|
|
12315
|
+
rows = this.locatorForAll('.tn-tree-node-wrapper[role="treeitem"]');
|
|
12316
|
+
scrollTopButton = this.locatorForOptional('.tn-tree-virtual-scroll-view__scroll-top');
|
|
12317
|
+
// --- Row queries ---
|
|
12318
|
+
/**
|
|
12319
|
+
* Gets the number of currently-materialised tree rows.
|
|
12320
|
+
*
|
|
12321
|
+
* @returns Promise resolving to the visible row count.
|
|
12322
|
+
*/
|
|
12323
|
+
async getRowCount() {
|
|
12324
|
+
return (await this.rows()).length;
|
|
12325
|
+
}
|
|
12326
|
+
/**
|
|
12327
|
+
* Gets the text label of every materialised row (the node's text cell only, excluding
|
|
12328
|
+
* the expand chevron glyph).
|
|
12329
|
+
*
|
|
12330
|
+
* @returns Promise resolving to an array of row label strings.
|
|
12331
|
+
*/
|
|
12332
|
+
async getRowTexts() {
|
|
12333
|
+
const cells = await this.locatorForAll('.tn-tree-node-wrapper[role="treeitem"] .tn-tree-node__text')();
|
|
12334
|
+
const texts = [];
|
|
12335
|
+
for (const cell of cells) {
|
|
12336
|
+
texts.push((await cell.text()).trim());
|
|
12337
|
+
}
|
|
12338
|
+
return texts;
|
|
12339
|
+
}
|
|
12340
|
+
/**
|
|
12341
|
+
* Gets the text label of a single materialised row.
|
|
12342
|
+
*
|
|
12343
|
+
* @param index Zero-based index within the visible rows.
|
|
12344
|
+
* @returns Promise resolving to the row's label.
|
|
12345
|
+
*/
|
|
12346
|
+
async getRowText(index) {
|
|
12347
|
+
const cell = await this.locatorFor(`.tn-tree-node-wrapper[role="treeitem"]:nth-of-type(${index + 1}) .tn-tree-node__text`)();
|
|
12348
|
+
return (await cell.text()).trim();
|
|
12349
|
+
}
|
|
12350
|
+
/**
|
|
12351
|
+
* Gets the 1-based `aria-level` of a row (root nodes are 1).
|
|
12352
|
+
*
|
|
12353
|
+
* @param index Zero-based index within the visible rows.
|
|
12354
|
+
* @returns Promise resolving to the aria-level, or null if unset.
|
|
12355
|
+
*/
|
|
12356
|
+
async getAriaLevel(index) {
|
|
12357
|
+
return this.getRowNumberAttribute(index, 'aria-level');
|
|
12358
|
+
}
|
|
12359
|
+
/**
|
|
12360
|
+
* Gets a row's `aria-posinset` — its 1-based position within its SIBLING set.
|
|
12361
|
+
*
|
|
12362
|
+
* @param index Zero-based index within the visible rows.
|
|
12363
|
+
* @returns Promise resolving to the aria-posinset, or null if unset.
|
|
12364
|
+
*/
|
|
12365
|
+
async getAriaPosInSet(index) {
|
|
12366
|
+
return this.getRowNumberAttribute(index, 'aria-posinset');
|
|
12367
|
+
}
|
|
12368
|
+
/**
|
|
12369
|
+
* Gets a row's `aria-setsize` — the size of its SIBLING set.
|
|
12370
|
+
*
|
|
12371
|
+
* @param index Zero-based index within the visible rows.
|
|
12372
|
+
* @returns Promise resolving to the aria-setsize, or null if unset.
|
|
12373
|
+
*/
|
|
12374
|
+
async getAriaSetSize(index) {
|
|
12375
|
+
return this.getRowNumberAttribute(index, 'aria-setsize');
|
|
12376
|
+
}
|
|
12377
|
+
/**
|
|
12378
|
+
* Whether a row is expandable (advertises `aria-expanded`).
|
|
12379
|
+
*
|
|
12380
|
+
* @param index Zero-based index within the visible rows.
|
|
12381
|
+
* @returns Promise resolving to true if the row is expandable.
|
|
12382
|
+
*/
|
|
12383
|
+
async isExpandable(index) {
|
|
12384
|
+
const row = await this.getRow(index);
|
|
12385
|
+
return (await row.getAttribute('aria-expanded')) !== null;
|
|
12386
|
+
}
|
|
12387
|
+
/**
|
|
12388
|
+
* Whether an expandable row is currently expanded.
|
|
12389
|
+
*
|
|
12390
|
+
* @param index Zero-based index within the visible rows.
|
|
12391
|
+
* @returns Promise resolving to true if `aria-expanded="true"`.
|
|
12392
|
+
*/
|
|
12393
|
+
async isExpanded(index) {
|
|
12394
|
+
const row = await this.getRow(index);
|
|
12395
|
+
return (await row.getAttribute('aria-expanded')) === 'true';
|
|
12396
|
+
}
|
|
12397
|
+
/**
|
|
12398
|
+
* Whether a row is keyboard tab-focusable (`tabindex="0"`). Only expandable rows are.
|
|
12399
|
+
*
|
|
12400
|
+
* @param index Zero-based index within the visible rows.
|
|
12401
|
+
* @returns Promise resolving to true if the row is tab-focusable.
|
|
12402
|
+
*/
|
|
12403
|
+
async isFocusable(index) {
|
|
12404
|
+
const row = await this.getRow(index);
|
|
12405
|
+
return (await row.getAttribute('tabindex')) === '0';
|
|
12406
|
+
}
|
|
12407
|
+
// --- Expansion ---
|
|
12408
|
+
/**
|
|
12409
|
+
* Toggles a row's expansion by clicking its toggle. No-op semantics are the tree's
|
|
12410
|
+
* (clicking a leaf does nothing).
|
|
12411
|
+
*
|
|
12412
|
+
* @param index Zero-based index within the visible rows.
|
|
12413
|
+
*/
|
|
12414
|
+
async toggle(index) {
|
|
12415
|
+
const toggle = await this.locatorFor(`.tn-tree-node-wrapper[role="treeitem"]:nth-of-type(${index + 1}) .tn-tree-node`)();
|
|
12416
|
+
await toggle.click();
|
|
12417
|
+
}
|
|
12418
|
+
/**
|
|
12419
|
+
* Expands a row if it is expandable and currently collapsed.
|
|
12420
|
+
*
|
|
12421
|
+
* @param index Zero-based index within the visible rows.
|
|
12422
|
+
*/
|
|
12423
|
+
async expand(index) {
|
|
12424
|
+
if ((await this.isExpandable(index)) && !(await this.isExpanded(index))) {
|
|
12425
|
+
await this.toggle(index);
|
|
12426
|
+
}
|
|
12427
|
+
}
|
|
12428
|
+
/**
|
|
12429
|
+
* Collapses a row if it is currently expanded.
|
|
12430
|
+
*
|
|
12431
|
+
* @param index Zero-based index within the visible rows.
|
|
12432
|
+
*/
|
|
12433
|
+
async collapse(index) {
|
|
12434
|
+
if (await this.isExpanded(index)) {
|
|
12435
|
+
await this.toggle(index);
|
|
12436
|
+
}
|
|
12437
|
+
}
|
|
12438
|
+
/**
|
|
12439
|
+
* Focuses a row and presses a key on it (Enter/Space toggle an expandable row).
|
|
12440
|
+
*
|
|
12441
|
+
* @param index Zero-based index within the visible rows.
|
|
12442
|
+
* @param key Which key to press — Enter or Space.
|
|
12443
|
+
*/
|
|
12444
|
+
async pressKeyOnRow(index, key) {
|
|
12445
|
+
const row = await this.getRow(index);
|
|
12446
|
+
await row.focus();
|
|
12447
|
+
await row.sendKeys(key === 'enter' ? TestKey.ENTER : ' ');
|
|
12448
|
+
}
|
|
12449
|
+
// --- Scroll-to-top button ---
|
|
12450
|
+
/**
|
|
12451
|
+
* Whether the floating scroll-to-top button is currently visible.
|
|
12452
|
+
*
|
|
12453
|
+
* @returns Promise resolving to true if the button is rendered.
|
|
12454
|
+
*/
|
|
12455
|
+
async isScrollToTopVisible() {
|
|
12456
|
+
return (await this.scrollTopButton()) !== null;
|
|
12457
|
+
}
|
|
12458
|
+
/**
|
|
12459
|
+
* Clicks the scroll-to-top button. Throws if it is not currently visible.
|
|
12460
|
+
*/
|
|
12461
|
+
async clickScrollToTop() {
|
|
12462
|
+
const button = await this.scrollTopButton();
|
|
12463
|
+
if (!button) {
|
|
12464
|
+
throw new Error('Scroll-to-top button is not currently visible');
|
|
12465
|
+
}
|
|
12466
|
+
await button.click();
|
|
12467
|
+
}
|
|
12468
|
+
// --- Internal helpers ---
|
|
12469
|
+
async getRow(index) {
|
|
12470
|
+
const rows = await this.rows();
|
|
12471
|
+
if (index >= rows.length) {
|
|
12472
|
+
throw new Error(`Row index ${index} out of bounds (${rows.length} materialised rows)`);
|
|
12473
|
+
}
|
|
12474
|
+
return rows[index];
|
|
12475
|
+
}
|
|
12476
|
+
async getRowNumberAttribute(index, attr) {
|
|
12477
|
+
const row = await this.getRow(index);
|
|
12478
|
+
const value = await row.getAttribute(attr);
|
|
12479
|
+
return value === null ? null : Number(value);
|
|
12480
|
+
}
|
|
12481
|
+
}
|
|
12482
|
+
|
|
11545
12483
|
var ModifierKeys;
|
|
11546
12484
|
(function (ModifierKeys) {
|
|
11547
12485
|
ModifierKeys["COMMAND"] = "\u2318";
|
|
@@ -14028,7 +14966,7 @@ class TnTimeInputComponent {
|
|
|
14028
14966
|
useExisting: forwardRef(() => TnTimeInputComponent),
|
|
14029
14967
|
multi: true
|
|
14030
14968
|
}
|
|
14031
|
-
], 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"] }] });
|
|
14969
|
+
], 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"] }] });
|
|
14032
14970
|
}
|
|
14033
14971
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTimeInputComponent, decorators: [{
|
|
14034
14972
|
type: Component,
|
|
@@ -17872,5 +18810,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
17872
18810
|
* Generated bundle index. Do not edit.
|
|
17873
18811
|
*/
|
|
17874
18812
|
|
|
17875
|
-
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 };
|
|
18813
|
+
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_CONTEXT, 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, injectTnFormFieldAria, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, parseLabelMarkup, parseSize, registerLucideIcons, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
|
|
17876
18814
|
//# sourceMappingURL=truenas-ui-components.mjs.map
|