@truenas/ui-components 0.2.6 → 0.3.1
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.
|
@@ -268,10 +268,20 @@ class TnAutocompleteComponent {
|
|
|
268
268
|
viewContainerRef = inject(ViewContainerRef);
|
|
269
269
|
/** Unique instance ID for ARIA linkage */
|
|
270
270
|
uid = `tn-autocomplete-${nextId$1++}`;
|
|
271
|
-
/**
|
|
271
|
+
/**
|
|
272
|
+
* All available options. The `label` is displayed; the `value` is committed
|
|
273
|
+
* to the form control. A written value is resolved back to its option's
|
|
274
|
+
* label for display — falling back to `String(value)` until the matching
|
|
275
|
+
* option is available, and upgraded once an async option load lands.
|
|
276
|
+
*/
|
|
272
277
|
options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
|
|
273
|
-
/**
|
|
274
|
-
|
|
278
|
+
/**
|
|
279
|
+
* Custom comparator for matching a written control value against option
|
|
280
|
+
* values during display resolution. Defaults to identity (`===`), which is
|
|
281
|
+
* right for primitive values; provide this when option values are objects.
|
|
282
|
+
* Mirrors `tn-select`'s `compareWith`.
|
|
283
|
+
*/
|
|
284
|
+
compareWith = input(undefined, ...(ngDevMode ? [{ debugName: "compareWith" }] : []));
|
|
275
285
|
/** Placeholder text for the input */
|
|
276
286
|
placeholder = input('Type to search...', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
|
|
277
287
|
/** Whether the input is disabled */
|
|
@@ -293,7 +303,7 @@ class TnAutocompleteComponent {
|
|
|
293
303
|
loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
294
304
|
/** Text shown next to the spinner while `loading` is set. */
|
|
295
305
|
loadingText = input('Loading...', ...(ngDevMode ? [{ debugName: "loadingText" }] : []));
|
|
296
|
-
/** Custom filter function. Defaults to case-insensitive includes on
|
|
306
|
+
/** Custom filter function. Defaults to case-insensitive includes on the option label */
|
|
297
307
|
filterFn = input(undefined, ...(ngDevMode ? [{ debugName: "filterFn" }] : []));
|
|
298
308
|
/** Text shown when no options match the search */
|
|
299
309
|
noResultsText = input('No results found', ...(ngDevMode ? [{ debugName: "noResultsText" }] : []));
|
|
@@ -314,7 +324,7 @@ class TnAutocompleteComponent {
|
|
|
314
324
|
panelMaxHeight = input('320px', ...(ngDevMode ? [{ debugName: "panelMaxHeight" }] : []));
|
|
315
325
|
/** Test ID attribute */
|
|
316
326
|
testId = input(undefined, ...(ngDevMode ? [{ debugName: "testId" }] : []));
|
|
317
|
-
/** Emits
|
|
327
|
+
/** Emits the full option (label + value) when one is selected */
|
|
318
328
|
optionSelected = output();
|
|
319
329
|
/**
|
|
320
330
|
* Emits the search term as the user types (not on programmatic writes or
|
|
@@ -354,7 +364,7 @@ class TnAutocompleteComponent {
|
|
|
354
364
|
isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
|
|
355
365
|
/** Index of the currently highlighted option for keyboard nav */
|
|
356
366
|
highlightedIndex = signal(-1, ...(ngDevMode ? [{ debugName: "highlightedIndex" }] : []));
|
|
357
|
-
/** The currently
|
|
367
|
+
/** The currently committed value (an option's `value`, or a custom-typed one) */
|
|
358
368
|
selectedValue = signal(null, ...(ngDevMode ? [{ debugName: "selectedValue" }] : []));
|
|
359
369
|
/** CVA disabled state from the form */
|
|
360
370
|
formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "formDisabled" }] : []));
|
|
@@ -365,7 +375,6 @@ class TnAutocompleteComponent {
|
|
|
365
375
|
const term = this.searchTerm();
|
|
366
376
|
const all = this.options();
|
|
367
377
|
const customFilter = this.filterFn();
|
|
368
|
-
const display = this.displayWith();
|
|
369
378
|
const max = this.maxResults();
|
|
370
379
|
if (!term) {
|
|
371
380
|
return all.slice(0, max);
|
|
@@ -373,7 +382,7 @@ class TnAutocompleteComponent {
|
|
|
373
382
|
const lowerTerm = term.toLowerCase();
|
|
374
383
|
const filtered = customFilter
|
|
375
384
|
? all.filter((opt) => customFilter(opt, term))
|
|
376
|
-
: all.filter((opt) =>
|
|
385
|
+
: all.filter((opt) => opt.label.toLowerCase().includes(lowerTerm));
|
|
377
386
|
return filtered.slice(0, max);
|
|
378
387
|
}, ...(ngDevMode ? [{ debugName: "filteredOptions" }] : []));
|
|
379
388
|
/** Whether there are any results to show */
|
|
@@ -392,7 +401,32 @@ class TnAutocompleteComponent {
|
|
|
392
401
|
lastOptionsCount = null;
|
|
393
402
|
/** Scroll distance (px) from the panel bottom that triggers `loadMore`. */
|
|
394
403
|
static loadMoreThresholdPx = 48;
|
|
404
|
+
/** Guards the object-without-compareWith warning so it fires only once. */
|
|
405
|
+
warnedAboutObjectCompare = false;
|
|
395
406
|
constructor() {
|
|
407
|
+
// A value written before its option loaded displays as the raw fallback —
|
|
408
|
+
// once options arrive (or are relabeled, e.g. a locale change), upgrade
|
|
409
|
+
// the text to the option's label. UPGRADE-ONLY: when no option matches
|
|
410
|
+
// (e.g. a server-search picker replaced `options` after a selection), the
|
|
411
|
+
// current text is left alone rather than downgraded back to the raw
|
|
412
|
+
// value. Skipped while the panel is open so active typing isn't clobbered.
|
|
413
|
+
effect(() => {
|
|
414
|
+
this.options();
|
|
415
|
+
this.compareWith();
|
|
416
|
+
untracked(() => {
|
|
417
|
+
if (this.isOpen()) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const value = this.selectedValue();
|
|
421
|
+
if (value === null || value === undefined) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const match = this.options().find((opt) => this.valueMatches(opt.value, value));
|
|
425
|
+
if (match) {
|
|
426
|
+
this.searchTerm.set(match.label);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
});
|
|
396
430
|
// A changed options COUNT means the consumer answered the last loadMore
|
|
397
431
|
// (or a new result set landed) — re-arm the emitter and request another
|
|
398
432
|
// page if the rows still don't fill the panel. Re-arming on length (not
|
|
@@ -440,7 +474,7 @@ class TnAutocompleteComponent {
|
|
|
440
474
|
writeValue(value) {
|
|
441
475
|
this.selectedValue.set(value);
|
|
442
476
|
if (value !== null && value !== undefined) {
|
|
443
|
-
this.searchTerm.set(this.
|
|
477
|
+
this.searchTerm.set(this.displayValue(value));
|
|
444
478
|
}
|
|
445
479
|
else {
|
|
446
480
|
this.searchTerm.set('');
|
|
@@ -459,7 +493,6 @@ class TnAutocompleteComponent {
|
|
|
459
493
|
onInput(event) {
|
|
460
494
|
const value = event.target.value;
|
|
461
495
|
this.searchTerm.set(value);
|
|
462
|
-
this.highlightedIndex.set(-1);
|
|
463
496
|
// A new term is a new pagination context — don't hold back its first page.
|
|
464
497
|
this.loadMorePending = false;
|
|
465
498
|
this.searchChange.emit(value);
|
|
@@ -471,6 +504,9 @@ class TnAutocompleteComponent {
|
|
|
471
504
|
// anchored position so it stays attached to the input.
|
|
472
505
|
this.overlayRef?.updatePosition();
|
|
473
506
|
}
|
|
507
|
+
// Typing is a fresh search, not navigation — no row is pre-highlighted.
|
|
508
|
+
// Set after open() so it overrides the committed-option seed below.
|
|
509
|
+
this.highlightedIndex.set(-1);
|
|
474
510
|
}
|
|
475
511
|
onFocus() {
|
|
476
512
|
if (!this.isDisabled()) {
|
|
@@ -486,8 +522,7 @@ class TnAutocompleteComponent {
|
|
|
486
522
|
}
|
|
487
523
|
if (this.requireSelection()) {
|
|
488
524
|
const term = this.searchTerm();
|
|
489
|
-
const
|
|
490
|
-
const match = this.options().find((opt) => display(opt).toLowerCase() === term.toLowerCase());
|
|
525
|
+
const match = this.options().find((opt) => !opt.disabled && opt.label.toLowerCase() === term.toLowerCase());
|
|
491
526
|
if (match) {
|
|
492
527
|
this.selectOption(match);
|
|
493
528
|
}
|
|
@@ -495,7 +530,7 @@ class TnAutocompleteComponent {
|
|
|
495
530
|
// Revert to last valid selection or clear
|
|
496
531
|
const current = this.selectedValue();
|
|
497
532
|
if (current !== null && current !== undefined) {
|
|
498
|
-
this.searchTerm.set(
|
|
533
|
+
this.searchTerm.set(this.displayValue(current));
|
|
499
534
|
}
|
|
500
535
|
else {
|
|
501
536
|
this.searchTerm.set('');
|
|
@@ -507,6 +542,9 @@ class TnAutocompleteComponent {
|
|
|
507
542
|
this.onTouched();
|
|
508
543
|
}
|
|
509
544
|
onOptionClick(option) {
|
|
545
|
+
if (option.disabled) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
510
548
|
this.selectOption(option);
|
|
511
549
|
}
|
|
512
550
|
onKeydown(event) {
|
|
@@ -518,21 +556,27 @@ class TnAutocompleteComponent {
|
|
|
518
556
|
this.open();
|
|
519
557
|
}
|
|
520
558
|
else {
|
|
521
|
-
this.highlightedIndex
|
|
522
|
-
|
|
559
|
+
const next = this.nextEnabledIndex(this.highlightedIndex(), 1);
|
|
560
|
+
if (next >= 0) {
|
|
561
|
+
this.highlightedIndex.set(next);
|
|
562
|
+
this.scrollToHighlighted();
|
|
563
|
+
}
|
|
523
564
|
}
|
|
524
565
|
break;
|
|
525
566
|
case 'ArrowUp':
|
|
526
567
|
event.preventDefault();
|
|
527
568
|
if (this.isOpen()) {
|
|
528
|
-
this.highlightedIndex
|
|
529
|
-
|
|
569
|
+
const prev = this.nextEnabledIndex(this.highlightedIndex(), -1);
|
|
570
|
+
if (prev >= 0) {
|
|
571
|
+
this.highlightedIndex.set(prev);
|
|
572
|
+
this.scrollToHighlighted();
|
|
573
|
+
}
|
|
530
574
|
}
|
|
531
575
|
break;
|
|
532
576
|
case 'Enter': {
|
|
533
577
|
event.preventDefault();
|
|
534
578
|
const idx = this.highlightedIndex();
|
|
535
|
-
if (this.isOpen() && idx >= 0 && idx < options.length) {
|
|
579
|
+
if (this.isOpen() && idx >= 0 && idx < options.length && !options[idx].disabled) {
|
|
536
580
|
this.selectOption(options[idx]);
|
|
537
581
|
}
|
|
538
582
|
else if (this.allowCustomValue()) {
|
|
@@ -547,7 +591,7 @@ class TnAutocompleteComponent {
|
|
|
547
591
|
// Escape means "cancel the draft": revert to the committed value's
|
|
548
592
|
// text so the upcoming blur doesn't commit the abandoned term.
|
|
549
593
|
const current = this.selectedValue();
|
|
550
|
-
this.searchTerm.set(current !== null && current !== undefined ? this.
|
|
594
|
+
this.searchTerm.set(current !== null && current !== undefined ? this.displayValue(current) : '');
|
|
551
595
|
}
|
|
552
596
|
this.close();
|
|
553
597
|
break;
|
|
@@ -596,7 +640,52 @@ class TnAutocompleteComponent {
|
|
|
596
640
|
this.loadMore.emit();
|
|
597
641
|
}
|
|
598
642
|
}
|
|
643
|
+
/**
|
|
644
|
+
* Whether `option` carries the committed value — drives `aria-selected` so
|
|
645
|
+
* assistive tech announces the current choice independently of the keyboard
|
|
646
|
+
* cursor (which is conveyed via `aria-activedescendant`).
|
|
647
|
+
*/
|
|
648
|
+
isOptionSelected(option) {
|
|
649
|
+
const value = this.selectedValue();
|
|
650
|
+
if (value === null || value === undefined) {
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
return this.valueMatches(option.value, value);
|
|
654
|
+
}
|
|
599
655
|
// ── Internal ──
|
|
656
|
+
/**
|
|
657
|
+
* Index of the committed value's option in the visible list, or -1 when
|
|
658
|
+
* nothing is committed or the match is absent/disabled. Used to seed the
|
|
659
|
+
* keyboard cursor when the panel opens.
|
|
660
|
+
*/
|
|
661
|
+
selectedOptionIndex() {
|
|
662
|
+
const value = this.selectedValue();
|
|
663
|
+
if (value === null || value === undefined) {
|
|
664
|
+
return -1;
|
|
665
|
+
}
|
|
666
|
+
return this.filteredOptions().findIndex((opt) => !opt.disabled && this.valueMatches(opt.value, value));
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Next selectable option index from `from` in `direction` (+1 down, -1 up),
|
|
670
|
+
* skipping disabled rows and wrapping around. `from` of -1 ("nothing
|
|
671
|
+
* highlighted") lands on the first row going down, the last going up.
|
|
672
|
+
* Returns -1 when every visible option is disabled.
|
|
673
|
+
*/
|
|
674
|
+
nextEnabledIndex(from, direction) {
|
|
675
|
+
const options = this.filteredOptions();
|
|
676
|
+
const count = options.length;
|
|
677
|
+
if (count === 0) {
|
|
678
|
+
return -1;
|
|
679
|
+
}
|
|
680
|
+
const start = from < 0 ? (direction === 1 ? -1 : 0) : from;
|
|
681
|
+
for (let step = 1; step <= count; step++) {
|
|
682
|
+
const idx = (((start + direction * step) % count) + count) % count;
|
|
683
|
+
if (!options[idx].disabled) {
|
|
684
|
+
return idx;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return -1;
|
|
688
|
+
}
|
|
600
689
|
/**
|
|
601
690
|
* Commit the current search term as the value (allowCustomValue mode). A
|
|
602
691
|
* display match (case-insensitive, same as the requireSelection path)
|
|
@@ -605,25 +694,61 @@ class TnAutocompleteComponent {
|
|
|
605
694
|
*/
|
|
606
695
|
commitCustomValue() {
|
|
607
696
|
const term = this.searchTerm();
|
|
608
|
-
const display = this.displayWith();
|
|
609
697
|
const lowerTerm = term.toLowerCase();
|
|
610
|
-
const match = this.options().find((opt) =>
|
|
698
|
+
const match = this.options().find((opt) => !opt.disabled && opt.label.toLowerCase() === lowerTerm);
|
|
611
699
|
if (match) {
|
|
612
700
|
this.selectOption(match);
|
|
613
701
|
return;
|
|
614
702
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
703
|
+
// Best-effort guard: silent when options haven't loaded yet (the common
|
|
704
|
+
// async + allowCustomValue case), where the value type can't be known.
|
|
705
|
+
if (isDevMode() && term !== '' && this.options().some((opt) => typeof opt.value !== 'string')) {
|
|
706
|
+
console.warn('[tn-autocomplete] allowCustomValue committed free text into a control whose option '
|
|
707
|
+
+ 'values are not strings — custom values are only sound for string-valued autocompletes.');
|
|
618
708
|
}
|
|
619
709
|
const value = term === '' ? null : term;
|
|
620
710
|
this.selectedValue.set(value);
|
|
621
711
|
this.onChange(value);
|
|
622
712
|
}
|
|
713
|
+
/**
|
|
714
|
+
* Resolves a committed value back to its option's display label, falling
|
|
715
|
+
* back to the raw value's string until the matching option is available.
|
|
716
|
+
*/
|
|
717
|
+
displayValue(value) {
|
|
718
|
+
const match = this.options().find((opt) => this.valueMatches(opt.value, value));
|
|
719
|
+
return match ? match.label : String(value);
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Compares option values, honoring `compareWith` when provided. Without one,
|
|
723
|
+
* falls back to identity (`===`) — which never matches structurally-equal
|
|
724
|
+
* objects from different references, so display resolution and selection
|
|
725
|
+
* silently fail. Warn once on that misuse (unconditional, like `tn-select`,
|
|
726
|
+
* so prod monitoring catches it) and return `false` to make it loud.
|
|
727
|
+
*/
|
|
728
|
+
valueMatches(a, b) {
|
|
729
|
+
const comparator = this.compareWith();
|
|
730
|
+
if (comparator) {
|
|
731
|
+
return comparator(a, b);
|
|
732
|
+
}
|
|
733
|
+
if (a === b) {
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
if (typeof a === 'object' && typeof b === 'object' && a !== null && b !== null) {
|
|
737
|
+
if (!this.warnedAboutObjectCompare) {
|
|
738
|
+
this.warnedAboutObjectCompare = true;
|
|
739
|
+
console.warn('[tn-autocomplete] Comparing object option values without a `compareWith` input. ' +
|
|
740
|
+
'Identity comparison will not match structurally-equal objects from different ' +
|
|
741
|
+
'references, so the committed value will not resolve to its label. ' +
|
|
742
|
+
'Provide `[compareWith]="(a, b) => a?.id === b?.id"` (or similar).');
|
|
743
|
+
}
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
623
748
|
selectOption(option) {
|
|
624
|
-
this.selectedValue.set(option);
|
|
625
|
-
this.searchTerm.set(
|
|
626
|
-
this.onChange(option);
|
|
749
|
+
this.selectedValue.set(option.value);
|
|
750
|
+
this.searchTerm.set(option.label);
|
|
751
|
+
this.onChange(option.value);
|
|
627
752
|
this.optionSelected.emit(option);
|
|
628
753
|
this.close();
|
|
629
754
|
}
|
|
@@ -632,7 +757,13 @@ class TnAutocompleteComponent {
|
|
|
632
757
|
return;
|
|
633
758
|
}
|
|
634
759
|
this.isOpen.set(true);
|
|
760
|
+
// Seed the keyboard cursor on the committed option so ArrowDown resumes
|
|
761
|
+
// from the current value (and it scrolls into view). -1 when nothing is
|
|
762
|
+
// committed or the value has no visible, enabled option — e.g. a custom
|
|
763
|
+
// free-text value, or a value whose option hasn't loaded yet.
|
|
764
|
+
this.highlightedIndex.set(this.selectedOptionIndex());
|
|
635
765
|
this.attachOverlay();
|
|
766
|
+
this.scrollToHighlighted();
|
|
636
767
|
this.opened.emit();
|
|
637
768
|
}
|
|
638
769
|
close() {
|
|
@@ -697,13 +828,13 @@ class TnAutocompleteComponent {
|
|
|
697
828
|
}
|
|
698
829
|
}
|
|
699
830
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnAutocompleteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
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 },
|
|
831
|
+
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: [
|
|
701
832
|
{
|
|
702
833
|
provide: NG_VALUE_ACCESSOR,
|
|
703
834
|
useExisting: forwardRef(() => TnAutocompleteComponent),
|
|
704
835
|
multi: true,
|
|
705
836
|
},
|
|
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]=\"
|
|
837
|
+
], 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"] }] });
|
|
707
838
|
}
|
|
708
839
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnAutocompleteComponent, decorators: [{
|
|
709
840
|
type: Component,
|
|
@@ -713,8 +844,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
713
844
|
useExisting: forwardRef(() => TnAutocompleteComponent),
|
|
714
845
|
multi: true,
|
|
715
846
|
},
|
|
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]=\"
|
|
717
|
-
}], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }],
|
|
847
|
+
], 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"] }]
|
|
848
|
+
}], 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 }] }] } });
|
|
718
849
|
|
|
719
850
|
/**
|
|
720
851
|
* Harness for interacting with tn-autocomplete in tests.
|
|
@@ -4076,6 +4207,164 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
4076
4207
|
args: [{ selector: 'tn-chip', standalone: true, imports: [CommonModule, A11yModule, TnIconComponent, TnTestIdDirective, LabelMarkupPipe, LabelTextPipe], template: "<div\n #chipEl\n role=\"button\"\n tnTestIdType=\"chip\"\n [ngClass]=\"classes()\"\n [tnTestId]=\"testId()\"\n [attr.aria-label]=\"label() | tnLabelText\"\n [attr.aria-disabled]=\"disabled()\"\n [attr.tabindex]=\"disabled() ? -1 : 0\"\n (click)=\"handleClick($event)\"\n (keydown)=\"handleKeyDown($event)\"\n>\n @if (icon()) {\n <tn-icon class=\"tn-chip__icon\" size=\"sm\" [name]=\"icon()!\" />\n }\n <span class=\"tn-chip__label\" [innerHTML]=\"label() | tnLabelMarkup\"></span>\n @if (closable()) {\n <button\n type=\"button\"\n class=\"tn-chip__close\"\n [attr.aria-label]=\"'Remove ' + (label() | tnLabelText)\"\n [disabled]=\"disabled()\"\n (click)=\"handleClose($event)\"\n >\n <span class=\"tn-chip__close-icon\">\u00D7</span>\n </button>\n }\n</div>", styles: [".tn-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:16px;font-family:var(--tn-font-family-body);font-size:14px;font-weight:500;line-height:1.2;cursor:pointer;transition:all .2s ease-in-out;border:1px solid transparent;outline:none;-webkit-user-select:none;user-select:none}.tn-chip:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-chip:hover:not(.tn-chip--disabled){transform:translateY(-1px);box-shadow:0 2px 4px #0000001a}.tn-chip--primary{background-color:var(--tn-primary);color:var(--tn-primary-txt);border-color:var(--tn-primary)}.tn-chip--primary:hover:not(.tn-chip--disabled){background-color:var(--tn-blue);border-color:var(--tn-blue)}.tn-chip--secondary{background-color:var(--tn-alt-bg1);color:var(--tn-alt-fg2);border-color:var(--tn-alt-bg2)}.tn-chip--secondary:hover:not(.tn-chip--disabled){background-color:var(--tn-alt-bg2);border-color:var(--tn-accent)}.tn-chip--accent{background-color:var(--tn-accent);color:var(--tn-fg1);border-color:var(--tn-accent)}.tn-chip--accent:hover:not(.tn-chip--disabled){background-color:var(--tn-alt-bg2);border-color:var(--tn-alt-bg2)}.tn-chip--disabled{opacity:.5;cursor:not-allowed;pointer-events:none}.tn-chip--closable{padding-right:8px}.tn-chip__icon{display:flex;align-items:center;justify-content:center;width:16px;height:16px;font-size:12px}.tn-chip__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-chip__label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px}.tn-chip__close{display:flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;margin:0;border:none;border-radius:50%;background-color:#fff3;color:inherit;cursor:pointer;transition:background-color .2s ease-in-out;outline:none}.tn-chip__close:hover:not(:disabled){background-color:#ffffff4d}.tn-chip__close:focus-visible{outline:1px solid currentColor;outline-offset:1px}.tn-chip__close:disabled{cursor:not-allowed;opacity:.5}.tn-chip__close-icon{font-size:14px;font-weight:700;line-height:1}.tn-dark .tn-chip--secondary .tn-chip__close{background-color:#0003}.tn-dark .tn-chip--secondary .tn-chip__close:hover:not(:disabled){background-color:#0000004d}.high-contrast .tn-chip{border-width:2px}.high-contrast .tn-chip:focus-visible{outline-width:3px}\n"] }]
|
|
4077
4208
|
}], propDecorators: { chipEl: [{ type: i0.ViewChild, args: ['chipEl', { isSignal: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], closable: [{ type: i0.Input, args: [{ isSignal: true, alias: "closable", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], onClose: [{ type: i0.Output, args: ["onClose"] }], onClick: [{ type: i0.Output, args: ["onClick"] }] } });
|
|
4078
4209
|
|
|
4210
|
+
/**
|
|
4211
|
+
* Harness for interacting with tn-chip in tests.
|
|
4212
|
+
* Provides methods for reading chip state and simulating user interactions
|
|
4213
|
+
* (selecting the chip and dismissing a closable chip).
|
|
4214
|
+
*
|
|
4215
|
+
* @example
|
|
4216
|
+
* ```typescript
|
|
4217
|
+
* // Find a chip by label and click it
|
|
4218
|
+
* const chip = await loader.getHarness(TnChipHarness.with({ label: 'Active' }));
|
|
4219
|
+
* await chip.click();
|
|
4220
|
+
*
|
|
4221
|
+
* // Dismiss a closable chip
|
|
4222
|
+
* const filter = await loader.getHarness(TnChipHarness.with({ label: 'Has SSH Access' }));
|
|
4223
|
+
* if (await filter.isClosable()) {
|
|
4224
|
+
* await filter.close();
|
|
4225
|
+
* }
|
|
4226
|
+
* ```
|
|
4227
|
+
*/
|
|
4228
|
+
class TnChipHarness extends ComponentHarness {
|
|
4229
|
+
/**
|
|
4230
|
+
* The selector for the host element of a `TnChipComponent` instance.
|
|
4231
|
+
*/
|
|
4232
|
+
static hostSelector = 'tn-chip';
|
|
4233
|
+
_chip = this.locatorFor('.tn-chip');
|
|
4234
|
+
_label = this.locatorFor('.tn-chip__label');
|
|
4235
|
+
_icon = this.locatorForOptional('.tn-chip__icon');
|
|
4236
|
+
_closeButton = this.locatorForOptional('.tn-chip__close');
|
|
4237
|
+
/**
|
|
4238
|
+
* Gets a `HarnessPredicate` that can be used to search for a chip
|
|
4239
|
+
* with specific attributes.
|
|
4240
|
+
*
|
|
4241
|
+
* @param options Options for filtering which chip instances are considered a match.
|
|
4242
|
+
* @returns A `HarnessPredicate` configured with the given options.
|
|
4243
|
+
*
|
|
4244
|
+
* @example
|
|
4245
|
+
* ```typescript
|
|
4246
|
+
* // Find chip by exact label
|
|
4247
|
+
* const chip = await loader.getHarness(TnChipHarness.with({ label: 'Production' }));
|
|
4248
|
+
*
|
|
4249
|
+
* // Find chip with regex pattern
|
|
4250
|
+
* const chip = await loader.getHarness(TnChipHarness.with({ label: /access/i }));
|
|
4251
|
+
* ```
|
|
4252
|
+
*/
|
|
4253
|
+
static with(options = {}) {
|
|
4254
|
+
return new HarnessPredicate(TnChipHarness, options)
|
|
4255
|
+
.addOption('label', options.label, (harness, label) => HarnessPredicate.stringMatches(harness.getLabel(), label));
|
|
4256
|
+
}
|
|
4257
|
+
/**
|
|
4258
|
+
* Gets the chip's label text.
|
|
4259
|
+
*
|
|
4260
|
+
* @returns Promise resolving to the chip's text content.
|
|
4261
|
+
*
|
|
4262
|
+
* @example
|
|
4263
|
+
* ```typescript
|
|
4264
|
+
* const chip = await loader.getHarness(TnChipHarness);
|
|
4265
|
+
* expect(await chip.getLabel()).toBe('Active');
|
|
4266
|
+
* ```
|
|
4267
|
+
*/
|
|
4268
|
+
async getLabel() {
|
|
4269
|
+
const label = await this._label();
|
|
4270
|
+
return (await label.text()).trim();
|
|
4271
|
+
}
|
|
4272
|
+
/**
|
|
4273
|
+
* Gets the chip's color variant (`primary`, `secondary`, or `accent`).
|
|
4274
|
+
*
|
|
4275
|
+
* @returns Promise resolving to the chip's color.
|
|
4276
|
+
*
|
|
4277
|
+
* @example
|
|
4278
|
+
* ```typescript
|
|
4279
|
+
* const chip = await loader.getHarness(TnChipHarness);
|
|
4280
|
+
* expect(await chip.getColor()).toBe('primary');
|
|
4281
|
+
* ```
|
|
4282
|
+
*/
|
|
4283
|
+
async getColor() {
|
|
4284
|
+
const chip = await this._chip();
|
|
4285
|
+
const classAttr = (await chip.getAttribute('class')) ?? '';
|
|
4286
|
+
const match = classAttr.match(/tn-chip--(primary|secondary|accent)/);
|
|
4287
|
+
return match?.[1] ?? 'primary';
|
|
4288
|
+
}
|
|
4289
|
+
/**
|
|
4290
|
+
* Checks whether the chip is disabled.
|
|
4291
|
+
*
|
|
4292
|
+
* @returns Promise resolving to true if the chip is disabled.
|
|
4293
|
+
*
|
|
4294
|
+
* @example
|
|
4295
|
+
* ```typescript
|
|
4296
|
+
* const chip = await loader.getHarness(TnChipHarness);
|
|
4297
|
+
* expect(await chip.isDisabled()).toBe(false);
|
|
4298
|
+
* ```
|
|
4299
|
+
*/
|
|
4300
|
+
async isDisabled() {
|
|
4301
|
+
const chip = await this._chip();
|
|
4302
|
+
return (await chip.getAttribute('aria-disabled')) === 'true';
|
|
4303
|
+
}
|
|
4304
|
+
/**
|
|
4305
|
+
* Checks whether the chip renders a close (dismiss) button.
|
|
4306
|
+
*
|
|
4307
|
+
* @returns Promise resolving to true if the chip is closable.
|
|
4308
|
+
*
|
|
4309
|
+
* @example
|
|
4310
|
+
* ```typescript
|
|
4311
|
+
* const chip = await loader.getHarness(TnChipHarness);
|
|
4312
|
+
* expect(await chip.isClosable()).toBe(true);
|
|
4313
|
+
* ```
|
|
4314
|
+
*/
|
|
4315
|
+
async isClosable() {
|
|
4316
|
+
return (await this._closeButton()) !== null;
|
|
4317
|
+
}
|
|
4318
|
+
/**
|
|
4319
|
+
* Checks whether the chip renders a leading icon.
|
|
4320
|
+
*
|
|
4321
|
+
* @returns Promise resolving to true if the chip has an icon.
|
|
4322
|
+
*
|
|
4323
|
+
* @example
|
|
4324
|
+
* ```typescript
|
|
4325
|
+
* const chip = await loader.getHarness(TnChipHarness);
|
|
4326
|
+
* expect(await chip.hasIcon()).toBe(true);
|
|
4327
|
+
* ```
|
|
4328
|
+
*/
|
|
4329
|
+
async hasIcon() {
|
|
4330
|
+
return (await this._icon()) !== null;
|
|
4331
|
+
}
|
|
4332
|
+
/**
|
|
4333
|
+
* Clicks the chip body, triggering its `onClick` output.
|
|
4334
|
+
*
|
|
4335
|
+
* @returns Promise that resolves when the click action is complete.
|
|
4336
|
+
*
|
|
4337
|
+
* @example
|
|
4338
|
+
* ```typescript
|
|
4339
|
+
* const chip = await loader.getHarness(TnChipHarness.with({ label: 'Active' }));
|
|
4340
|
+
* await chip.click();
|
|
4341
|
+
* ```
|
|
4342
|
+
*/
|
|
4343
|
+
async click() {
|
|
4344
|
+
const chip = await this._chip();
|
|
4345
|
+
return chip.click();
|
|
4346
|
+
}
|
|
4347
|
+
/**
|
|
4348
|
+
* Clicks the chip's close button, triggering its `onClose` output.
|
|
4349
|
+
* Throws if the chip is not closable.
|
|
4350
|
+
*
|
|
4351
|
+
* @returns Promise that resolves when the close action is complete.
|
|
4352
|
+
*
|
|
4353
|
+
* @example
|
|
4354
|
+
* ```typescript
|
|
4355
|
+
* const chip = await loader.getHarness(TnChipHarness.with({ label: 'Has SSH Access' }));
|
|
4356
|
+
* await chip.close();
|
|
4357
|
+
* ```
|
|
4358
|
+
*/
|
|
4359
|
+
async close() {
|
|
4360
|
+
const closeButton = await this._closeButton();
|
|
4361
|
+
if (!closeButton) {
|
|
4362
|
+
throw new Error('Cannot close a chip that is not closable (no close button rendered).');
|
|
4363
|
+
}
|
|
4364
|
+
return closeButton.click();
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4079
4368
|
class TnCardHeaderDirective {
|
|
4080
4369
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCardHeaderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
4081
4370
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnCardHeaderDirective, isStandalone: true, selector: "[tnCardHeader]", ngImport: i0 });
|
|
@@ -16066,5 +16355,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
16066
16355
|
* Generated bundle index. Do not edit.
|
|
16067
16356
|
*/
|
|
16068
16357
|
|
|
16069
|
-
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, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, 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, 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, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, formatSize, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, parseLabelMarkup, parseSize, registerLucideIcons, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
|
|
16358
|
+
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, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnChipHarness, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, 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, 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, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, formatSize, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, parseLabelMarkup, parseSize, registerLucideIcons, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
|
|
16070
16359
|
//# sourceMappingURL=truenas-ui-components.mjs.map
|