@truenas/ui-components 0.2.3 → 0.2.5
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,7 +1,7 @@
|
|
|
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 { InjectionToken, inject, Renderer2, ElementRef,
|
|
4
|
+
import { input, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, InjectionToken, inject, Renderer2, ElementRef, effect, Directive, ViewContainerRef, output, viewChild, signal, untracked, isDevMode, forwardRef, model, afterNextRender, Injectable, contentChildren, HostListener, linkedSignal, contentChild, ChangeDetectorRef, DestroyRef, TemplateRef, isSignal, IterableDiffers, Pipe, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID } from '@angular/core';
|
|
5
5
|
import * as i1$3 from '@angular/forms';
|
|
6
6
|
import { NG_VALUE_ACCESSOR, FormsModule, NgControl, Validators } from '@angular/forms';
|
|
7
7
|
import { ComponentHarness, HarnessPredicate, TestKey } from '@angular/cdk/testing';
|
|
@@ -29,6 +29,51 @@ import { map } from 'rxjs/operators';
|
|
|
29
29
|
import { DialogRef, DIALOG_DATA, Dialog } from '@angular/cdk/dialog';
|
|
30
30
|
import { ScrollingModule } from '@angular/cdk/scrolling';
|
|
31
31
|
|
|
32
|
+
class TnSpinnerComponent {
|
|
33
|
+
mode = input('indeterminate', ...(ngDevMode ? [{ debugName: "mode" }] : []));
|
|
34
|
+
value = input(0, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
35
|
+
diameter = input(40, ...(ngDevMode ? [{ debugName: "diameter" }] : []));
|
|
36
|
+
strokeWidth = input(4, ...(ngDevMode ? [{ debugName: "strokeWidth" }] : []));
|
|
37
|
+
ariaLabel = input(null, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
38
|
+
ariaLabelledby = input(null, ...(ngDevMode ? [{ debugName: "ariaLabelledby" }] : []));
|
|
39
|
+
radius = computed(() => {
|
|
40
|
+
return (this.diameter() - this.strokeWidth()) / 2;
|
|
41
|
+
}, ...(ngDevMode ? [{ debugName: "radius" }] : []));
|
|
42
|
+
circumference = computed(() => {
|
|
43
|
+
return 2 * Math.PI * this.radius();
|
|
44
|
+
}, ...(ngDevMode ? [{ debugName: "circumference" }] : []));
|
|
45
|
+
strokeDasharray = computed(() => {
|
|
46
|
+
return `${this.circumference()} ${this.circumference()}`;
|
|
47
|
+
}, ...(ngDevMode ? [{ debugName: "strokeDasharray" }] : []));
|
|
48
|
+
strokeDashoffset = computed(() => {
|
|
49
|
+
if (this.mode() === 'indeterminate') {
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
const progress = Math.max(0, Math.min(100, this.value()));
|
|
53
|
+
return this.circumference() - (progress / 100) * this.circumference();
|
|
54
|
+
}, ...(ngDevMode ? [{ debugName: "strokeDashoffset" }] : []));
|
|
55
|
+
viewBox = computed(() => {
|
|
56
|
+
const size = this.diameter();
|
|
57
|
+
return `0 0 ${size} ${size}`;
|
|
58
|
+
}, ...(ngDevMode ? [{ debugName: "viewBox" }] : []));
|
|
59
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSpinnerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
60
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.0", type: TnSpinnerComponent, isStandalone: true, selector: "tn-spinner", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, diameter: { classPropertyName: "diameter", publicName: "diameter", isSignal: true, isRequired: false, transformFunction: null }, strokeWidth: { classPropertyName: "strokeWidth", publicName: "strokeWidth", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "ariaLabelledby", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "progressbar" }, properties: { "class.tn-spinner-indeterminate": "mode() === \"indeterminate\"", "class.tn-spinner-determinate": "mode() === \"determinate\"", "attr.aria-valuenow": "mode() === \"determinate\" ? value() : null", "attr.aria-valuemin": "mode() === \"determinate\" ? 0 : null", "attr.aria-valuemax": "mode() === \"determinate\" ? 100 : null", "attr.aria-label": "ariaLabel() || null", "attr.aria-labelledby": "ariaLabelledby() || null" }, classAttribute: "tn-spinner" }, ngImport: i0, template: "<svg\n class=\"tn-spinner-svg\"\n [attr.width]=\"diameter()\"\n [attr.height]=\"diameter()\"\n [attr.viewBox]=\"viewBox()\">\n <circle\n class=\"tn-spinner-circle\"\n fill=\"none\"\n [attr.cx]=\"diameter() / 2\"\n [attr.cy]=\"diameter() / 2\"\n [attr.r]=\"radius()\"\n [attr.stroke-width]=\"strokeWidth()\"\n [attr.stroke-dasharray]=\"strokeDasharray()\"\n [attr.stroke-dashoffset]=\"strokeDashoffset()\" />\n</svg>", styles: [".tn-spinner{display:inline-block;vertical-align:middle}.tn-spinner-svg{animation:tn-spinner-rotate 2s linear infinite;transform-origin:center}.tn-spinner-circle{stroke:var(--tn-primary, #007bff);stroke-linecap:round;transition:stroke-dashoffset .35s cubic-bezier(.4,0,.2,1)}.tn-spinner-indeterminate .tn-spinner-circle{animation:tn-spinner-dash 1.4s ease-in-out infinite}.tn-spinner-determinate .tn-spinner-svg{animation:none;transform:rotate(-90deg)}@keyframes tn-spinner-rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes tn-spinner-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:100,200;stroke-dashoffset:-15}to{stroke-dasharray:100,200;stroke-dashoffset:-125}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
61
|
+
}
|
|
62
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSpinnerComponent, decorators: [{
|
|
63
|
+
type: Component,
|
|
64
|
+
args: [{ selector: 'tn-spinner', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
|
|
65
|
+
'class': 'tn-spinner',
|
|
66
|
+
'[class.tn-spinner-indeterminate]': 'mode() === "indeterminate"',
|
|
67
|
+
'[class.tn-spinner-determinate]': 'mode() === "determinate"',
|
|
68
|
+
'[attr.aria-valuenow]': 'mode() === "determinate" ? value() : null',
|
|
69
|
+
'[attr.aria-valuemin]': 'mode() === "determinate" ? 0 : null',
|
|
70
|
+
'[attr.aria-valuemax]': 'mode() === "determinate" ? 100 : null',
|
|
71
|
+
'role': 'progressbar',
|
|
72
|
+
'[attr.aria-label]': 'ariaLabel() || null',
|
|
73
|
+
'[attr.aria-labelledby]': 'ariaLabelledby() || null'
|
|
74
|
+
}, template: "<svg\n class=\"tn-spinner-svg\"\n [attr.width]=\"diameter()\"\n [attr.height]=\"diameter()\"\n [attr.viewBox]=\"viewBox()\">\n <circle\n class=\"tn-spinner-circle\"\n fill=\"none\"\n [attr.cx]=\"diameter() / 2\"\n [attr.cy]=\"diameter() / 2\"\n [attr.r]=\"radius()\"\n [attr.stroke-width]=\"strokeWidth()\"\n [attr.stroke-dasharray]=\"strokeDasharray()\"\n [attr.stroke-dashoffset]=\"strokeDashoffset()\" />\n</svg>", styles: [".tn-spinner{display:inline-block;vertical-align:middle}.tn-spinner-svg{animation:tn-spinner-rotate 2s linear infinite;transform-origin:center}.tn-spinner-circle{stroke:var(--tn-primary, #007bff);stroke-linecap:round;transition:stroke-dashoffset .35s cubic-bezier(.4,0,.2,1)}.tn-spinner-indeterminate .tn-spinner-circle{animation:tn-spinner-dash 1.4s ease-in-out infinite}.tn-spinner-determinate .tn-spinner-svg{animation:none;transform:rotate(-90deg)}@keyframes tn-spinner-rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes tn-spinner-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:100,200;stroke-dashoffset:-15}to{stroke-dasharray:100,200;stroke-dashoffset:-125}}\n"] }]
|
|
75
|
+
}], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], diameter: [{ type: i0.Input, args: [{ isSignal: true, alias: "diameter", required: false }] }], strokeWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "strokeWidth", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelledby", required: false }] }] } });
|
|
76
|
+
|
|
32
77
|
/**
|
|
33
78
|
* Normalize one segment to a kebab-case token: split camelCase, lower-case,
|
|
34
79
|
* collapse any run of non-alphanumeric characters to a single hyphen, and trim
|
|
@@ -233,6 +278,21 @@ class TnAutocompleteComponent {
|
|
|
233
278
|
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
234
279
|
/** Require the user to select from the dropdown — reverts on blur if no match */
|
|
235
280
|
requireSelection = input(false, ...(ngDevMode ? [{ debugName: "requireSelection" }] : []));
|
|
281
|
+
/**
|
|
282
|
+
* Commit free text as the value: on blur or Enter, an unmatched search term
|
|
283
|
+
* becomes the control value instead of being discarded. For string-valued
|
|
284
|
+
* autocompletes (the typed text IS the value) — e.g. a device path picker
|
|
285
|
+
* where known devices are suggested but any path is acceptable. Mutually
|
|
286
|
+
* exclusive with `requireSelection`, which reverts unmatched text.
|
|
287
|
+
*/
|
|
288
|
+
allowCustomValue = input(false, ...(ngDevMode ? [{ debugName: "allowCustomValue" }] : []));
|
|
289
|
+
/**
|
|
290
|
+
* Show a loading row in the dropdown panel while options are being fetched.
|
|
291
|
+
* Pair with `searchChange`/`loadMore` for server-driven options.
|
|
292
|
+
*/
|
|
293
|
+
loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
294
|
+
/** Text shown next to the spinner while `loading` is set. */
|
|
295
|
+
loadingText = input('Loading...', ...(ngDevMode ? [{ debugName: "loadingText" }] : []));
|
|
236
296
|
/** Custom filter function. Defaults to case-insensitive includes on displayWith text */
|
|
237
297
|
filterFn = input(undefined, ...(ngDevMode ? [{ debugName: "filterFn" }] : []));
|
|
238
298
|
/** Text shown when no options match the search */
|
|
@@ -256,6 +316,29 @@ class TnAutocompleteComponent {
|
|
|
256
316
|
testId = input(undefined, ...(ngDevMode ? [{ debugName: "testId" }] : []));
|
|
257
317
|
/** Emits when an option is selected */
|
|
258
318
|
optionSelected = output();
|
|
319
|
+
/**
|
|
320
|
+
* Emits the search term as the user types (not on programmatic writes or
|
|
321
|
+
* option selection). Drive server-side filtering from this: fetch matches
|
|
322
|
+
* and update `options`. The component still applies its client-side filter
|
|
323
|
+
* on top — for pre-filtered server results, pass `[filterFn]` that returns
|
|
324
|
+
* `true`. Emissions are not debounced; debounce in the consumer if the
|
|
325
|
+
* lookup is expensive.
|
|
326
|
+
*/
|
|
327
|
+
searchChange = output();
|
|
328
|
+
/**
|
|
329
|
+
* Emits when the open dropdown is scrolled near its bottom — append the
|
|
330
|
+
* next page to `options` (and use `loading` while it fetches). Suppressed
|
|
331
|
+
* until the `options` COUNT changes so a slow consumer is not spammed —
|
|
332
|
+
* which also means replacing the array with a same-length page (e.g. a
|
|
333
|
+
* fixed-size window) does not re-arm the emitter; pagination must append.
|
|
334
|
+
*/
|
|
335
|
+
loadMore = output();
|
|
336
|
+
/**
|
|
337
|
+
* Emits every time the panel opens (focus, click, typing, ArrowDown). For
|
|
338
|
+
* click-to-suggest pickers, prime the first page from here when `options`
|
|
339
|
+
* is still empty — `searchChange` alone never fires until the user types.
|
|
340
|
+
*/
|
|
341
|
+
opened = output();
|
|
259
342
|
/** Reference to the input element */
|
|
260
343
|
inputEl = viewChild('inputEl', ...(ngDevMode ? [{ debugName: "inputEl" }] : []));
|
|
261
344
|
/** Template for the dropdown panel, portaled into a CDK overlay on open. */
|
|
@@ -299,8 +382,55 @@ class TnAutocompleteComponent {
|
|
|
299
382
|
onTouched = () => { };
|
|
300
383
|
/** Live overlay holding the dropdown panel, or undefined when closed. */
|
|
301
384
|
overlayRef;
|
|
385
|
+
/** Embedded view of the portaled panel — re-rendered before underfill measurement. */
|
|
386
|
+
panelViewRef;
|
|
302
387
|
/** Subscriptions tied to the current overlay; torn down on close. */
|
|
303
388
|
overlaySubs = [];
|
|
389
|
+
/** Set after a loadMore emission; cleared when the options count changes. */
|
|
390
|
+
loadMorePending = false;
|
|
391
|
+
/** Options count at the last effect run — length changes re-arm `loadMore`. */
|
|
392
|
+
lastOptionsCount = null;
|
|
393
|
+
/** Scroll distance (px) from the panel bottom that triggers `loadMore`. */
|
|
394
|
+
static loadMoreThresholdPx = 48;
|
|
395
|
+
constructor() {
|
|
396
|
+
// A changed options COUNT means the consumer answered the last loadMore
|
|
397
|
+
// (or a new result set landed) — re-arm the emitter and request another
|
|
398
|
+
// page if the rows still don't fill the panel. Re-arming on length (not
|
|
399
|
+
// identity) is what makes auto-fill loop-safe: an exhausted source that
|
|
400
|
+
// answers loadMore with the same rows leaves the count unchanged, so no
|
|
401
|
+
// further request is made. Also triggered by the panel opening (the
|
|
402
|
+
// already-loaded first page may be too short) and by loading() clearing —
|
|
403
|
+
// a consumer may update options while still loading and only clear the
|
|
404
|
+
// flag in a later tick, and without the loading() dependency that page's
|
|
405
|
+
// underfill check would be skipped (checkUnderfill bails while loading)
|
|
406
|
+
// and never re-run.
|
|
407
|
+
effect(() => {
|
|
408
|
+
const count = this.options().length;
|
|
409
|
+
this.isOpen();
|
|
410
|
+
this.loading();
|
|
411
|
+
untracked(() => {
|
|
412
|
+
if (count !== this.lastOptionsCount) {
|
|
413
|
+
this.lastOptionsCount = count;
|
|
414
|
+
this.loadMorePending = false;
|
|
415
|
+
}
|
|
416
|
+
this.checkUnderfill();
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
// Async option/loading changes resize the open panel without any input
|
|
420
|
+
// event; nudge CDK so the anchored position tracks the new height.
|
|
421
|
+
effect(() => {
|
|
422
|
+
this.filteredOptions();
|
|
423
|
+
this.loading();
|
|
424
|
+
this.overlayRef?.updatePosition();
|
|
425
|
+
});
|
|
426
|
+
if (isDevMode()) {
|
|
427
|
+
effect(() => {
|
|
428
|
+
if (this.requireSelection() && this.allowCustomValue()) {
|
|
429
|
+
console.warn('[tn-autocomplete] requireSelection and allowCustomValue are mutually exclusive; allowCustomValue wins.');
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
304
434
|
ngOnDestroy() {
|
|
305
435
|
// If the component is destroyed while the dropdown is open (e.g. router
|
|
306
436
|
// navigates away), close() never runs — clean up the overlay directly.
|
|
@@ -330,6 +460,9 @@ class TnAutocompleteComponent {
|
|
|
330
460
|
const value = event.target.value;
|
|
331
461
|
this.searchTerm.set(value);
|
|
332
462
|
this.highlightedIndex.set(-1);
|
|
463
|
+
// A new term is a new pagination context — don't hold back its first page.
|
|
464
|
+
this.loadMorePending = false;
|
|
465
|
+
this.searchChange.emit(value);
|
|
333
466
|
if (!this.isOpen()) {
|
|
334
467
|
this.open();
|
|
335
468
|
}
|
|
@@ -345,6 +478,12 @@ class TnAutocompleteComponent {
|
|
|
345
478
|
}
|
|
346
479
|
}
|
|
347
480
|
onBlur() {
|
|
481
|
+
if (this.allowCustomValue()) {
|
|
482
|
+
this.commitCustomValue();
|
|
483
|
+
this.close();
|
|
484
|
+
this.onTouched();
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
348
487
|
if (this.requireSelection()) {
|
|
349
488
|
const term = this.searchTerm();
|
|
350
489
|
const display = this.displayWith();
|
|
@@ -390,20 +529,97 @@ class TnAutocompleteComponent {
|
|
|
390
529
|
this.scrollToHighlighted();
|
|
391
530
|
}
|
|
392
531
|
break;
|
|
393
|
-
case 'Enter':
|
|
532
|
+
case 'Enter': {
|
|
394
533
|
event.preventDefault();
|
|
395
534
|
const idx = this.highlightedIndex();
|
|
396
535
|
if (this.isOpen() && idx >= 0 && idx < options.length) {
|
|
397
536
|
this.selectOption(options[idx]);
|
|
398
537
|
}
|
|
538
|
+
else if (this.allowCustomValue()) {
|
|
539
|
+
this.commitCustomValue();
|
|
540
|
+
this.close();
|
|
541
|
+
}
|
|
399
542
|
break;
|
|
400
|
-
|
|
543
|
+
}
|
|
544
|
+
case 'Escape': {
|
|
401
545
|
event.preventDefault();
|
|
546
|
+
if (this.allowCustomValue()) {
|
|
547
|
+
// Escape means "cancel the draft": revert to the committed value's
|
|
548
|
+
// text so the upcoming blur doesn't commit the abandoned term.
|
|
549
|
+
const current = this.selectedValue();
|
|
550
|
+
this.searchTerm.set(current !== null && current !== undefined ? this.displayWith()(current) : '');
|
|
551
|
+
}
|
|
402
552
|
this.close();
|
|
403
553
|
break;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* `loadMore` normally fires from a scroll event, which never happens when
|
|
559
|
+
* the current page is too short to overflow the panel — pagination would
|
|
560
|
+
* dead-end with data still available. When new rows land (or the panel
|
|
561
|
+
* opens), emit once more if the panel has no scrollbar.
|
|
562
|
+
*/
|
|
563
|
+
checkUnderfill() {
|
|
564
|
+
if (!this.isOpen() || this.loading() || this.loadMorePending) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
// No rows to measure, or rendering is capped by maxResults — more data
|
|
568
|
+
// could never fill the panel, so requesting it would loop until the
|
|
569
|
+
// source is exhausted.
|
|
570
|
+
const filteredCount = this.filteredOptions().length;
|
|
571
|
+
if (filteredCount === 0 || filteredCount >= this.maxResults()) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
// The portaled rows may not have been change-detected yet when this runs
|
|
575
|
+
// (effect timing vs. embedded-view refresh) — render them first so the
|
|
576
|
+
// measurement below never sees a stale, shorter panel.
|
|
577
|
+
this.panelViewRef?.detectChanges();
|
|
578
|
+
const panel = this.overlayRef?.overlayElement
|
|
579
|
+
?.querySelector('.tn-autocomplete__dropdown');
|
|
580
|
+
if (panel && panel.scrollHeight <= panel.clientHeight) {
|
|
581
|
+
this.loadMorePending = true;
|
|
582
|
+
this.loadMore.emit();
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
onPanelScroll(event) {
|
|
586
|
+
// While loading, the rendered rows belong to the previous page/term —
|
|
587
|
+
// scrolling them must not request a page of data that hasn't landed yet.
|
|
588
|
+
if (this.loadMorePending || this.loading()) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const el = event.target;
|
|
592
|
+
const nearBottom = el.scrollTop + el.clientHeight
|
|
593
|
+
>= el.scrollHeight - TnAutocompleteComponent.loadMoreThresholdPx;
|
|
594
|
+
if (nearBottom) {
|
|
595
|
+
this.loadMorePending = true;
|
|
596
|
+
this.loadMore.emit();
|
|
404
597
|
}
|
|
405
598
|
}
|
|
406
599
|
// ── Internal ──
|
|
600
|
+
/**
|
|
601
|
+
* Commit the current search term as the value (allowCustomValue mode). A
|
|
602
|
+
* display match (case-insensitive, same as the requireSelection path)
|
|
603
|
+
* commits the matching option instead, so picking an existing entry by
|
|
604
|
+
* typing its full text behaves like clicking it.
|
|
605
|
+
*/
|
|
606
|
+
commitCustomValue() {
|
|
607
|
+
const term = this.searchTerm();
|
|
608
|
+
const display = this.displayWith();
|
|
609
|
+
const lowerTerm = term.toLowerCase();
|
|
610
|
+
const match = this.options().find((opt) => display(opt).toLowerCase() === lowerTerm);
|
|
611
|
+
if (match) {
|
|
612
|
+
this.selectOption(match);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (isDevMode() && term !== '' && this.options().some((opt) => typeof opt !== 'string')) {
|
|
616
|
+
console.warn('[tn-autocomplete] allowCustomValue committed free text into a control whose options are '
|
|
617
|
+
+ 'not strings — custom values are only sound for string-valued autocompletes.');
|
|
618
|
+
}
|
|
619
|
+
const value = term === '' ? null : term;
|
|
620
|
+
this.selectedValue.set(value);
|
|
621
|
+
this.onChange(value);
|
|
622
|
+
}
|
|
407
623
|
selectOption(option) {
|
|
408
624
|
this.selectedValue.set(option);
|
|
409
625
|
this.searchTerm.set(this.displayWith()(option));
|
|
@@ -417,6 +633,7 @@ class TnAutocompleteComponent {
|
|
|
417
633
|
}
|
|
418
634
|
this.isOpen.set(true);
|
|
419
635
|
this.attachOverlay();
|
|
636
|
+
this.opened.emit();
|
|
420
637
|
}
|
|
421
638
|
close() {
|
|
422
639
|
this.isOpen.set(false);
|
|
@@ -450,7 +667,7 @@ class TnAutocompleteComponent {
|
|
|
450
667
|
hasBackdrop: false,
|
|
451
668
|
width: anchor.offsetWidth,
|
|
452
669
|
});
|
|
453
|
-
this.overlayRef.attach(new TemplatePortal(this.dropdownTemplate(), this.viewContainerRef));
|
|
670
|
+
this.panelViewRef = this.overlayRef.attach(new TemplatePortal(this.dropdownTemplate(), this.viewContainerRef));
|
|
454
671
|
// Non-intercepting click-outside: the pointer event still reaches whatever
|
|
455
672
|
// the user clicked; we just notice and close. Ignore targets inside the
|
|
456
673
|
// host (the input itself) so we don't fight the focus/typing handlers.
|
|
@@ -467,6 +684,7 @@ class TnAutocompleteComponent {
|
|
|
467
684
|
this.overlaySubs = [];
|
|
468
685
|
this.overlayRef?.dispose();
|
|
469
686
|
this.overlayRef = undefined;
|
|
687
|
+
this.panelViewRef = undefined;
|
|
470
688
|
}
|
|
471
689
|
scrollToHighlighted() {
|
|
472
690
|
const idx = this.highlightedIndex();
|
|
@@ -479,24 +697,24 @@ class TnAutocompleteComponent {
|
|
|
479
697
|
}
|
|
480
698
|
}
|
|
481
699
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnAutocompleteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
482
|
-
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 }, displayWith: { classPropertyName: "displayWith", publicName: "displayWith", 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 }, 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" }, providers: [
|
|
700
|
+
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 }, displayWith: { classPropertyName: "displayWith", publicName: "displayWith", 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: [
|
|
483
701
|
{
|
|
484
702
|
provide: NG_VALUE_ACCESSOR,
|
|
485
703
|
useExisting: forwardRef(() => TnAutocompleteComponent),
|
|
486
704
|
multi: true,
|
|
487
705
|
},
|
|
488
|
-
], 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 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
|
|
706
|
+
], 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 [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"highlightedIndex() === $index\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\">\n {{ displayWith()(option) }}\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{background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-autocomplete__option.highlighted{background-color:var(--tn-alt-bg1, #f8f9fa)}.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"] }] });
|
|
489
707
|
}
|
|
490
708
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnAutocompleteComponent, decorators: [{
|
|
491
709
|
type: Component,
|
|
492
|
-
args: [{ selector: 'tn-autocomplete', standalone: true, imports: [TnTestIdDirective], providers: [
|
|
710
|
+
args: [{ selector: 'tn-autocomplete', standalone: true, imports: [TnSpinnerComponent, TnTestIdDirective], providers: [
|
|
493
711
|
{
|
|
494
712
|
provide: NG_VALUE_ACCESSOR,
|
|
495
713
|
useExisting: forwardRef(() => TnAutocompleteComponent),
|
|
496
714
|
multi: true,
|
|
497
715
|
},
|
|
498
|
-
], template: "<div class=\"tn-autocomplete\">\n <input\n #inputEl\n type=\"text\"\n class=\"tn-autocomplete__input\"\n role=\"combobox\"\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
|
|
499
|
-
}], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], displayWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayWith", 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 }] }], 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"] }], inputEl: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdownTemplate', { isSignal: true }] }] } });
|
|
716
|
+
], 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 [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"highlightedIndex() === $index\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\">\n {{ displayWith()(option) }}\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{background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-autocomplete__option.highlighted{background-color:var(--tn-alt-bg1, #f8f9fa)}.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"] }]
|
|
717
|
+
}], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], displayWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayWith", 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 }] }] } });
|
|
500
718
|
|
|
501
719
|
/**
|
|
502
720
|
* Harness for interacting with tn-autocomplete in tests.
|
|
@@ -722,6 +940,23 @@ class TnAutocompleteHarness extends ComponentHarness {
|
|
|
722
940
|
const input = await this._input();
|
|
723
941
|
return input.blur();
|
|
724
942
|
}
|
|
943
|
+
/**
|
|
944
|
+
* Checks whether the dropdown panel is showing the loading row.
|
|
945
|
+
*
|
|
946
|
+
* @returns Promise resolving to true if the loading indicator is visible.
|
|
947
|
+
*
|
|
948
|
+
* @example
|
|
949
|
+
* ```typescript
|
|
950
|
+
* const ac = await loader.getHarness(TnAutocompleteHarness);
|
|
951
|
+
* await ac.focus();
|
|
952
|
+
* expect(await ac.isLoading()).toBe(true);
|
|
953
|
+
* ```
|
|
954
|
+
*/
|
|
955
|
+
async isLoading() {
|
|
956
|
+
const loading = await this.documentRootLocatorFactory()
|
|
957
|
+
.locatorForOptional('.tn-autocomplete__loading')();
|
|
958
|
+
return loading !== null;
|
|
959
|
+
}
|
|
725
960
|
}
|
|
726
961
|
|
|
727
962
|
class TnDrawerComponent {
|
|
@@ -6448,7 +6683,18 @@ class TnFormFieldComponent {
|
|
|
6448
6683
|
required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : []));
|
|
6449
6684
|
testId = input(undefined, ...(ngDevMode ? [{ debugName: "testId" }] : []));
|
|
6450
6685
|
subscriptSizing = input('dynamic', ...(ngDevMode ? [{ debugName: "subscriptSizing" }] : []));
|
|
6451
|
-
/**
|
|
6686
|
+
/**
|
|
6687
|
+
* Optional tooltip shown via a help icon.
|
|
6688
|
+
*
|
|
6689
|
+
* With a `label`, the icon renders next to the label in the label row. Without
|
|
6690
|
+
* one, the icon renders inline after the projected control instead — for
|
|
6691
|
+
* controls that carry their own label (e.g. `tn-checkbox`), where a detached
|
|
6692
|
+
* icon row above the control would look orphaned.
|
|
6693
|
+
*
|
|
6694
|
+
* Inline mode targets compact, self-labeled controls: the wrapper becomes a
|
|
6695
|
+
* flex row, so a full-width control (e.g. a label-less `tn-input`) would
|
|
6696
|
+
* shrink toward its content width. Give such fields a `label` instead.
|
|
6697
|
+
*/
|
|
6452
6698
|
tooltip = input('', ...(ngDevMode ? [{ debugName: "tooltip" }] : []));
|
|
6453
6699
|
/** Placement of the tooltip relative to its help icon. */
|
|
6454
6700
|
tooltipPosition = input('above', ...(ngDevMode ? [{ debugName: "tooltipPosition" }] : []));
|
|
@@ -6481,10 +6727,24 @@ class TnFormFieldComponent {
|
|
|
6481
6727
|
/**
|
|
6482
6728
|
* Whether the required indicator renders: forced via the `required` input, or
|
|
6483
6729
|
* inferred from the projected control's validators (mirrors Angular Material's
|
|
6484
|
-
* `hasValidator(Validators.required)` approach
|
|
6485
|
-
* or custom required-like
|
|
6730
|
+
* `hasValidator(Validators.required)` approach, extended to `requiredTrue` for
|
|
6731
|
+
* boolean controls — reference equality, so composed or custom required-like
|
|
6732
|
+
* validators need the explicit input).
|
|
6486
6733
|
*/
|
|
6487
6734
|
showRequired = computed(() => this.required() || this.controlState().required, ...(ngDevMode ? [{ debugName: "showRequired" }] : []));
|
|
6735
|
+
/**
|
|
6736
|
+
* Whether the tooltip icon renders inline after the projected control rather
|
|
6737
|
+
* than in the label row — true when a tooltip is set but no label is.
|
|
6738
|
+
*/
|
|
6739
|
+
showInlineTooltip = computed(() => !!this.tooltip() && !this.label(), ...(ngDevMode ? [{ debugName: "showInlineTooltip" }] : []));
|
|
6740
|
+
/**
|
|
6741
|
+
* Whether the required indicator renders inline after the projected control —
|
|
6742
|
+
* with no label there is no label row to host the asterisk, so a required
|
|
6743
|
+
* self-labeled control (e.g. a consent `tn-checkbox`) still gets one.
|
|
6744
|
+
*/
|
|
6745
|
+
showInlineRequired = computed(() => !this.label() && this.showRequired(), ...(ngDevMode ? [{ debugName: "showInlineRequired" }] : []));
|
|
6746
|
+
/** Whether the wrapper hosts any inline extras and lays out as a flex row. */
|
|
6747
|
+
showInlineExtras = computed(() => this.showInlineTooltip() || this.showInlineRequired(), ...(ngDevMode ? [{ debugName: "showInlineExtras" }] : []));
|
|
6488
6748
|
hasError = computed(() => {
|
|
6489
6749
|
const state = this.controlState();
|
|
6490
6750
|
return state.invalid && state.interacted;
|
|
@@ -6517,7 +6777,8 @@ class TnFormFieldComponent {
|
|
|
6517
6777
|
invalid: !!control.invalid,
|
|
6518
6778
|
interacted: !!(control.dirty || control.touched),
|
|
6519
6779
|
errors: control.errors ?? null,
|
|
6520
|
-
required: !!control.control?.hasValidator(Validators.required)
|
|
6780
|
+
required: !!(control.control?.hasValidator(Validators.required)
|
|
6781
|
+
|| control.control?.hasValidator(Validators.requiredTrue)),
|
|
6521
6782
|
});
|
|
6522
6783
|
}
|
|
6523
6784
|
}
|
|
@@ -6589,11 +6850,11 @@ class TnFormFieldComponent {
|
|
|
6589
6850
|
return this.subscriptSizing() === 'fixed' || this.showError() || this.showHint();
|
|
6590
6851
|
}, ...(ngDevMode ? [{ debugName: "showSubscript" }] : []));
|
|
6591
6852
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFormFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6592
|
-
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 } }, 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()
|
|
6853
|
+
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 } }, 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 {{ label() }}\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 <div\n class=\"tn-form-field-error\"\n role=\"alert\"\n aria-live=\"polite\">\n {{ errorMessage() }}\n </div>\n }\n @if (showHint()) {\n <div class=\"tn-form-field-hint\">\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:500;color:var(--tn-fg1, #333);line-height:1.4}.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"] }] });
|
|
6593
6854
|
}
|
|
6594
6855
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFormFieldComponent, decorators: [{
|
|
6595
6856
|
type: Component,
|
|
6596
|
-
args: [{ selector: 'tn-form-field', standalone: true, imports: [TnTestIdDirective, TnIconComponent, TnTooltipDirective], template: "<div class=\"tn-form-field\" tnTestIdType=\"form-field\" [tnTestId]=\"testId()\">\n <!-- Label -->\n @if (label()
|
|
6857
|
+
args: [{ selector: 'tn-form-field', standalone: true, imports: [NgTemplateOutlet, TnTestIdDirective, TnIconComponent, TnTooltipDirective], 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 {{ label() }}\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 <div\n class=\"tn-form-field-error\"\n role=\"alert\"\n aria-live=\"polite\">\n {{ errorMessage() }}\n </div>\n }\n @if (showHint()) {\n <div class=\"tn-form-field-hint\">\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:500;color:var(--tn-fg1, #333);line-height:1.4}.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"] }]
|
|
6597
6858
|
}], 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 }] }] } });
|
|
6598
6859
|
|
|
6599
6860
|
/**
|
|
@@ -6749,6 +7010,22 @@ class TnFormFieldHarness extends ComponentHarness {
|
|
|
6749
7010
|
const tooltip = await this._tooltip();
|
|
6750
7011
|
return tooltip ? tooltip.getAttribute('aria-label') : null;
|
|
6751
7012
|
}
|
|
7013
|
+
/**
|
|
7014
|
+
* Checks whether the tooltip help icon renders inline after the projected
|
|
7015
|
+
* control (label-less mode) rather than in the label row.
|
|
7016
|
+
*
|
|
7017
|
+
* @returns Promise resolving to true if the inline tooltip trigger is present.
|
|
7018
|
+
*
|
|
7019
|
+
* @example
|
|
7020
|
+
* ```typescript
|
|
7021
|
+
* const field = await loader.getHarness(TnFormFieldHarness.with({ testId: 'enable-fxp' }));
|
|
7022
|
+
* expect(await field.isTooltipInline()).toBe(true);
|
|
7023
|
+
* ```
|
|
7024
|
+
*/
|
|
7025
|
+
async isTooltipInline() {
|
|
7026
|
+
const inline = await this.locatorForOptional('.tn-form-field-wrapper .tn-form-field-tooltip')();
|
|
7027
|
+
return inline !== null;
|
|
7028
|
+
}
|
|
6752
7029
|
/**
|
|
6753
7030
|
* Checks whether the form field is marked as required.
|
|
6754
7031
|
*
|
|
@@ -6762,10 +7039,12 @@ class TnFormFieldHarness extends ComponentHarness {
|
|
|
6762
7039
|
*/
|
|
6763
7040
|
async isRequired() {
|
|
6764
7041
|
const label = await this._label();
|
|
6765
|
-
if (
|
|
6766
|
-
return
|
|
7042
|
+
if (label) {
|
|
7043
|
+
return label.hasClass('required');
|
|
6767
7044
|
}
|
|
6768
|
-
|
|
7045
|
+
// Label-less mode renders the asterisk inline after the projected control.
|
|
7046
|
+
const inlineAsterisk = await this.locatorForOptional('.tn-form-field-wrapper .required-asterisk')();
|
|
7047
|
+
return inlineAsterisk !== null;
|
|
6769
7048
|
}
|
|
6770
7049
|
/**
|
|
6771
7050
|
* Gets the test ID attribute value.
|
|
@@ -8335,51 +8614,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
8335
8614
|
}, template: "@if (icon()) {\n <div class=\"tn-empty__icon\">\n <tn-icon\n aria-hidden=\"true\"\n [name]=\"icon()!\"\n [library]=\"iconLibrary()\"\n [size]=\"iconSizePreset()\"\n [customSize]=\"iconSize()\"\n />\n </div>\n}\n\n<div class=\"tn-empty__title\">\n {{ title() }}\n</div>\n\n@if (description()) {\n <div class=\"tn-empty__description\">\n {{ description() }}\n </div>\n}\n\n@if (hasAction()) {\n <div class=\"tn-empty__action\">\n <tn-button\n color=\"primary\"\n variant=\"outline\"\n [label]=\"actionText()!\"\n (onClick)=\"onAction.emit()\"\n />\n </div>\n}\n", styles: [":host{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:8px;padding:48px 24px;border-radius:8px}:host(.tn-empty--bordered){border:1px solid var(--tn-lines)}:host(.tn-empty--compact){padding:24px 16px}:host(.tn-empty--compact) .tn-empty__title{font-size:1rem}:host(.tn-empty--compact) .tn-empty__description{font-size:.8125rem}:host(.tn-empty--compact) .tn-empty__icon{margin-bottom:4px}:host(.tn-empty--compact) .tn-empty__action{margin-top:4px}.tn-empty__icon{color:var(--tn-fg2, #6b7280);margin-bottom:8px}.tn-empty__title{font-size:1.125rem;font-weight:600;color:var(--tn-fg1, #e5e7eb);line-height:1.4}.tn-empty__description{font-size:1rem;color:var(--tn-fg2, #6b7280);line-height:1.5;max-width:420px}.tn-empty__action{margin-top:8px}\n"] }]
|
|
8336
8615
|
}], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], iconLibrary: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconLibrary", required: false }] }], iconSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconSize", required: false }] }], actionText: [{ type: i0.Input, args: [{ isSignal: true, alias: "actionText", required: false }] }], bordered: [{ type: i0.Input, args: [{ isSignal: true, alias: "bordered", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], onAction: [{ type: i0.Output, args: ["onAction"] }] } });
|
|
8337
8616
|
|
|
8338
|
-
class TnSpinnerComponent {
|
|
8339
|
-
mode = input('indeterminate', ...(ngDevMode ? [{ debugName: "mode" }] : []));
|
|
8340
|
-
value = input(0, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
8341
|
-
diameter = input(40, ...(ngDevMode ? [{ debugName: "diameter" }] : []));
|
|
8342
|
-
strokeWidth = input(4, ...(ngDevMode ? [{ debugName: "strokeWidth" }] : []));
|
|
8343
|
-
ariaLabel = input(null, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
8344
|
-
ariaLabelledby = input(null, ...(ngDevMode ? [{ debugName: "ariaLabelledby" }] : []));
|
|
8345
|
-
radius = computed(() => {
|
|
8346
|
-
return (this.diameter() - this.strokeWidth()) / 2;
|
|
8347
|
-
}, ...(ngDevMode ? [{ debugName: "radius" }] : []));
|
|
8348
|
-
circumference = computed(() => {
|
|
8349
|
-
return 2 * Math.PI * this.radius();
|
|
8350
|
-
}, ...(ngDevMode ? [{ debugName: "circumference" }] : []));
|
|
8351
|
-
strokeDasharray = computed(() => {
|
|
8352
|
-
return `${this.circumference()} ${this.circumference()}`;
|
|
8353
|
-
}, ...(ngDevMode ? [{ debugName: "strokeDasharray" }] : []));
|
|
8354
|
-
strokeDashoffset = computed(() => {
|
|
8355
|
-
if (this.mode() === 'indeterminate') {
|
|
8356
|
-
return 0;
|
|
8357
|
-
}
|
|
8358
|
-
const progress = Math.max(0, Math.min(100, this.value()));
|
|
8359
|
-
return this.circumference() - (progress / 100) * this.circumference();
|
|
8360
|
-
}, ...(ngDevMode ? [{ debugName: "strokeDashoffset" }] : []));
|
|
8361
|
-
viewBox = computed(() => {
|
|
8362
|
-
const size = this.diameter();
|
|
8363
|
-
return `0 0 ${size} ${size}`;
|
|
8364
|
-
}, ...(ngDevMode ? [{ debugName: "viewBox" }] : []));
|
|
8365
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSpinnerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8366
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.0", type: TnSpinnerComponent, isStandalone: true, selector: "tn-spinner", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, diameter: { classPropertyName: "diameter", publicName: "diameter", isSignal: true, isRequired: false, transformFunction: null }, strokeWidth: { classPropertyName: "strokeWidth", publicName: "strokeWidth", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "ariaLabelledby", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "progressbar" }, properties: { "class.tn-spinner-indeterminate": "mode() === \"indeterminate\"", "class.tn-spinner-determinate": "mode() === \"determinate\"", "attr.aria-valuenow": "mode() === \"determinate\" ? value() : null", "attr.aria-valuemin": "mode() === \"determinate\" ? 0 : null", "attr.aria-valuemax": "mode() === \"determinate\" ? 100 : null", "attr.aria-label": "ariaLabel() || null", "attr.aria-labelledby": "ariaLabelledby() || null" }, classAttribute: "tn-spinner" }, ngImport: i0, template: "<svg\n class=\"tn-spinner-svg\"\n [attr.width]=\"diameter()\"\n [attr.height]=\"diameter()\"\n [attr.viewBox]=\"viewBox()\">\n <circle\n class=\"tn-spinner-circle\"\n fill=\"none\"\n [attr.cx]=\"diameter() / 2\"\n [attr.cy]=\"diameter() / 2\"\n [attr.r]=\"radius()\"\n [attr.stroke-width]=\"strokeWidth()\"\n [attr.stroke-dasharray]=\"strokeDasharray()\"\n [attr.stroke-dashoffset]=\"strokeDashoffset()\" />\n</svg>", styles: [".tn-spinner{display:inline-block;vertical-align:middle}.tn-spinner-svg{animation:tn-spinner-rotate 2s linear infinite;transform-origin:center}.tn-spinner-circle{stroke:var(--tn-primary, #007bff);stroke-linecap:round;transition:stroke-dashoffset .35s cubic-bezier(.4,0,.2,1)}.tn-spinner-indeterminate .tn-spinner-circle{animation:tn-spinner-dash 1.4s ease-in-out infinite}.tn-spinner-determinate .tn-spinner-svg{animation:none;transform:rotate(-90deg)}@keyframes tn-spinner-rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes tn-spinner-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:100,200;stroke-dashoffset:-15}to{stroke-dasharray:100,200;stroke-dashoffset:-125}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
8367
|
-
}
|
|
8368
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSpinnerComponent, decorators: [{
|
|
8369
|
-
type: Component,
|
|
8370
|
-
args: [{ selector: 'tn-spinner', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
|
|
8371
|
-
'class': 'tn-spinner',
|
|
8372
|
-
'[class.tn-spinner-indeterminate]': 'mode() === "indeterminate"',
|
|
8373
|
-
'[class.tn-spinner-determinate]': 'mode() === "determinate"',
|
|
8374
|
-
'[attr.aria-valuenow]': 'mode() === "determinate" ? value() : null',
|
|
8375
|
-
'[attr.aria-valuemin]': 'mode() === "determinate" ? 0 : null',
|
|
8376
|
-
'[attr.aria-valuemax]': 'mode() === "determinate" ? 100 : null',
|
|
8377
|
-
'role': 'progressbar',
|
|
8378
|
-
'[attr.aria-label]': 'ariaLabel() || null',
|
|
8379
|
-
'[attr.aria-labelledby]': 'ariaLabelledby() || null'
|
|
8380
|
-
}, template: "<svg\n class=\"tn-spinner-svg\"\n [attr.width]=\"diameter()\"\n [attr.height]=\"diameter()\"\n [attr.viewBox]=\"viewBox()\">\n <circle\n class=\"tn-spinner-circle\"\n fill=\"none\"\n [attr.cx]=\"diameter() / 2\"\n [attr.cy]=\"diameter() / 2\"\n [attr.r]=\"radius()\"\n [attr.stroke-width]=\"strokeWidth()\"\n [attr.stroke-dasharray]=\"strokeDasharray()\"\n [attr.stroke-dashoffset]=\"strokeDashoffset()\" />\n</svg>", styles: [".tn-spinner{display:inline-block;vertical-align:middle}.tn-spinner-svg{animation:tn-spinner-rotate 2s linear infinite;transform-origin:center}.tn-spinner-circle{stroke:var(--tn-primary, #007bff);stroke-linecap:round;transition:stroke-dashoffset .35s cubic-bezier(.4,0,.2,1)}.tn-spinner-indeterminate .tn-spinner-circle{animation:tn-spinner-dash 1.4s ease-in-out infinite}.tn-spinner-determinate .tn-spinner-svg{animation:none;transform:rotate(-90deg)}@keyframes tn-spinner-rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes tn-spinner-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:100,200;stroke-dashoffset:-15}to{stroke-dasharray:100,200;stroke-dashoffset:-125}}\n"] }]
|
|
8381
|
-
}], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], diameter: [{ type: i0.Input, args: [{ isSignal: true, alias: "diameter", required: false }] }], strokeWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "strokeWidth", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelledby", required: false }] }] } });
|
|
8382
|
-
|
|
8383
8617
|
class TnHeaderCellDefDirective {
|
|
8384
8618
|
template = inject((TemplateRef));
|
|
8385
8619
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnHeaderCellDefDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|