@truenas/ui-components 0.1.9 → 0.1.11
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.
package/README.md
CHANGED
|
@@ -54,6 +54,30 @@ Or import in your main styles file:
|
|
|
54
54
|
@import '@truenas/ui-components/src/styles/themes.css';
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
### 3. Configure Icon System (Required)
|
|
58
|
+
|
|
59
|
+
The icon system requires the sprite to be loaded before the application renders. Add this to your `main.ts` or application bootstrap:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { provideAppInitializer } from '@angular/core';
|
|
63
|
+
import { TnSpriteLoaderService } from '@truenas/ui-components';
|
|
64
|
+
|
|
65
|
+
bootstrapApplication(AppComponent, {
|
|
66
|
+
providers: [
|
|
67
|
+
// ... other providers
|
|
68
|
+
provideAppInitializer(() => {
|
|
69
|
+
const spriteLoader = inject(TnSpriteLoaderService);
|
|
70
|
+
return spriteLoader.ensureSpriteLoaded();
|
|
71
|
+
}),
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Why is this required?**
|
|
77
|
+
- Icons are rendered synchronously for optimal performance and test reliability
|
|
78
|
+
- The sprite must be preloaded at app startup to avoid showing fallback text
|
|
79
|
+
- Without this initializer, icons will display as abbreviations (e.g., "FO" for "folder") until the sprite loads
|
|
80
|
+
|
|
57
81
|
## Icon System
|
|
58
82
|
|
|
59
83
|
### Using Icons
|
|
@@ -324,6 +348,46 @@ projects/truenas-ui/
|
|
|
324
348
|
- Firefox (latest)
|
|
325
349
|
- Safari (latest)
|
|
326
350
|
|
|
351
|
+
## Migration Guide
|
|
352
|
+
|
|
353
|
+
### Upgrading to v0.2.0+ (Icon System Changes)
|
|
354
|
+
|
|
355
|
+
**Breaking Change:** The icon system now requires `APP_INITIALIZER` setup.
|
|
356
|
+
|
|
357
|
+
**Before (v0.1.x):**
|
|
358
|
+
```typescript
|
|
359
|
+
// Icons worked without any special setup
|
|
360
|
+
bootstrapApplication(AppComponent, {
|
|
361
|
+
providers: [/* your providers */]
|
|
362
|
+
});
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**After (v0.2.0+):**
|
|
366
|
+
```typescript
|
|
367
|
+
import { provideAppInitializer, inject } from '@angular/core';
|
|
368
|
+
import { TnSpriteLoaderService } from '@truenas/ui-components';
|
|
369
|
+
|
|
370
|
+
bootstrapApplication(AppComponent, {
|
|
371
|
+
providers: [
|
|
372
|
+
provideAppInitializer(() => {
|
|
373
|
+
const spriteLoader = inject(TnSpriteLoaderService);
|
|
374
|
+
return spriteLoader.ensureSpriteLoaded();
|
|
375
|
+
}),
|
|
376
|
+
// ... your other providers
|
|
377
|
+
]
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**What changed:**
|
|
382
|
+
- Icons now resolve synchronously for better performance and test reliability
|
|
383
|
+
- Sprite must be preloaded at app startup via `APP_INITIALIZER`
|
|
384
|
+
- Without this setup, icons will show text fallbacks until sprite loads asynchronously
|
|
385
|
+
|
|
386
|
+
**Benefits:**
|
|
387
|
+
- ✅ Faster icon rendering (no async delay)
|
|
388
|
+
- ✅ More reliable unit tests (no timing issues)
|
|
389
|
+
- ✅ Cleaner code (no `whenStable()` workarounds needed)
|
|
390
|
+
|
|
327
391
|
## License
|
|
328
392
|
|
|
329
393
|
Copyright © iXsystems, Inc.
|
|
@@ -1 +1,3 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg"
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 29">
|
|
2
|
+
<path fill-rule="evenodd" d="M34.436 10.874A12.849 12.849 0 0 0 21.755.004 14.536 14.536 0 0 0 9.073 7.25 10.404 10.404 0 0 0 .015 18.12c0 6.003 4.866 10.87 10.87 10.87h23.551a9.058 9.058 0 0 0 9.058-9.058 9.67 9.67 0 0 0-9.058-9.058ZM34.319 14.237v5.358l-11.47 6.626v-5.358ZM20.654 20.863v5.358L9.175 19.594v-5.357l5.187 2.99a.076.076 0 0 0 .019.012ZM26.392 16.283l-4.641 2.685-4.644-2.68 4.644-2.681ZM33.226 12.339l-4.643 2.68-5.734-3.314V6.343ZM20.654 6.343v5.362l-5.741 3.311-4.644-2.677Z"/>
|
|
3
|
+
</svg>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { input, ChangeDetectionStrategy, Component, inject, Injectable, viewChild,
|
|
2
|
+
import { input, ChangeDetectionStrategy, Component, inject, Injectable, viewChild, signal, effect, computed, ViewEncapsulation, Directive, contentChildren, output, forwardRef, ElementRef, ViewContainerRef, contentChild, ChangeDetectorRef, HostListener, TemplateRef, IterableDiffers, Pipe, model, PLATFORM_ID } from '@angular/core';
|
|
3
3
|
import * as i1$1 from '@angular/common';
|
|
4
4
|
import { CommonModule, DOCUMENT, isPlatformBrowser } from '@angular/common';
|
|
5
5
|
import { mdiCheckCircle, mdiAlertCircle, mdiAlert, mdiInformation, mdiDotsVertical, mdiFolderOpen, mdiLock, mdiLoading, mdiFolderPlus, mdiFolderNetwork, mdiHarddisk, mdiDatabase, mdiFile, mdiFolder } from '@mdi/js';
|
|
@@ -403,78 +403,55 @@ class TnIconComponent {
|
|
|
403
403
|
*/
|
|
404
404
|
fullSize = input(false, ...(ngDevMode ? [{ debugName: "fullSize" }] : []));
|
|
405
405
|
svgContainer = viewChild('svgContainer', ...(ngDevMode ? [{ debugName: "svgContainer" }] : []));
|
|
406
|
-
iconResult = { source: 'text', content: '?' };
|
|
406
|
+
iconResult = signal({ source: 'text', content: '?' }, ...(ngDevMode ? [{ debugName: "iconResult" }] : []));
|
|
407
407
|
iconRegistry = inject(TnIconRegistryService);
|
|
408
|
-
cdr = inject(ChangeDetectorRef);
|
|
409
408
|
sanitizer = inject(DomSanitizer);
|
|
410
409
|
constructor() {
|
|
411
|
-
//
|
|
410
|
+
// Effect 1: Resolve icons when name or library changes
|
|
412
411
|
effect(() => {
|
|
413
|
-
|
|
414
|
-
this.
|
|
415
|
-
this.library
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
412
|
+
const name = this.name();
|
|
413
|
+
const library = this.library();
|
|
414
|
+
const result = this.resolveIcon(name, library);
|
|
415
|
+
this.iconResult.set(result);
|
|
416
|
+
});
|
|
417
|
+
// Effect 2: Update SVG DOM when container is available and content changes
|
|
418
|
+
effect(() => {
|
|
419
|
+
const container = this.svgContainer();
|
|
420
|
+
const content = this.sanitizedContent();
|
|
421
|
+
const result = this.iconResult();
|
|
422
|
+
if (result.source === 'svg' && container && typeof content === 'string') {
|
|
423
|
+
// Bypass Angular's sanitization by setting innerHTML directly
|
|
424
|
+
container.nativeElement.innerHTML = content;
|
|
425
|
+
}
|
|
427
426
|
});
|
|
428
|
-
}
|
|
429
|
-
ngAfterViewInit() {
|
|
430
|
-
this.updateSvgContent();
|
|
431
427
|
}
|
|
432
428
|
effectiveAriaLabel = computed(() => {
|
|
433
429
|
return this.ariaLabel() || this.name() || 'Icon';
|
|
434
430
|
}, ...(ngDevMode ? [{ debugName: "effectiveAriaLabel" }] : []));
|
|
435
431
|
sanitizedContent = computed(() => {
|
|
436
|
-
const content = this.iconResult.content;
|
|
432
|
+
const content = this.iconResult().content;
|
|
437
433
|
// Handle mock SafeHtml objects from Storybook
|
|
438
434
|
if (content && typeof content === 'object' && content.changingThisBreaksApplicationSecurity) {
|
|
439
435
|
return content.changingThisBreaksApplicationSecurity;
|
|
440
436
|
}
|
|
441
437
|
return content;
|
|
442
438
|
}, ...(ngDevMode ? [{ debugName: "sanitizedContent" }] : []));
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const content = this.sanitizedContent();
|
|
447
|
-
if (typeof content === 'string') {
|
|
448
|
-
// Bypass Angular's sanitization by setting innerHTML directly
|
|
449
|
-
svgContainer.nativeElement.innerHTML = content;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
async resolveIcon() {
|
|
454
|
-
if (!this.name()) {
|
|
455
|
-
this.iconResult = { source: 'text', content: '?' };
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
// Wait for sprite to load (if it's being loaded)
|
|
459
|
-
try {
|
|
460
|
-
await this.iconRegistry.getSpriteLoader().ensureSpriteLoaded();
|
|
461
|
-
}
|
|
462
|
-
catch (error) {
|
|
463
|
-
// Sprite loading failed, continue with other resolution methods
|
|
464
|
-
console.warn('[TnIcon] Sprite loading failed, falling back to other icon sources:', error);
|
|
439
|
+
resolveIcon(name, library) {
|
|
440
|
+
if (!name) {
|
|
441
|
+
return { source: 'text', content: '?' };
|
|
465
442
|
}
|
|
466
443
|
// Construct the effective icon name based on library attribute
|
|
467
|
-
let effectiveIconName =
|
|
468
|
-
if (
|
|
469
|
-
effectiveIconName = `mdi-${
|
|
444
|
+
let effectiveIconName = name;
|
|
445
|
+
if (library === 'mdi' && !name.startsWith('mdi-')) {
|
|
446
|
+
effectiveIconName = `mdi-${name}`;
|
|
470
447
|
}
|
|
471
|
-
else if (
|
|
448
|
+
else if (library === 'material' && !name.startsWith('mat-')) {
|
|
472
449
|
// Material icons get mat- prefix in sprite
|
|
473
|
-
effectiveIconName = `mat-${
|
|
450
|
+
effectiveIconName = `mat-${name}`;
|
|
474
451
|
}
|
|
475
|
-
else if (
|
|
452
|
+
else if (library === 'lucide' && !name.includes(':')) {
|
|
476
453
|
// Convert to registry format for Lucide icons
|
|
477
|
-
effectiveIconName = `lucide:${
|
|
454
|
+
effectiveIconName = `lucide:${name}`;
|
|
478
455
|
}
|
|
479
456
|
// 1. Try icon registry (libraries and custom icons)
|
|
480
457
|
const iconOptions = {
|
|
@@ -490,29 +467,25 @@ class TnIconComponent {
|
|
|
490
467
|
}
|
|
491
468
|
}
|
|
492
469
|
if (registryResult) {
|
|
493
|
-
|
|
494
|
-
return;
|
|
470
|
+
return registryResult;
|
|
495
471
|
}
|
|
496
472
|
// 2. Try built-in third-party patterns (deprecated - use registry instead)
|
|
497
473
|
const thirdPartyResult = this.tryThirdPartyIcon(effectiveIconName);
|
|
498
474
|
if (thirdPartyResult) {
|
|
499
|
-
|
|
500
|
-
return;
|
|
475
|
+
return thirdPartyResult;
|
|
501
476
|
}
|
|
502
477
|
// 3. Try CSS class (Font Awesome, Material Icons, etc.)
|
|
503
478
|
const cssResult = this.tryCssIcon(effectiveIconName);
|
|
504
479
|
if (cssResult) {
|
|
505
|
-
|
|
506
|
-
return;
|
|
480
|
+
return cssResult;
|
|
507
481
|
}
|
|
508
482
|
// 4. Try Unicode mapping
|
|
509
483
|
const unicodeResult = this.tryUnicodeIcon(effectiveIconName);
|
|
510
484
|
if (unicodeResult) {
|
|
511
|
-
|
|
512
|
-
return;
|
|
485
|
+
return unicodeResult;
|
|
513
486
|
}
|
|
514
487
|
// 5. Fallback to text abbreviation
|
|
515
|
-
|
|
488
|
+
return {
|
|
516
489
|
source: 'text',
|
|
517
490
|
content: this.generateTextAbbreviation(effectiveIconName)
|
|
518
491
|
};
|
|
@@ -557,13 +530,6 @@ class TnIconComponent {
|
|
|
557
530
|
content: `material-icons material-icons-${materialName}`
|
|
558
531
|
};
|
|
559
532
|
}
|
|
560
|
-
// Check if class exists in document
|
|
561
|
-
if (this.cssClassExists(name)) {
|
|
562
|
-
return {
|
|
563
|
-
source: 'css',
|
|
564
|
-
content: name
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
533
|
return null;
|
|
568
534
|
}
|
|
569
535
|
tryUnicodeIcon(name) {
|
|
@@ -604,16 +570,8 @@ class TnIconComponent {
|
|
|
604
570
|
// Default to first 2 characters
|
|
605
571
|
return name.substring(0, 2).toUpperCase();
|
|
606
572
|
}
|
|
607
|
-
cssClassExists(_className) {
|
|
608
|
-
if (typeof document === 'undefined') {
|
|
609
|
-
return false;
|
|
610
|
-
}
|
|
611
|
-
// For now, only return true for known CSS icon patterns
|
|
612
|
-
// In real implementation, consumers would override this method
|
|
613
|
-
return false; // Disable generic CSS class checking for now
|
|
614
|
-
}
|
|
615
573
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
616
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnIconComponent, isStandalone: true, selector: "tn-icon", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, library: { classPropertyName: "library", publicName: "library", isSignal: true, isRequired: false, transformFunction: null }, fullSize: { classPropertyName: "fullSize", publicName: "fullSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.name": "name()", "attr.library": "library()", "attr.size": "size()", "attr.color": "color()", "attr.full-size": "fullSize() || null" } }, viewQueries: [{ propertyName: "svgContainer", first: true, predicate: ["svgContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"tn-icon\"\n role=\"img\"\n [ngClass]=\"fullSize() ? 'tn-icon--full' : 'tn-icon--' + size()\"\n [style.color]=\"color()\"\n [attr.aria-label]=\"effectiveAriaLabel()\"\n [attr.title]=\"tooltip()\">\n \n\n @switch (iconResult.source) {\n <!-- Sprite icons (from generated sprite.svg) -->\n @case ('sprite') {\n <svg\n class=\"tn-icon__sprite\"\n aria-hidden=\"true\">\n <use [attr.href]=\"iconResult.spriteUrl\" />\n </svg>\n }\n\n <!-- SVG content (from third-party libraries or assets) -->\n @case ('svg') {\n <div\n #svgContainer\n class=\"tn-icon__svg\">\n </div>\n }\n\n <!-- CSS class icons (Font Awesome, Material Icons, etc.) -->\n @case ('css') {\n <i\n aria-hidden=\"true\"\n [class]=\"'tn-icon__css ' + iconResult.content\">\n </i>\n }\n\n <!-- Unicode characters -->\n @case ('unicode') {\n <span\n class=\"tn-icon__unicode\"\n aria-hidden=\"true\">{{ iconResult.content }}</span>\n }\n\n <!-- Text abbreviation fallback -->\n @default {\n <span\n class=\"tn-icon__text\"\n aria-hidden=\"true\">{{ iconResult.content }}</span>\n }\n }\n</div>", styles: [".tn-icon{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.tn-icon--xs{width:var(--tn-icon-xs)!important;height:var(--tn-icon-xs)!important;font-size:var(--tn-icon-xs)!important}.tn-icon--sm{width:var(--tn-icon-sm)!important;height:var(--tn-icon-sm)!important;font-size:var(--tn-icon-sm)!important}.tn-icon--md{width:var(--tn-icon-md)!important;height:var(--tn-icon-md)!important;font-size:var(--tn-icon-md)!important}.tn-icon--lg{width:var(--tn-icon-lg)!important;height:var(--tn-icon-lg)!important;font-size:var(--tn-icon-lg)!important}.tn-icon--xl{width:var(--tn-icon-xl)!important;height:var(--tn-icon-xl)!important;font-size:var(--tn-icon-xl)!important}.tn-icon--full{width:100%!important;height:100%!important}.tn-icon__sprite{width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__svg{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.tn-icon__svg :global(svg){width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__css{font-size:inherit;line-height:1;color:inherit}.tn-icon__unicode{font-size:inherit;line-height:1;color:inherit;text-align:center}.tn-icon__text{font-size:.75em;font-weight:600;line-height:1;color:inherit;text-align:center;opacity:.7}.tn-icon{width:var(--tn-icon-size, var(--tn-icon-md));height:var(--tn-icon-size, var(--tn-icon-md));color:var(--tn-icon-color, currentColor)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
574
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnIconComponent, isStandalone: true, selector: "tn-icon", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, library: { classPropertyName: "library", publicName: "library", isSignal: true, isRequired: false, transformFunction: null }, fullSize: { classPropertyName: "fullSize", publicName: "fullSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.name": "name()", "attr.library": "library()", "attr.size": "size()", "attr.color": "color()", "attr.full-size": "fullSize() || null" } }, viewQueries: [{ propertyName: "svgContainer", first: true, predicate: ["svgContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"tn-icon\"\n role=\"img\"\n [ngClass]=\"fullSize() ? 'tn-icon--full' : 'tn-icon--' + size()\"\n [style.color]=\"color()\"\n [attr.aria-label]=\"effectiveAriaLabel()\"\n [attr.title]=\"tooltip()\">\n \n\n @switch (iconResult().source) {\n <!-- Sprite icons (from generated sprite.svg) -->\n @case ('sprite') {\n <svg\n class=\"tn-icon__sprite\"\n aria-hidden=\"true\">\n <use [attr.href]=\"iconResult().spriteUrl\" />\n </svg>\n }\n\n <!-- SVG content (from third-party libraries or assets) -->\n @case ('svg') {\n <div\n #svgContainer\n class=\"tn-icon__svg\">\n </div>\n }\n\n <!-- CSS class icons (Font Awesome, Material Icons, etc.) -->\n @case ('css') {\n <i\n aria-hidden=\"true\"\n [class]=\"'tn-icon__css ' + iconResult().content\">\n </i>\n }\n\n <!-- Unicode characters -->\n @case ('unicode') {\n <span\n class=\"tn-icon__unicode\"\n aria-hidden=\"true\">{{ iconResult().content }}</span>\n }\n\n <!-- Text abbreviation fallback -->\n @default {\n <span\n class=\"tn-icon__text\"\n aria-hidden=\"true\">{{ iconResult().content }}</span>\n }\n }\n</div>", styles: [".tn-icon{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.tn-icon--xs{width:var(--tn-icon-xs)!important;height:var(--tn-icon-xs)!important;font-size:var(--tn-icon-xs)!important}.tn-icon--sm{width:var(--tn-icon-sm)!important;height:var(--tn-icon-sm)!important;font-size:var(--tn-icon-sm)!important}.tn-icon--md{width:var(--tn-icon-md)!important;height:var(--tn-icon-md)!important;font-size:var(--tn-icon-md)!important}.tn-icon--lg{width:var(--tn-icon-lg)!important;height:var(--tn-icon-lg)!important;font-size:var(--tn-icon-lg)!important}.tn-icon--xl{width:var(--tn-icon-xl)!important;height:var(--tn-icon-xl)!important;font-size:var(--tn-icon-xl)!important}.tn-icon--full{width:100%!important;height:100%!important}.tn-icon__sprite{width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__svg{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.tn-icon__svg :global(svg){width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__css{font-size:inherit;line-height:1;color:inherit}.tn-icon__unicode{font-size:inherit;line-height:1;color:inherit;text-align:center}.tn-icon__text{font-size:.75em;font-weight:600;line-height:1;color:inherit;text-align:center;opacity:.7}.tn-icon{width:var(--tn-icon-size, var(--tn-icon-md));height:var(--tn-icon-size, var(--tn-icon-md));color:var(--tn-icon-color, currentColor)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
617
575
|
}
|
|
618
576
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnIconComponent, decorators: [{
|
|
619
577
|
type: Component,
|
|
@@ -623,7 +581,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
623
581
|
'[attr.size]': 'size()',
|
|
624
582
|
'[attr.color]': 'color()',
|
|
625
583
|
'[attr.full-size]': 'fullSize() || null'
|
|
626
|
-
}, template: "<div\n class=\"tn-icon\"\n role=\"img\"\n [ngClass]=\"fullSize() ? 'tn-icon--full' : 'tn-icon--' + size()\"\n [style.color]=\"color()\"\n [attr.aria-label]=\"effectiveAriaLabel()\"\n [attr.title]=\"tooltip()\">\n \n\n @switch (iconResult.source) {\n <!-- Sprite icons (from generated sprite.svg) -->\n @case ('sprite') {\n <svg\n class=\"tn-icon__sprite\"\n aria-hidden=\"true\">\n <use [attr.href]=\"iconResult.spriteUrl\" />\n </svg>\n }\n\n <!-- SVG content (from third-party libraries or assets) -->\n @case ('svg') {\n <div\n #svgContainer\n class=\"tn-icon__svg\">\n </div>\n }\n\n <!-- CSS class icons (Font Awesome, Material Icons, etc.) -->\n @case ('css') {\n <i\n aria-hidden=\"true\"\n [class]=\"'tn-icon__css ' + iconResult.content\">\n </i>\n }\n\n <!-- Unicode characters -->\n @case ('unicode') {\n <span\n class=\"tn-icon__unicode\"\n aria-hidden=\"true\">{{ iconResult.content }}</span>\n }\n\n <!-- Text abbreviation fallback -->\n @default {\n <span\n class=\"tn-icon__text\"\n aria-hidden=\"true\">{{ iconResult.content }}</span>\n }\n }\n</div>", styles: [".tn-icon{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.tn-icon--xs{width:var(--tn-icon-xs)!important;height:var(--tn-icon-xs)!important;font-size:var(--tn-icon-xs)!important}.tn-icon--sm{width:var(--tn-icon-sm)!important;height:var(--tn-icon-sm)!important;font-size:var(--tn-icon-sm)!important}.tn-icon--md{width:var(--tn-icon-md)!important;height:var(--tn-icon-md)!important;font-size:var(--tn-icon-md)!important}.tn-icon--lg{width:var(--tn-icon-lg)!important;height:var(--tn-icon-lg)!important;font-size:var(--tn-icon-lg)!important}.tn-icon--xl{width:var(--tn-icon-xl)!important;height:var(--tn-icon-xl)!important;font-size:var(--tn-icon-xl)!important}.tn-icon--full{width:100%!important;height:100%!important}.tn-icon__sprite{width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__svg{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.tn-icon__svg :global(svg){width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__css{font-size:inherit;line-height:1;color:inherit}.tn-icon__unicode{font-size:inherit;line-height:1;color:inherit;text-align:center}.tn-icon__text{font-size:.75em;font-weight:600;line-height:1;color:inherit;text-align:center;opacity:.7}.tn-icon{width:var(--tn-icon-size, var(--tn-icon-md));height:var(--tn-icon-size, var(--tn-icon-md));color:var(--tn-icon-color, currentColor)}\n"] }]
|
|
584
|
+
}, template: "<div\n class=\"tn-icon\"\n role=\"img\"\n [ngClass]=\"fullSize() ? 'tn-icon--full' : 'tn-icon--' + size()\"\n [style.color]=\"color()\"\n [attr.aria-label]=\"effectiveAriaLabel()\"\n [attr.title]=\"tooltip()\">\n \n\n @switch (iconResult().source) {\n <!-- Sprite icons (from generated sprite.svg) -->\n @case ('sprite') {\n <svg\n class=\"tn-icon__sprite\"\n aria-hidden=\"true\">\n <use [attr.href]=\"iconResult().spriteUrl\" />\n </svg>\n }\n\n <!-- SVG content (from third-party libraries or assets) -->\n @case ('svg') {\n <div\n #svgContainer\n class=\"tn-icon__svg\">\n </div>\n }\n\n <!-- CSS class icons (Font Awesome, Material Icons, etc.) -->\n @case ('css') {\n <i\n aria-hidden=\"true\"\n [class]=\"'tn-icon__css ' + iconResult().content\">\n </i>\n }\n\n <!-- Unicode characters -->\n @case ('unicode') {\n <span\n class=\"tn-icon__unicode\"\n aria-hidden=\"true\">{{ iconResult().content }}</span>\n }\n\n <!-- Text abbreviation fallback -->\n @default {\n <span\n class=\"tn-icon__text\"\n aria-hidden=\"true\">{{ iconResult().content }}</span>\n }\n }\n</div>", styles: [".tn-icon{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.tn-icon--xs{width:var(--tn-icon-xs)!important;height:var(--tn-icon-xs)!important;font-size:var(--tn-icon-xs)!important}.tn-icon--sm{width:var(--tn-icon-sm)!important;height:var(--tn-icon-sm)!important;font-size:var(--tn-icon-sm)!important}.tn-icon--md{width:var(--tn-icon-md)!important;height:var(--tn-icon-md)!important;font-size:var(--tn-icon-md)!important}.tn-icon--lg{width:var(--tn-icon-lg)!important;height:var(--tn-icon-lg)!important;font-size:var(--tn-icon-lg)!important}.tn-icon--xl{width:var(--tn-icon-xl)!important;height:var(--tn-icon-xl)!important;font-size:var(--tn-icon-xl)!important}.tn-icon--full{width:100%!important;height:100%!important}.tn-icon__sprite{width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__svg{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.tn-icon__svg :global(svg){width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__css{font-size:inherit;line-height:1;color:inherit}.tn-icon__unicode{font-size:inherit;line-height:1;color:inherit;text-align:center}.tn-icon__text{font-size:.75em;font-weight:600;line-height:1;color:inherit;text-align:center;opacity:.7}.tn-icon{width:var(--tn-icon-size, var(--tn-icon-md));height:var(--tn-icon-size, var(--tn-icon-md));color:var(--tn-icon-color, currentColor)}\n"] }]
|
|
627
585
|
}], ctorParameters: () => [], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], library: [{ type: i0.Input, args: [{ isSignal: true, alias: "library", required: false }] }], fullSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullSize", required: false }] }], svgContainer: [{ type: i0.ViewChild, args: ['svgContainer', { isSignal: true }] }] } });
|
|
628
586
|
|
|
629
587
|
/**
|
|
@@ -2587,6 +2545,196 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
2587
2545
|
], template: "<div class=\"tn-select-container\" [attr.data-testid]=\"testId()\">\n <!-- Select Trigger -->\n <div\n class=\"tn-select-trigger\"\n role=\"combobox\"\n tabindex=\"0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"true\"\n [attr.aria-controls]=\"isOpen() ? 'tn-select-dropdown-' + testId() : null\"\n [attr.aria-label]=\"placeholder()\"\n (click)=\"toggleDropdown()\"\n (keydown)=\"onKeydown($event)\">\n\n <!-- Display Text -->\n <span\n class=\"tn-select-text\"\n [class.placeholder]=\"selectedValue() === null || selectedValue() === undefined\">\n {{ getDisplayText() }}\n </span>\n\n <!-- Dropdown Arrow -->\n <div class=\"tn-select-arrow\" [class.open]=\"isOpen()\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <polyline points=\"6,9 12,15 18,9\" />\n </svg>\n </div>\n </div>\n\n <!-- Dropdown Menu -->\n @if (isOpen()) {\n <div\n class=\"tn-select-dropdown\"\n role=\"listbox\"\n [attr.id]=\"'tn-select-dropdown-' + testId()\">\n\n <!-- Options List -->\n <div class=\"tn-select-options\">\n <!-- Regular Options -->\n @for (option of options(); track $index) {\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [attr.tabindex]=\"option.disabled ? null : 0\"\n [class.selected]=\"isSelected()(option)\"\n [class.disabled]=\"option.disabled\"\n [attr.aria-selected]=\"isSelected()(option)\"\n [attr.aria-disabled]=\"option.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\"\n (keydown.space)=\"onOptionClick(option)\">\n\n {{ option.label }}\n </div>\n }\n\n <!-- Option Groups -->\n @for (group of optionGroups(); track $index; let isFirst = $first) {\n <!-- Group Separator (not shown before first group if we have regular options) -->\n @if (!isFirst || options().length > 0) {\n <div\n class=\"tn-select-separator\"\n role=\"separator\">\n </div>\n }\n\n <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\n [class.disabled]=\"group.disabled\">\n {{ group.label }}\n </div>\n\n <!-- Group Options -->\n @for (option of group.options; track $index) {\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [attr.tabindex]=\"option.disabled || group.disabled ? null : 0\"\n [class.selected]=\"isSelected()(option)\"\n [class.disabled]=\"option.disabled || group.disabled\"\n [attr.aria-selected]=\"isSelected()(option)\"\n [attr.aria-disabled]=\"option.disabled || group.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\"\n (keydown.space)=\"onOptionClick(option)\">\n\n {{ option.label }}\n </div>\n }\n }\n\n <!-- No Options Message -->\n @if (!hasAnyOptions()) {\n <div class=\"tn-select-no-options\">\n No options available\n </div>\n }\n </div>\n </div>\n }\n</div>", styles: [".tn-select-container{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-select-label{display:block;margin-bottom:.5rem;font-size:.875rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-select-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-select-trigger{position:relative;display:flex;align-items:center;min-height:2.5rem;padding:.5rem 2.5rem .5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:pointer;transition:all .15s ease-in-out;outline:none;box-sizing:border-box}.tn-select-trigger:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-select-trigger:focus-visible{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-select-trigger.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-select-trigger.error{border-color:var(--tn-error, #dc3545)}.tn-select-trigger.error:focus-visible,.tn-select-trigger.error.open{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px #dc354540}.tn-select-trigger.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-text{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--tn-fg1, #212529)}.tn-select-text.placeholder{color:var(--tn-alt-fg1, #999)}.tn-select-arrow{position:absolute;right:.75rem;top:50%;transform:translateY(-50%);color:var(--tn-fg2, #6c757d);transition:transform .15s ease-in-out;pointer-events:none}.tn-select-arrow.open{transform:translateY(-50%) rotate(180deg)}.tn-select-arrow svg{display:block}.tn-select-dropdown{position:absolute;top:100%;left:0;right:0;z-index:1000;margin-top:.25rem;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:200px;overflow:hidden}.tn-select-options{overflow-y:auto;padding:.25rem 0;max-height:200px}.tn-select-option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out;pointer-events:auto;position:relative;z-index:1001}.tn-select-option:hover:not(.disabled){background-color:var(--tn-alt-bg2, #f8f9fa)!important}.tn-select-option.selected{background-color:var(--tn-alt-bg1, #f8f9fa)!important;color:var(--tn-fg1, #212529)}.tn-select-option.selected:hover{background-color:var(--tn-alt-bg2, #f8f9fa)!important}.tn-select-option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:.25rem 0}.tn-select-group-label{padding:.375rem .75rem;font-size:.75rem;font-weight:600;color:var(--tn-alt-fg1, #9ca3af);text-transform:uppercase;letter-spacing:.05em;cursor:default;-webkit-user-select:none;user-select:none}.tn-select-group-label.disabled{opacity:.6}.tn-select-no-options{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}.tn-select-error{margin-top:.25rem;font-size:.75rem;color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-select-trigger,.tn-select-option,.tn-select-arrow{transition:none}}@media(prefers-contrast:high){.tn-select-trigger{border-width:2px}.tn-select-option.selected{outline:2px solid var(--tn-fg1, #000);outline-offset:-2px}}\n"] }]
|
|
2588
2546
|
}], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionGroups: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionGroups", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }] } });
|
|
2589
2547
|
|
|
2548
|
+
/**
|
|
2549
|
+
* Harness for interacting with tn-select in tests.
|
|
2550
|
+
* Provides methods for querying select state, opening/closing the dropdown,
|
|
2551
|
+
* and selecting options.
|
|
2552
|
+
*
|
|
2553
|
+
* @example
|
|
2554
|
+
* ```typescript
|
|
2555
|
+
* // Find a select and choose an option
|
|
2556
|
+
* const select = await loader.getHarness(TnSelectHarness);
|
|
2557
|
+
* await select.selectOption('Option 2');
|
|
2558
|
+
*
|
|
2559
|
+
* // Check the displayed text
|
|
2560
|
+
* expect(await select.getDisplayText()).toBe('Option 2');
|
|
2561
|
+
*
|
|
2562
|
+
* // Filter by displayed text
|
|
2563
|
+
* const fruitSelect = await loader.getHarness(
|
|
2564
|
+
* TnSelectHarness.with({ displayText: /Apple/i })
|
|
2565
|
+
* );
|
|
2566
|
+
*
|
|
2567
|
+
* // Check disabled state
|
|
2568
|
+
* const disabledSelect = await loader.getHarness(TnSelectHarness);
|
|
2569
|
+
* expect(await disabledSelect.isDisabled()).toBe(true);
|
|
2570
|
+
* ```
|
|
2571
|
+
*/
|
|
2572
|
+
class TnSelectHarness extends ComponentHarness {
|
|
2573
|
+
/**
|
|
2574
|
+
* The selector for the host element of a `TnSelectComponent` instance.
|
|
2575
|
+
*/
|
|
2576
|
+
static hostSelector = 'tn-select';
|
|
2577
|
+
_trigger = this.locatorFor('.tn-select-trigger');
|
|
2578
|
+
_text = this.locatorFor('.tn-select-text');
|
|
2579
|
+
/**
|
|
2580
|
+
* Gets a `HarnessPredicate` that can be used to search for a select
|
|
2581
|
+
* with specific attributes.
|
|
2582
|
+
*
|
|
2583
|
+
* @param options Options for filtering which select instances are considered a match.
|
|
2584
|
+
* @returns A `HarnessPredicate` configured with the given options.
|
|
2585
|
+
*
|
|
2586
|
+
* @example
|
|
2587
|
+
* ```typescript
|
|
2588
|
+
* // Find select by exact display text
|
|
2589
|
+
* const select = await loader.getHarness(TnSelectHarness.with({ displayText: 'Apple' }));
|
|
2590
|
+
*
|
|
2591
|
+
* // Find select by regex pattern
|
|
2592
|
+
* const select = await loader.getHarness(TnSelectHarness.with({ displayText: /Option/i }));
|
|
2593
|
+
* ```
|
|
2594
|
+
*/
|
|
2595
|
+
static with(options = {}) {
|
|
2596
|
+
return new HarnessPredicate(TnSelectHarness, options)
|
|
2597
|
+
.addOption('displayText', options.displayText, (harness, text) => HarnessPredicate.stringMatches(harness.getDisplayText(), text));
|
|
2598
|
+
}
|
|
2599
|
+
/**
|
|
2600
|
+
* Gets the currently displayed text (selected label or placeholder).
|
|
2601
|
+
*
|
|
2602
|
+
* @returns Promise resolving to the display text content.
|
|
2603
|
+
*
|
|
2604
|
+
* @example
|
|
2605
|
+
* ```typescript
|
|
2606
|
+
* const select = await loader.getHarness(TnSelectHarness);
|
|
2607
|
+
* const text = await select.getDisplayText();
|
|
2608
|
+
* expect(text).toBe('Select an option');
|
|
2609
|
+
* ```
|
|
2610
|
+
*/
|
|
2611
|
+
async getDisplayText() {
|
|
2612
|
+
const text = await this._text();
|
|
2613
|
+
return (await text.text()).trim();
|
|
2614
|
+
}
|
|
2615
|
+
/**
|
|
2616
|
+
* Checks whether the select is disabled.
|
|
2617
|
+
*
|
|
2618
|
+
* @returns Promise resolving to true if the select trigger has the disabled class.
|
|
2619
|
+
*
|
|
2620
|
+
* @example
|
|
2621
|
+
* ```typescript
|
|
2622
|
+
* const select = await loader.getHarness(TnSelectHarness);
|
|
2623
|
+
* expect(await select.isDisabled()).toBe(false);
|
|
2624
|
+
* ```
|
|
2625
|
+
*/
|
|
2626
|
+
async isDisabled() {
|
|
2627
|
+
const trigger = await this._trigger();
|
|
2628
|
+
return trigger.hasClass('disabled');
|
|
2629
|
+
}
|
|
2630
|
+
/**
|
|
2631
|
+
* Checks whether the dropdown is currently open.
|
|
2632
|
+
*
|
|
2633
|
+
* @returns Promise resolving to true if the dropdown is open.
|
|
2634
|
+
*
|
|
2635
|
+
* @example
|
|
2636
|
+
* ```typescript
|
|
2637
|
+
* const select = await loader.getHarness(TnSelectHarness);
|
|
2638
|
+
* expect(await select.isOpen()).toBe(false);
|
|
2639
|
+
* await select.open();
|
|
2640
|
+
* expect(await select.isOpen()).toBe(true);
|
|
2641
|
+
* ```
|
|
2642
|
+
*/
|
|
2643
|
+
async isOpen() {
|
|
2644
|
+
const trigger = await this._trigger();
|
|
2645
|
+
return trigger.hasClass('open');
|
|
2646
|
+
}
|
|
2647
|
+
/**
|
|
2648
|
+
* Opens the dropdown. No-op if already open.
|
|
2649
|
+
*
|
|
2650
|
+
* @returns Promise that resolves when the dropdown is open.
|
|
2651
|
+
*
|
|
2652
|
+
* @example
|
|
2653
|
+
* ```typescript
|
|
2654
|
+
* const select = await loader.getHarness(TnSelectHarness);
|
|
2655
|
+
* await select.open();
|
|
2656
|
+
* ```
|
|
2657
|
+
*/
|
|
2658
|
+
async open() {
|
|
2659
|
+
if (!(await this.isOpen())) {
|
|
2660
|
+
const trigger = await this._trigger();
|
|
2661
|
+
await trigger.click();
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
/**
|
|
2665
|
+
* Closes the dropdown. No-op if already closed.
|
|
2666
|
+
*
|
|
2667
|
+
* @returns Promise that resolves when the dropdown is closed.
|
|
2668
|
+
*
|
|
2669
|
+
* @example
|
|
2670
|
+
* ```typescript
|
|
2671
|
+
* const select = await loader.getHarness(TnSelectHarness);
|
|
2672
|
+
* await select.open();
|
|
2673
|
+
* await select.close();
|
|
2674
|
+
* expect(await select.isOpen()).toBe(false);
|
|
2675
|
+
* ```
|
|
2676
|
+
*/
|
|
2677
|
+
async close() {
|
|
2678
|
+
if (await this.isOpen()) {
|
|
2679
|
+
const trigger = await this._trigger();
|
|
2680
|
+
await trigger.click();
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
/**
|
|
2684
|
+
* Selects an option by its label text. Opens the dropdown if needed.
|
|
2685
|
+
*
|
|
2686
|
+
* @param filter The text to match against option labels. Supports string or RegExp.
|
|
2687
|
+
* @returns Promise that resolves when the option has been selected.
|
|
2688
|
+
*
|
|
2689
|
+
* @example
|
|
2690
|
+
* ```typescript
|
|
2691
|
+
* const select = await loader.getHarness(TnSelectHarness);
|
|
2692
|
+
*
|
|
2693
|
+
* // Select by exact text
|
|
2694
|
+
* await select.selectOption('Option 2');
|
|
2695
|
+
*
|
|
2696
|
+
* // Select by regex
|
|
2697
|
+
* await select.selectOption(/option 2/i);
|
|
2698
|
+
* ```
|
|
2699
|
+
*/
|
|
2700
|
+
async selectOption(filter) {
|
|
2701
|
+
await this.open();
|
|
2702
|
+
const options = await this.locatorForAll('.tn-select-option')();
|
|
2703
|
+
for (const option of options) {
|
|
2704
|
+
const text = (await option.text()).trim();
|
|
2705
|
+
const matches = filter instanceof RegExp
|
|
2706
|
+
? filter.test(text)
|
|
2707
|
+
: text === filter;
|
|
2708
|
+
if (matches) {
|
|
2709
|
+
await option.click();
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
throw new Error(`Could not find option matching "${filter}"`);
|
|
2714
|
+
}
|
|
2715
|
+
/**
|
|
2716
|
+
* Gets the labels of all available options. Opens the dropdown if needed.
|
|
2717
|
+
*
|
|
2718
|
+
* @returns Promise resolving to an array of option label strings.
|
|
2719
|
+
*
|
|
2720
|
+
* @example
|
|
2721
|
+
* ```typescript
|
|
2722
|
+
* const select = await loader.getHarness(TnSelectHarness);
|
|
2723
|
+
* const options = await select.getOptions();
|
|
2724
|
+
* expect(options).toEqual(['Option 1', 'Option 2', 'Option 3']);
|
|
2725
|
+
* ```
|
|
2726
|
+
*/
|
|
2727
|
+
async getOptions() {
|
|
2728
|
+
await this.open();
|
|
2729
|
+
const options = await this.locatorForAll('.tn-select-option')();
|
|
2730
|
+
const labels = [];
|
|
2731
|
+
for (const option of options) {
|
|
2732
|
+
labels.push((await option.text()).trim());
|
|
2733
|
+
}
|
|
2734
|
+
return labels;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2590
2738
|
/**
|
|
2591
2739
|
* Harness for interacting with tn-icon in tests.
|
|
2592
2740
|
* Provides filtering by icon name and library for existence checks, as well as click interaction.
|
|
@@ -8501,5 +8649,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
8501
8649
|
* Generated bundle index. Do not edit.
|
|
8502
8650
|
*/
|
|
8503
8651
|
|
|
8504
|
-
export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, StripMntPrefixPipe, THEME_MAP, THEME_STORAGE_KEY, TN_THEME_DEFINITIONS, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCellDefDirective, TnCheckboxComponent, TnChipComponent, TnConfirmDialogComponent, TnDateInputComponent, TnDateRangeInputComponent, TnDialog, TnDialogShellComponent, TnDividerComponent, TnDividerDirective, TnExpansionPanelComponent, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuComponent, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnSelectComponent, TnSelectionListComponent, TnSlideToggleComponent, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnTabComponent, TnTabPanelComponent, TnTableColumnDirective, TnTableComponent, TnTabsComponent, TnTheme, TnThemeService, TnTimeInputComponent, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeNodeComponent, TnTreeNodeOutletDirective, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, libIconMarker, registerLucideIcons, setupLucideIntegration, tnIconMarker };
|
|
8652
|
+
export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, StripMntPrefixPipe, THEME_MAP, THEME_STORAGE_KEY, TN_THEME_DEFINITIONS, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCellDefDirective, TnCheckboxComponent, TnChipComponent, TnConfirmDialogComponent, TnDateInputComponent, TnDateRangeInputComponent, TnDialog, TnDialogShellComponent, TnDividerComponent, TnDividerDirective, TnExpansionPanelComponent, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuComponent, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSlideToggleComponent, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnTabComponent, TnTabPanelComponent, TnTableColumnDirective, TnTableComponent, TnTabsComponent, TnTheme, TnThemeService, TnTimeInputComponent, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeNodeComponent, TnTreeNodeOutletDirective, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, libIconMarker, registerLucideIcons, setupLucideIntegration, tnIconMarker };
|
|
8505
8653
|
//# sourceMappingURL=truenas-ui-components.mjs.map
|